diff --git a/prisma/migrations/20260314233637_add_privileged_container_mode/migration.sql b/prisma/migrations/20260314233637_add_privileged_container_mode/migration.sql new file mode 100644 index 0000000..837741e --- /dev/null +++ b/prisma/migrations/20260314233637_add_privileged_container_mode/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "App" ADD COLUMN "securityContextPrivileged" BOOLEAN DEFAULT false; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 73e7bb6..576e7c1 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -188,6 +188,7 @@ model App { securityContextRunAsUser Int? securityContextRunAsGroup Int? securityContextFsGroup Int? + securityContextPrivileged Boolean? @default(false) gitUrl String? gitBranch String? diff --git a/src/app/project/app/[appId]/general/actions.ts b/src/app/project/app/[appId]/general/actions.ts index e1f2260..a33f3b0 100644 --- a/src/app/project/app/[appId]/general/actions.ts +++ b/src/app/project/app/[appId]/general/actions.ts @@ -75,6 +75,7 @@ export const saveGeneralAppContainerConfig = async (prevState: any, inputData: A securityContextRunAsUser: validatedData.securityContextRunAsUser ?? null, securityContextRunAsGroup: validatedData.securityContextRunAsGroup ?? null, securityContextFsGroup: validatedData.securityContextFsGroup ?? null, + securityContextPrivileged: validatedData.securityContextPrivileged ?? false, id: appId, }); }); diff --git a/src/app/project/app/[appId]/general/app-container-config.tsx b/src/app/project/app/[appId]/general/app-container-config.tsx index a9e4428..7aa4cb4 100644 --- a/src/app/project/app/[appId]/general/app-container-config.tsx +++ b/src/app/project/app/[appId]/general/app-container-config.tsx @@ -3,7 +3,8 @@ import { SubmitButton } from "@/components/custom/submit-button"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; -import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; import { FormUtils } from "@/frontend/utils/form.utilts"; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm, useFieldArray } from "react-hook-form"; @@ -11,16 +12,44 @@ import { saveGeneralAppContainerConfig } from "./actions"; import { useFormState } from "react-dom"; import { ServerActionResult } from "@/shared/model/server-action-error-return.model"; import { Input } from "@/components/ui/input"; -import { useEffect } from "react"; +import { Separator } from "@/components/ui/separator"; +import { Switch } from "@/components/ui/switch"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; +import { type ReactNode, useEffect } from "react"; import { toast } from "sonner"; import { AppExtendedModel } from "@/shared/model/app-extended.model"; -import { Trash2, Plus } from "lucide-react"; +import { HelpCircle, Plus, Trash2 } from "lucide-react"; import { z } from "zod"; import { appContainerConfigZodModel } from "@/shared/model/app-container-config.model"; -import FormLabelWithQuestion from "@/components/custom/form-label-with-question"; export type AppContainerConfigInputModel = z.infer; +function LabelWithHint({ children, hint }: { children: ReactNode; hint?: ReactNode }) { + return ( +
+ {children} + {hint && ( + + + + + +
{hint}
+
+
+ )} +
+ ); +} + export default function GeneralAppContainerConfig({ app, readonly }: { app: AppExtendedModel; readonly: boolean; @@ -38,6 +67,7 @@ export default function GeneralAppContainerConfig({ app, readonly }: { securityContextRunAsUser: app.securityContextRunAsUser ?? undefined, securityContextRunAsGroup: app.securityContextRunAsGroup ?? undefined, securityContextFsGroup: app.securityContextFsGroup ?? undefined, + securityContextPrivileged: app.securityContextPrivileged ?? false, }, disabled: readonly, }); @@ -62,173 +92,238 @@ export default function GeneralAppContainerConfig({ app, readonly }: { FormUtils.mapValidationErrorsToForm(state, form) }, [state]); + const values = form.watch(); + return ( Container Configuration - Override the container's command and arguments. Leave empty to use the image defaults. + Override image defaults only when your workload needs custom startup behavior or Linux security settings.
- form.handleSubmit((data) => { - return formAction(data); - })()}> - - ( - - Command (optional) - - - - - Override the container's ENTRYPOINT. - - - - )} - /> - -
- Arguments (optional) - - Override the container's CMD. Each argument should be a separate item. - - -
- {fields.map((field, index) => ( -
- ( - - - - - - - )} - /> - -
- ))} -
+ + form.handleSubmit((data) => { + return formAction(data); + })()}> + +
+
+

