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
@@ -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}
>