feat: allow k8 dynamic secret multi namespace and show proper error

This commit is contained in:
Sheen Capadngan
2025-06-11 01:11:29 +08:00
parent 3ee8f7aa20
commit f7fb015bd8
15 changed files with 534 additions and 142 deletions
@@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { DynamicSecretProviders } from "../dynamicSecret/types";
import { dynamicSecretLeaseKeys } from "./queries";
import {
TCreateDynamicSecretLeaseDTO,
@@ -19,6 +20,14 @@ export const useCreateDynamicSecretLease = () => {
TCreateDynamicSecretLeaseDTO
>({
mutationFn: async (dto) => {
if (dto.provider === DynamicSecretProviders.Kubernetes) {
const { data } = await apiRequest.post<{ lease: TDynamicSecretLease; data: unknown }>(
"/api/v1/dynamic-secrets/leases/kubernetes",
dto
);
return data;
}
const { data } = await apiRequest.post<{ lease: TDynamicSecretLease; data: unknown }>(
"/api/v1/dynamic-secrets/leases",
dto
@@ -1,3 +1,5 @@
import { DynamicSecretProviders } from "../dynamicSecret/types";
export enum DynamicSecretLeaseStatus {
FailedDeletion = "Failed to delete"
}
@@ -13,12 +15,20 @@ export type TDynamicSecretLease = {
updatedAt: string;
};
export type TDynamicSecretKubernetesLeaseConfig = {
namespace?: string;
};
export type TDynamicSecretLeaseConfig = TDynamicSecretKubernetesLeaseConfig;
export type TCreateDynamicSecretLeaseDTO = {
dynamicSecretName: string;
projectSlug: string;
ttl?: string;
path: string;
environmentSlug: string;
config?: TDynamicSecretLeaseConfig;
provider: DynamicSecretProviders;
};
export type TRenewDynamicSecretLeaseDTO = {
@@ -67,7 +67,14 @@ const formSchema = z
sslEnabled: z.boolean().default(false),
credentialType: z.literal(KubernetesDynamicSecretCredentialType.Static),
serviceAccountName: z.string().trim().min(1),
namespace: z.string().trim().min(1),
namespace: z
.string()
.trim()
.min(1)
.refine(
(val) => !val.includes(","),
"Namespace must be a single value, not a comma-separated list"
),
gatewayId: z.string().optional(),
audiences: z.array(z.string().trim().min(1)),
authMethod: z.nativeEnum(AuthMethod).default(AuthMethod.Api)
@@ -78,7 +85,14 @@ const formSchema = z
ca: z.string().optional(),
sslEnabled: z.boolean().default(false),
credentialType: z.literal(KubernetesDynamicSecretCredentialType.Dynamic),
namespace: z.string().trim().min(1),
namespace: z
.string()
.trim()
.min(1)
.refine((val) => {
const namespaces = val.split(",").map((ns) => ns.trim());
return namespaces.length > 0 && namespaces.every((ns) => ns.length > 0);
}, "Must be a valid comma-separated list of namespace values"),
gatewayId: z.string().optional(),
audiences: z.array(z.string().trim().min(1)),
roleType: z.nativeEnum(RoleType),
@@ -507,7 +521,11 @@ export const KubernetesInputForm = ({
name="provider.namespace"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Namespace"
label={
credentialType === KubernetesDynamicSecretCredentialType.Static
? "Namespace"
: "Allowed Namespace(s)"
}
isError={Boolean(error?.message)}
errorText={error?.message}
>
@@ -353,12 +353,149 @@ const renderOutputForm = (
return null;
};
const kubernetesFormSchema = z.object({
ttl: z
.string()
.refine((val) => ms(val) > 0, "TTL must be a positive number")
.optional(),
namespace: z.string().optional()
});
type TKubernetesForm = z.infer<typeof kubernetesFormSchema>;
export const CreateKubernetesDynamicSecretLease = ({
onClose,
projectSlug,
dynamicSecretName,
provider,
secretPath,
environment
}: Props) => {
const {
control,
formState: { isSubmitting },
handleSubmit
} = useForm<TKubernetesForm>({
resolver: zodResolver(kubernetesFormSchema),
defaultValues: {
ttl: "1h"
}
});
const createDynamicSecretLease = useCreateDynamicSecretLease();
const handleDynamicSecretLeaseCreate = async ({ ttl, namespace }: TKubernetesForm) => {
if (createDynamicSecretLease.isPending) return;
try {
await createDynamicSecretLease.mutateAsync({
environmentSlug: environment,
projectSlug,
path: secretPath,
ttl,
dynamicSecretName,
config: {
namespace: namespace || undefined
},
provider
});
createNotification({
type: "success",
text: "Successfully leased dynamic secret"
});
} catch (error) {
console.log(error);
createNotification({
type: "error",
text: "Failed to lease dynamic secret"
});
}
};
const handleLeaseRegeneration = async (data: { ttl?: string }) => {
handleDynamicSecretLeaseCreate(data);
};
const isOutputMode = Boolean(createDynamicSecretLease?.data);
return (
<div>
<AnimatePresence>
{!isOutputMode && (
<motion.div
key="lease-input"
transition={{ duration: 0.1 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<form onSubmit={handleSubmit(handleDynamicSecretLeaseCreate)}>
<Controller
control={control}
name="namespace"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Namespace"
isError={Boolean(error?.message)}
errorText={error?.message}
helperText="The Kubernetes namespace to lease the dynamic secret to. If not specified, the first namespace defined in the configuration will be used."
>
<Input {...field} />
</FormControl>
)}
/>
<Controller
control={control}
name="ttl"
defaultValue="1h"
render={({ field, fieldState: { error } }) => (
<FormControl
label={<TtlFormLabel label="Default TTL" />}
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
<div className="mt-4 flex items-center space-x-4">
<Button type="submit" isLoading={isSubmitting}>
Submit
</Button>
<Button variant="outline_bg" onClick={onClose}>
Cancel
</Button>
</div>
</form>
</motion.div>
)}
{isOutputMode && (
<motion.div
key="lease-output"
transition={{ duration: 0.1 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
{renderOutputForm(
provider,
createDynamicSecretLease.data?.data,
handleLeaseRegeneration
)}
</motion.div>
)}
</AnimatePresence>
</div>
);
};
const formSchema = z.object({
ttl: z
.string()
.refine((val) => ms(val) > 0, "TTL must be a positive number")
.optional()
});
type TForm = z.infer<typeof formSchema>;
type Props = {
@@ -404,7 +541,8 @@ export const CreateDynamicSecretLease = ({
projectSlug,
path: secretPath,
ttl,
dynamicSecretName
dynamicSecretName,
provider
});
createNotification({
@@ -433,6 +571,19 @@ export const CreateDynamicSecretLease = ({
}
}, [provider]);
if (provider === DynamicSecretProviders.Kubernetes) {
return (
<CreateKubernetesDynamicSecretLease
onClose={onClose}
projectSlug={projectSlug}
dynamicSecretName={dynamicSecretName}
provider={provider}
secretPath={secretPath}
environment={environment}
/>
);
}
const isOutputMode = Boolean(createDynamicSecretLease?.data);
if (isPreloading) {
@@ -65,7 +65,14 @@ const formSchema = z
sslEnabled: z.boolean().default(false),
credentialType: z.literal(KubernetesDynamicSecretCredentialType.Static),
serviceAccountName: z.string().trim().min(1),
namespace: z.string().trim().min(1),
namespace: z
.string()
.trim()
.min(1)
.refine(
(val) => !val.includes(","),
"Namespace must be a single value, not a comma-separated list"
),
gatewayId: z.string().optional(),
audiences: z.array(z.string().trim().min(1)),
authMethod: z.nativeEnum(AuthMethod).default(AuthMethod.Api)
@@ -76,7 +83,14 @@ const formSchema = z
ca: z.string().optional(),
sslEnabled: z.boolean().default(false),
credentialType: z.literal(KubernetesDynamicSecretCredentialType.Dynamic),
namespace: z.string().trim().min(1),
namespace: z
.string()
.trim()
.min(1)
.refine((val) => {
const namespaces = val.split(",").map((ns) => ns.trim());
return namespaces.length > 0 && namespaces.every((ns) => ns.length > 0);
}, "Must be a valid comma-separated list of namespace values"),
gatewayId: z.string().optional(),
audiences: z.array(z.string().trim().min(1)),
roleType: z.nativeEnum(RoleType),
@@ -502,7 +516,11 @@ export const EditDynamicSecretKubernetesForm = ({
name="inputs.namespace"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Namespace"
label={
credentialType === KubernetesDynamicSecretCredentialType.Static
? "Namespace"
: "Allowed Namespace(s)"
}
isError={Boolean(error?.message)}
errorText={error?.message}
>