mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-09-12 19:51:00 +05:00
fix(compose): prevent unsupported models in Swarm stack deployments
Prevents Dokploy from running docker stack deploy when the effective Compose specification uses Compose models.
This commit is contained in:
parent
853ca33659
commit
cac1325236
414
apps/dokploy/__test__/compose/stack-compose-models.test.ts
Normal file
414
apps/dokploy/__test__/compose/stack-compose-models.test.ts
Normal file
@ -0,0 +1,414 @@
|
|||||||
|
import { db } from "@dokploy/server/db";
|
||||||
|
import {
|
||||||
|
createCommand,
|
||||||
|
getBuildComposeCommand,
|
||||||
|
} from "@dokploy/server/utils/builders/compose";
|
||||||
|
import {
|
||||||
|
addDomainToCompose,
|
||||||
|
composeSpecificationUsesModels,
|
||||||
|
isStackDeployCommand,
|
||||||
|
STACK_COMPOSE_MODELS_ERROR,
|
||||||
|
writeDomainsToCompose,
|
||||||
|
} from "@dokploy/server/utils/docker/domain";
|
||||||
|
import type { ComposeSpecification } from "@dokploy/server/utils/docker/types";
|
||||||
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { parse, stringify } from "yaml";
|
||||||
|
|
||||||
|
const disk = vi.hoisted(() => ({
|
||||||
|
yaml: "services:\n app:\n image: nginx:alpine\n",
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("node:fs", async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import("node:fs")>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
existsSync: (p: Parameters<typeof actual.existsSync>[0]) => {
|
||||||
|
const path = String(p);
|
||||||
|
if (path.includes("/code/") && path.endsWith("docker-compose.yml")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return actual.existsSync(p);
|
||||||
|
},
|
||||||
|
readFileSync: (
|
||||||
|
p: Parameters<typeof actual.readFileSync>[0],
|
||||||
|
enc?: BufferEncoding,
|
||||||
|
) => {
|
||||||
|
const path = String(p);
|
||||||
|
if (path.includes("/code/") && path.endsWith("docker-compose.yml")) {
|
||||||
|
return disk.yaml;
|
||||||
|
}
|
||||||
|
return actual.readFileSync(p, enc as never);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const yaml = (spec: unknown) => stringify(spec, { lineWidth: 1000 });
|
||||||
|
|
||||||
|
const spec = (extra: Record<string, unknown> = {}): ComposeSpecification => ({
|
||||||
|
services: {
|
||||||
|
app: { image: "nginx:alpine" },
|
||||||
|
},
|
||||||
|
...extra,
|
||||||
|
});
|
||||||
|
|
||||||
|
const compose = (
|
||||||
|
overrides: Record<string, unknown> = {},
|
||||||
|
): Parameters<typeof writeDomainsToCompose>[0] =>
|
||||||
|
({
|
||||||
|
appName: "demo",
|
||||||
|
composeFile: yaml(spec()),
|
||||||
|
composePath: "docker-compose.yml",
|
||||||
|
composeType: "stack",
|
||||||
|
command: "",
|
||||||
|
isolatedDeployment: false,
|
||||||
|
isolatedDeploymentsVolume: false,
|
||||||
|
randomize: false,
|
||||||
|
serverId: null,
|
||||||
|
sourceType: "raw",
|
||||||
|
suffix: "",
|
||||||
|
env: "",
|
||||||
|
createEnvFile: false,
|
||||||
|
mounts: [],
|
||||||
|
domains: [],
|
||||||
|
environment: { project: { env: "" }, env: "" },
|
||||||
|
...overrides,
|
||||||
|
}) as unknown as Parameters<typeof writeDomainsToCompose>[0];
|
||||||
|
|
||||||
|
const writeDeploy = (c: ReturnType<typeof compose>) =>
|
||||||
|
writeDomainsToCompose(c, [], createCommand(c as never));
|
||||||
|
|
||||||
|
describe("composeSpecificationUsesModels", () => {
|
||||||
|
it("detects top-level and service-level models structurally", () => {
|
||||||
|
expect(composeSpecificationUsesModels(spec())).toBe(false);
|
||||||
|
expect(
|
||||||
|
composeSpecificationUsesModels(
|
||||||
|
spec({ models: { llm: { model: "ai/smollm2" } } }),
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
composeSpecificationUsesModels({
|
||||||
|
services: { app: { image: "nginx:alpine", models: ["llm"] } },
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
expect(composeSpecificationUsesModels(spec({ models: {} }))).toBe(true);
|
||||||
|
expect(composeSpecificationUsesModels(spec({ models: null }))).toBe(true);
|
||||||
|
expect(composeSpecificationUsesModels(spec({ models: [] }))).toBe(true);
|
||||||
|
expect(composeSpecificationUsesModels(spec({ models: "foo" }))).toBe(true);
|
||||||
|
expect(composeSpecificationUsesModels(spec({ models: 123 }))).toBe(true);
|
||||||
|
expect(
|
||||||
|
composeSpecificationUsesModels({
|
||||||
|
services: { app: { image: "nginx:alpine", models: [] } },
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
composeSpecificationUsesModels({
|
||||||
|
services: { app: { image: "nginx:alpine", models: null } },
|
||||||
|
} as unknown as ComposeSpecification),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not treat x-models or the word models in strings as models", () => {
|
||||||
|
expect(
|
||||||
|
composeSpecificationUsesModels(
|
||||||
|
spec({ "x-models": { llm: { model: "ai/smollm2" } } }),
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
const fromStrings = parse(`
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: nginx:alpine
|
||||||
|
# models: fake
|
||||||
|
environment:
|
||||||
|
NOTE: "models: in env"
|
||||||
|
labels:
|
||||||
|
info: "models: in label"
|
||||||
|
command: ["echo", "models: in command"]
|
||||||
|
`) as ComposeSpecification;
|
||||||
|
expect(composeSpecificationUsesModels(fromStrings)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("detects stack deploy from the first two command tokens", () => {
|
||||||
|
expect(
|
||||||
|
isStackDeployCommand("stack deploy -c docker-compose.yml demo"),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
isStackDeployCommand(" stack deploy -c docker-compose.yml demo"),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
isStackDeployCommand("compose -p demo -f docker-compose.yml up -d"),
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
isStackDeployCommand("compose -f my-stack deploy-file.yml up -d"),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not crash on malformed specs", () => {
|
||||||
|
expect(composeSpecificationUsesModels(null)).toBe(false);
|
||||||
|
expect(composeSpecificationUsesModels(undefined)).toBe(false);
|
||||||
|
expect(composeSpecificationUsesModels("nope" as never)).toBe(false);
|
||||||
|
expect(
|
||||||
|
composeSpecificationUsesModels({
|
||||||
|
services: null,
|
||||||
|
} as unknown as ComposeSpecification),
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
composeSpecificationUsesModels({
|
||||||
|
services: { app: "bad" },
|
||||||
|
} as unknown as ComposeSpecification),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("stack deploy compatibility", () => {
|
||||||
|
it("allows docker compose with models and stack without models", async () => {
|
||||||
|
const withModels = compose({
|
||||||
|
composeType: "docker-compose",
|
||||||
|
composeFile: yaml(
|
||||||
|
spec({
|
||||||
|
models: { llm: { model: "ai/smollm2" } },
|
||||||
|
services: { app: { image: "nginx:alpine", models: ["llm"] } },
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
await expect(writeDeploy(withModels)).resolves.toContain("base64 -d");
|
||||||
|
await expect(writeDeploy(compose())).resolves.toContain("base64 -d");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects default stack deploy when the final spec has models", async () => {
|
||||||
|
const top = compose({
|
||||||
|
composeFile: yaml(spec({ models: { llm: { model: "ai/smollm2" } } })),
|
||||||
|
});
|
||||||
|
const svc = compose({
|
||||||
|
composeFile: yaml({
|
||||||
|
services: { app: { image: "nginx:alpine", models: ["llm"] } },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const both = compose({
|
||||||
|
composeFile: yaml({
|
||||||
|
services: { app: { image: "nginx:alpine", models: ["llm"] } },
|
||||||
|
models: { llm: { model: "ai/smollm2" } },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
await expect(writeDeploy(top)).rejects.toThrow(STACK_COMPOSE_MODELS_ERROR);
|
||||||
|
await expect(writeDeploy(svc)).rejects.toThrow(STACK_COMPOSE_MODELS_ERROR);
|
||||||
|
await expect(writeDeploy(both)).rejects.toThrow(STACK_COMPOSE_MODELS_ERROR);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows x-models and textual 'models:' that are not Compose models keys", async () => {
|
||||||
|
await expect(
|
||||||
|
writeDeploy(
|
||||||
|
compose({
|
||||||
|
composeFile: yaml(
|
||||||
|
spec({ "x-models": { llm: { model: "ai/smollm2" } } }),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).resolves.toContain("base64 -d");
|
||||||
|
await expect(
|
||||||
|
writeDeploy(
|
||||||
|
compose({
|
||||||
|
composeFile: `
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: nginx:alpine
|
||||||
|
# models: fake
|
||||||
|
environment:
|
||||||
|
NOTE: "models: in env"
|
||||||
|
labels:
|
||||||
|
info: "models: in label"
|
||||||
|
command: ["echo", "models: in command"]
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).resolves.toContain("base64 -d");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("still converts stack compose with models for preview", async () => {
|
||||||
|
const converted = await addDomainToCompose(
|
||||||
|
compose({
|
||||||
|
composeFile: yaml(spec({ models: { llm: { model: "ai/smollm2" } } })),
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
expect(converted?.models).toEqual({ llm: { model: "ai/smollm2" } });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("inspects the transformed spec so randomization keeps models detectable", async () => {
|
||||||
|
await expect(
|
||||||
|
writeDeploy(
|
||||||
|
compose({
|
||||||
|
randomize: true,
|
||||||
|
suffix: "abc123",
|
||||||
|
composeFile: yaml(spec({ models: { llm: { model: "ai/smollm2" } } })),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).rejects.toThrow(STACK_COMPOSE_MODELS_ERROR);
|
||||||
|
await expect(
|
||||||
|
writeDeploy(
|
||||||
|
compose({
|
||||||
|
isolatedDeployment: true,
|
||||||
|
suffix: "iso",
|
||||||
|
composeFile: yaml({
|
||||||
|
services: { app: { image: "nginx:alpine", models: ["llm"] } },
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).rejects.toThrow(STACK_COMPOSE_MODELS_ERROR);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows stack records whose custom command is compose up", async () => {
|
||||||
|
await expect(
|
||||||
|
writeDeploy(
|
||||||
|
compose({
|
||||||
|
command: "compose -p demo -f docker-compose.yml up -d",
|
||||||
|
composeFile: yaml(spec({ models: { llm: { model: "ai/smollm2" } } })),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).resolves.toContain("base64 -d");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects docker-compose records whose custom command is stack deploy", async () => {
|
||||||
|
const models = yaml(spec({ models: { llm: { model: "ai/smollm2" } } }));
|
||||||
|
await expect(
|
||||||
|
writeDeploy(
|
||||||
|
compose({
|
||||||
|
composeType: "docker-compose",
|
||||||
|
command: "stack deploy -c docker-compose.yml demo",
|
||||||
|
composeFile: models,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).rejects.toThrow(STACK_COMPOSE_MODELS_ERROR);
|
||||||
|
await expect(
|
||||||
|
writeDeploy(
|
||||||
|
compose({
|
||||||
|
composeType: "docker-compose",
|
||||||
|
command: " stack deploy -c docker-compose.yml demo",
|
||||||
|
composeFile: models,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).rejects.toThrow(STACK_COMPOSE_MODELS_ERROR);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not treat a compose file token containing 'stack deploy' as stack deploy", async () => {
|
||||||
|
await expect(
|
||||||
|
writeDeploy(
|
||||||
|
compose({
|
||||||
|
composeType: "docker-compose",
|
||||||
|
command: "compose -f my-stack deploy-file.yml up -d",
|
||||||
|
composeFile: yaml(spec({ models: { llm: { model: "ai/smollm2" } } })),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).resolves.toContain("base64 -d");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("compose-file patch order", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
disk.yaml = yaml(spec());
|
||||||
|
});
|
||||||
|
|
||||||
|
const gitCompose = (overrides: Record<string, unknown> = {}) =>
|
||||||
|
compose({
|
||||||
|
sourceType: "github",
|
||||||
|
composeId: "compose-1",
|
||||||
|
composePath: "docker-compose.yml",
|
||||||
|
...overrides,
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects when a patch adds models to a stack deploy", async () => {
|
||||||
|
disk.yaml = yaml(spec());
|
||||||
|
vi.spyOn(db.query.patch, "findMany").mockResolvedValue([
|
||||||
|
{
|
||||||
|
enabled: true,
|
||||||
|
type: "update",
|
||||||
|
filePath: "docker-compose.yml",
|
||||||
|
content: yaml(spec({ models: { llm: { model: "ai/smollm2" } } })),
|
||||||
|
},
|
||||||
|
] as never);
|
||||||
|
await expect(writeDeploy(gitCompose())).rejects.toThrow(
|
||||||
|
STACK_COMPOSE_MODELS_ERROR,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows stack deploy when a patch removes models", async () => {
|
||||||
|
disk.yaml = yaml(spec({ models: { llm: { model: "ai/smollm2" } } }));
|
||||||
|
vi.spyOn(db.query.patch, "findMany").mockResolvedValue([
|
||||||
|
{
|
||||||
|
enabled: true,
|
||||||
|
type: "update",
|
||||||
|
filePath: "docker-compose.yml",
|
||||||
|
content: yaml(spec()),
|
||||||
|
},
|
||||||
|
] as never);
|
||||||
|
await expect(writeDeploy(gitCompose())).resolves.toContain("base64 -d");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getBuildComposeCommand stack models", () => {
|
||||||
|
const buildArgs = (overrides: Record<string, unknown> = {}) =>
|
||||||
|
({
|
||||||
|
...compose(overrides),
|
||||||
|
type: "compose" as const,
|
||||||
|
}) as unknown as Parameters<typeof getBuildComposeCommand>[0];
|
||||||
|
|
||||||
|
it("does not emit stack deploy for default stack + models", async () => {
|
||||||
|
await expect(
|
||||||
|
getBuildComposeCommand(
|
||||||
|
buildArgs({
|
||||||
|
composeFile: yaml(spec({ models: { llm: { model: "ai/smollm2" } } })),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).rejects.toThrow(STACK_COMPOSE_MODELS_ERROR);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("still emits stack deploy without models and compose up with models", async () => {
|
||||||
|
const stack = await getBuildComposeCommand(buildArgs());
|
||||||
|
expect(stack).toContain("stack deploy");
|
||||||
|
expect(stack).not.toContain("docker network inspect");
|
||||||
|
const up = await getBuildComposeCommand(
|
||||||
|
buildArgs({
|
||||||
|
composeType: "docker-compose",
|
||||||
|
composeFile: yaml(spec({ models: { llm: { model: "ai/smollm2" } } })),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(up).toContain("compose -p demo");
|
||||||
|
expect(up).toContain("up -d");
|
||||||
|
expect(up).not.toContain("stack deploy");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not create an isolated overlay network when stack + models is rejected", async () => {
|
||||||
|
await expect(
|
||||||
|
getBuildComposeCommand(
|
||||||
|
buildArgs({
|
||||||
|
isolatedDeployment: true,
|
||||||
|
suffix: "iso",
|
||||||
|
composeFile: yaml(spec({ models: { llm: { model: "ai/smollm2" } } })),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).rejects.toThrow(STACK_COMPOSE_MODELS_ERROR);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps a custom compose command for stack + models", async () => {
|
||||||
|
const command = await getBuildComposeCommand(
|
||||||
|
buildArgs({
|
||||||
|
command: "compose -p demo -f docker-compose.yml up -d",
|
||||||
|
composeFile: yaml(spec({ models: { llm: { model: "ai/smollm2" } } })),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(command).toContain("compose -p demo");
|
||||||
|
expect(command).not.toContain("stack deploy");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not emit stack deploy for a docker-compose record with a custom stack command and models", async () => {
|
||||||
|
await expect(
|
||||||
|
getBuildComposeCommand(
|
||||||
|
buildArgs({
|
||||||
|
composeType: "docker-compose",
|
||||||
|
command: "stack deploy -c docker-compose.yml demo",
|
||||||
|
composeFile: yaml(spec({ models: { llm: { model: "ai/smollm2" } } })),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).rejects.toThrow(STACK_COMPOSE_MODELS_ERROR);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -31,7 +31,7 @@ export const getBuildComposeCommand = async (rawCompose: ComposeNested) => {
|
|||||||
: "";
|
: "";
|
||||||
const exportEnvCommand = getExportEnvCommand(compose);
|
const exportEnvCommand = getExportEnvCommand(compose);
|
||||||
|
|
||||||
const newCompose = await writeDomainsToCompose(compose, domains);
|
const newCompose = await writeDomainsToCompose(compose, domains, command);
|
||||||
const logContent = `
|
const logContent = `
|
||||||
App Name: ${appName}
|
App Name: ${appName}
|
||||||
Build Compose 🐳
|
Build Compose 🐳
|
||||||
|
|||||||
@ -110,24 +110,41 @@ export const readComposeFile = async (compose: Compose) => {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const STACK_COMPOSE_MODELS_ERROR =
|
||||||
|
"Compose models are not supported with Docker Swarm stack deployments. Use Docker Compose deployment type or remove the models configuration.";
|
||||||
|
|
||||||
|
export const composeSpecificationUsesModels = (
|
||||||
|
spec: ComposeSpecification | null | undefined,
|
||||||
|
): boolean => {
|
||||||
|
if (!spec || typeof spec !== "object") return false;
|
||||||
|
if (Object.hasOwn(spec, "models")) return true;
|
||||||
|
const services = spec.services;
|
||||||
|
if (!services || typeof services !== "object") return false;
|
||||||
|
for (const service of Object.values(services)) {
|
||||||
|
if (
|
||||||
|
service &&
|
||||||
|
typeof service === "object" &&
|
||||||
|
Object.hasOwn(service, "models")
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isStackDeployCommand = (command: string) => {
|
||||||
|
const [first, second] = command.trim().split(/\s+/);
|
||||||
|
return first === "stack" && second === "deploy";
|
||||||
|
};
|
||||||
|
|
||||||
export const writeDomainsToCompose = async (
|
export const writeDomainsToCompose = async (
|
||||||
compose: Compose,
|
compose: Compose,
|
||||||
domains: Domain[],
|
domains: Domain[],
|
||||||
|
dockerCommand = "",
|
||||||
) => {
|
) => {
|
||||||
|
let composeConverted: ComposeSpecification | null;
|
||||||
try {
|
try {
|
||||||
const composeConverted = await addDomainToCompose(compose, domains);
|
composeConverted = await addDomainToCompose(compose, domains);
|
||||||
const path = getComposePath(compose);
|
|
||||||
|
|
||||||
if (!composeConverted) {
|
|
||||||
return `
|
|
||||||
echo "❌ Error: Compose file not found";
|
|
||||||
exit 1;
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const composeString = stringify(composeConverted, { lineWidth: 1000 });
|
|
||||||
const encodedContent = encodeBase64(composeString);
|
|
||||||
return `echo "${encodedContent}" | base64 -d > "${path}";`;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message =
|
const message =
|
||||||
error instanceof Error ? error.message : String(error ?? "");
|
error instanceof Error ? error.message : String(error ?? "");
|
||||||
@ -137,6 +154,26 @@ exit 1;
|
|||||||
exit 1;
|
exit 1;
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const path = getComposePath(compose);
|
||||||
|
|
||||||
|
if (!composeConverted) {
|
||||||
|
return `
|
||||||
|
echo "❌ Error: Compose file not found";
|
||||||
|
exit 1;
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
isStackDeployCommand(dockerCommand) &&
|
||||||
|
composeSpecificationUsesModels(composeConverted)
|
||||||
|
) {
|
||||||
|
throw new Error(STACK_COMPOSE_MODELS_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
const composeString = stringify(composeConverted, { lineWidth: 1000 });
|
||||||
|
const encodedContent = encodeBase64(composeString);
|
||||||
|
return `echo "${encodedContent}" | base64 -d > "${path}";`;
|
||||||
};
|
};
|
||||||
export const applyComposeFilePatch = async (
|
export const applyComposeFilePatch = async (
|
||||||
compose: Compose,
|
compose: Compose,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user