Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 154 additions & 0 deletions apps/dokploy/__test__/server/resource-schema.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import {
apiUpdateApplication,
apiUpdateLibsql,
apiUpdateMariaDB,
apiUpdateMongo,
apiUpdateMySql,
apiUpdatePostgres,
apiUpdateRedis,
} from "@dokploy/server/db/schema";
import {
resourceBytesSchema,
resourceNanoCpusSchema,
} from "@dokploy/server/db/schema/shared";
import { calculateResources } from "@dokploy/server/utils/docker/utils";
import { describe, expect, it } from "vitest";

const messagesOf = (result: { success: boolean; error?: unknown }) =>
result.success
? []
: (result.error as { issues: { message: string }[] }).issues.map(
(issue) => issue.message,
);

describe("resourceBytesSchema", () => {
it.each([undefined, null, "", "0", "00", "6291456", "1879048192"])(
"accepts %j",
(value) => {
expect(resourceBytesSchema.safeParse(value).success).toBe(true);
},
);

it.each(["1792MB", "1.5", "2e9", "-1", " 1073741824", "1GB"])(
"refuses %j as not a whole number of bytes",
(value) => {
expect(messagesOf(resourceBytesSchema.safeParse(value))).toEqual([
"Must be a whole number of bytes",
]);
},
);

it.each(["1792", "6291455"])("refuses %j as below 6 MiB", (value) => {
expect(messagesOf(resourceBytesSchema.safeParse(value))).toEqual([
"Must be at least 6291456 bytes",
]);
});
});

describe("resourceNanoCpusSchema", () => {
it.each([undefined, null, "", "0", "00", "1000000", "1500000000"])(
"accepts %j",
(value) => {
expect(resourceNanoCpusSchema.safeParse(value).success).toBe(true);
},
);

it.each(["1.5", "2e9", "1792MB", "-1"])(
"refuses %j as not a whole number of nanoCPUs",
(value) => {
expect(messagesOf(resourceNanoCpusSchema.safeParse(value))).toEqual([
"Must be a whole number of nanoCPUs (1000000000 = 1 CPU)",
]);
},
);

it.each(["1", "999999"])("refuses %j as below 0.001 CPU", (value) => {
expect(messagesOf(resourceNanoCpusSchema.safeParse(value))).toEqual([
"Must be at least 1000000 nanoCPUs (1000000000 = 1 CPU)",
]);
});
});

describe("update schemas validate resource values", () => {
it.each([
["application", apiUpdateApplication, { applicationId: "id" }],
["libsql", apiUpdateLibsql, { libsqlId: "id" }],
["mariadb", apiUpdateMariaDB, { mariadbId: "id" }],
["mongo", apiUpdateMongo, { mongoId: "id" }],
["mysql", apiUpdateMySql, { mysqlId: "id" }],
["postgres", apiUpdatePostgres, { postgresId: "id" }],
["redis", apiUpdateRedis, { redisId: "id" }],
] as const)("%s", (_, schema, ids) => {
const parse = (input: Record<string, unknown>) =>
schema.safeParse({ ...ids, ...input }).success;

expect(parse({ memoryLimit: "1792MB" })).toBe(false);
expect(parse({ memoryReservation: "1792MB" })).toBe(false);
expect(parse({ cpuLimit: "1.5" })).toBe(false);
expect(parse({ cpuReservation: "1.5" })).toBe(false);

expect(
parse({
memoryLimit: "1879048192",
memoryReservation: "268435456",
cpuLimit: "1500000000",
cpuReservation: "250000000",
}),
).toBe(true);
expect(
parse({
memoryLimit: null,
memoryReservation: null,
cpuLimit: null,
cpuReservation: null,
}),
).toBe(true);
expect(parse({})).toBe(true);
});

it("reports the field and unit for the application", () => {
const result = apiUpdateApplication.safeParse({
applicationId: "id",
memoryLimit: "1792MB",
});
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues).toMatchObject([
{
path: ["memoryLimit"],
message: "Must be a whole number of bytes",
},
]);
}
});
});

