Feat: improved mc secrets (#1585)

* make encryption service public

* generate

* generate

* with update

* revert

* fix: type
This commit is contained in:
Gabe Ruttner
2025-04-21 10:43:27 -04:00
committed by GitHub
parent 0e17a83cfe
commit 28bc541d85
10 changed files with 742 additions and 349 deletions
+153 -82
View File
@@ -1,6 +1,6 @@
import React, { useEffect } from 'react';
import { Input } from '@/components/ui/input';
import { Textarea } from './textarea';
import { Textarea } from '@/components/ui/textarea';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { TrashIcon } from '@radix-ui/react-icons';
@@ -11,6 +11,9 @@ export type KeyValueType = {
hidden: boolean;
locked: boolean;
deleted: boolean;
id?: string; // Optional ID for existing env vars
isEditing?: boolean; // Whether the value is being edited
hint?: string; // Hint value for existing secrets
};
type PropsType = {
@@ -20,6 +23,9 @@ type PropsType = {
disabled?: boolean;
fileUpload?: boolean;
secretOption?: boolean;
onDeleteIds?: (ids: string[]) => void; // Callback for deleted IDs
onAdd?: (values: KeyValueType[]) => void; // Callback for added values
onUpdate?: (values: KeyValueType[]) => void; // Callback for updated values
};
const EnvGroupArray: React.FC<PropsType> = ({
@@ -28,6 +34,9 @@ const EnvGroupArray: React.FC<PropsType> = ({
setValues = () => {},
disabled,
secretOption,
onDeleteIds,
onAdd,
onUpdate,
}) => {
useEffect(() => {
if (!values) {
@@ -37,102 +46,164 @@ const EnvGroupArray: React.FC<PropsType> = ({
const handleValueChange = (index: number, key: string, value: any) => {
const newValues = [...values];
newValues[index] = { ...newValues[index], [key]: value };
const entry = newValues[index];
// If this is an existing secret and we're editing the value, set isEditing to true
if (key === 'value' && entry.hint && value !== entry.hint) {
newValues[index] = { ...entry, [key]: value, isEditing: true };
} else {
newValues[index] = { ...entry, [key]: value };
}
setValues(newValues);
// If this is an existing entry (has an ID), notify about updates
if (entry.id && onUpdate) {
onUpdate([newValues[index]]);
}
};
const handleDeleteToggle = (index: number) => {
const newValues = [...values];
const entry = newValues[index];
const newDeletedState = !entry.deleted;
newValues[index] = { ...entry, deleted: newDeletedState };
setValues(newValues);
// If this is an existing env var with an ID, notify parent of deleted IDs
if (entry.id && onDeleteIds) {
const deletedIds = values
.filter((v) => v.id && v.deleted)
.map((v) => v.id as string);
onDeleteIds(deletedIds);
}
};
const handleRemove = (index: number) => {
const entry = values[index];
if (entry.id) {
// For existing entries, mark as deleted
const newValues = [...values];
newValues[index] = { ...entry, deleted: true };
setValues(newValues);
if (onDeleteIds) {
const deletedIds = values
.filter((v) => v.id && v.deleted)
.map((v) => v.id as string);
onDeleteIds(deletedIds);
}
} else {
// For new entries, remove completely
const newValues = values.filter((_, i) => i !== index);
setValues(newValues);
if (onAdd) {
onAdd(newValues.filter((v) => !v.id));
}
}
};
return (
<>
{label && <div className="mb-2 text-white">{label}</div>}
{values?.map((entry: KeyValueType, i: number) => {
if (!entry.deleted) {
return (
<div className="mb-2 flex items-center" key={i}>
<Input
placeholder="ex: key"
value={entry.key}
onChange={(e) => handleValueChange(i, 'key', e.target.value)}
disabled={disabled || entry.locked}
className={cn(
'w-64',
entry.locked && 'bg-gray-200 cursor-not-allowed',
)}
/>
<div className="mx-2" />
{entry.hidden ? (
<Input
placeholder="ex: value"
value={entry.value}
onChange={(e) =>
handleValueChange(i, 'value', e.target.value)
}
type="password"
disabled={disabled || entry.locked}
className={cn(
'flex-1',
entry.locked && 'bg-gray-200 cursor-not-allowed',
)}
/>
) : (
<Textarea
placeholder="ex: value"
value={entry.value}
onChange={(e: any) =>
handleValueChange(i, 'value', e.target.value)
}
rows={entry.value?.split('\n').length || 0}
disabled={disabled || entry.locked}
className={cn(
'flex-1',
entry.locked && 'bg-gray-200 cursor-not-allowed',
)}
/>
{values?.map((entry: KeyValueType, i: number) => (
<div
className={cn(
'mb-2 flex items-center gap-2',
entry.deleted && 'opacity-50 [&>*]:line-through',
)}
key={i}
>
<Input
placeholder="ex: key"
value={entry.key}
onChange={(e) => handleValueChange(i, 'key', e.target.value)}
disabled={disabled || entry.locked || entry.deleted}
className={cn(
'w-64',
entry.locked && 'bg-gray-200 cursor-not-allowed',
)}
/>
{entry.hidden ? (
<Input
placeholder={entry.hint}
value={entry.isEditing ? entry.value : undefined}
onChange={(e) => handleValueChange(i, 'value', e.target.value)}
type="password"
disabled={disabled || entry.locked || entry.deleted}
className={cn(
'flex-1',
entry.locked && 'bg-gray-200 cursor-not-allowed',
!entry.isEditing && entry.hint && 'text-gray-400',
)}
{secretOption && (
<Button
variant="ghost"
onClick={() =>
!entry.locked &&
handleValueChange(i, 'hidden', !entry.hidden)
/>
) : (
<Textarea
placeholder={entry.hint}
value={entry.isEditing ? entry.value : undefined}
onChange={(e: any) =>
handleValueChange(i, 'value', e.target.value)
}
rows={entry.value?.split('\n').length || 0}
disabled={disabled || entry.locked || entry.deleted}
className={cn(
'flex-1',
entry.locked && 'bg-gray-200 cursor-not-allowed',
!entry.isEditing && entry.hint && 'text-gray-400',
)}
/>
)}
{secretOption && (
<Button
variant="ghost"
onClick={() =>
!entry.locked && handleValueChange(i, 'hidden', !entry.hidden)
}
disabled={entry.locked || entry.deleted}
>
{entry.hidden ? 'Unlock' : 'Lock'}
</Button>
)}
{!disabled && (
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => {
if (entry.id) {
handleDeleteToggle(i);
} else {
handleRemove(i);
}
disabled={entry.locked}
>
{entry.hidden ? 'Unlock' : 'Lock'}
</Button>
)}
{!disabled && (
<Button
variant="ghost"
className="ml-2"
size="sm"
onClick={() => {
const newValues = values.filter((_, index) => index !== i);
setValues(newValues);
}}
>
<TrashIcon className="h-4 w-4" />
</Button>
)}
}}
>
<TrashIcon className="h-4 w-4" />
</Button>
</div>
);
}
})}
)}
</div>
))}
{!disabled && (
<div className="flex items-center">
<Button
variant="secondary"
onClick={() => {
const newValues = [
...values,
{
key: '',
value: '',
hidden: false,
locked: false,
deleted: false,
},
];
const newEntry = {
key: '',
value: '',
hidden: false,
locked: false,
deleted: false,
};
const newValues = [...values, newEntry];
setValues(newValues);
if (onAdd) {
onAdd([...values.filter((v) => !v.id), newEntry]);
}
}}
>
Add row
+152 -81
View File
@@ -11,6 +11,9 @@ export type KeyValueType = {
hidden: boolean;
locked: boolean;
deleted: boolean;
id?: string; // Optional ID for existing env vars
isEditing?: boolean; // Whether the value is being edited
hint?: string; // Hint value for existing secrets
};
type PropsType = {
@@ -20,6 +23,9 @@ type PropsType = {
disabled?: boolean;
fileUpload?: boolean;
secretOption?: boolean;
onDeleteIds?: (ids: string[]) => void; // Callback for deleted IDs
onAdd?: (values: KeyValueType[]) => void; // Callback for added values
onUpdate?: (values: KeyValueType[]) => void; // Callback for updated values
};
const EnvGroupArray: React.FC<PropsType> = ({
@@ -28,6 +34,9 @@ const EnvGroupArray: React.FC<PropsType> = ({
setValues = () => {},
disabled,
secretOption,
onDeleteIds,
onAdd,
onUpdate,
}) => {
useEffect(() => {
if (!values) {
@@ -37,102 +46,164 @@ const EnvGroupArray: React.FC<PropsType> = ({
const handleValueChange = (index: number, key: string, value: any) => {
const newValues = [...values];
newValues[index] = { ...newValues[index], [key]: value };
const entry = newValues[index];
// If this is an existing secret and we're editing the value, set isEditing to true
if (key === 'value' && entry.hint && value !== entry.hint) {
newValues[index] = { ...entry, [key]: value, isEditing: true };
} else {
newValues[index] = { ...entry, [key]: value };
}
setValues(newValues);
// If this is an existing entry (has an ID), notify about updates
if (entry.id && onUpdate) {
onUpdate([newValues[index]]);
}
};
const handleDeleteToggle = (index: number) => {
const newValues = [...values];
const entry = newValues[index];
const newDeletedState = !entry.deleted;
newValues[index] = { ...entry, deleted: newDeletedState };
setValues(newValues);
// If this is an existing env var with an ID, notify parent of deleted IDs
if (entry.id && onDeleteIds) {
const deletedIds = values
.filter((v) => v.id && v.deleted)
.map((v) => v.id as string);
onDeleteIds(deletedIds);
}
};
const handleRemove = (index: number) => {
const entry = values[index];
if (entry.id) {
// For existing entries, mark as deleted
const newValues = [...values];
newValues[index] = { ...entry, deleted: true };
setValues(newValues);
if (onDeleteIds) {
const deletedIds = values
.filter((v) => v.id && v.deleted)
.map((v) => v.id as string);
onDeleteIds(deletedIds);
}
} else {
// For new entries, remove completely
const newValues = values.filter((_, i) => i !== index);
setValues(newValues);
if (onAdd) {
onAdd(newValues.filter((v) => !v.id));
}
}
};
return (
<>
{label && <div className="mb-2 text-white">{label}</div>}
{values?.map((entry: KeyValueType, i: number) => {
if (!entry.deleted) {
return (
<div className="mb-2 flex items-center" key={i}>
<Input
placeholder="ex: key"
value={entry.key}
onChange={(e) => handleValueChange(i, 'key', e.target.value)}
disabled={disabled || entry.locked}
className={cn(
'w-64',
entry.locked && 'bg-gray-200 cursor-not-allowed',
)}
/>
<div className="mx-2" />
{entry.hidden ? (
<Input
placeholder="ex: value"
value={entry.value}
onChange={(e) =>
handleValueChange(i, 'value', e.target.value)
}
type="password"
disabled={disabled || entry.locked}
className={cn(
'flex-1',
entry.locked && 'bg-gray-200 cursor-not-allowed',
)}
/>
) : (
<Textarea
placeholder="ex: value"
value={entry.value}
onChange={(e: any) =>
handleValueChange(i, 'value', e.target.value)
}
rows={entry.value?.split('\n').length || 0}
disabled={disabled || entry.locked}
className={cn(
'flex-1',
entry.locked && 'bg-gray-200 cursor-not-allowed',
)}
/>
{values?.map((entry: KeyValueType, i: number) => (
<div
className={cn(
'mb-2 flex items-center gap-2',
entry.deleted && 'opacity-50 [&>*]:line-through',
)}
key={i}
>
<Input
placeholder="ex: key"
value={entry.key}
onChange={(e) => handleValueChange(i, 'key', e.target.value)}
disabled={disabled || entry.locked || entry.deleted}
className={cn(
'w-64',
entry.locked && 'bg-gray-200 cursor-not-allowed',
)}
/>
{entry.hidden ? (
<Input
placeholder={entry.hint}
value={entry.isEditing ? entry.value : undefined}
onChange={(e) => handleValueChange(i, 'value', e.target.value)}
type="password"
disabled={disabled || entry.locked || entry.deleted}
className={cn(
'flex-1',
entry.locked && 'bg-gray-200 cursor-not-allowed',
!entry.isEditing && entry.hint && 'text-gray-400',
)}
{secretOption && (
<Button
variant="ghost"
onClick={() =>
!entry.locked &&
handleValueChange(i, 'hidden', !entry.hidden)
/>
) : (
<Textarea
placeholder={entry.hint}
value={entry.isEditing ? entry.value : undefined}
onChange={(e: any) =>
handleValueChange(i, 'value', e.target.value)
}
rows={entry.value?.split('\n').length || 0}
disabled={disabled || entry.locked || entry.deleted}
className={cn(
'flex-1',
entry.locked && 'bg-gray-200 cursor-not-allowed',
!entry.isEditing && entry.hint && 'text-gray-400',
)}
/>
)}
{secretOption && (
<Button
variant="ghost"
onClick={() =>
!entry.locked && handleValueChange(i, 'hidden', !entry.hidden)
}
disabled={entry.locked || entry.deleted}
>
{entry.hidden ? 'Unlock' : 'Lock'}
</Button>
)}
{!disabled && (
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => {
if (entry.id) {
handleDeleteToggle(i);
} else {
handleRemove(i);
}
disabled={entry.locked}
>
{entry.hidden ? 'Unlock' : 'Lock'}
</Button>
)}
{!disabled && (
<Button
variant="ghost"
className="ml-2"
size="sm"
onClick={() => {
const newValues = values.filter((_, index) => index !== i);
setValues(newValues);
}}
>
<TrashIcon className="h-4 w-4" />
</Button>
)}
}}
>
<TrashIcon className="h-4 w-4" />
</Button>
</div>
);
}
})}
)}
</div>
))}
{!disabled && (
<div className="flex items-center">
<Button
variant="secondary"
onClick={() => {
const newValues = [
...values,
{
key: '',
value: '',
hidden: false,
locked: false,
deleted: false,
},
];
const newEntry = {
key: '',
value: '',
hidden: false,
locked: false,
deleted: false,
};
const newValues = [...values, newEntry];
setValues(newValues);
if (onAdd) {
onAdd([...values.filter((v) => !v.id), newEntry]);
}
}}
>
Add row
@@ -40,7 +40,9 @@ import {
} from "./data-contracts";
import { ContentType, HttpClient, RequestParams } from "./http-client";
export class Api<SecurityDataType = unknown> extends HttpClient<SecurityDataType> {
export class Api<
SecurityDataType = unknown,
> extends HttpClient<SecurityDataType> {
/**
* @description Gets metadata for the Hatchet instance
*
@@ -262,7 +264,11 @@ export class Api<SecurityDataType = unknown> extends HttpClient<SecurityDataType
* @request POST:/api/v1/cloud/tenants/{tenant}/managed-worker
* @secure
*/
managedWorkerCreate = (tenant: string, data: CreateManagedWorkerRequest, params: RequestParams = {}) =>
managedWorkerCreate = (
tenant: string,
data: CreateManagedWorkerRequest,
params: RequestParams = {},
) =>
this.request<ManagedWorker, APIErrors>({
path: `/api/v1/cloud/tenants/${tenant}/managed-worker`,
method: "POST",
@@ -338,7 +344,11 @@ export class Api<SecurityDataType = unknown> extends HttpClient<SecurityDataType
* @request POST:/api/v1/cloud/managed-worker/{managed-worker}
* @secure
*/
managedWorkerUpdate = (managedWorker: string, data: UpdateManagedWorkerRequest, params: RequestParams = {}) =>
managedWorkerUpdate = (
managedWorker: string,
data: UpdateManagedWorkerRequest,
params: RequestParams = {},
) =>
this.request<ManagedWorker, APIErrors>({
path: `/api/v1/cloud/managed-worker/${managedWorker}`,
method: "POST",
@@ -374,7 +384,11 @@ export class Api<SecurityDataType = unknown> extends HttpClient<SecurityDataType
* @request POST:/api/v1/cloud/infra-as-code/{infra-as-code-request}
* @secure
*/
infraAsCodeCreate = (infraAsCodeRequest: string, data: InfraAsCodeRequest, params: RequestParams = {}) =>
infraAsCodeCreate = (
infraAsCodeRequest: string,
data: InfraAsCodeRequest,
params: RequestParams = {},
) =>
this.request<void, APIErrors>({
path: `/api/v1/cloud/infra-as-code/${infraAsCodeRequest}`,
method: "POST",
@@ -392,7 +406,10 @@ export class Api<SecurityDataType = unknown> extends HttpClient<SecurityDataType
* @request GET:/api/v1/cloud/runtime-config/{runtime-config}/actions
* @secure
*/
runtimeConfigListActions = (runtimeConfig: string, params: RequestParams = {}) =>
runtimeConfigListActions = (
runtimeConfig: string,
params: RequestParams = {},
) =>
this.request<RuntimeConfigActionsResponse, APIErrors>({
path: `/api/v1/cloud/runtime-config/${runtimeConfig}/actions`,
method: "GET",
@@ -570,7 +587,10 @@ export class Api<SecurityDataType = unknown> extends HttpClient<SecurityDataType
* @request GET:/api/v1/cloud/managed-worker/{managed-worker}/instances
* @secure
*/
managedWorkerInstancesList = (managedWorker: string, params: RequestParams = {}) =>
managedWorkerInstancesList = (
managedWorker: string,
params: RequestParams = {},
) =>
this.request<InstanceList, APIErrors>({
path: `/api/v1/cloud/managed-worker/${managedWorker}/instances`,
method: "GET",
@@ -621,7 +641,10 @@ export class Api<SecurityDataType = unknown> extends HttpClient<SecurityDataType
* @request GET:/api/v1/cloud/managed-worker/{managed-worker}/events
* @secure
*/
managedWorkerEventsList = (managedWorker: string, params: RequestParams = {}) =>
managedWorkerEventsList = (
managedWorker: string,
params: RequestParams = {},
) =>
this.request<ManagedWorkerEventList, APIErrors>({
path: `/api/v1/cloud/managed-worker/${managedWorker}/events`,
method: "GET",
@@ -669,7 +692,11 @@ export class Api<SecurityDataType = unknown> extends HttpClient<SecurityDataType
* @request PATCH:/api/v1/billing/tenants/{tenant}/subscription
* @secure
*/
subscriptionUpsert = (tenant: string, data: UpdateTenantSubscription, params: RequestParams = {}) =>
subscriptionUpsert = (
tenant: string,
data: UpdateTenantSubscription,
params: RequestParams = {},
) =>
this.request<TenantSubscription, APIErrors>({
path: `/api/v1/billing/tenants/${tenant}/subscription`,
method: "PATCH",
@@ -711,7 +738,11 @@ export class Api<SecurityDataType = unknown> extends HttpClient<SecurityDataType
* @request POST:/api/v1/cloud/tenants/{tenant}/logs
* @secure
*/
logCreate = (tenant: string, data: VectorPushRequest, params: RequestParams = {}) =>
logCreate = (
tenant: string,
data: VectorPushRequest,
params: RequestParams = {},
) =>
this.request<void, APIErrors>({
path: `/api/v1/cloud/tenants/${tenant}/logs`,
method: "POST",
@@ -10,6 +10,72 @@
* ---------------------------------------------------------------
*/
export enum TemplateOptions {
QUICKSTART_PYTHON = "QUICKSTART_PYTHON",
QUICKSTART_TYPESCRIPT = "QUICKSTART_TYPESCRIPT",
QUICKSTART_GO = "QUICKSTART_GO",
}
export enum AutoscalingTargetKind {
PORTER = "PORTER",
FLY = "FLY",
}
export enum CouponFrequency {
Once = "once",
Recurring = "recurring",
}
export enum TenantSubscriptionStatus {
Active = "active",
Pending = "pending",
Terminated = "terminated",
Canceled = "canceled",
}
export enum ManagedWorkerRegion {
Ams = "ams",
Arn = "arn",
Atl = "atl",
Bog = "bog",
Bos = "bos",
Cdg = "cdg",
Den = "den",
Dfw = "dfw",
Ewr = "ewr",
Eze = "eze",
Gdl = "gdl",
Gig = "gig",
Gru = "gru",
Hkg = "hkg",
Iad = "iad",
Jnb = "jnb",
Lax = "lax",
Lhr = "lhr",
Mad = "mad",
Mia = "mia",
Nrt = "nrt",
Ord = "ord",
Otp = "otp",
Phx = "phx",
Qro = "qro",
Scl = "scl",
Sea = "sea",
Sin = "sin",
Sjc = "sjc",
Syd = "syd",
Waw = "waw",
Yul = "yul",
Yyz = "yyz",
}
export enum ManagedWorkerEventStatus {
IN_PROGRESS = "IN_PROGRESS",
SUCCEEDED = "SUCCEEDED",
FAILED = "FAILED",
CANCELLED = "CANCELLED",
}
export interface APICloudMetadata {
/**
* whether the tenant can be billed
@@ -138,8 +204,8 @@ export interface ManagedWorker {
name: string;
buildConfig?: ManagedWorkerBuildConfig;
isIac: boolean;
/** A map of environment variables to set for the worker */
envVars: Record<string, string>;
directSecrets: ManagedWorkerSecret[];
globalSecrets: ManagedWorkerSecret[];
runtimeConfigs?: ManagedWorkerRuntimeConfig[];
}
@@ -161,6 +227,46 @@ export interface ManagedWorkerBuildConfig {
steps?: BuildStep[];
}
export interface ManagedWorkerSecret {
key: string;
id: string;
hint: string;
}
export interface CreateManagedWorkerSecretRequest {
/** array of secret keys and values to add to the worker */
add?: {
key: string;
value: string;
}[];
/** array of global secret ids to add to the worker */
addGlobal?: string[];
}
export interface UpdateManagedWorkerSecretRequest {
/** array of secret keys and values to add to the worker */
add?: {
key: string;
value: string;
}[];
/** array of global secret ids to add to the worker */
addGlobal?: string[];
/** array of secret ids to delete from the worker */
delete?: string[];
/** array of existing secret ids and values to update in the worker */
update?: {
/**
* @format uuid
* @minLength 36
* @maxLength 36
*/
id: string;
/** @minLength 1 */
key: string;
value: string;
}[];
}
export interface BuildStep {
metadata: APIResourceMeta;
/** The relative path to the build directory */
@@ -185,13 +291,6 @@ export interface ManagedWorkerRuntimeConfig {
actions?: string[];
}
export enum ManagedWorkerEventStatus {
IN_PROGRESS = "IN_PROGRESS",
SUCCEEDED = "SUCCEEDED",
FAILED = "FAILED",
CANCELLED = "CANCELLED",
}
export interface ManagedWorkerEvent {
id: number;
/** @format date-time */
@@ -212,8 +311,7 @@ export interface ManagedWorkerEventList {
export interface CreateManagedWorkerRequest {
name: string;
buildConfig: CreateManagedWorkerBuildConfigRequest;
/** A map of environment variables to set for the worker */
envVars: Record<string, string>;
secrets?: CreateManagedWorkerSecretRequest;
isIac: boolean;
runtimeConfig?: CreateManagedWorkerRuntimeConfigRequest;
}
@@ -221,8 +319,7 @@ export interface CreateManagedWorkerRequest {
export interface UpdateManagedWorkerRequest {
name?: string;
buildConfig?: CreateManagedWorkerBuildConfigRequest;
/** A map of environment variables to set for the worker */
envVars?: Record<string, string>;
secrets?: UpdateManagedWorkerSecretRequest;
isIac?: boolean;
runtimeConfig?: CreateManagedWorkerRuntimeConfigRequest;
}
@@ -255,42 +352,6 @@ export interface CreateBuildStepRequest {
dockerfilePath: string;
}
export enum ManagedWorkerRegion {
Ams = "ams",
Arn = "arn",
Atl = "atl",
Bog = "bog",
Bos = "bos",
Cdg = "cdg",
Den = "den",
Dfw = "dfw",
Ewr = "ewr",
Eze = "eze",
Gdl = "gdl",
Gig = "gig",
Gru = "gru",
Hkg = "hkg",
Iad = "iad",
Jnb = "jnb",
Lax = "lax",
Lhr = "lhr",
Mad = "mad",
Mia = "mia",
Nrt = "nrt",
Ord = "ord",
Otp = "otp",
Phx = "phx",
Qro = "qro",
Scl = "scl",
Sea = "sea",
Sin = "sin",
Sjc = "sjc",
Syd = "syd",
Waw = "waw",
Yul = "yul",
Yyz = "yyz",
}
export interface CreateManagedWorkerRuntimeConfigRequest {
/**
* @min 0
@@ -382,13 +443,6 @@ export interface TenantPaymentMethod {
description?: string;
}
export enum TenantSubscriptionStatus {
Active = "active",
Pending = "pending",
Terminated = "terminated",
Canceled = "canceled",
}
export interface Coupon {
/** The name of the coupon. */
name: string;
@@ -408,11 +462,6 @@ export interface Coupon {
percent?: number;
}
export enum CouponFrequency {
Once = "once",
Recurring = "recurring",
}
export type VectorPushRequest = EventObject[];
export interface EventObject {
@@ -557,11 +606,6 @@ export interface AutoscalingConfig {
scaleToZero: boolean;
}
export enum AutoscalingTargetKind {
PORTER = "PORTER",
FLY = "FLY",
}
export interface CreateOrUpdateAutoscalingRequest {
waitDuration: string;
rollingWindowDuration: string;
@@ -589,12 +633,6 @@ export interface CreateFlyAutoscalingRequest {
currentReplicas: number;
}
export enum TemplateOptions {
QUICKSTART_PYTHON = "QUICKSTART_PYTHON",
QUICKSTART_TYPESCRIPT = "QUICKSTART_TYPESCRIPT",
QUICKSTART_GO = "QUICKSTART_GO",
}
export interface CreateManagedWorkerFromTemplateRequest {
name: TemplateOptions;
}
@@ -10,12 +10,19 @@
* ---------------------------------------------------------------
*/
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse, HeadersDefaults, ResponseType } from "axios";
import type {
AxiosInstance,
AxiosRequestConfig,
AxiosResponse,
HeadersDefaults,
ResponseType,
} from "axios";
import axios from "axios";
export type QueryParamsType = Record<string | number, any>;
export interface FullRequestParams extends Omit<AxiosRequestConfig, "data" | "params" | "url" | "responseType"> {
export interface FullRequestParams
extends Omit<AxiosRequestConfig, "data" | "params" | "url" | "responseType"> {
/** set parameter to `true` for call `securityWorker` for this request */
secure?: boolean;
/** request path */
@@ -30,9 +37,13 @@ export interface FullRequestParams extends Omit<AxiosRequestConfig, "data" | "pa
body?: unknown;
}
export type RequestParams = Omit<FullRequestParams, "body" | "method" | "query" | "path">;
export type RequestParams = Omit<
FullRequestParams,
"body" | "method" | "query" | "path"
>;
export interface ApiConfig<SecurityDataType = unknown> extends Omit<AxiosRequestConfig, "data" | "cancelToken"> {
export interface ApiConfig<SecurityDataType = unknown>
extends Omit<AxiosRequestConfig, "data" | "cancelToken"> {
securityWorker?: (
securityData: SecurityDataType | null,
) => Promise<AxiosRequestConfig | void> | AxiosRequestConfig | void;
@@ -54,8 +65,16 @@ export class HttpClient<SecurityDataType = unknown> {
private secure?: boolean;
private format?: ResponseType;
constructor({ securityWorker, secure, format, ...axiosConfig }: ApiConfig<SecurityDataType> = {}) {
this.instance = axios.create({ ...axiosConfig, baseURL: axiosConfig.baseURL || "" });
constructor({
securityWorker,
secure,
format,
...axiosConfig
}: ApiConfig<SecurityDataType> = {}) {
this.instance = axios.create({
...axiosConfig,
baseURL: axiosConfig.baseURL || "",
});
this.secure = secure;
this.format = format;
this.securityWorker = securityWorker;
@@ -65,7 +84,10 @@ export class HttpClient<SecurityDataType = unknown> {
this.securityData = data;
};
protected mergeRequestParams(params1: AxiosRequestConfig, params2?: AxiosRequestConfig): AxiosRequestConfig {
protected mergeRequestParams(
params1: AxiosRequestConfig,
params2?: AxiosRequestConfig,
): AxiosRequestConfig {
const method = params1.method || (params2 && params2.method);
return {
@@ -73,7 +95,11 @@ export class HttpClient<SecurityDataType = unknown> {
...params1,
...(params2 || {}),
headers: {
...((method && this.instance.defaults.headers[method.toLowerCase() as keyof HeadersDefaults]) || {}),
...((method &&
this.instance.defaults.headers[
method.toLowerCase() as keyof HeadersDefaults
]) ||
{}),
...(params1.headers || {}),
...((params2 && params2.headers) || {}),
},
@@ -94,11 +120,15 @@ export class HttpClient<SecurityDataType = unknown> {
}
return Object.keys(input || {}).reduce((formData, key) => {
const property = input[key];
const propertyContent: any[] = property instanceof Array ? property : [property];
const propertyContent: any[] =
property instanceof Array ? property : [property];
for (const formItem of propertyContent) {
const isFileType = formItem instanceof Blob || formItem instanceof File;
formData.append(key, isFileType ? formItem : this.stringifyFormItem(formItem));
formData.append(
key,
isFileType ? formItem : this.stringifyFormItem(formItem),
);
}
return formData;
@@ -122,11 +152,21 @@ export class HttpClient<SecurityDataType = unknown> {
const requestParams = this.mergeRequestParams(params, secureParams);
const responseFormat = format || this.format || undefined;
if (type === ContentType.FormData && body && body !== null && typeof body === "object") {
if (
type === ContentType.FormData &&
body &&
body !== null &&
typeof body === "object"
) {
body = this.createFormData(body as Record<string, unknown>);
}
if (type === ContentType.Text && body && body !== null && typeof body !== "string") {
if (
type === ContentType.Text &&
body &&
body !== null &&
typeof body !== "string"
) {
body = JSON.stringify(body);
}
@@ -16,7 +16,6 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { Label } from '@/components/v1/ui/label';
import { Alert, AlertDescription, AlertTitle } from '@/components/v1/ui/alert';
import { Input } from '@/components/v1/ui/input';
import EnvGroupArray, { KeyValueType } from '@/components/v1/ui/envvar';
import {
getRepoName,
getRepoOwner,
@@ -49,6 +48,7 @@ import {
managedCompute,
} from '@/lib/can/features/managed-compute';
import { useTenant } from '@/lib/atoms';
import EnvGroupArray, { KeyValueType } from '@/components/v1/ui/envvar';
interface UpdateWorkerFormProps {
onSubmit: (opts: z.infer<typeof updateManagedWorkerSchema>) => void;
@@ -75,7 +75,22 @@ const updateManagedWorkerSchema = z.object({
})
.optional(),
isIac: z.boolean().default(false).optional(),
envVars: z.record(z.string()).optional(),
secrets: z.object({
add: z.array(
z.object({
key: z.string(),
value: z.string(),
}),
),
update: z.array(
z.object({
id: z.string(),
key: z.string(),
value: z.string(),
}),
),
delete: z.array(z.string()),
}),
runtimeConfig: z
.object({
numReplicas: z.number().min(0).max(16).optional(),
@@ -139,7 +154,11 @@ export default function UpdateWorkerForm({
},
],
},
envVars: managedWorker.envVars,
secrets: {
add: [],
update: [],
delete: [],
},
isIac: managedWorker.isIac,
runtimeConfig:
!managedWorker.isIac && managedWorker.runtimeConfigs?.length == 1
@@ -193,6 +212,25 @@ export default function UpdateWorkerForm({
'1 CPU, 1 GB RAM (shared CPU)',
);
// Set initial machine type based on current worker configuration
useEffect(() => {
if (
managedWorker?.runtimeConfigs &&
managedWorker.runtimeConfigs.length > 0
) {
const config = managedWorker.runtimeConfigs[0];
const matchingType = machineTypes.find(
(m) =>
m.cpuKind === config.cpuKind &&
m.cpus === config.cpus &&
m.memoryMb === config.memoryMb,
);
if (matchingType) {
setMachineType(matchingType.title);
}
}
}, [managedWorker]);
const region = watch('runtimeConfig.regions');
const installation = watch('buildConfig.githubInstallationId');
const repoOwner = watch('buildConfig.githubRepositoryOwner');
@@ -212,8 +250,16 @@ export default function UpdateWorkerForm({
...queries.github.listBranches(tenantId, installation, repoOwner, repoName),
});
const [envVars, setEnvVars] = useState<KeyValueType[]>(
envVarsRecordToKeyValueType(managedWorker.envVars),
const [secrets, setSecrets] = useState<KeyValueType[]>(
managedWorker.directSecrets?.map((secret) => ({
key: secret.key,
value: secret.hint || '',
hidden: false,
locked: false,
deleted: false,
id: secret.id,
hint: secret.hint,
})) || [],
);
const [isIac, setIsIac] = useState(managedWorker.isIac);
@@ -234,8 +280,8 @@ export default function UpdateWorkerForm({
const numReplicasError =
errors.runtimeConfig?.numReplicas?.message?.toString() ||
fieldErrors?.numReplicas;
const envVarsError =
errors.envVars?.message?.toString() || fieldErrors?.envVars;
const secretsError =
errors.secrets?.add?.message?.toString() || fieldErrors?.secrets;
const cpuKindError =
errors.runtimeConfig?.cpuKind?.message?.toString() || fieldErrors?.cpuKind;
const cpusError =
@@ -325,6 +371,33 @@ export default function UpdateWorkerForm({
return can(managedCompute.selectCompute(computeType));
}, [can, machineType]);
// Update form values when secrets change
useEffect(() => {
// Split secrets into add/update/delete
const toAdd = secrets.filter((s) => !s.id && !s.deleted);
const toUpdate = secrets.filter((s) => s.id && !s.deleted);
const toDelete = secrets.filter((s) => s.id && s.deleted).map((s) => s.id!);
setValue(
'secrets.add',
toAdd.map((s) => ({
key: s.key,
value: s.value,
})),
);
setValue(
'secrets.update',
toUpdate.map((s) => ({
id: s.id!,
key: s.key,
value: s.value,
})),
);
setValue('secrets.delete', toDelete);
}, [secrets, setValue]);
// if there are no github accounts linked, ask the user to link one
if (
listInstallationsQuery.isSuccess &&
@@ -565,20 +638,21 @@ export default function UpdateWorkerForm({
</div>
<Label>Environment Variables</Label>
<EnvGroupArray
values={envVars}
setValues={(value) => {
setEnvVars(value);
setValue(
'envVars',
value.reduce<Record<string, string>>((acc, item) => {
acc[item.key] = item.value;
return acc;
}, {}),
);
values={secrets}
setValues={setSecrets}
onUpdate={(updatedSecrets) => {
// Update the secrets state with the new values
setSecrets((prev) => {
const newSecrets = prev.map((s) => {
const updated = updatedSecrets.find((u) => u.id === s.id);
return updated ? { ...s, ...updated } : s;
});
return newSecrets;
});
}}
/>
{envVarsError && (
<div className="text-sm text-red-500">{envVarsError}</div>
{secretsError && (
<div className="text-sm text-red-500">{secretsError}</div>
)}
<Label>Machine Configuration Method</Label>
<Tabs
@@ -1008,15 +1082,3 @@ export default function UpdateWorkerForm({
</>
);
}
function envVarsRecordToKeyValueType(
envVars: Record<string, string>,
): KeyValueType[] {
return Object.entries(envVars).map(([key, value]) => ({
key,
value,
hidden: false,
locked: false,
deleted: false,
}));
}
@@ -265,7 +265,14 @@ const createManagedWorkerSchema = z.object({
),
}),
isIac: z.boolean().default(false),
envVars: z.record(z.string()),
secrets: z.object({
add: z.array(
z.object({
key: z.string(),
value: z.string(),
}),
),
}),
runtimeConfig: z.object({
numReplicas: z.number().min(0).max(16).optional(),
cpuKind: z.string(),
@@ -323,7 +330,9 @@ export default function CreateWorkerForm({
},
],
},
envVars: {},
secrets: {
add: [],
},
runtimeConfig: {
numReplicas: 1,
cpuKind: 'shared',
@@ -411,7 +420,7 @@ export default function CreateWorkerForm({
return allowed;
}, [can, autoscalingMaxReplicas]);
const [envVars, setEnvVars] = useState<KeyValueType[]>([]);
const [secrets, setSecrets] = useState<KeyValueType[]>([]);
const [isIac, setIsIac] = useState(false);
const [scalingType, setScalingType] = useState<ScalingType>('Static');
@@ -425,8 +434,8 @@ export default function CreateWorkerForm({
const numReplicasError =
errors.runtimeConfig?.numReplicas?.message?.toString() ||
fieldErrors?.numReplicas;
const envVarsError =
errors.envVars?.message?.toString() || fieldErrors?.envVars;
const secretsError =
errors.secrets?.add?.message?.toString() || fieldErrors?.secrets;
const cpuKindError =
errors.runtimeConfig?.cpuKind?.message?.toString() || fieldErrors?.cpuKind;
const cpusError =
@@ -710,20 +719,20 @@ export default function CreateWorkerForm({
</div>
<Label>Environment Variables</Label>
<EnvGroupArray
values={envVars}
values={secrets}
setValues={(value) => {
setEnvVars(value);
setSecrets(value);
setValue(
'envVars',
value.reduce<Record<string, string>>((acc, item) => {
acc[item.key] = item.value;
return acc;
}, {}),
'secrets.add',
value.map((item) => ({
key: item.key,
value: item.value,
})),
);
}}
/>
{envVarsError && (
<div className="text-sm text-red-500">{envVarsError}</div>
{secretsError && (
<div className="text-sm text-red-500">{secretsError}</div>
)}
<Label>Machine Configuration Method</Label>
<Tabs
@@ -16,7 +16,6 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { Label } from '@/components/v1/ui/label';
import { Alert, AlertDescription, AlertTitle } from '@/components/v1/ui/alert';
import { Input } from '@/components/v1/ui/input';
import EnvGroupArray, { KeyValueType } from '@/components/v1/ui/envvar';
import {
getRepoName,
getRepoOwner,
@@ -49,6 +48,7 @@ import {
managedCompute,
} from '@/lib/can/features/managed-compute';
import { useTenant } from '@/lib/atoms';
import EnvGroupArray, { KeyValueType } from '@/components/v1/ui/envvar';
interface UpdateWorkerFormProps {
onSubmit: (opts: z.infer<typeof updateManagedWorkerSchema>) => void;
@@ -75,7 +75,22 @@ const updateManagedWorkerSchema = z.object({
})
.optional(),
isIac: z.boolean().default(false).optional(),
envVars: z.record(z.string()).optional(),
secrets: z.object({
add: z.array(
z.object({
key: z.string(),
value: z.string(),
}),
),
update: z.array(
z.object({
id: z.string(),
key: z.string(),
value: z.string(),
}),
),
delete: z.array(z.string()),
}),
runtimeConfig: z
.object({
numReplicas: z.number().min(0).max(16).optional(),
@@ -139,7 +154,11 @@ export default function UpdateWorkerForm({
},
],
},
envVars: managedWorker.envVars,
secrets: {
add: [],
update: [],
delete: [],
},
isIac: managedWorker.isIac,
runtimeConfig:
!managedWorker.isIac && managedWorker.runtimeConfigs?.length == 1
@@ -193,6 +212,25 @@ export default function UpdateWorkerForm({
'1 CPU, 1 GB RAM (shared CPU)',
);
// Set initial machine type based on current worker configuration
useEffect(() => {
if (
managedWorker?.runtimeConfigs &&
managedWorker.runtimeConfigs.length > 0
) {
const config = managedWorker.runtimeConfigs[0];
const matchingType = machineTypes.find(
(m) =>
m.cpuKind === config.cpuKind &&
m.cpus === config.cpus &&
m.memoryMb === config.memoryMb,
);
if (matchingType) {
setMachineType(matchingType.title);
}
}
}, [managedWorker]);
const region = watch('runtimeConfig.regions');
const installation = watch('buildConfig.githubInstallationId');
const repoOwner = watch('buildConfig.githubRepositoryOwner');
@@ -212,8 +250,16 @@ export default function UpdateWorkerForm({
...queries.github.listBranches(tenantId, installation, repoOwner, repoName),
});
const [envVars, setEnvVars] = useState<KeyValueType[]>(
envVarsRecordToKeyValueType(managedWorker.envVars),
const [secrets, setSecrets] = useState<KeyValueType[]>(
managedWorker.directSecrets?.map((secret) => ({
key: secret.key,
value: secret.hint || '',
hidden: false,
locked: false,
deleted: false,
id: secret.id,
hint: secret.hint,
})) || [],
);
const [isIac, setIsIac] = useState(managedWorker.isIac);
@@ -234,8 +280,8 @@ export default function UpdateWorkerForm({
const numReplicasError =
errors.runtimeConfig?.numReplicas?.message?.toString() ||
fieldErrors?.numReplicas;
const envVarsError =
errors.envVars?.message?.toString() || fieldErrors?.envVars;
const secretsError =
errors.secrets?.add?.message?.toString() || fieldErrors?.secrets;
const cpuKindError =
errors.runtimeConfig?.cpuKind?.message?.toString() || fieldErrors?.cpuKind;
const cpusError =
@@ -325,6 +371,33 @@ export default function UpdateWorkerForm({
return can(managedCompute.selectCompute(computeType));
}, [can, machineType]);
// Update form values when secrets change
useEffect(() => {
// Split secrets into add/update/delete
const toAdd = secrets.filter((s) => !s.id && !s.deleted);
const toUpdate = secrets.filter((s) => s.id && !s.deleted);
const toDelete = secrets.filter((s) => s.id && s.deleted).map((s) => s.id!);
setValue(
'secrets.add',
toAdd.map((s) => ({
key: s.key,
value: s.value,
})),
);
setValue(
'secrets.update',
toUpdate.map((s) => ({
id: s.id!,
key: s.key,
value: s.value,
})),
);
setValue('secrets.delete', toDelete);
}, [secrets, setValue]);
// if there are no github accounts linked, ask the user to link one
if (
listInstallationsQuery.isSuccess &&
@@ -565,20 +638,21 @@ export default function UpdateWorkerForm({
</div>
<Label>Environment Variables</Label>
<EnvGroupArray
values={envVars}
setValues={(value) => {
setEnvVars(value);
setValue(
'envVars',
value.reduce<Record<string, string>>((acc, item) => {
acc[item.key] = item.value;
return acc;
}, {}),
);
values={secrets}
setValues={setSecrets}
onUpdate={(updatedSecrets) => {
// Update the secrets state with the new values
setSecrets((prev) => {
const newSecrets = prev.map((s) => {
const updated = updatedSecrets.find((u) => u.id === s.id);
return updated ? { ...s, ...updated } : s;
});
return newSecrets;
});
}}
/>
{envVarsError && (
<div className="text-sm text-red-500">{envVarsError}</div>
{secretsError && (
<div className="text-sm text-red-500">{secretsError}</div>
)}
<Label>Machine Configuration Method</Label>
<Tabs
@@ -1008,15 +1082,3 @@ export default function UpdateWorkerForm({
</>
);
}
function envVarsRecordToKeyValueType(
envVars: Record<string, string>,
): KeyValueType[] {
return Object.entries(envVars).map(([key, value]) => ({
key,
value,
hidden: false,
locked: false,
deleted: false,
}));
}
@@ -265,7 +265,14 @@ const createManagedWorkerSchema = z.object({
),
}),
isIac: z.boolean().default(false),
envVars: z.record(z.string()),
secrets: z.object({
add: z.array(
z.object({
key: z.string(),
value: z.string(),
}),
),
}),
runtimeConfig: z.object({
numReplicas: z.number().min(0).max(16).optional(),
cpuKind: z.string(),
@@ -323,7 +330,9 @@ export default function CreateWorkerForm({
},
],
},
envVars: {},
secrets: {
add: [],
},
runtimeConfig: {
numReplicas: 1,
cpuKind: 'shared',
@@ -411,7 +420,7 @@ export default function CreateWorkerForm({
return allowed;
}, [can, autoscalingMaxReplicas]);
const [envVars, setEnvVars] = useState<KeyValueType[]>([]);
const [secrets, setSecrets] = useState<KeyValueType[]>([]);
const [isIac, setIsIac] = useState(false);
const [scalingType, setScalingType] = useState<ScalingType>('Static');
@@ -425,8 +434,8 @@ export default function CreateWorkerForm({
const numReplicasError =
errors.runtimeConfig?.numReplicas?.message?.toString() ||
fieldErrors?.numReplicas;
const envVarsError =
errors.envVars?.message?.toString() || fieldErrors?.envVars;
const secretsError =
errors.secrets?.add?.message?.toString() || fieldErrors?.secrets;
const cpuKindError =
errors.runtimeConfig?.cpuKind?.message?.toString() || fieldErrors?.cpuKind;
const cpusError =
@@ -710,20 +719,20 @@ export default function CreateWorkerForm({
</div>
<Label>Environment Variables</Label>
<EnvGroupArray
values={envVars}
values={secrets}
setValues={(value) => {
setEnvVars(value);
setSecrets(value);
setValue(
'envVars',
value.reduce<Record<string, string>>((acc, item) => {
acc[item.key] = item.value;
return acc;
}, {}),
'secrets.add',
value.map((item) => ({
key: item.key,
value: item.value,
})),
);
}}
/>
{envVarsError && (
<div className="text-sm text-red-500">{envVarsError}</div>
{secretsError && (
<div className="text-sm text-red-500">{secretsError}</div>
)}
<Label>Machine Configuration Method</Label>
<Tabs
@@ -1290,7 +1299,7 @@ export function getRepoName(repoOwnerName?: string) {
// URL for the billing portal to upgrade
const getBillingPortalUrl = () => {
// Replace with your actual billing portal URL or API call
return '/v1/tenant-settings/billing-and-limits';
return '/tenant-settings/billing-and-limits';
};
export const UpgradeMessage = ({ feature }: { feature: string }) => (
+2 -2
View File
@@ -486,7 +486,7 @@ func createControllerLayer(dc *database.Layer, cf *server.ServerConfigFile, vers
})
}
encryptionSvc, err := loadEncryptionSvc(cf)
encryptionSvc, err := LoadEncryptionSvc(cf)
if err != nil {
return nil, nil, fmt.Errorf("could not load encryption service: %w", err)
@@ -618,7 +618,7 @@ func getStrArr(v string) []string {
return strings.Split(v, " ")
}
func loadEncryptionSvc(cf *server.ServerConfigFile) (encryption.EncryptionService, error) {
func LoadEncryptionSvc(cf *server.ServerConfigFile) (encryption.EncryptionService, error) {
var err error
hasLocalMasterKeyset := cf.Encryption.MasterKeyset != "" || cf.Encryption.MasterKeysetFile != ""