@podosoft/podokit 0.16.4 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/deploy-compose-exec.d.ts +93 -0
- package/dist/deploy-compose-exec.js +198 -0
- package/dist/deploy-compose-profile.d.ts +107 -0
- package/dist/deploy-compose-profile.js +361 -0
- package/dist/deploy-compose-render.d.ts +48 -0
- package/dist/deploy-compose-render.js +333 -0
- package/dist/deploy-compose-sync.d.ts +144 -0
- package/dist/deploy-compose-sync.js +412 -0
- package/dist/deploy-compose.d.ts +98 -0
- package/dist/deploy-compose.js +667 -0
- package/dist/deploy-dispatch.d.ts +33 -0
- package/dist/deploy-dispatch.js +58 -0
- package/dist/deploy-driver.d.ts +25 -0
- package/dist/deploy-driver.js +78 -0
- package/dist/deploy-profile.d.ts +3 -10
- package/dist/deploy-profile.js +82 -291
- package/dist/deploy-schema.d.ts +56 -0
- package/dist/deploy-schema.js +261 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +178 -6
- package/dist/lib.d.ts +9 -0
- package/dist/lib.js +30 -1
- package/dist/templates/fullstack-nest-svelte/AGENTS.md +2 -1
- package/dist/templates/fullstack-nest-svelte/Dockerfile.dev +34 -3
- package/dist/templates/fullstack-nest-svelte/apps/api/Dockerfile +68 -7
- package/dist/templates/fullstack-nest-svelte/apps/web/Dockerfile +60 -8
- package/dist/templates/fullstack-nest-svelte/apps/web/server.js +31 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/server/backend-proxy.test.ts +21 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/server/backend-proxy.ts +15 -1
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/server/ws-upgrade.js +267 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/server/ws-upgrade.test.js +134 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/vite.config.ts +17 -7
- package/dist/templates/fullstack-nest-svelte/dot-agents/skills/podokit-deploy/SKILL.md +55 -10
- package/dist/templates/fullstack-nest-svelte/dot-agents/skills/podokit-deploy/references/profile.md +50 -1
- package/dist/templates/fullstack-nest-svelte/dot-agents/skills/podokit-deploy/references/release.md +64 -0
- package/dist/templates/fullstack-nest-svelte/dot-agents/skills/podokit-deploy-fast/SKILL.md +92 -0
- package/dist/templates/fullstack-nest-svelte/dot-claude/skills/podokit-deploy/SKILL.md +1 -1
- package/dist/templates/fullstack-nest-svelte/dot-claude/skills/podokit-deploy-fast/SKILL.md +8 -0
- package/dist/templates/fullstack-nest-svelte/dot-dockerignore +7 -0
- package/dist/templates/fullstack-nest-svelte/dot-env.example +7 -0
- package/dist/templates/fullstack-nest-svelte/dot-github/workflows/release.yml +142 -0
- package/dist/templates/fullstack-nest-svelte/infra/k3s/README.md +18 -0
- package/dist/templates/fullstack-nest-svelte/infra/k3s/api-deployment.yaml +6 -1
- package/dist/templates/fullstack-nest-svelte/infra/k3s/ingress.yaml +8 -0
- package/dist/templates/fullstack-nest-svelte/infra/k3s/web-deployment.yaml +11 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-page.svelte +15 -2
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/device-label.test.ts +62 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/device-label.ts +50 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/en.json +5 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/ko.json +5 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/sessions/+page.svelte +3 -2
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/users/+page.svelte +3 -2
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/reset-password/+page.svelte +12 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/signup/+page.svelte +13 -1
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/accounts.ts +27 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/account.ui.spec.ts +20 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/auth.ui.spec.ts +26 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/email.ui.spec.ts +10 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/two-factor.ui.spec.ts +3 -3
- package/dist/templates/modules/bullmq/files/infra/k3s/worker-deployment.yaml +2 -1
- package/dist/templates/todo/AGENTS.md +2 -1
- package/dist/templates/todo/Dockerfile.dev +34 -3
- package/dist/templates/todo/apps/api/Dockerfile +68 -7
- package/dist/templates/todo/apps/web/Dockerfile +60 -8
- package/dist/templates/todo/apps/web/server.js +31 -0
- package/dist/templates/todo/apps/web/src/lib/server/backend-proxy.test.ts +21 -0
- package/dist/templates/todo/apps/web/src/lib/server/backend-proxy.ts +15 -1
- package/dist/templates/todo/apps/web/src/lib/server/ws-upgrade.js +267 -0
- package/dist/templates/todo/apps/web/src/lib/server/ws-upgrade.test.js +134 -0
- package/dist/templates/todo/apps/web/vite.config.ts +17 -7
- package/dist/templates/todo/dot-agents/skills/podokit-deploy/SKILL.md +55 -10
- package/dist/templates/todo/dot-agents/skills/podokit-deploy/references/profile.md +47 -0
- package/dist/templates/todo/dot-agents/skills/podokit-deploy/references/release.md +64 -0
- package/dist/templates/todo/dot-agents/skills/podokit-deploy-fast/SKILL.md +92 -0
- package/dist/templates/todo/dot-claude/skills/podokit-deploy/SKILL.md +1 -1
- package/dist/templates/todo/dot-claude/skills/podokit-deploy-fast/SKILL.md +8 -0
- package/dist/templates/todo/dot-dockerignore +7 -0
- package/dist/templates/todo/dot-env.example +7 -0
- package/dist/templates/todo/dot-github/workflows/release.yml +142 -0
- package/dist/templates/todo/infra/k3s/README.md +18 -0
- package/dist/templates/todo/infra/k3s/api-deployment.yaml +6 -1
- package/dist/templates/todo/infra/k3s/ingress.yaml +8 -0
- package/dist/templates/todo/infra/k3s/web-deployment.yaml +11 -1
- package/package.json +1 -1
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { CommandRunner } from "./dev";
|
|
2
|
+
import type { DockerComposeProfileV1 } from "./deploy-compose-profile";
|
|
3
|
+
import type { ComposeImages, ComposeRuntime } from "./deploy-compose-render";
|
|
4
|
+
/**
|
|
5
|
+
* The primitives every docker-compose driver action runs commands through.
|
|
6
|
+
*
|
|
7
|
+
* They live apart from the driver because more than one action needs them and
|
|
8
|
+
* because two of them encode a rule that is easy to get wrong: which machine a
|
|
9
|
+
* command has to run on. See `sshDestination` below.
|
|
10
|
+
*/
|
|
11
|
+
export declare function defaultRunner(command: string, args: string[], options: {
|
|
12
|
+
capture: boolean;
|
|
13
|
+
}): ReturnType<CommandRunner>;
|
|
14
|
+
export declare function checked(runner: CommandRunner, command: string, args: string[], capture?: boolean): string;
|
|
15
|
+
export declare function parseJson<T>(value: string, description: string): T;
|
|
16
|
+
export declare function dockerArgs(profile: DockerComposeProfileV1, args: string[]): string[];
|
|
17
|
+
/**
|
|
18
|
+
* The SSH destination behind a `ssh://` Docker context, or null for a local one.
|
|
19
|
+
*
|
|
20
|
+
* This matters more than it looks. `docker compose` resolves `env_file` paths and
|
|
21
|
+
* expands `${VAR}` **where the CLI runs**, not where the daemon runs. Pointed at a
|
|
22
|
+
* remote context from a laptop, it looks for the host's secret files on the laptop,
|
|
23
|
+
* fails to find them, and interpolates every referenced variable to an empty string
|
|
24
|
+
* -- which is how a deployment silently comes up with a blank database password.
|
|
25
|
+
*
|
|
26
|
+
* So the Compose file is executed on the target instead: the whole point of keeping
|
|
27
|
+
* credentials in files on that host is that nothing else ever reads them.
|
|
28
|
+
*/
|
|
29
|
+
export declare function sshDestination(profile: DockerComposeProfileV1, runner: CommandRunner): string | null;
|
|
30
|
+
/** Where the rendered project lives on the target when the context is remote. */
|
|
31
|
+
export declare function remoteProjectDirectory(profile: DockerComposeProfileV1): string;
|
|
32
|
+
/**
|
|
33
|
+
* Run `docker compose` where the Compose file's own references resolve.
|
|
34
|
+
*
|
|
35
|
+
* Local context: here. Remote context: on the target, after copying the rendered
|
|
36
|
+
* file across -- because `env_file` paths and `${...}` expansion are resolved by
|
|
37
|
+
* whoever runs compose, not by the daemon it talks to.
|
|
38
|
+
*
|
|
39
|
+
* ⚠ This OVERWRITES the project file on the target, so it is for mutations only.
|
|
40
|
+
* A read-only query must never come through here: `podo deploy status` renders with
|
|
41
|
+
* a placeholder release to get a file path, and routing its `ps` through this
|
|
42
|
+
* replaced the applied project on the target with one naming an image tag that does
|
|
43
|
+
* not exist. Nothing failed at the time -- the next `compose up` on that host did,
|
|
44
|
+
* with "artifact ...:v0.0.0 not found", long after the status call that caused it.
|
|
45
|
+
* Read-only queries use `composeProjectPs` below, which needs no file at all.
|
|
46
|
+
*/
|
|
47
|
+
export declare function runCompose(profile: DockerComposeProfileV1, runtime: Pick<ComposeRuntime, "composeFile" | "migrationFile">, file: "compose" | "migration", args: string[], runner: CommandRunner, capture: boolean): string;
|
|
48
|
+
/**
|
|
49
|
+
* What the project is running, asked by project name rather than by file.
|
|
50
|
+
*
|
|
51
|
+
* Compose finds a project's containers from their labels, so this needs neither a
|
|
52
|
+
* rendered file nor a release tag -- and therefore cannot disturb the target. It
|
|
53
|
+
* also runs against the daemon through the context, so there is nothing here for
|
|
54
|
+
* `env_file` resolution to get wrong.
|
|
55
|
+
*/
|
|
56
|
+
export declare function composeProjectPs(profile: DockerComposeProfileV1, runner: CommandRunner): string;
|
|
57
|
+
export declare const STATE_MOUNT = "/podokit-state";
|
|
58
|
+
export declare const LOCK_PATH = "/podokit-state/deploy.lock";
|
|
59
|
+
export declare const LEDGER_PATH = "/podokit-state/releases.json";
|
|
60
|
+
export interface LedgerEntry {
|
|
61
|
+
revision: number;
|
|
62
|
+
release: string;
|
|
63
|
+
images: ComposeImages;
|
|
64
|
+
composeDocumentDigest: string;
|
|
65
|
+
rolloutStateDigest: string;
|
|
66
|
+
}
|
|
67
|
+
export interface Ledger {
|
|
68
|
+
schemaVersion: 1;
|
|
69
|
+
entries: LedgerEntry[];
|
|
70
|
+
}
|
|
71
|
+
export declare function currentLedgerEntry(ledger: Ledger): LedgerEntry | null;
|
|
72
|
+
export declare function readLedger(profile: DockerComposeProfileV1, image: string, runner: CommandRunner): Ledger;
|
|
73
|
+
export declare function stateVolumeName(profile: DockerComposeProfileV1): string;
|
|
74
|
+
/**
|
|
75
|
+
* An image the target can pull, for the checks that need a shell on the host.
|
|
76
|
+
*
|
|
77
|
+
* The release image is the right one whenever a release is known -- it is the image
|
|
78
|
+
* about to run, so proving it pulls is part of the check. Without one (a bare
|
|
79
|
+
* `podo deploy doctor` before anything is published) fall back to a managed
|
|
80
|
+
* dependency image, which the profile already pins by digest and the deployment
|
|
81
|
+
* needs anyway. Fabricating a release tag would ask the registry for something that
|
|
82
|
+
* can never exist.
|
|
83
|
+
*/
|
|
84
|
+
export declare function probeImage(profile: DockerComposeProfileV1, release?: string): string | null;
|
|
85
|
+
export declare function stateScript(profile: DockerComposeProfileV1, image: string, script: string): string[];
|
|
86
|
+
/**
|
|
87
|
+
* Who holds the deployment lock, for actions that must not run during an apply.
|
|
88
|
+
*
|
|
89
|
+
* Returns null both when nothing holds it and when the lock cannot be read at all --
|
|
90
|
+
* a project with no managed dependency has no image to borrow a shell from, and
|
|
91
|
+
* refusing every other action because of that would be worse than not checking.
|
|
92
|
+
*/
|
|
93
|
+
export declare function composeLockHolder(profile: DockerComposeProfileV1, runner: CommandRunner): string | null;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LEDGER_PATH = exports.LOCK_PATH = exports.STATE_MOUNT = void 0;
|
|
4
|
+
exports.defaultRunner = defaultRunner;
|
|
5
|
+
exports.checked = checked;
|
|
6
|
+
exports.parseJson = parseJson;
|
|
7
|
+
exports.dockerArgs = dockerArgs;
|
|
8
|
+
exports.sshDestination = sshDestination;
|
|
9
|
+
exports.remoteProjectDirectory = remoteProjectDirectory;
|
|
10
|
+
exports.runCompose = runCompose;
|
|
11
|
+
exports.composeProjectPs = composeProjectPs;
|
|
12
|
+
exports.currentLedgerEntry = currentLedgerEntry;
|
|
13
|
+
exports.readLedger = readLedger;
|
|
14
|
+
exports.stateVolumeName = stateVolumeName;
|
|
15
|
+
exports.probeImage = probeImage;
|
|
16
|
+
exports.stateScript = stateScript;
|
|
17
|
+
exports.composeLockHolder = composeLockHolder;
|
|
18
|
+
const node_child_process_1 = require("node:child_process");
|
|
19
|
+
/**
|
|
20
|
+
* The primitives every docker-compose driver action runs commands through.
|
|
21
|
+
*
|
|
22
|
+
* They live apart from the driver because more than one action needs them and
|
|
23
|
+
* because two of them encode a rule that is easy to get wrong: which machine a
|
|
24
|
+
* command has to run on. See `sshDestination` below.
|
|
25
|
+
*/
|
|
26
|
+
function defaultRunner(command, args, options) {
|
|
27
|
+
const result = (0, node_child_process_1.spawnSync)(command, args, {
|
|
28
|
+
encoding: "utf8",
|
|
29
|
+
stdio: options.capture ? "pipe" : "inherit",
|
|
30
|
+
});
|
|
31
|
+
if (result.error)
|
|
32
|
+
throw result.error;
|
|
33
|
+
return {
|
|
34
|
+
status: result.status,
|
|
35
|
+
stdout: options.capture ? (result.stdout ?? "") : "",
|
|
36
|
+
stderr: options.capture ? (result.stderr ?? "") : "",
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function checked(runner, command, args, capture = true) {
|
|
40
|
+
const result = runner(command, args, { capture });
|
|
41
|
+
if (result.status !== 0) {
|
|
42
|
+
const detail = capture ? result.stderr.trim() || result.stdout.trim() : "";
|
|
43
|
+
throw new Error(`${command} ${args.join(" ")} failed with status ${String(result.status)}${detail ? `: ${detail}` : ""}`);
|
|
44
|
+
}
|
|
45
|
+
return result.stdout;
|
|
46
|
+
}
|
|
47
|
+
function parseJson(value, description) {
|
|
48
|
+
if (!value.trim())
|
|
49
|
+
throw new Error(`${description} returned empty JSON.`);
|
|
50
|
+
try {
|
|
51
|
+
return JSON.parse(value);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
throw new Error(`${description} returned invalid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function dockerArgs(profile, args) {
|
|
58
|
+
return ["--context", profile.target.context, ...args];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The SSH destination behind a `ssh://` Docker context, or null for a local one.
|
|
62
|
+
*
|
|
63
|
+
* This matters more than it looks. `docker compose` resolves `env_file` paths and
|
|
64
|
+
* expands `${VAR}` **where the CLI runs**, not where the daemon runs. Pointed at a
|
|
65
|
+
* remote context from a laptop, it looks for the host's secret files on the laptop,
|
|
66
|
+
* fails to find them, and interpolates every referenced variable to an empty string
|
|
67
|
+
* -- which is how a deployment silently comes up with a blank database password.
|
|
68
|
+
*
|
|
69
|
+
* So the Compose file is executed on the target instead: the whole point of keeping
|
|
70
|
+
* credentials in files on that host is that nothing else ever reads them.
|
|
71
|
+
*/
|
|
72
|
+
function sshDestination(profile, runner) {
|
|
73
|
+
const output = runner("docker", ["context", "inspect", profile.target.context, "--format", "{{.Endpoints.docker.Host}}"], { capture: true });
|
|
74
|
+
if (output.status !== 0)
|
|
75
|
+
return null;
|
|
76
|
+
const host = output.stdout.trim();
|
|
77
|
+
return host.startsWith("ssh://") ? host.slice("ssh://".length) : null;
|
|
78
|
+
}
|
|
79
|
+
/** Where the rendered project lives on the target when the context is remote. */
|
|
80
|
+
function remoteProjectDirectory(profile) {
|
|
81
|
+
return `.local/share/podokit/${profile.target.project}`;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Run `docker compose` where the Compose file's own references resolve.
|
|
85
|
+
*
|
|
86
|
+
* Local context: here. Remote context: on the target, after copying the rendered
|
|
87
|
+
* file across -- because `env_file` paths and `${...}` expansion are resolved by
|
|
88
|
+
* whoever runs compose, not by the daemon it talks to.
|
|
89
|
+
*
|
|
90
|
+
* ⚠ This OVERWRITES the project file on the target, so it is for mutations only.
|
|
91
|
+
* A read-only query must never come through here: `podo deploy status` renders with
|
|
92
|
+
* a placeholder release to get a file path, and routing its `ps` through this
|
|
93
|
+
* replaced the applied project on the target with one naming an image tag that does
|
|
94
|
+
* not exist. Nothing failed at the time -- the next `compose up` on that host did,
|
|
95
|
+
* with "artifact ...:v0.0.0 not found", long after the status call that caused it.
|
|
96
|
+
* Read-only queries use `composeProjectPs` below, which needs no file at all.
|
|
97
|
+
*/
|
|
98
|
+
function runCompose(profile, runtime, file, args, runner, capture) {
|
|
99
|
+
const localFile = file === "compose" ? runtime.composeFile : runtime.migrationFile;
|
|
100
|
+
const destination = sshDestination(profile, runner);
|
|
101
|
+
if (!destination) {
|
|
102
|
+
return checked(runner, "docker", dockerArgs(profile, ["compose", "-f", localFile, ...args]), capture);
|
|
103
|
+
}
|
|
104
|
+
const directory = remoteProjectDirectory(profile);
|
|
105
|
+
const remoteFile = `${directory}/${file === "compose" ? "compose.yaml" : "compose.migrate.yaml"}`;
|
|
106
|
+
checked(runner, "ssh", [destination, `mkdir -p ${directory}`], true);
|
|
107
|
+
checked(runner, "scp", ["-q", localFile, `${destination}:${remoteFile}`], true);
|
|
108
|
+
return checked(runner, "ssh", [destination, ["docker", "compose", "-f", remoteFile, ...args].join(" ")], capture);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* What the project is running, asked by project name rather than by file.
|
|
112
|
+
*
|
|
113
|
+
* Compose finds a project's containers from their labels, so this needs neither a
|
|
114
|
+
* rendered file nor a release tag -- and therefore cannot disturb the target. It
|
|
115
|
+
* also runs against the daemon through the context, so there is nothing here for
|
|
116
|
+
* `env_file` resolution to get wrong.
|
|
117
|
+
*/
|
|
118
|
+
function composeProjectPs(profile, runner) {
|
|
119
|
+
const result = runner("docker", dockerArgs(profile, ["compose", "-p", profile.target.project, "ps", "--format", "json"]), { capture: true });
|
|
120
|
+
return result.status === 0 ? result.stdout.trim() : "";
|
|
121
|
+
}
|
|
122
|
+
exports.STATE_MOUNT = "/podokit-state";
|
|
123
|
+
exports.LOCK_PATH = `${exports.STATE_MOUNT}/deploy.lock`;
|
|
124
|
+
exports.LEDGER_PATH = `${exports.STATE_MOUNT}/releases.json`;
|
|
125
|
+
function currentLedgerEntry(ledger) {
|
|
126
|
+
return ledger.entries.length ? (ledger.entries[ledger.entries.length - 1] ?? null) : null;
|
|
127
|
+
}
|
|
128
|
+
function readLedger(profile, image, runner) {
|
|
129
|
+
const result = runner("docker", stateScript(profile, image, `cat ${exports.LEDGER_PATH} 2>/dev/null || echo ""`), { capture: true });
|
|
130
|
+
const body = result.status === 0 ? result.stdout.trim() : "";
|
|
131
|
+
if (!body)
|
|
132
|
+
return { schemaVersion: 1, entries: [] };
|
|
133
|
+
const parsed = parseJson(body, "Release ledger");
|
|
134
|
+
if (parsed.schemaVersion !== 1 || !Array.isArray(parsed.entries)) {
|
|
135
|
+
throw new Error("Release ledger is not a supported schema version 1 document.");
|
|
136
|
+
}
|
|
137
|
+
return parsed;
|
|
138
|
+
}
|
|
139
|
+
function stateVolumeName(profile) {
|
|
140
|
+
return `${profile.target.project}-podokit-state`;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* An image the target can pull, for the checks that need a shell on the host.
|
|
144
|
+
*
|
|
145
|
+
* The release image is the right one whenever a release is known -- it is the image
|
|
146
|
+
* about to run, so proving it pulls is part of the check. Without one (a bare
|
|
147
|
+
* `podo deploy doctor` before anything is published) fall back to a managed
|
|
148
|
+
* dependency image, which the profile already pins by digest and the deployment
|
|
149
|
+
* needs anyway. Fabricating a release tag would ask the registry for something that
|
|
150
|
+
* can never exist.
|
|
151
|
+
*/
|
|
152
|
+
function probeImage(profile, release) {
|
|
153
|
+
if (release)
|
|
154
|
+
return `${profile.release.apiRepository}:${release}`;
|
|
155
|
+
for (const dependency of [
|
|
156
|
+
profile.dependencies.postgres,
|
|
157
|
+
profile.dependencies.redis,
|
|
158
|
+
profile.dependencies.objectStorage,
|
|
159
|
+
]) {
|
|
160
|
+
if (dependency.mode === "managed")
|
|
161
|
+
return dependency.image;
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
function stateScript(profile, image, script) {
|
|
166
|
+
return dockerArgs(profile, [
|
|
167
|
+
"run",
|
|
168
|
+
"--rm",
|
|
169
|
+
"--network",
|
|
170
|
+
"none",
|
|
171
|
+
// The state volume is root-owned; the application images are not.
|
|
172
|
+
"--user",
|
|
173
|
+
"0:0",
|
|
174
|
+
"-v",
|
|
175
|
+
`${stateVolumeName(profile)}:${exports.STATE_MOUNT}`,
|
|
176
|
+
"--entrypoint",
|
|
177
|
+
"sh",
|
|
178
|
+
image,
|
|
179
|
+
"-c",
|
|
180
|
+
script,
|
|
181
|
+
]);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Who holds the deployment lock, for actions that must not run during an apply.
|
|
185
|
+
*
|
|
186
|
+
* Returns null both when nothing holds it and when the lock cannot be read at all --
|
|
187
|
+
* a project with no managed dependency has no image to borrow a shell from, and
|
|
188
|
+
* refusing every other action because of that would be worse than not checking.
|
|
189
|
+
*/
|
|
190
|
+
function composeLockHolder(profile, runner) {
|
|
191
|
+
const probe = probeImage(profile);
|
|
192
|
+
if (!probe)
|
|
193
|
+
return null;
|
|
194
|
+
const result = runner("docker", stateScript(profile, probe, `cat ${exports.LOCK_PATH} 2>/dev/null || true`), { capture: true });
|
|
195
|
+
if (result.status !== 0)
|
|
196
|
+
return null;
|
|
197
|
+
return result.stdout.trim() || null;
|
|
198
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { MigrationProfile, ReleaseProfile, VerificationProfile } from "./deploy-schema";
|
|
2
|
+
/**
|
|
3
|
+
* The Docker Compose deployment driver.
|
|
4
|
+
*
|
|
5
|
+
* Where the Kubernetes driver targets a cluster context and a namespace, this one
|
|
6
|
+
* targets a Docker context — which may be local or `ssh://` — and a Compose project.
|
|
7
|
+
* The boundary is the same in both: PodoKit operates the application, and never
|
|
8
|
+
* creates the host, the reverse proxy, the TLS certificate, or the secret values.
|
|
9
|
+
*/
|
|
10
|
+
export type ComposeDependencyMode = "managed" | "external" | "disabled";
|
|
11
|
+
/**
|
|
12
|
+
* Compose resource ceilings, in the units the Compose spec uses rather than the
|
|
13
|
+
* Kubernetes ones: `cpus` is a decimal core count and `memory` a byte suffix.
|
|
14
|
+
*/
|
|
15
|
+
export interface ComposeResourceProfile {
|
|
16
|
+
cpuLimit: string;
|
|
17
|
+
memoryLimit: string;
|
|
18
|
+
}
|
|
19
|
+
export interface ComposeWorkloadProfile {
|
|
20
|
+
replicas: number;
|
|
21
|
+
resources: ComposeResourceProfile;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A dependency PodoKit runs as a Compose service. Its credentials live in an env
|
|
25
|
+
* file on the target host — the profile records only the path and the key names,
|
|
26
|
+
* exactly as the Kubernetes driver records only Secret names and key names.
|
|
27
|
+
*/
|
|
28
|
+
export interface ComposeDependencyProfile {
|
|
29
|
+
mode: ComposeDependencyMode;
|
|
30
|
+
image: string;
|
|
31
|
+
envFile: string;
|
|
32
|
+
requiredKeys: string[];
|
|
33
|
+
volume: string;
|
|
34
|
+
}
|
|
35
|
+
export interface ComposeObjectStorageProfile extends ComposeDependencyProfile {
|
|
36
|
+
bucket: string;
|
|
37
|
+
clientImage: string;
|
|
38
|
+
}
|
|
39
|
+
export interface ComposeEnvFileProfile {
|
|
40
|
+
path: string;
|
|
41
|
+
requiredKeys: string[];
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Development-time artifact sync. Read only by `podo deploy sync`, never rendered
|
|
45
|
+
* into the Compose project — so adding it changes what that one command copies and
|
|
46
|
+
* nothing about what is deployed.
|
|
47
|
+
*
|
|
48
|
+
* `exclude` names project-relative paths the sync must leave alone. It exists
|
|
49
|
+
* because part of a build output can come from a toolchain the developer's machine
|
|
50
|
+
* does not have: overwriting that part with a local build replaces real artifacts
|
|
51
|
+
* with an index of artifacts that are no longer there, and the application keeps
|
|
52
|
+
* serving pages while the missing files 404.
|
|
53
|
+
*/
|
|
54
|
+
export interface ComposeSyncProfile {
|
|
55
|
+
exclude: string[];
|
|
56
|
+
}
|
|
57
|
+
export interface DockerComposeProfileV1 {
|
|
58
|
+
schemaVersion: 1;
|
|
59
|
+
driver: "docker-compose";
|
|
60
|
+
target: {
|
|
61
|
+
context: string;
|
|
62
|
+
endpointFingerprint: string;
|
|
63
|
+
project: string;
|
|
64
|
+
};
|
|
65
|
+
release: ReleaseProfile;
|
|
66
|
+
exposure: {
|
|
67
|
+
mode: "publishedPort";
|
|
68
|
+
host: string;
|
|
69
|
+
bindAddress: string;
|
|
70
|
+
port: number;
|
|
71
|
+
};
|
|
72
|
+
workloads: {
|
|
73
|
+
api: ComposeWorkloadProfile;
|
|
74
|
+
web: ComposeWorkloadProfile;
|
|
75
|
+
worker: ComposeWorkloadProfile | null;
|
|
76
|
+
};
|
|
77
|
+
dependencies: {
|
|
78
|
+
postgres: ComposeDependencyProfile;
|
|
79
|
+
redis: ComposeDependencyProfile;
|
|
80
|
+
objectStorage: ComposeObjectStorageProfile;
|
|
81
|
+
};
|
|
82
|
+
secrets: {
|
|
83
|
+
api: ComposeEnvFileProfile;
|
|
84
|
+
web: ComposeEnvFileProfile | null;
|
|
85
|
+
registryLogin: boolean;
|
|
86
|
+
};
|
|
87
|
+
migration?: MigrationProfile;
|
|
88
|
+
sync?: ComposeSyncProfile;
|
|
89
|
+
runtimeConfig: Record<string, string>;
|
|
90
|
+
verification: VerificationProfile;
|
|
91
|
+
}
|
|
92
|
+
export declare function parseComposeProfile(value: unknown): DockerComposeProfileV1;
|
|
93
|
+
export interface InitializeComposeProfileOptions {
|
|
94
|
+
context: string;
|
|
95
|
+
endpointFingerprint: string;
|
|
96
|
+
host?: string;
|
|
97
|
+
/** Directory on the target host holding the env files the deployment reads. */
|
|
98
|
+
secretsDirectory?: string;
|
|
99
|
+
}
|
|
100
|
+
export declare function buildDefaultComposeProfile(projectName: string, options: InitializeComposeProfileOptions): DockerComposeProfileV1;
|
|
101
|
+
export interface ComposeProfileSummary {
|
|
102
|
+
name: string;
|
|
103
|
+
path: string;
|
|
104
|
+
profile: DockerComposeProfileV1;
|
|
105
|
+
}
|
|
106
|
+
export declare function initializeComposeProfile(projectRoot: string, name: string, options: InitializeComposeProfileOptions): ComposeProfileSummary;
|
|
107
|
+
export declare function composeProfileDigest(profile: DockerComposeProfileV1): string;
|