From 07b4731c10377b81f9e3d721c25eec6a33ef077f Mon Sep 17 00:00:00 2001 From: Vlad Vladov Date: Wed, 3 Sep 2025 21:54:05 +0300 Subject: [PATCH 1/4] feat(mount): Add permissions to volumes mount --- .../advanced/volumes/add-volumes.tsx | 124 ++++++++++- .../advanced/volumes/update-volume.tsx | 118 ++++++++++- .../components/shared/permission-mode.tsx | 195 ++++++++++++++++++ packages/server/src/db/schema/mount.ts | 34 ++- packages/server/src/services/mount.ts | 75 +++++++ 5 files changed, 522 insertions(+), 24 deletions(-) create mode 100644 apps/dokploy/components/shared/permission-mode.tsx diff --git a/apps/dokploy/components/dashboard/application/advanced/volumes/add-volumes.tsx b/apps/dokploy/components/dashboard/application/advanced/volumes/add-volumes.tsx index 06d1983e14..4c61d19396 100644 --- a/apps/dokploy/components/dashboard/application/advanced/volumes/add-volumes.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/volumes/add-volumes.tsx @@ -7,6 +7,7 @@ import { toast } from "sonner"; import { z } from "zod"; import { AlertBlock } from "@/components/shared/alert-block"; import { CodeEditor } from "@/components/shared/code-editor"; +import { PermissionMode } from "@/components/shared/permission-mode"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -26,6 +27,7 @@ import { } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { Badge } from "@/components/ui/badge"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; @@ -49,13 +51,35 @@ const mountSchema = z.object({ mountPath: z.string().min(1, "Mount path required"), }); +const uidSchema = z.preprocess( + (v) => (v === "" || v === null ? undefined : v), + z.coerce.number().int().positive().optional(), +); +const gidSchema = z.preprocess( + (v) => (v === "" || v === null ? undefined : v), + z.coerce.number().int().positive().optional(), +); +const modeSchema = z.preprocess( + (v) => (v === "" || v === null ? undefined : v), + z + .string() + .regex(/^[0-7]{3,4}$/, "Use octal digits like 644 or 0775") + .optional(), +); +const ownershipSchema = z.object({ + uid: uidSchema, + gid: gidSchema, + mode: modeSchema, +}); + const mySchema = z.discriminatedUnion("type", [ z .object({ type: z.literal("bind"), hostPath: z.string().min(1, "Host path required"), }) - .merge(mountSchema), + .merge(mountSchema) + .merge(ownershipSchema), z .object({ type: z.literal("volume"), @@ -67,14 +91,16 @@ const mySchema = z.discriminatedUnion("type", [ "Invalid volume name. Use letters, numbers, '._-' and start with a letter/number.", ), }) - .merge(mountSchema), + .merge(mountSchema) + .merge(ownershipSchema), z .object({ type: z.literal("file"), filePath: z.string().min(1, "File path required"), content: z.string().optional(), }) - .merge(mountSchema), + .merge(mountSchema) + .merge(ownershipSchema), ]); type AddMount = z.infer; @@ -92,6 +118,9 @@ export const AddVolumes = ({ type: serviceType === "compose" ? "file" : "bind", hostPath: "", mountPath: serviceType === "compose" ? "/" : "", + uid: undefined, + gid: undefined, + mode: "", }, resolver: zodResolver(mySchema), }); @@ -109,6 +138,9 @@ export const AddVolumes = ({ mountPath: data.mountPath, type: data.type, serviceType, + uid: data.uid, + gid: data.gid, + mode: data.mode, }) .then(() => { toast.success("Mount Created"); @@ -124,6 +156,9 @@ export const AddVolumes = ({ mountPath: data.mountPath, type: data.type, serviceType, + uid: data.uid, + gid: data.gid, + mode: data.mode, }) .then(() => { toast.success("Mount Created"); @@ -140,6 +175,9 @@ export const AddVolumes = ({ filePath: data.filePath, type: data.type, serviceType, + uid: data.uid, + gid: data.gid, + mode: data.mode, }) .then(() => { toast.success("Mount Created"); @@ -163,13 +201,13 @@ export const AddVolumes = ({ Volumes / Mounts {/* {isError && ( -
- - - {error?.message} - -
- )} */} +
+ + + {error?.message} + +
+ )} */}
)} + +
+ + Ownership / Permissions (optional) + +

+ If unset, mounts remain root-owned with default permissions. +

+
+ ( + + UID + + + + + + )} + /> + ( + + GID + + + + + + )} + /> + ( + + + Mode + + {(() => { + const v = field.value ?? ""; + if (!/^\d{3,4}$/.test(v)) return "644"; + if (v.length === 4 && v[0] === "0") + return v.slice(1); + return v.slice(-3); + })()} + + + + + field.onChange(v)} + showAdvancedInput={false} + /> + + + + )} + /> +
+
diff --git a/apps/dokploy/components/dashboard/application/advanced/volumes/update-volume.tsx b/apps/dokploy/components/dashboard/application/advanced/volumes/update-volume.tsx index 246a25da24..429c558c65 100644 --- a/apps/dokploy/components/dashboard/application/advanced/volumes/update-volume.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/volumes/update-volume.tsx @@ -25,19 +25,43 @@ import { FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; +import { Badge } from "@/components/ui/badge"; import { api } from "@/utils/api"; +import { PermissionMode } from "@/components/shared/permission-mode"; const mountSchema = z.object({ mountPath: z.string().min(1, "Mount path required"), }); +const uidSchema = z.preprocess( + (v) => (v === "" || v === null ? undefined : v), + z.coerce.number().int().positive().optional(), +); +const gidSchema = z.preprocess( + (v) => (v === "" || v === null ? undefined : v), + z.coerce.number().int().positive().optional(), +); +const modeSchema = z.preprocess( + (v) => (v === "" || v === null ? undefined : v), + z + .string() + .regex(/^[0-7]{3,4}$/, "Use octal digits like 644 or 0775") + .optional(), +); +const ownershipSchema = z.object({ + uid: uidSchema, + gid: gidSchema, + mode: modeSchema, +}); + const mySchema = z.discriminatedUnion("type", [ z .object({ type: z.literal("bind"), hostPath: z.string().min(1, "Host path required"), }) - .merge(mountSchema), + .merge(mountSchema) + .merge(ownershipSchema), z .object({ type: z.literal("volume"), @@ -49,14 +73,16 @@ const mySchema = z.discriminatedUnion("type", [ "Invalid volume name. Use letters, numbers, '._-' and start with a letter/number.", ), }) - .merge(mountSchema), + .merge(mountSchema) + .merge(ownershipSchema), z .object({ type: z.literal("file"), content: z.string().optional(), filePath: z.string().min(1, "File path required"), }) - .merge(mountSchema), + .merge(mountSchema) + .merge(ownershipSchema), ]); type UpdateMount = z.infer; @@ -101,6 +127,9 @@ export const UpdateVolume = ({ type, hostPath: "", mountPath: "", + uid: undefined, + gid: undefined, + mode: "", }, resolver: zodResolver(mySchema), }); @@ -114,12 +143,18 @@ export const UpdateVolume = ({ hostPath: data.hostPath || "", mountPath: data.mountPath, type: "bind", + uid: data.uid ?? undefined, + gid: data.gid ?? undefined, + mode: data.mode ?? "", }); } else if (typeForm === "volume") { form.reset({ volumeName: data.volumeName || "", mountPath: data.mountPath, type: "volume", + uid: data.uid ?? undefined, + gid: data.gid ?? undefined, + mode: data.mode ?? "", }); } else if (typeForm === "file") { form.reset({ @@ -127,6 +162,9 @@ export const UpdateVolume = ({ mountPath: serviceType === "compose" ? "/" : data.mountPath, filePath: data.filePath || "", type: "file", + uid: data.uid ?? undefined, + gid: data.gid ?? undefined, + mode: data.mode ?? "", }); } } @@ -139,6 +177,9 @@ export const UpdateVolume = ({ mountPath: data.mountPath, type: data.type, mountId, + uid: data.uid, + gid: data.gid, + mode: data.mode, }) .then(() => { toast.success("Mount Update"); @@ -153,6 +194,9 @@ export const UpdateVolume = ({ mountPath: data.mountPath, type: data.type, mountId, + uid: data.uid, + gid: data.gid, + mode: data.mode, }) .then(() => { toast.success("Mount Update"); @@ -168,6 +212,9 @@ export const UpdateVolume = ({ type: data.type, filePath: data.filePath, mountId, + uid: data.uid, + gid: data.gid, + mode: data.mode, }) .then(() => { toast.success("Mount Update"); @@ -307,6 +354,71 @@ PORT=3000 )} /> )} + +
+ + Ownership / Permissions (optional) + +

+ If unset, mounts remain root-owned with default permissions. +

+
+ ( + + UID + + + + + + )} + /> + ( + + GID + + + + + + )} + /> + ( + + + Mode + + {(() => { + const v = field.value ?? ""; + if (!/^\d{3,4}$/.test(v)) return "644"; + if (v.length === 4 && v[0] === "0") + return v.slice(1); + return v.slice(-3); + })()} + + + + field.onChange(v)} + showAdvancedInput={false} + /> + + + + )} + /> +
+