describe("calculateResources", () => {
it("leaves empty values unset", () => {
expect(
calculateResources({
memoryLimit: null,
memoryReservation: "",
cpuLimit: null,
cpuReservation: "",
}),
).toEqual({
Limits: { MemoryBytes: undefined, NanoCPUs: undefined },
Reservations: { MemoryBytes: undefined, NanoCPUs: undefined },
});
});

it("converts digit strings to numbers", () => {
expect(
calculateResources({
memoryLimit: "1879048192",
memoryReservation: "268435456",
cpuLimit: "1500000000",
cpuReservation: "250000000",
}),
).toEqual({
Limits: { MemoryBytes: 1879048192, NanoCPUs: 1500000000 },
Reservations: { MemoryBytes: 268435456, NanoCPUs: 250000000 },
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,31 @@ const ulimitSchema = z.object({
Hard: z.coerce.number().int().min(-1, "Must be >= -1"),
});

// Same rules as the API: empty or zero means no limit, otherwise a whole
// number no lower than Docker's minimum (6 MiB of memory, 0.001 CPU).
const resourceValueSchema = (minimum: number, unit: string) =>
z
.string()
.regex(/^\d*$/, {
message: `Must be a whole number of ${unit}`,
abort: true,
})
.refine((value) => /^0*$/.test(value) || Number(value) >= minimum, {
message: `Must be at least ${minimum} ${unit}`,
})
.optional();

const memorySchema = resourceValueSchema(6 * 1024 * 1024, "bytes");
const cpuSchema = resourceValueSchema(
1_000_000,
"nanoCPUs (1000000000 = 1 CPU)",
);

const addResourcesSchema = z.object({
memoryReservation: z.string().optional(),
cpuLimit: z.string().optional(),
memoryLimit: z.string().optional(),
cpuReservation: z.string().optional(),
memoryReservation: memorySchema,
cpuLimit: cpuSchema,
memoryLimit: memorySchema,
cpuReservation: cpuSchema,
ulimitsSwarm: z.array(ulimitSchema).optional(),
});

Expand Down Expand Up @@ -268,9 +288,10 @@ export const ShowResources = ({ id, type }: Props) => {
</TooltipTrigger>
<TooltipContent>
<p>
Memory soft limit in bytes. Example: 256MB =
268435456 bytes. Use +/- buttons to adjust by 256
MB.
Memory in bytes reserved on the node when
scheduling; it does not limit the container.
Example: 256MB = 268435456 bytes. Use +/- buttons
to adjust by 256 MB.
</p>
</TooltipContent>
</Tooltip>
Expand Down Expand Up @@ -348,9 +369,10 @@ export const ShowResources = ({ id, type }: Props) => {
</TooltipTrigger>
<TooltipContent>
<p>
CPU shares (relative weight). Example: 1 CPU =
1000000000. Use +/- buttons to adjust by 0.25
CPU.
CPU in units of 10^-9 CPUs reserved on the node
when scheduling; it does not limit the
container. Example: 1 CPU = 1000000000. Use +/-
buttons to adjust by 0.25 CPU.
</p>
</TooltipContent>
</Tooltip>
Expand Down
10 changes: 6 additions & 4 deletions packages/server/src/db/schema/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ import {
PlacementSwarmSchema,
type RestartPolicySwarm,
RestartPolicySwarmSchema,
resourceBytesSchema,
resourceNanoCpusSchema,
type ServiceModeSwarm,
ServiceModeSwarmSchema,
triggerType,
Expand Down Expand Up @@ -317,10 +319,10 @@ const createSchema = createInsertSchema(applications, {
buildSecrets: z.string().optional(),
name: z.string().min(1),
description: z.string().optional(),
memoryReservation: z.string().optional(),
memoryLimit: z.string().optional(),
cpuReservation: z.string().optional(),
cpuLimit: z.string().optional(),
memoryReservation: resourceBytesSchema,
memoryLimit: resourceBytesSchema,
cpuReservation: resourceNanoCpusSchema,
cpuLimit: resourceNanoCpusSchema,
title: z.string().optional(),
enabled: z.boolean().optional(),
subtitle: z.string().optional(),
Expand Down
10 changes: 6 additions & 4 deletions packages/server/src/db/schema/libsql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import {
PlacementSwarmSchema,
type RestartPolicySwarm,
RestartPolicySwarmSchema,
resourceBytesSchema,
resourceNanoCpusSchema,
type ServiceModeSwarm,
ServiceModeSwarmSchema,
sqldNode,
Expand Down Expand Up @@ -131,10 +133,10 @@ const createSchema = createInsertSchema(libsql, {
.default("ghcr.io/tursodatabase/libsql-server:v0.24.32"),
command: z.string().optional(),
env: z.string().optional(),
memoryReservation: z.string().optional(),
memoryLimit: z.string().optional(),
cpuReservation: z.string().optional(),
cpuLimit: z.string().optional(),
memoryReservation: resourceBytesSchema,
memoryLimit: resourceBytesSchema,
cpuReservation: resourceNanoCpusSchema,
cpuLimit: resourceNanoCpusSchema,
environmentId: z.string(),
applicationStatus: z.enum(["idle", "running", "done", "error"]),
externalPort: z.number(),
Expand Down
10 changes: 6 additions & 4 deletions packages/server/src/db/schema/mariadb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import {
PlacementSwarmSchema,
type RestartPolicySwarm,
RestartPolicySwarmSchema,
resourceBytesSchema,
resourceNanoCpusSchema,
type ServiceModeSwarm,
ServiceModeSwarmSchema,
type UlimitsSwarm,
Expand Down Expand Up @@ -139,10 +141,10 @@ const createSchema = createInsertSchema(mariadb, {
command: z.string().optional(),
args: z.array(z.string()).optional(),
env: z.string().optional(),
memoryReservation: z.string().optional(),
memoryLimit: z.string().optional(),
cpuReservation: z.string().optional(),
cpuLimit: z.string().optional(),
memoryReservation: resourceBytesSchema,
memoryLimit: resourceBytesSchema,
cpuReservation: resourceNanoCpusSchema,
cpuLimit: resourceNanoCpusSchema,
environmentId: z.string(),
applicationStatus: z.enum(["idle", "running", "done", "error"]),
externalPort: z.number(),
Expand Down
10 changes: 6 additions & 4 deletions packages/server/src/db/schema/mongo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import {
PlacementSwarmSchema,
type RestartPolicySwarm,
RestartPolicySwarmSchema,
resourceBytesSchema,
resourceNanoCpusSchema,
type ServiceModeSwarm,
ServiceModeSwarmSchema,
type UlimitsSwarm,
Expand Down Expand Up @@ -129,10 +131,10 @@ const createSchema = createInsertSchema(mongo, {
command: z.string().optional(),
args: z.array(z.string()).optional(),
env: z.string().optional(),
memoryReservation: z.string().optional(),
memoryLimit: z.string().optional(),
cpuReservation: z.string().optional(),
cpuLimit: z.string().optional(),
memoryReservation: resourceBytesSchema,
memoryLimit: resourceBytesSchema,
cpuReservation: resourceNanoCpusSchema,
cpuLimit: resourceNanoCpusSchema,
environmentId: z.string(),
applicationStatus: z.enum(["idle", "running", "done", "error"]),
externalPort: z.number(),
Expand Down
10 changes: 6 additions & 4 deletions packages/server/src/db/schema/mysql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import {
PlacementSwarmSchema,
type RestartPolicySwarm,
RestartPolicySwarmSchema,
resourceBytesSchema,
resourceNanoCpusSchema,
type ServiceModeSwarm,
ServiceModeSwarmSchema,
type UlimitsSwarm,
Expand Down Expand Up @@ -137,10 +139,10 @@ const createSchema = createInsertSchema(mysql, {
command: z.string().optional(),
args: z.array(z.string()).optional(),
env: z.string().optional(),
memoryReservation: z.string().optional(),
memoryLimit: z.string().optional(),
cpuReservation: z.string().optional(),
cpuLimit: z.string().optional(),
memoryReservation: resourceBytesSchema,
memoryLimit: resourceBytesSchema,
cpuReservation: resourceNanoCpusSchema,
cpuLimit: resourceNanoCpusSchema,
applicationStatus: z.enum(["idle", "running", "done", "error"]),
externalPort: z.number(),
description: z.string().optional(),
Expand Down
10 changes: 6 additions & 4 deletions packages/server/src/db/schema/postgres.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import {
PlacementSwarmSchema,
type RestartPolicySwarm,
RestartPolicySwarmSchema,
resourceBytesSchema,
resourceNanoCpusSchema,
type ServiceModeSwarm,
ServiceModeSwarmSchema,
type UlimitsSwarm,
Expand Down Expand Up @@ -130,10 +132,10 @@ const createSchema = createInsertSchema(postgres, {
command: z.string().optional(),
args: z.array(z.string()).optional(),
env: z.string().optional(),
memoryReservation: z.string().optional(),
memoryLimit: z.string().optional(),
cpuReservation: z.string().optional(),
cpuLimit: z.string().optional(),
memoryReservation: resourceBytesSchema,
memoryLimit: resourceBytesSchema,
cpuReservation: resourceNanoCpusSchema,
cpuLimit: resourceNanoCpusSchema,
environmentId: z.string(),
applicationStatus: z.enum(["idle", "running", "done", "error"]),
externalPort: z.number(),
Expand Down
10 changes: 6 additions & 4 deletions packages/server/src/db/schema/redis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import {
PlacementSwarmSchema,
type RestartPolicySwarm,
RestartPolicySwarmSchema,
resourceBytesSchema,
resourceNanoCpusSchema,
type ServiceModeSwarm,
ServiceModeSwarmSchema,
type UlimitsSwarm,
Expand Down Expand Up @@ -120,10 +122,10 @@ const createSchema = createInsertSchema(redis, {
command: z.string().optional(),
args: z.array(z.string()).optional(),
env: z.string().optional(),
memoryReservation: z.string().optional(),
memoryLimit: z.string().optional(),
cpuReservation: z.string().optional(),
cpuLimit: z.string().optional(),
memoryReservation: resourceBytesSchema,
memoryLimit: resourceBytesSchema,
cpuReservation: resourceNanoCpusSchema,
cpuLimit: resourceNanoCpusSchema,
environmentId: z.string(),
applicationStatus: z.enum(["idle", "running", "done", "error"]),
externalPort: z.number(),
Expand Down
Loading