mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-09-12 19:51:00 +05:00
Merge 03fd951763 into 853ca33659
This commit is contained in:
commit
a1121a643b
109
apps/dokploy/__test__/compose/compose-replicas-scaling.test.ts
Normal file
109
apps/dokploy/__test__/compose/compose-replicas-scaling.test.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import {
|
||||
createCommand,
|
||||
getBuildComposeCommand,
|
||||
} from "@dokploy/server/utils/builders/compose";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const base = {
|
||||
composeType: "docker-compose" as const,
|
||||
appName: "compose-app",
|
||||
sourceType: "raw" as const,
|
||||
composePath: "./docker-compose.yml",
|
||||
command: "",
|
||||
mounts: [],
|
||||
domains: [],
|
||||
environment: {
|
||||
environmentId: "env-1",
|
||||
env: "",
|
||||
project: {
|
||||
projectId: "proj-1",
|
||||
organizationId: "org-1",
|
||||
env: "",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe("compose createCommand with serviceScales", () => {
|
||||
it("adds --scale <service>=<replicas> when serviceScales is provided", () => {
|
||||
const cmd = createCommand({
|
||||
...base,
|
||||
serviceScales: [{ serviceName: "web", replicas: 3 }],
|
||||
} as any);
|
||||
|
||||
expect(cmd).toContain("up -d --build --remove-orphans --scale web=3");
|
||||
});
|
||||
|
||||
it("adds multiple --scale flags sorted by serviceName", () => {
|
||||
const cmd = createCommand({
|
||||
...base,
|
||||
serviceScales: [
|
||||
{ serviceName: "worker", replicas: 5 },
|
||||
{ serviceName: "api", replicas: 2 },
|
||||
],
|
||||
} as any);
|
||||
|
||||
expect(cmd).toContain("--scale api=2 --scale worker=5");
|
||||
});
|
||||
|
||||
it("omits --scale when serviceScales is empty or undefined", () => {
|
||||
const cmdEmpty = createCommand({
|
||||
...base,
|
||||
serviceScales: [],
|
||||
} as any);
|
||||
expect(cmdEmpty).not.toContain("--scale");
|
||||
|
||||
const cmdUndefined = createCommand({
|
||||
...base,
|
||||
} as any);
|
||||
expect(cmdUndefined).not.toContain("--scale");
|
||||
});
|
||||
|
||||
it("does not add --scale to stack deploy createCommand", () => {
|
||||
const cmd = createCommand({
|
||||
...base,
|
||||
composeType: "stack",
|
||||
serviceScales: [{ serviceName: "web", replicas: 4 }],
|
||||
} as any);
|
||||
|
||||
expect(cmd).not.toContain("--scale");
|
||||
});
|
||||
|
||||
it("leaves custom command untouched even with serviceScales", () => {
|
||||
const cmd = createCommand({
|
||||
...base,
|
||||
command: "compose -p custom-app up -d",
|
||||
serviceScales: [{ serviceName: "web", replicas: 3 }],
|
||||
} as any);
|
||||
|
||||
expect(cmd).toBe("compose -p custom-app up -d");
|
||||
});
|
||||
});
|
||||
|
||||
describe("compose getBuildComposeCommand with Swarm serviceScales", () => {
|
||||
it("appends docker service scale commands for stack composeType", async () => {
|
||||
const cmd = await getBuildComposeCommand({
|
||||
...base,
|
||||
composeType: "stack",
|
||||
serviceScales: [
|
||||
{ serviceName: "web", replicas: 3 },
|
||||
{ serviceName: "worker", replicas: 2 },
|
||||
],
|
||||
} as any);
|
||||
|
||||
expect(cmd).toContain("docker stack deploy");
|
||||
expect(cmd).toContain('docker service scale "compose-app_web=3"');
|
||||
expect(cmd).toContain('docker service scale "compose-app_worker=2"');
|
||||
expect(cmd).toContain("Docker service scale failed for web");
|
||||
});
|
||||
|
||||
it("does not append swarm service scale commands for docker-compose type", async () => {
|
||||
const cmd = await getBuildComposeCommand({
|
||||
...base,
|
||||
composeType: "docker-compose",
|
||||
serviceScales: [{ serviceName: "web", replicas: 3 }],
|
||||
} as any);
|
||||
|
||||
expect(cmd).not.toContain("docker service scale");
|
||||
expect(cmd).toContain("--scale web=3");
|
||||
});
|
||||
});
|
||||
1
apps/dokploy/drizzle/0197_breezy_mentallo.sql
Normal file
1
apps/dokploy/drizzle/0197_breezy_mentallo.sql
Normal file
@ -0,0 +1 @@
|
||||
ALTER TABLE "compose" ADD COLUMN "serviceScales" jsonb DEFAULT '[]'::jsonb;
|
||||
9177
apps/dokploy/drizzle/meta/0197_snapshot.json
Normal file
9177
apps/dokploy/drizzle/meta/0197_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -1380,6 +1380,13 @@
|
||||
"when": 1788995453103,
|
||||
"tag": "0196_worthless_ravenous",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 197,
|
||||
"version": "7",
|
||||
"when": 1789141420032,
|
||||
"tag": "0197_breezy_mentallo",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -28,6 +28,7 @@ import {
|
||||
removeComposeDirectory,
|
||||
removeDeploymentsByComposeId,
|
||||
removeDomainById,
|
||||
scaleComposeService,
|
||||
startCompose,
|
||||
stopCompose,
|
||||
updateCompose,
|
||||
@ -66,6 +67,7 @@ import {
|
||||
apiRandomizeCompose,
|
||||
apiRedeployCompose,
|
||||
apiSaveEnvironmentVariablesCompose,
|
||||
apiScaleComposeService,
|
||||
apiUpdateCompose,
|
||||
compose as composeTable,
|
||||
environments,
|
||||
@ -544,6 +546,26 @@ export const composeRouter = createTRPCRouter({
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
scaleService: protectedProcedure
|
||||
.input(apiScaleComposeService)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const updated = await scaleComposeService(
|
||||
input.composeId,
|
||||
input.serviceName,
|
||||
input.replicas,
|
||||
);
|
||||
const compose = await findComposeById(input.composeId);
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "service",
|
||||
resourceId: input.composeId,
|
||||
resourceName: `${compose.name} (${input.serviceName} -> ${input.replicas} replicas)`,
|
||||
});
|
||||
return updated;
|
||||
}),
|
||||
getDefaultCommand: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.query(async ({ input, ctx }) => {
|
||||
|
||||
@ -132,6 +132,14 @@ export const compose = pgTable("compose", {
|
||||
}>
|
||||
>()
|
||||
.default([]),
|
||||
serviceScales: jsonb("serviceScales")
|
||||
.$type<
|
||||
Array<{
|
||||
serviceName: string;
|
||||
replicas: number;
|
||||
}>
|
||||
>()
|
||||
.default([]),
|
||||
});
|
||||
|
||||
export const composeRelations = relations(compose, ({ one, many }) => ({
|
||||
@ -208,6 +216,17 @@ const createSchema = createInsertSchema(compose, {
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
serviceScales: z
|
||||
.array(
|
||||
z.object({
|
||||
serviceName: z
|
||||
.string()
|
||||
.min(1)
|
||||
.regex(/^[a-zA-Z0-9._-]+$/, "Invalid service name"),
|
||||
replicas: z.number().int().min(0),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const apiCreateCompose = createSchema.pick({
|
||||
@ -285,3 +304,13 @@ export const apiRandomizeCompose = createSchema
|
||||
suffix: z.string().optional(),
|
||||
composeId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiScaleComposeService = z.object({
|
||||
composeId: z.string().min(1),
|
||||
serviceName: z
|
||||
.string()
|
||||
.min(1)
|
||||
.regex(/^[a-zA-Z0-9._-]+$/, "Invalid service name"),
|
||||
replicas: z.number().int().min(0),
|
||||
});
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { join } from "node:path";
|
||||
import { dirname, join } from "node:path";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
@ -583,3 +583,49 @@ export const stopCompose = async (composeId: string) => {
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const scaleComposeService = async (
|
||||
composeId: string,
|
||||
serviceName: string,
|
||||
replicas: number,
|
||||
) => {
|
||||
const compose = await findComposeById(composeId);
|
||||
const existingScales = compose.serviceScales || [];
|
||||
const updatedScales = existingScales.filter(
|
||||
(s) => s.serviceName !== serviceName,
|
||||
);
|
||||
updatedScales.push({ serviceName, replicas });
|
||||
|
||||
await updateCompose(composeId, {
|
||||
serviceScales: updatedScales,
|
||||
});
|
||||
|
||||
if (compose.composeType === "docker-compose") {
|
||||
const { COMPOSE_PATH } = paths(!!compose.serverId);
|
||||
const projectPath = join(COMPOSE_PATH, compose.appName, "code");
|
||||
const path =
|
||||
compose.sourceType === "raw"
|
||||
? "docker-compose.yml"
|
||||
: compose.composePath || "docker-compose.yml";
|
||||
const projectDirectoryFlag = `--project-directory ${quote([projectPath])} `;
|
||||
const envFileFlag = compose.createEnvFile
|
||||
? `--env-file ${quote([join(dirname(compose.composePath || "docker-compose.yml"), ".env")])} `
|
||||
: "";
|
||||
const scaleCommand = `cd ${quote([projectPath])} && env -i PATH="$PATH" HOME="$HOME" docker compose -p ${quote([compose.appName])} ${projectDirectoryFlag}${envFileFlag}-f ${quote([path])} up -d --no-build --scale ${quote([`${serviceName}=${replicas}`])} ${quote([serviceName])}`;
|
||||
if (compose.serverId) {
|
||||
await execAsyncRemote(compose.serverId, scaleCommand);
|
||||
} else {
|
||||
await execAsync(scaleCommand);
|
||||
}
|
||||
} else if (compose.composeType === "stack") {
|
||||
const swarmScaleCommand = `docker service scale ${quote([`${compose.appName}_${serviceName}=${replicas}`])}`;
|
||||
if (compose.serverId) {
|
||||
await execAsyncRemote(compose.serverId, swarmScaleCommand);
|
||||
} else {
|
||||
await execAsync(swarmScaleCommand);
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true, serviceName, replicas };
|
||||
};
|
||||
|
||||
|
||||
@ -64,6 +64,19 @@ Compose Type: ${composeType} ✅`;
|
||||
${compose.isolatedDeployment ? `docker network inspect ${compose.appName} >/dev/null 2>&1 || docker network create ${compose.composeType === "stack" ? "--driver overlay" : ""} --attachable ${compose.appName}` : ""}
|
||||
env -i PATH="$PATH" HOME="$HOME" ${exportEnvCommand} docker ${command.split(" ").join(" ")} 2>&1 || { echo "Error: ❌ Docker command failed"; exit 1; }
|
||||
${compose.isolatedDeployment ? `docker network connect ${compose.appName} $(docker ps --filter "name=dokploy-traefik" -q) >/dev/null 2>&1` : ""}
|
||||
${
|
||||
compose.composeType === "stack" &&
|
||||
compose.serviceScales &&
|
||||
compose.serviceScales.length > 0
|
||||
? [...compose.serviceScales]
|
||||
.sort((a, b) => a.serviceName.localeCompare(b.serviceName))
|
||||
.map(
|
||||
(s) =>
|
||||
`docker service scale "${compose.appName}_${s.serviceName}=${s.replicas}" 2>&1 || { echo "Error: ❌ Docker service scale failed for ${s.serviceName}"; exit 1; };`,
|
||||
)
|
||||
.join("\n\t\t")
|
||||
: ""
|
||||
}
|
||||
|
||||
echo "Docker Compose Deployed: ✅";
|
||||
} || {
|
||||
@ -139,8 +152,18 @@ export const createCommand = (compose: ComposeNested, projectPath?: string) => {
|
||||
const envFileFlag = compose.createEnvFile
|
||||
? `--env-file ${quote([join(dirname(compose.composePath || "docker-compose.yml"), ".env")])} `
|
||||
: "";
|
||||
const scaleFlags =
|
||||
compose.serviceScales && compose.serviceScales.length > 0
|
||||
? `${[...compose.serviceScales]
|
||||
.sort((a, b) => a.serviceName.localeCompare(b.serviceName))
|
||||
.map((scale) => `--scale ${scale.serviceName}=${scale.replicas}`)
|
||||
.join(" ")} `
|
||||
: "";
|
||||
const pullFlag = compose.pullImages ? " --pull always" : "";
|
||||
command = `compose -p ${quote([appName])} ${projectDirectoryFlag}${envFileFlag}-f ${quote([path])} up -d --build --remove-orphans${pullFlag}`;
|
||||
command =
|
||||
`compose -p ${quote([appName])} ${projectDirectoryFlag}${envFileFlag}-f ${quote([path])} up -d --build --remove-orphans ${scaleFlags}${pullFlag}`
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
} else if (composeType === "stack") {
|
||||
command = `stack deploy -c ${quote([path])} ${quote([appName])} --prune --with-registry-auth`;
|
||||
}
|
||||
@ -194,8 +217,8 @@ const getExportEnvCommand = (compose: ComposeNested) => {
|
||||
|
||||
const envVars = getEnvironmentVariablesObject(
|
||||
compose.env,
|
||||
compose.environment.project.env,
|
||||
compose.environment.env,
|
||||
compose.environment?.project?.env,
|
||||
compose.environment?.env,
|
||||
);
|
||||
const exports = Object.entries(envVars)
|
||||
.map(([key, value]) => `${key}=${quote([value])}`)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user