Runtime

+

+ Leave these fields empty to keep the command and arguments from the container image. +

+
- {!readonly && ( - - )} -
- -
-
-

Security Context (optional)

-

- Use this when your app requires specific user/group permissions or needs to run with a specific filesystem group for volume access. -

-
-
( - - Run As User - + + Command + field.onChange(e.target.value === '' ? null : Number(e.target.value))} + value={field.value as string | number | readonly string[] | undefined} /> )} /> - ( - - - Run As Group - - - field.onChange(e.target.value === '' ? null : Number(e.target.value))} + +
+ + Arguments + + +
+ {fields.length === 0 && ( +
+ No arguments configured. +
+ )} + + {fields.map((field, index) => ( +
+ ( + + + + + + + )} /> - - - + +
+ ))} +
+ + {!readonly && ( + )} - /> +
+
+ + + +
+
+

Security Context

+

+ Change these values only when your image, mounted volumes, or tooling require specific Linux permissions. +

+
+ +
+ ( + + + Run As User + + + field.onChange(e.target.value === '' ? null : Number(e.target.value))} + /> + + + + )} + /> + ( + + + Run As Group + + + field.onChange(e.target.value === '' ? null : Number(e.target.value))} + /> + + + + )} + /> + ( + + + FS Group + + + field.onChange(e.target.value === '' ? null : Number(e.target.value))} + /> + + + + )} + /> +
+ ( - - - FS Group - - - field.onChange(e.target.value === '' ? null : Number(e.target.value))} - /> - + +
+ + + +
+ +

+ Removes most container isolation. The container gets all Linux capabilities, + access to host devices, and can interact with the node almost like a root + process on the host. +

+

+ If the container is compromised, it can affect the Kubernetes node and + other workloads. Use this only for workloads such as Docker-in-Docker + or low-level system tooling. +

+ + )} + > + Privileged Mode +
+ + {values.securityContextPrivileged && + + Enable this only if you fully understand the implications and risks. + + } +
+ +
)} />
-
-
- {!readonly && ( - - Save -

{state?.message}

-
- )} - + + {!readonly && ( + + Save +

{state?.message}

+
+ )} + +
); diff --git a/src/server/services/deployment.service.ts b/src/server/services/deployment.service.ts index 45b1501..801d174 100644 --- a/src/server/services/deployment.service.ts +++ b/src/server/services/deployment.service.ts @@ -139,6 +139,7 @@ class DeploymentService { imagePullPolicy: 'Always', ...(app.containerCommand ? { command: [app.containerCommand] } : {}), ...(app.containerArgs ? { args: JSON.parse(app.containerArgs) } : {}), + ...(app.securityContextPrivileged ? { securityContext: { privileged: true } } : {}), ...(envVars.length > 0 ? { env: envVars } : {}), ...(allVolumeMounts.length > 0 ? { volumeMounts: allVolumeMounts } : {}), } diff --git a/src/shared/model/app-container-config.model.ts b/src/shared/model/app-container-config.model.ts index 72d7d71..08cda88 100644 --- a/src/shared/model/app-container-config.model.ts +++ b/src/shared/model/app-container-config.model.ts @@ -9,6 +9,7 @@ export const appContainerConfigZodModel = z.object({ securityContextRunAsUser: stringToOptionalNumber, securityContextRunAsGroup: stringToOptionalNumber, securityContextFsGroup: stringToOptionalNumber, + securityContextPrivileged: z.boolean().default(false), }); export type AppContainerConfigModel = z.infer; \ No newline at end of file diff --git a/src/shared/model/generated-zod/app.ts b/src/shared/model/generated-zod/app.ts index c2df344..76ea068 100644 --- a/src/shared/model/generated-zod/app.ts +++ b/src/shared/model/generated-zod/app.ts @@ -16,6 +16,7 @@ export const AppModel = z.object({ securityContextRunAsUser: z.number().int().nullish(), securityContextRunAsGroup: z.number().int().nullish(), securityContextFsGroup: z.number().int().nullish(), + securityContextPrivileged: z.boolean().nullish(), gitUrl: z.string().nullish(), gitBranch: z.string().nullish(), gitUsername: z.string().nullish(),