@geekmidas/cli 10.0.0-alpha.73 → 10.0.0-alpha.75
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/{LegacyStateStore-D_78eaHM.cjs → LegacyStateStore-BFnXUFlx.cjs} +2 -2
- package/dist/{LegacyStateStore-D_78eaHM.cjs.map → LegacyStateStore-BFnXUFlx.cjs.map} +1 -1
- package/dist/{LegacyStateStore-DTo7z924.mjs → LegacyStateStore-cpt3K1vx.mjs} +2 -2
- package/dist/{LegacyStateStore-DTo7z924.mjs.map → LegacyStateStore-cpt3K1vx.mjs.map} +1 -1
- package/dist/{LocalStateStore-CNjgkRk4.mjs → LocalStateStore-RH9F2ZEy.mjs} +2 -2
- package/dist/{LocalStateStore-CNjgkRk4.mjs.map → LocalStateStore-RH9F2ZEy.mjs.map} +1 -1
- package/dist/{LocalStateStore-BhrlQIXD.cjs → LocalStateStore-zQGZ9b4J.cjs} +2 -2
- package/dist/{LocalStateStore-BhrlQIXD.cjs.map → LocalStateStore-zQGZ9b4J.cjs.map} +1 -1
- package/dist/{S3StateStore-oCxRs6HM.cjs → S3StateStore-CT-ZMlQY.cjs} +2 -2
- package/dist/{S3StateStore-oCxRs6HM.cjs.map → S3StateStore-CT-ZMlQY.cjs.map} +1 -1
- package/dist/{S3StateStore-g2P7T_Mo.mjs → S3StateStore-DBeBc0c0.mjs} +2 -2
- package/dist/{S3StateStore-g2P7T_Mo.mjs.map → S3StateStore-DBeBc0c0.mjs.map} +1 -1
- package/dist/{SSMStateStore-DXp0m7XI.cjs → SSMStateStore-HmsH3c4v.cjs} +2 -2
- package/dist/{SSMStateStore-DXp0m7XI.cjs.map → SSMStateStore-HmsH3c4v.cjs.map} +1 -1
- package/dist/{SSMStateStore-CYenunmK.mjs → SSMStateStore-rnzNfOaV.mjs} +2 -2
- package/dist/{SSMStateStore-CYenunmK.mjs.map → SSMStateStore-rnzNfOaV.mjs.map} +1 -1
- package/dist/{SubscriberGenerator-DZk_pKDh.mjs → SubscriberGenerator-5looQ3gB.mjs} +18 -2
- package/dist/SubscriberGenerator-5looQ3gB.mjs.map +1 -0
- package/dist/{SubscriberGenerator-Bixm28fN.cjs → SubscriberGenerator-CajwOa5W.cjs} +29 -1
- package/dist/SubscriberGenerator-CajwOa5W.cjs.map +1 -0
- package/dist/{add-C1ojB2Yy.cjs → add-CHBin8TF.cjs} +2 -2
- package/dist/{add-C1ojB2Yy.cjs.map → add-CHBin8TF.cjs.map} +1 -1
- package/dist/{add-BiPlBUq3.mjs → add-Cvzdw8cf.mjs} +2 -2
- package/dist/{add-BiPlBUq3.mjs.map → add-Cvzdw8cf.mjs.map} +1 -1
- package/dist/{bundler-C35h0WKt.cjs → bundler-_8AEZhrs.cjs} +5 -5
- package/dist/{bundler-C35h0WKt.cjs.map → bundler-_8AEZhrs.cjs.map} +1 -1
- package/dist/{bundler-C2LZoLdP.mjs → bundler-xjp5RwRS.mjs} +5 -5
- package/dist/{bundler-C2LZoLdP.mjs.map → bundler-xjp5RwRS.mjs.map} +1 -1
- package/dist/{cli-DJVL2sL9.cjs → cli-B4ktxAsM.cjs} +3 -3
- package/dist/{cli-DJVL2sL9.cjs.map → cli-B4ktxAsM.cjs.map} +1 -1
- package/dist/{cli-BfdfyTF6.mjs → cli-BTGfA0B7.mjs} +3 -3
- package/dist/{cli-BfdfyTF6.mjs.map → cli-BTGfA0B7.mjs.map} +1 -1
- package/dist/{compose-9II-0IsK.mjs → compose-DdH2JQJD.mjs} +418 -159
- package/dist/compose-DdH2JQJD.mjs.map +1 -0
- package/dist/{compose-CNnw8meq.cjs → compose-UKyv_1yU.cjs} +440 -157
- package/dist/compose-UKyv_1yU.cjs.map +1 -0
- package/dist/{config-RLuR2o9A.d.cts → config-B38YWicj.d.cts} +3 -3
- package/dist/{config-B8OR_TrO.d.mts.map → config-B38YWicj.d.cts.map} +1 -1
- package/dist/{config-B8OR_TrO.d.mts → config-DA0kRTQe.d.mts} +3 -3
- package/dist/{config-RLuR2o9A.d.cts.map → config-DA0kRTQe.d.mts.map} +1 -1
- package/dist/config.d.cts +3 -3
- package/dist/config.d.mts +3 -3
- package/dist/deploy/public.cjs +7 -6
- package/dist/deploy/public.d.cts +14 -3
- package/dist/deploy/public.d.cts.map +1 -1
- package/dist/deploy/public.d.mts +14 -3
- package/dist/deploy/public.d.mts.map +1 -1
- package/dist/deploy/public.mjs +7 -7
- package/dist/{deploy-nmov4j0B.mjs → deploy-CuSsiFIc.mjs} +21 -5
- package/dist/deploy-CuSsiFIc.mjs.map +1 -0
- package/dist/{deploy-BitZLdnY.cjs → deploy-XUZoA1us.cjs} +26 -4
- package/dist/deploy-XUZoA1us.cjs.map +1 -0
- package/dist/{dokploy-Ce_VTytN.cjs → dokploy-Cvgg_pID.cjs} +2 -2
- package/dist/{dokploy-Ce_VTytN.cjs.map → dokploy-Cvgg_pID.cjs.map} +1 -1
- package/dist/{dokploy-CZ1vZt21.mjs → dokploy-DZXTQX10.mjs} +2 -2
- package/dist/{dokploy-CZ1vZt21.mjs.map → dokploy-DZXTQX10.mjs.map} +1 -1
- package/dist/drivers-BdvtCKc7.cjs +200 -0
- package/dist/drivers-BdvtCKc7.cjs.map +1 -0
- package/dist/drivers-Ch1rJitn.mjs +177 -0
- package/dist/drivers-Ch1rJitn.mjs.map +1 -0
- package/dist/{encryption-CmfPICSN.mjs → encryption-JkeMPugf.mjs} +2 -2
- package/dist/{encryption-CmfPICSN.mjs.map → encryption-JkeMPugf.mjs.map} +1 -1
- package/dist/{encryption-CDGRdcMr.cjs → encryption-w-Ehh9DW.cjs} +2 -2
- package/dist/{encryption-CDGRdcMr.cjs.map → encryption-w-Ehh9DW.cjs.map} +1 -1
- package/dist/{engine-B37qBO5W.mjs → engine-CmozkHxq.mjs} +192 -4
- package/dist/engine-CmozkHxq.mjs.map +1 -0
- package/dist/{engine-DL2-PW6m.cjs → engine-zpWBMsQq.cjs} +195 -7
- package/dist/engine-zpWBMsQq.cjs.map +1 -0
- package/dist/{github-C6qF8kHg.cjs → github-BGdJ-Lnm.cjs} +2 -2
- package/dist/{github-C6qF8kHg.cjs.map → github-BGdJ-Lnm.cjs.map} +1 -1
- package/dist/{github-hjHHm7jM.mjs → github-BNngHoId.mjs} +2 -2
- package/dist/{github-hjHHm7jM.mjs.map → github-BNngHoId.mjs.map} +1 -1
- package/dist/{index-DHyTqGG3.d.mts → index-B-1lLGzy.d.mts} +2 -2
- package/dist/{index-DHyTqGG3.d.mts.map → index-B-1lLGzy.d.mts.map} +1 -1
- package/dist/{index-Dg-UZ4Hm.d.cts → index-vMidfqhU.d.cts} +2 -2
- package/dist/{index-Dg-UZ4Hm.d.cts.map → index-vMidfqhU.d.cts.map} +1 -1
- package/dist/index.cjs +89 -52
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +86 -49
- package/dist/index.mjs.map +1 -1
- package/dist/{migrate-CFFC3esE.mjs → migrate-BLL1RTAj.mjs} +2 -2
- package/dist/{migrate-CFFC3esE.mjs.map → migrate-BLL1RTAj.mjs.map} +1 -1
- package/dist/{migrate-kA_j1TPK.cjs → migrate-CgjlYeun.cjs} +2 -2
- package/dist/{migrate-kA_j1TPK.cjs.map → migrate-CgjlYeun.cjs.map} +1 -1
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.mts +1 -1
- package/dist/{ready-BIuMw6wH.cjs → ready-BB9xS4S9.cjs} +6 -188
- package/dist/ready-BB9xS4S9.cjs.map +1 -0
- package/dist/{ready-C0U0NYfB.mjs → ready-ZZoNskrr.mjs} +5 -169
- package/dist/ready-ZZoNskrr.mjs.map +1 -0
- package/dist/reconcile/public.d.cts +1 -1
- package/dist/reconcile/public.d.mts +1 -1
- package/dist/{resolve-CV4eQkDa.mjs → resolve-DZNx7rLy.mjs} +3 -3
- package/dist/{resolve-CV4eQkDa.mjs.map → resolve-DZNx7rLy.mjs.map} +1 -1
- package/dist/{resolve-E5RfY0bm.cjs → resolve-fevXn-wZ.cjs} +3 -3
- package/dist/{resolve-E5RfY0bm.cjs.map → resolve-fevXn-wZ.cjs.map} +1 -1
- package/dist/{rollback-C-V-Rf4a.mjs → rollback-CLi5Pln_.mjs} +4 -4
- package/dist/{rollback-C-V-Rf4a.mjs.map → rollback-CLi5Pln_.mjs.map} +1 -1
- package/dist/{rollback-QGwomD64.cjs → rollback-Dw-FW3X2.cjs} +4 -4
- package/dist/{rollback-QGwomD64.cjs.map → rollback-Dw-FW3X2.cjs.map} +1 -1
- package/dist/{runtime-CWYLIeCU.d.mts → runtime-BzZD5HHi.d.cts} +2 -2
- package/dist/{runtime-CWYLIeCU.d.mts.map → runtime-BzZD5HHi.d.cts.map} +1 -1
- package/dist/{runtime-CmuQOT6A.d.cts → runtime-Cm6J6L7n.d.mts} +2 -2
- package/dist/{runtime-CmuQOT6A.d.cts.map → runtime-Cm6J6L7n.d.mts.map} +1 -1
- package/dist/{sniffer-BFmdZpZ2.mjs → sniffer-BmCvVyhq.mjs} +59 -7
- package/dist/sniffer-BmCvVyhq.mjs.map +1 -0
- package/dist/{sniffer-C-T7dSMN.cjs → sniffer-bMzrX4cO.cjs} +75 -5
- package/dist/{sniffer-C-T7dSMN.cjs.map → sniffer-bMzrX4cO.cjs.map} +1 -1
- package/dist/{state-DtS2HEfi.mjs → state-BrWIOce_.mjs} +60 -293
- package/dist/state-BrWIOce_.mjs.map +1 -0
- package/dist/{state-DMjFPOhf.cjs → state-kA4Fkufx.cjs} +70 -321
- package/dist/state-kA4Fkufx.cjs.map +1 -0
- package/dist/target/public.cjs +1 -1
- package/dist/target/public.d.cts +2 -2
- package/dist/target/public.d.mts +2 -2
- package/dist/target/public.mjs +1 -1
- package/dist/{types-BLdCL4fB.d.mts → types-Dp9lgx_S.d.mts} +6 -2
- package/dist/{types-BLdCL4fB.d.mts.map → types-Dp9lgx_S.d.mts.map} +1 -1
- package/dist/{types-DwCQCkC-.d.cts → types-TzCChNrA.d.cts} +6 -2
- package/dist/{types-DwCQCkC-.d.cts.map → types-TzCChNrA.d.cts.map} +1 -1
- package/dist/vitest.cjs +1 -1
- package/dist/vitest.mjs +1 -1
- package/dist/workspace/index.d.cts +2 -2
- package/dist/workspace/index.d.mts +2 -2
- package/package.json +17 -17
- package/dist/SubscriberGenerator-Bixm28fN.cjs.map +0 -1
- package/dist/SubscriberGenerator-DZk_pKDh.mjs.map +0 -1
- package/dist/compose-9II-0IsK.mjs.map +0 -1
- package/dist/compose-CNnw8meq.cjs.map +0 -1
- package/dist/deploy-BitZLdnY.cjs.map +0 -1
- package/dist/deploy-nmov4j0B.mjs.map +0 -1
- package/dist/engine-B37qBO5W.mjs.map +0 -1
- package/dist/engine-DL2-PW6m.cjs.map +0 -1
- package/dist/ready-BIuMw6wH.cjs.map +0 -1
- package/dist/ready-C0U0NYfB.mjs.map +0 -1
- package/dist/sniffer-BFmdZpZ2.mjs.map +0 -1
- package/dist/state-DMjFPOhf.cjs.map +0 -1
- package/dist/state-DtS2HEfi.mjs.map +0 -1
|
@@ -4,13 +4,14 @@ import { r as discover } from "./discover-BPelzeGW.mjs";
|
|
|
4
4
|
import { t as appKey } from "./derive-B4vDpzgP.mjs";
|
|
5
5
|
import { M as LOGS_PORT, N as resolveLogs } from "./workspace-C7NqCsQF.mjs";
|
|
6
6
|
import { t as defineTarget } from "./define-DR8xGYfL.mjs";
|
|
7
|
-
import { $ as
|
|
8
|
-
import { i as DEFAULT_EVENTS
|
|
7
|
+
import { $t as imageRef, At as DEFAULT_IMAGES, B as suppliedOnly, C as NoDomainForStage, Ct as WORKER_PORT, D as otelEnv, E as isOtlpDestination, N as assertExternalServices, Nt as portKeys, P as devServicesUsed, Pt as primaryPortKey, R as reportDevServices, S as validateImageRef, St as TURBO_VERSION, T as resolveHost, Tt as findBuildRoot, V as workerUnits, Y as constructGlobs, _ as appDockerfile, _t as planFor, a as recordRelease, b as workerDockerfile, bt as composeBuildPaths, ct as composeFor, dt as appEnvKeys, gt as localRolePassword, ht as envFor, in as initStageSecrets, it as bucketPolicies, lt as bucketClient, m as withGeneratedSecrets, mt as workerEnvKeys, nt as applyPolicies, ot as postgresStatements, p as DeployJournal, pt as networkEnv, rt as applyPostgres, t as createEmptyState, tt as applyBuckets, ut as pgClient, w as isMainFrontendApp, xt as imageLayout, y as ensureDockerignore, yt as hostFor } from "./state-BrWIOce_.mjs";
|
|
8
|
+
import { i as DEFAULT_EVENTS } from "./SubscriberGenerator-5looQ3gB.mjs";
|
|
9
|
+
import { i as installedFrom } from "./drivers-Ch1rJitn.mjs";
|
|
9
10
|
import { u as migrateDatabases } from "./databases-hUHTuwhx.mjs";
|
|
10
11
|
import { n as assertStageCredentials } from "./credentialSchemas-BJqRJNK4.mjs";
|
|
11
12
|
import { dirname, join } from "node:path";
|
|
12
13
|
import { existsSync } from "node:fs";
|
|
13
|
-
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
14
|
+
import { chmod, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
14
15
|
import { hostname, userInfo } from "node:os";
|
|
15
16
|
import { provideKey, provisionOrder, publicEnvFor } from "@geekmidas/manifest";
|
|
16
17
|
import { spawn } from "node:child_process";
|
|
@@ -63,6 +64,33 @@ var ServicePortUnknown = class extends Error {
|
|
|
63
64
|
this.name = "ServicePortUnknown";
|
|
64
65
|
}
|
|
65
66
|
};
|
|
67
|
+
/**
|
|
68
|
+
* Whether `ref` names a registry on this machine's loopback — which the
|
|
69
|
+
* daemon pulls from and pushes to over plain HTTP by default, and `docker
|
|
70
|
+
* manifest inspect` reaches only when told `--insecure` (without it, a local
|
|
71
|
+
* registry's image is reported as no such manifest).
|
|
72
|
+
*/
|
|
73
|
+
function isLoopbackRegistry(ref) {
|
|
74
|
+
const first = ref.split("/")[0];
|
|
75
|
+
if (!ref.includes("/") || !/[.:]|^localhost$/.test(first)) return false;
|
|
76
|
+
const host = first.replace(/:\d+$/, "");
|
|
77
|
+
return host === "localhost" || /^127(?:\.\d{1,3}){3}$/.test(host);
|
|
78
|
+
}
|
|
79
|
+
/** `docker push` finished without saying what digest the registry stored. */
|
|
80
|
+
var PushDigestUnknown = class extends Error {
|
|
81
|
+
ref;
|
|
82
|
+
output;
|
|
83
|
+
constructor(ref, output) {
|
|
84
|
+
super(`Pushed ${ref}, but docker did not say which digest the registry stored (it said: ${output.trim().split("\n").slice(-3).join(" / ") || "nothing"}). Check the push with \`docker buildx imagetools inspect ${ref}\`.`);
|
|
85
|
+
this.ref = ref;
|
|
86
|
+
this.output = output;
|
|
87
|
+
this.name = "PushDigestUnknown";
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
/** The digest `docker push` reports last: `<tag>: digest: sha256:… size: …`. */
|
|
91
|
+
function pushedDigest(output) {
|
|
92
|
+
return [...output.matchAll(/digest: (sha256:[0-9a-f]{64})/g)].at(-1)?.[1];
|
|
93
|
+
}
|
|
66
94
|
function compose(stack, args) {
|
|
67
95
|
return [
|
|
68
96
|
"compose",
|
|
@@ -126,6 +154,7 @@ const dockerCompose = {
|
|
|
126
154
|
const { code, stdout, stderr } = await capture("docker", [
|
|
127
155
|
"manifest",
|
|
128
156
|
"inspect",
|
|
157
|
+
...isLoopbackRegistry(ref) ? ["--insecure"] : [],
|
|
129
158
|
ref
|
|
130
159
|
]);
|
|
131
160
|
if (code === 0) return {
|
|
@@ -195,6 +224,15 @@ const dockerCompose = {
|
|
|
195
224
|
to
|
|
196
225
|
]), runOptions(stack));
|
|
197
226
|
},
|
|
227
|
+
async push(stack, ref) {
|
|
228
|
+
const output = await runOutput("docker", ["push", ref], {
|
|
229
|
+
cwd: stack.cwd,
|
|
230
|
+
...stack.signal ? { signal: stack.signal } : {}
|
|
231
|
+
});
|
|
232
|
+
const digest = pushedDigest(output);
|
|
233
|
+
if (!digest) throw new PushDigestUnknown(ref, output);
|
|
234
|
+
return digest;
|
|
235
|
+
},
|
|
198
236
|
async digest(ref) {
|
|
199
237
|
const { code, stdout } = await capture("docker", [
|
|
200
238
|
"image",
|
|
@@ -265,6 +303,92 @@ async function assertImagesExist(docker, tag, images) {
|
|
|
265
303
|
const missing = lookups.filter((lookup) => lookup.status === "missing").map((lookup) => lookup.ref);
|
|
266
304
|
if (missing.length > 0) throw new ImageTagNotFound(tag, missing);
|
|
267
305
|
}
|
|
306
|
+
/**
|
|
307
|
+
* A push or a pull with no `deploy.registry` to push to or pull from.
|
|
308
|
+
*
|
|
309
|
+
* Without one an image is named `<namespace>/<project>-<app>:<tag>`, which
|
|
310
|
+
* Docker resolves to Docker Hub — so a release would be pushed to, or pulled
|
|
311
|
+
* from, somebody else's account. It is never a fallback.
|
|
312
|
+
*/
|
|
313
|
+
var RegistryRequired = class extends Error {
|
|
314
|
+
operation;
|
|
315
|
+
ref;
|
|
316
|
+
constructor(operation, ref) {
|
|
317
|
+
super(`${operation === "push" ? "Pushing" : "Pulling"} images needs a registry, and gkm.config.ts sets no deploy.registry: '${ref}' would be ${operation === "push" ? "pushed to" : "pulled from"} Docker Hub. Set deploy.registry (e.g. registry: 'ghcr.io/acme') in gkm.config.ts.` + (operation === "pull" ? " To run images built here instead, pass --build." : ""));
|
|
318
|
+
this.operation = operation;
|
|
319
|
+
this.ref = ref;
|
|
320
|
+
this.name = "RegistryRequired";
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
/** An image pinned to its digest: `<ref>@sha256:…`. */
|
|
324
|
+
function pinnedRef(ref, digest) {
|
|
325
|
+
return `${ref.split("@")[0]}@${digest}`;
|
|
326
|
+
}
|
|
327
|
+
/** A digests file with no entry for an app the stack runs. */
|
|
328
|
+
var ImageDigestMissing = class extends Error {
|
|
329
|
+
file;
|
|
330
|
+
apps;
|
|
331
|
+
constructor(file, apps) {
|
|
332
|
+
super(`${file} pins no digest for ${apps.join(", ")}. Use the file the same release's \`gkm compose --build --push --digests-file\` wrote, or run without --digests-file to pull by tag.`);
|
|
333
|
+
this.file = file;
|
|
334
|
+
this.apps = apps;
|
|
335
|
+
this.name = "ImageDigestMissing";
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
/** A digests file entry that is not this app's image. */
|
|
339
|
+
var ImageDigestMismatch = class extends Error {
|
|
340
|
+
app;
|
|
341
|
+
expected;
|
|
342
|
+
found;
|
|
343
|
+
constructor(app, expected, found) {
|
|
344
|
+
super(`The digests file pins '${app}' to ${found}, but this release runs ${expected}. It was written for another registry, project, stage or tag; use the file this stage's push of this tag wrote.`);
|
|
345
|
+
this.app = app;
|
|
346
|
+
this.expected = expected;
|
|
347
|
+
this.found = found;
|
|
348
|
+
this.name = "ImageDigestMismatch";
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
/** A digests file that is not a JSON object of app to pinned ref. */
|
|
352
|
+
var ImageDigestsInvalid = class extends Error {
|
|
353
|
+
file;
|
|
354
|
+
detail;
|
|
355
|
+
constructor(file, detail) {
|
|
356
|
+
super(`${file} is not a digests file (${detail}). It should be the JSON object \`gkm compose --build --push --digests-file\` writes: { "api": "<registry>/<image>:<tag>@sha256:…" }.`);
|
|
357
|
+
this.file = file;
|
|
358
|
+
this.detail = detail;
|
|
359
|
+
this.name = "ImageDigestsInvalid";
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
/** Parse a digests file's content, checking its shape. */
|
|
363
|
+
function parseDigests(file, content) {
|
|
364
|
+
let parsed;
|
|
365
|
+
try {
|
|
366
|
+
parsed = JSON.parse(content);
|
|
367
|
+
} catch (error) {
|
|
368
|
+
throw new ImageDigestsInvalid(file, error instanceof Error ? error.message : String(error));
|
|
369
|
+
}
|
|
370
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new ImageDigestsInvalid(file, "not a JSON object");
|
|
371
|
+
for (const [app, value] of Object.entries(parsed)) if (typeof value !== "string" || !/@sha256:[0-9a-f]{64}$/.test(value)) throw new ImageDigestsInvalid(file, `'${app}' is not an <image>:<tag>@sha256:… ref`);
|
|
372
|
+
return parsed;
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Each image pinned to the digest the file names for it.
|
|
376
|
+
*
|
|
377
|
+
* @throws {ImageDigestMissing} naming every app the file has no entry for
|
|
378
|
+
* @throws {ImageDigestMismatch} when an entry is another image or tag
|
|
379
|
+
*/
|
|
380
|
+
function pinImages(file, digests, images) {
|
|
381
|
+
const missing = images.filter((image) => !digests[image.app]).map((image) => image.app);
|
|
382
|
+
if (missing.length > 0) throw new ImageDigestMissing(file, missing);
|
|
383
|
+
return images.map((image) => {
|
|
384
|
+
const pinned = digests[image.app];
|
|
385
|
+
if (pinned.split("@")[0] !== image.ref) throw new ImageDigestMismatch(image.app, image.ref, pinned);
|
|
386
|
+
return {
|
|
387
|
+
...image,
|
|
388
|
+
ref: pinned
|
|
389
|
+
};
|
|
390
|
+
});
|
|
391
|
+
}
|
|
268
392
|
//#endregion
|
|
269
393
|
//#region src/compose/logs.ts
|
|
270
394
|
/**
|
|
@@ -444,6 +568,152 @@ function logsAccess(logs, ctx) {
|
|
|
444
568
|
];
|
|
445
569
|
}
|
|
446
570
|
//#endregion
|
|
571
|
+
//#region src/compose/redis.ts
|
|
572
|
+
/**
|
|
573
|
+
* The stack's Redis: what every declared cache lives in when a workspace runs
|
|
574
|
+
* as a `gkm compose` stack, on the local stage and on a deployed one alike.
|
|
575
|
+
*
|
|
576
|
+
* A cache is the one store whose contents can always be rebuilt, so unlike
|
|
577
|
+
* Mailpit and MinIO it is a production service, not a dev stand-in: it runs
|
|
578
|
+
* for every stage, on the compose network only, with nothing published. It
|
|
579
|
+
* is bounded (`maxmemory`, evicting the least recently used key), persisted
|
|
580
|
+
* (an append-only file on a named volume, kept by `--down`) and password
|
|
581
|
+
* protected — the stage's own password, generated once like its seed.
|
|
582
|
+
*
|
|
583
|
+
* A stage that sets a cache's URL in its secrets — a managed Redis — keeps
|
|
584
|
+
* it, and a stack whose every cache is set that way runs no Redis at all.
|
|
585
|
+
*
|
|
586
|
+
* Pure: the stack decides it, and the phases keep the password.
|
|
587
|
+
*/
|
|
588
|
+
/**
|
|
589
|
+
* Where a stack's caches live: its own Redis, whatever the deploy target's
|
|
590
|
+
* default is. A stack is one machine running containers, and a Redis beside
|
|
591
|
+
* the apps is cheaper for a cache than a table in the database they share.
|
|
592
|
+
*/
|
|
593
|
+
const STACK_CACHE = "redis";
|
|
594
|
+
/** Its compose service name — the host every cache URL points at. */
|
|
595
|
+
const REDIS_SERVICE = "redis";
|
|
596
|
+
/** The port it answers on, inside the network. */
|
|
597
|
+
const REDIS_PORT = 6379;
|
|
598
|
+
/** Reconcile's pin, so `gkm dev` and the stack run one Redis. */
|
|
599
|
+
const REDIS_IMAGE = DEFAULT_IMAGES.redis;
|
|
600
|
+
/**
|
|
601
|
+
* How much it may hold before it evicts. A cache that grows until the box
|
|
602
|
+
* runs out of memory takes the apps with it; 256 MB is a lot of sessions and
|
|
603
|
+
* little of a small server. A project's `docker-compose.<stage>.yml` can
|
|
604
|
+
* replace the command to change it.
|
|
605
|
+
*/
|
|
606
|
+
const REDIS_MAXMEMORY = "256mb";
|
|
607
|
+
/** The stage secret holding its password. */
|
|
608
|
+
const REDIS_PASSWORD_KEY = "REDIS_PASSWORD";
|
|
609
|
+
/** Redis's own count of logical databases, before `--databases` raises it. */
|
|
610
|
+
const REDIS_DATABASES = 16;
|
|
611
|
+
/** A deployed stage that runs the stack's Redis and has no password yet. */
|
|
612
|
+
var RedisPasswordMissing = class extends Error {
|
|
613
|
+
stage;
|
|
614
|
+
constructor(stage) {
|
|
615
|
+
super(`The stage '${stage}' runs the stack's Redis and has no ${REDIS_PASSWORD_KEY} in its secrets. Run gkm compose --stage ${stage} without --dry-run once to generate it, or set one: gkm secrets:set ${REDIS_PASSWORD_KEY} '…' --stage ${stage}`);
|
|
616
|
+
this.stage = stage;
|
|
617
|
+
this.name = "RedisPasswordMissing";
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
/**
|
|
621
|
+
* Whether a stage's stack runs Redis: a cache is declared and the stage's
|
|
622
|
+
* secrets do not set the URL of every one.
|
|
623
|
+
*/
|
|
624
|
+
function runsRedis(manifest, custom) {
|
|
625
|
+
return Object.values(manifest).some((declaration) => declaration?.kind === "cache" && (declaration.provides ?? []).some((key) => custom[key] === void 0));
|
|
626
|
+
}
|
|
627
|
+
/** A password: 256 random bits, URL-safe, so it sits in a URL unescaped. */
|
|
628
|
+
function generateRedisPassword() {
|
|
629
|
+
return randomBytes(32).toString("base64url");
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* The stage's secrets with a generated Redis password, where it has none.
|
|
633
|
+
* Pure, like `withGeneratedSecrets`: the caller keeps the result once the run
|
|
634
|
+
* goes ahead.
|
|
635
|
+
*/
|
|
636
|
+
function withRedisPassword(secrets) {
|
|
637
|
+
if (secrets.custom?.["REDIS_PASSWORD"]) return {
|
|
638
|
+
secrets,
|
|
639
|
+
generated: []
|
|
640
|
+
};
|
|
641
|
+
return {
|
|
642
|
+
secrets: {
|
|
643
|
+
...secrets,
|
|
644
|
+
custom: {
|
|
645
|
+
...secrets.custom,
|
|
646
|
+
[REDIS_PASSWORD_KEY]: generateRedisPassword()
|
|
647
|
+
},
|
|
648
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
649
|
+
},
|
|
650
|
+
generated: [REDIS_PASSWORD_KEY]
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* The stack's Redis for one stage, or nothing where no cache needs it.
|
|
655
|
+
*
|
|
656
|
+
* Each cache it holds gets a logical database of its own, by the order of
|
|
657
|
+
* their ids, so two caches never read each other's keys.
|
|
658
|
+
*/
|
|
659
|
+
function stackRedis(options) {
|
|
660
|
+
const { plan, stage, local, custom } = options;
|
|
661
|
+
const caches = plan.resources.filter((r) => r.kind === "cache" && custom[r.envKey] === void 0).sort((a, b) => a.id.localeCompare(b.id));
|
|
662
|
+
if (caches.length === 0) return void 0;
|
|
663
|
+
const set = custom[REDIS_PASSWORD_KEY];
|
|
664
|
+
if (!set && !local) throw new RedisPasswordMissing(stage);
|
|
665
|
+
const password = set ?? "geekmidas";
|
|
666
|
+
const auth = encodeURIComponent(password);
|
|
667
|
+
return {
|
|
668
|
+
password,
|
|
669
|
+
passwordFromSecrets: Boolean(set),
|
|
670
|
+
urls: Object.fromEntries(caches.map((cache, index) => [cache.envKey, `redis://:${auth}@${REDIS_SERVICE}:${REDIS_PORT}/${index}`])),
|
|
671
|
+
env: {
|
|
672
|
+
[REDIS_PASSWORD_KEY]: password,
|
|
673
|
+
REDISCLI_AUTH: password
|
|
674
|
+
},
|
|
675
|
+
databases: Math.max(REDIS_DATABASES, caches.length)
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Redis's service: bounded, persisted and password protected, on the
|
|
680
|
+
* network alone.
|
|
681
|
+
*
|
|
682
|
+
* The command runs the image's own entrypoint with `redis-server` first, so
|
|
683
|
+
* it still drops to the `redis` user and owns `/data`; the password is read
|
|
684
|
+
* from the env file by the shell (`$$` is compose's escape for `$`), so
|
|
685
|
+
* neither the compose file nor the container's command holds it.
|
|
686
|
+
*/
|
|
687
|
+
function redisService(redis) {
|
|
688
|
+
const flags = [
|
|
689
|
+
`--requirepass "$$${REDIS_PASSWORD_KEY}"`,
|
|
690
|
+
`--maxmemory ${REDIS_MAXMEMORY}`,
|
|
691
|
+
"--maxmemory-policy allkeys-lru",
|
|
692
|
+
"--appendonly yes",
|
|
693
|
+
...redis.databases > REDIS_DATABASES ? [`--databases ${redis.databases}`] : []
|
|
694
|
+
];
|
|
695
|
+
return {
|
|
696
|
+
image: REDIS_IMAGE,
|
|
697
|
+
restart: "unless-stopped",
|
|
698
|
+
command: [
|
|
699
|
+
"sh",
|
|
700
|
+
"-c",
|
|
701
|
+
`exec docker-entrypoint.sh redis-server ${flags.join(" ")}`
|
|
702
|
+
],
|
|
703
|
+
env_file: [{
|
|
704
|
+
path: `./${REDIS_SERVICE}.env`,
|
|
705
|
+
format: "raw"
|
|
706
|
+
}],
|
|
707
|
+
volumes: ["redis-data:/data"],
|
|
708
|
+
healthcheck: {
|
|
709
|
+
test: ["CMD-SHELL", "redis-cli ping | grep -q PONG"],
|
|
710
|
+
interval: "5s",
|
|
711
|
+
timeout: "3s",
|
|
712
|
+
retries: 10
|
|
713
|
+
}
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
//#endregion
|
|
447
717
|
//#region src/compose/caddyfile.ts
|
|
448
718
|
/** The Caddyfile for a stack's edge. */
|
|
449
719
|
function edgeCaddyfile(sites, options) {
|
|
@@ -640,7 +910,7 @@ function composeStack(input) {
|
|
|
640
910
|
const plan = planFor(manifest, stage, provisionOrder(manifest), {
|
|
641
911
|
localStage: workspace.stages.local,
|
|
642
912
|
events: DEFAULT_EVENTS.server,
|
|
643
|
-
cache:
|
|
913
|
+
cache: STACK_CACHE,
|
|
644
914
|
edge: false
|
|
645
915
|
});
|
|
646
916
|
const byId = new Map(plan.resources.map((r) => [r.id, r]));
|
|
@@ -700,7 +970,13 @@ function composeStack(input) {
|
|
|
700
970
|
allow: input.allowDevServices ?? [],
|
|
701
971
|
...domain ? { domain } : {}
|
|
702
972
|
});
|
|
703
|
-
const
|
|
973
|
+
const redis = stackRedis({
|
|
974
|
+
plan,
|
|
975
|
+
stage,
|
|
976
|
+
local,
|
|
977
|
+
custom
|
|
978
|
+
});
|
|
979
|
+
const infra = [...plan.containers.filter((c) => !NOT_RUN[c]).filter((c) => c !== "minio" || services.minio.length > 0).filter((c) => c !== "mailpit" || services.mailpit.length > 0).filter((c) => c !== "redis" || redis !== void 0), ...logs ? [LOGS_SERVICE] : []].sort();
|
|
704
980
|
const storage = infra.includes("minio") ? (() => {
|
|
705
981
|
const buckets = services.minio.map((id) => byId.get(id)?.name).filter((name) => Boolean(name)).sort();
|
|
706
982
|
return {
|
|
@@ -764,6 +1040,7 @@ function composeStack(input) {
|
|
|
764
1040
|
return value;
|
|
765
1041
|
}
|
|
766
1042
|
if (custom[key] !== void 0) return custom[key];
|
|
1043
|
+
if (owner?.kind === "cache") return redis?.urls[key];
|
|
767
1044
|
const surface = surfaceUrls.get(key);
|
|
768
1045
|
if (app.kind === "rest-api" && surface && surface !== app.id) return inside[key];
|
|
769
1046
|
return outside[key];
|
|
@@ -796,6 +1073,7 @@ function composeStack(input) {
|
|
|
796
1073
|
if (app.build && Object.keys(args).length > 0) app.build.args = args;
|
|
797
1074
|
continue;
|
|
798
1075
|
}
|
|
1076
|
+
if (input.buildOnly) continue;
|
|
799
1077
|
const keys = appEnvKeys(manifest, app.name, input.runnables) ?? /* @__PURE__ */ new Set();
|
|
800
1078
|
const values = valuesFor(app, keys);
|
|
801
1079
|
const trusted = provideKey(app.id, "trustedOrigins");
|
|
@@ -816,7 +1094,7 @@ function composeStack(input) {
|
|
|
816
1094
|
};
|
|
817
1095
|
for (const [key, value] of Object.entries(app.env)) if (/[\r\n]/.test(value)) throw new EnvValueMultiline(app.name, key);
|
|
818
1096
|
}
|
|
819
|
-
for (const worker of workers) {
|
|
1097
|
+
for (const worker of input.buildOnly ? [] : workers) {
|
|
820
1098
|
const keys = workerEnvKeys(manifest, worker.id, input.runnables) ?? /* @__PURE__ */ new Set();
|
|
821
1099
|
worker.env = {
|
|
822
1100
|
...valuesFor({
|
|
@@ -844,11 +1122,13 @@ function composeStack(input) {
|
|
|
844
1122
|
master: credential.master,
|
|
845
1123
|
...storage ? { storage } : {},
|
|
846
1124
|
...logs ? { logs } : {},
|
|
1125
|
+
...redis ? { redis } : {},
|
|
847
1126
|
https,
|
|
848
1127
|
http,
|
|
849
1128
|
composeDir: join(workspace.root, stackDir(stage)),
|
|
850
1129
|
buildRoot: layout.buildRoot,
|
|
851
|
-
workspaceRoot: workspace.root
|
|
1130
|
+
workspaceRoot: workspace.root,
|
|
1131
|
+
envFiles: !input.buildOnly
|
|
852
1132
|
});
|
|
853
1133
|
const caddyfile = edgeCaddyfile([
|
|
854
1134
|
...apps.map((app) => ({
|
|
@@ -868,7 +1148,8 @@ function composeStack(input) {
|
|
|
868
1148
|
dockerfiles[app.build.dockerfile] = appDockerfile(app.name, workspace.apps[app.name], {
|
|
869
1149
|
layout,
|
|
870
1150
|
workspaceRoot: workspace.root,
|
|
871
|
-
manifest
|
|
1151
|
+
manifest,
|
|
1152
|
+
cache: STACK_CACHE
|
|
872
1153
|
});
|
|
873
1154
|
}
|
|
874
1155
|
for (const worker of workers) {
|
|
@@ -879,7 +1160,8 @@ function composeStack(input) {
|
|
|
879
1160
|
app: worker.host
|
|
880
1161
|
}, workspace.apps[worker.host], {
|
|
881
1162
|
layout,
|
|
882
|
-
workspaceRoot: workspace.root
|
|
1163
|
+
workspaceRoot: workspace.root,
|
|
1164
|
+
cache: STACK_CACHE
|
|
883
1165
|
});
|
|
884
1166
|
}
|
|
885
1167
|
return {
|
|
@@ -896,7 +1178,8 @@ function composeStack(input) {
|
|
|
896
1178
|
credential,
|
|
897
1179
|
...storage ? { storage } : {},
|
|
898
1180
|
devServices: local ? [] : devServicesUsed(services),
|
|
899
|
-
...logs ? { logs } : {}
|
|
1181
|
+
...logs ? { logs } : {},
|
|
1182
|
+
...redis ? { redis } : {}
|
|
900
1183
|
};
|
|
901
1184
|
}
|
|
902
1185
|
/**
|
|
@@ -1036,6 +1319,7 @@ function stackFile(options) {
|
|
|
1036
1319
|
};
|
|
1037
1320
|
minio.ports = ["127.0.0.1::9000"];
|
|
1038
1321
|
}
|
|
1322
|
+
if (services["redis"] && options.redis) services[REDIS_SERVICE] = redisService(options.redis);
|
|
1039
1323
|
if (options.logs) services[LOGS_SERVICE] = logsService(options.logs);
|
|
1040
1324
|
for (const app of apps) services[app.name] = {
|
|
1041
1325
|
image: app.ref,
|
|
@@ -1095,10 +1379,13 @@ function stackFile(options) {
|
|
|
1095
1379
|
retries: 5
|
|
1096
1380
|
}
|
|
1097
1381
|
};
|
|
1098
|
-
for (const service of Object.values(services))
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1382
|
+
for (const service of Object.values(services)) {
|
|
1383
|
+
if (!options.envFiles) delete service.env_file;
|
|
1384
|
+
service.logging = {
|
|
1385
|
+
driver: LOG_ROTATION.driver,
|
|
1386
|
+
options: { ...LOG_ROTATION.options }
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1102
1389
|
return {
|
|
1103
1390
|
name: project,
|
|
1104
1391
|
services,
|
|
@@ -1121,93 +1408,6 @@ function defaultLayout(root) {
|
|
|
1121
1408
|
gkmPaths: ["gkm.config.*"]
|
|
1122
1409
|
};
|
|
1123
1410
|
}
|
|
1124
|
-
/** The id the stack's compose file names an app's build credentials by. */
|
|
1125
|
-
function credentialsSecret(app) {
|
|
1126
|
-
return `${app}_credentials`;
|
|
1127
|
-
}
|
|
1128
|
-
/** The file beside the compose file an app's build credentials are read from. */
|
|
1129
|
-
function credentialsFile(app) {
|
|
1130
|
-
return `${app}.credentials`;
|
|
1131
|
-
}
|
|
1132
|
-
/**
|
|
1133
|
-
* The stack with each backend's image built with its encrypted credentials:
|
|
1134
|
-
* a BuildKit secret per app — the file `credentialsFile` names, mounted as
|
|
1135
|
-
* `gkm_credentials` — and the hash naming them as a build arg, so a new key
|
|
1136
|
-
* rebuilds the layer that embeds them. Each backend's env file gains the
|
|
1137
|
-
* `GKM_MASTER_KEY` that decrypts them, the way a Dokploy deploy injects it.
|
|
1138
|
-
*
|
|
1139
|
-
* Pure: the caller encrypted them.
|
|
1140
|
-
*/
|
|
1141
|
-
function withBuildCredentials(stack, credentials) {
|
|
1142
|
-
const services = { ...stack.compose.services };
|
|
1143
|
-
const secrets = { ...stack.compose.secrets };
|
|
1144
|
-
const apps = stack.apps.map((app) => {
|
|
1145
|
-
const own = credentials[app.name];
|
|
1146
|
-
const service = services[app.name];
|
|
1147
|
-
if (!own || !service?.build) return app;
|
|
1148
|
-
const [key, value] = own.buildArg.split("=");
|
|
1149
|
-
services[app.name] = {
|
|
1150
|
-
...service,
|
|
1151
|
-
build: {
|
|
1152
|
-
...service.build,
|
|
1153
|
-
args: {
|
|
1154
|
-
...service.build.args,
|
|
1155
|
-
[key]: value
|
|
1156
|
-
},
|
|
1157
|
-
secrets: [{
|
|
1158
|
-
source: credentialsSecret(app.name),
|
|
1159
|
-
target: "gkm_credentials"
|
|
1160
|
-
}]
|
|
1161
|
-
}
|
|
1162
|
-
};
|
|
1163
|
-
secrets[credentialsSecret(app.name)] = { file: `./${credentialsFile(app.name)}` };
|
|
1164
|
-
return {
|
|
1165
|
-
...app,
|
|
1166
|
-
env: {
|
|
1167
|
-
...app.env,
|
|
1168
|
-
GKM_MASTER_KEY: own.masterKey
|
|
1169
|
-
}
|
|
1170
|
-
};
|
|
1171
|
-
});
|
|
1172
|
-
const workers = stack.workers.map((worker) => {
|
|
1173
|
-
const own = credentials[worker.name];
|
|
1174
|
-
const service = services[worker.name];
|
|
1175
|
-
if (!own || !service?.build) return worker;
|
|
1176
|
-
const [key, value] = own.buildArg.split("=");
|
|
1177
|
-
services[worker.name] = {
|
|
1178
|
-
...service,
|
|
1179
|
-
build: {
|
|
1180
|
-
...service.build,
|
|
1181
|
-
args: {
|
|
1182
|
-
...service.build.args,
|
|
1183
|
-
[key]: value
|
|
1184
|
-
},
|
|
1185
|
-
secrets: [{
|
|
1186
|
-
source: credentialsSecret(worker.name),
|
|
1187
|
-
target: "gkm_credentials"
|
|
1188
|
-
}]
|
|
1189
|
-
}
|
|
1190
|
-
};
|
|
1191
|
-
secrets[credentialsSecret(worker.name)] = { file: `./${credentialsFile(worker.name)}` };
|
|
1192
|
-
return {
|
|
1193
|
-
...worker,
|
|
1194
|
-
env: {
|
|
1195
|
-
...worker.env,
|
|
1196
|
-
GKM_MASTER_KEY: own.masterKey
|
|
1197
|
-
}
|
|
1198
|
-
};
|
|
1199
|
-
});
|
|
1200
|
-
return {
|
|
1201
|
-
...stack,
|
|
1202
|
-
apps,
|
|
1203
|
-
workers,
|
|
1204
|
-
compose: {
|
|
1205
|
-
...stack.compose,
|
|
1206
|
-
services,
|
|
1207
|
-
...Object.keys(secrets).length ? { secrets } : {}
|
|
1208
|
-
}
|
|
1209
|
-
};
|
|
1210
|
-
}
|
|
1211
1411
|
/** Render an app's env file: one `KEY=value` per line, read raw by compose. */
|
|
1212
1412
|
function envFile(env) {
|
|
1213
1413
|
return `${Object.entries(env).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${key}=${value}`).join("\n")}\n`;
|
|
@@ -1303,6 +1503,20 @@ var ComposeAppsUnhealthy = class extends Error {
|
|
|
1303
1503
|
* injected, so the rules are asserted without a daemon; the defaults are the
|
|
1304
1504
|
* real ones.
|
|
1305
1505
|
*/
|
|
1506
|
+
/** `--push` without `--build`: there is nothing built here to push. */
|
|
1507
|
+
var ComposePushNeedsBuild = class extends Error {
|
|
1508
|
+
constructor() {
|
|
1509
|
+
super("--push pushes the images this checkout builds, so it needs --build (and never --pull): gkm compose --stage <stage> --build --push --tag <tag>.");
|
|
1510
|
+
this.name = "ComposePushNeedsBuild";
|
|
1511
|
+
}
|
|
1512
|
+
};
|
|
1513
|
+
/** Digests to run by, on a run that pulls nothing. */
|
|
1514
|
+
var ComposePinNeedsPull = class extends Error {
|
|
1515
|
+
constructor() {
|
|
1516
|
+
super("--digests-file pins the images a release pulls, so it needs --tag (or --pull) — or, with --build --push, it is where the pushed digests are written.");
|
|
1517
|
+
this.name = "ComposePinNeedsPull";
|
|
1518
|
+
}
|
|
1519
|
+
};
|
|
1306
1520
|
/** A port variable that does not hold a port. */
|
|
1307
1521
|
var EdgePortInvalid = class extends Error {
|
|
1308
1522
|
variable;
|
|
@@ -1381,10 +1595,15 @@ const defaultDeps = {
|
|
|
1381
1595
|
* every image it names exists. Nothing is written: a missing image stops the
|
|
1382
1596
|
* run here, before a file, a secret or a container is touched.
|
|
1383
1597
|
*/
|
|
1384
|
-
async function validateCompose(ctx, deps,
|
|
1598
|
+
async function validateCompose(ctx, deps, options = {}) {
|
|
1385
1599
|
const { workspace, stage, identity, tag } = ctx;
|
|
1386
1600
|
const root = workspace.root;
|
|
1387
|
-
const mode =
|
|
1601
|
+
const mode = options.mode ?? (ctx.tagGiven ? "pull" : "build");
|
|
1602
|
+
const push = options.push === true;
|
|
1603
|
+
if (push && mode !== "build") throw new ComposePushNeedsBuild();
|
|
1604
|
+
if (options.pin && mode !== "pull") throw new ComposePinNeedsPull();
|
|
1605
|
+
const registry = workspace.deploy?.registry;
|
|
1606
|
+
if (!registry && (push || mode === "pull")) throw new RegistryRequired(push ? "push" : "pull", imageRef(identity, ctx.apps[0] ?? workspace.name, void 0, tag));
|
|
1388
1607
|
const runnables = {};
|
|
1389
1608
|
const background = {};
|
|
1390
1609
|
const manifest = await discover({
|
|
@@ -1394,14 +1613,13 @@ async function validateCompose(ctx, deps, forced) {
|
|
|
1394
1613
|
background
|
|
1395
1614
|
});
|
|
1396
1615
|
const { secrets, generated } = await stageSecrets(ctx, manifest);
|
|
1397
|
-
await assertStageCredentials({
|
|
1616
|
+
if (!push) await assertStageCredentials({
|
|
1398
1617
|
root,
|
|
1399
1618
|
patterns: constructGlobs(workspace),
|
|
1400
1619
|
manifest,
|
|
1401
1620
|
stage,
|
|
1402
1621
|
supplied: secrets?.custom ?? {}
|
|
1403
1622
|
});
|
|
1404
|
-
const masterKeys = {};
|
|
1405
1623
|
const layout = mode === "build" ? imageLayout(workspace) : void 0;
|
|
1406
1624
|
const composed = composeStack({
|
|
1407
1625
|
workspace,
|
|
@@ -1413,51 +1631,19 @@ async function validateCompose(ctx, deps, forced) {
|
|
|
1413
1631
|
images: {
|
|
1414
1632
|
mode,
|
|
1415
1633
|
tag,
|
|
1416
|
-
...
|
|
1634
|
+
...registry ? { registry } : {}
|
|
1417
1635
|
},
|
|
1418
1636
|
secrets,
|
|
1419
1637
|
allowDevServices: ctx.allowDevServices,
|
|
1420
1638
|
ports: edgePorts(deps.env),
|
|
1421
|
-
...layout ? { layout } : {}
|
|
1639
|
+
...layout ? { layout } : {},
|
|
1640
|
+
...push ? { buildOnly: true } : {}
|
|
1422
1641
|
});
|
|
1423
1642
|
reportDevServices(ctx, composed.devServices);
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
for (const app of composed.apps) {
|
|
1427
|
-
if (app.kind !== "rest-api" || !app.env || !app.build) continue;
|
|
1428
|
-
const { encrypted, iv, masterKey } = encryptSecrets(app.env);
|
|
1429
|
-
credentials[app.name] = {
|
|
1430
|
-
encrypted,
|
|
1431
|
-
iv
|
|
1432
|
-
};
|
|
1433
|
-
ctx.secrets.mask(masterKey);
|
|
1434
|
-
masterKeys[app.name] = {
|
|
1435
|
-
masterKey,
|
|
1436
|
-
buildArg: credentialsBuildArg({
|
|
1437
|
-
encrypted,
|
|
1438
|
-
iv
|
|
1439
|
-
})
|
|
1440
|
-
};
|
|
1441
|
-
}
|
|
1442
|
-
for (const worker of composed.workers) {
|
|
1443
|
-
if (!worker.env || !worker.build) continue;
|
|
1444
|
-
const { encrypted, iv, masterKey } = encryptSecrets(worker.env);
|
|
1445
|
-
credentials[worker.name] = {
|
|
1446
|
-
encrypted,
|
|
1447
|
-
iv
|
|
1448
|
-
};
|
|
1449
|
-
ctx.secrets.mask(masterKey);
|
|
1450
|
-
masterKeys[worker.name] = {
|
|
1451
|
-
masterKey,
|
|
1452
|
-
buildArg: credentialsBuildArg({
|
|
1453
|
-
encrypted,
|
|
1454
|
-
iv
|
|
1455
|
-
})
|
|
1456
|
-
};
|
|
1457
|
-
}
|
|
1458
|
-
}
|
|
1459
|
-
const stack = withBuildCredentials(composed, masterKeys);
|
|
1643
|
+
if (mode === "build") assertRedisClient(workspace, composed);
|
|
1644
|
+
const stack = options.pin ? withPinnedImages(composed, options.pin.file, options.pin.digests) : composed;
|
|
1460
1645
|
if (mode === "pull" && !ctx.dryRun) await assertImagesExist(deps.docker, tag, [...stack.apps, ...stack.workers]);
|
|
1646
|
+
if (stack.redis && !stack.local) ctx.secrets.mask(stack.redis.password);
|
|
1461
1647
|
if (stack.logs && (!stack.local || stack.logs.passwordFromSecrets)) {
|
|
1462
1648
|
ctx.secrets.mask(stack.logs.password);
|
|
1463
1649
|
ctx.secrets.mask(stack.logs.appEnv.OTEL_EXPORTER_OTLP_HEADERS);
|
|
@@ -1466,7 +1652,8 @@ async function validateCompose(ctx, deps, forced) {
|
|
|
1466
1652
|
const override = stackOverrideFile(root, stage);
|
|
1467
1653
|
const overrides = existsSync(override) ? [override] : [];
|
|
1468
1654
|
if (overrides.length > 0) ctx.logger.info(`🧩 Merging docker-compose.${stage}.yml over the generated stack`);
|
|
1469
|
-
const
|
|
1655
|
+
const recorded = push ? void 0 : await ctx.state.read(stage);
|
|
1656
|
+
const previous = Object.fromEntries(Object.entries(recorded?.state.releases ?? {}).map(([name, releases]) => {
|
|
1470
1657
|
const { ref, tag, digest } = releases.current;
|
|
1471
1658
|
return [name, {
|
|
1472
1659
|
ref,
|
|
@@ -1476,6 +1663,7 @@ async function validateCompose(ctx, deps, forced) {
|
|
|
1476
1663
|
}));
|
|
1477
1664
|
return {
|
|
1478
1665
|
mode,
|
|
1666
|
+
push,
|
|
1479
1667
|
stack,
|
|
1480
1668
|
dir,
|
|
1481
1669
|
ref: {
|
|
@@ -1490,11 +1678,32 @@ async function validateCompose(ctx, deps, forced) {
|
|
|
1490
1678
|
generated,
|
|
1491
1679
|
previous,
|
|
1492
1680
|
files: [],
|
|
1493
|
-
credentials,
|
|
1494
1681
|
images: {},
|
|
1495
1682
|
changes: []
|
|
1496
1683
|
};
|
|
1497
1684
|
}
|
|
1685
|
+
/** A stack whose caches are in Redis, built for an app that has no client. */
|
|
1686
|
+
var RedisClientMissing = class extends Error {
|
|
1687
|
+
apps;
|
|
1688
|
+
constructor(apps) {
|
|
1689
|
+
super(`The stack keeps every cache in Redis, and ${apps.join(", ")} ${apps.length === 1 ? "does" : "do"} not depend on ioredis, the client the generated server reaches it with. Add it to the workspace: pnpm add ioredis`);
|
|
1690
|
+
this.apps = apps;
|
|
1691
|
+
this.name = "RedisClientMissing";
|
|
1692
|
+
}
|
|
1693
|
+
};
|
|
1694
|
+
/**
|
|
1695
|
+
* Every backend and worker image built for a stack with a cache can resolve
|
|
1696
|
+
* `ioredis` — listed by its app or a directory above it, the way Node and
|
|
1697
|
+
* the image's install find it.
|
|
1698
|
+
*/
|
|
1699
|
+
function assertRedisClient(workspace, stack) {
|
|
1700
|
+
if (!stack.plan.resources.some((r) => r.kind === "cache")) return;
|
|
1701
|
+
const missing = [.../* @__PURE__ */ new Set([...stack.apps.filter((app) => app.kind === "rest-api").map((a) => a.name), ...stack.workers.map((worker) => worker.host)])].filter((name) => {
|
|
1702
|
+
const app = workspace.apps[name];
|
|
1703
|
+
return app && !installedFrom(join(workspace.root, app.path), "ioredis");
|
|
1704
|
+
}).sort();
|
|
1705
|
+
if (missing.length > 0) throw new RedisClientMissing(missing);
|
|
1706
|
+
}
|
|
1498
1707
|
/**
|
|
1499
1708
|
* The stage's secrets — and, for a deployed stage, everything it generates
|
|
1500
1709
|
* once: its seed, and each declared secret and keyring. Kept only once the
|
|
@@ -1512,8 +1721,16 @@ async function stageSecrets(ctx, manifest) {
|
|
|
1512
1721
|
secrets: withSeed.secrets,
|
|
1513
1722
|
generated: []
|
|
1514
1723
|
};
|
|
1515
|
-
const
|
|
1516
|
-
|
|
1724
|
+
const withRedis = runsRedis(manifest, withLogs.secrets.custom ?? {}) ? withRedisPassword(withLogs.secrets) : {
|
|
1725
|
+
secrets: withLogs.secrets,
|
|
1726
|
+
generated: []
|
|
1727
|
+
};
|
|
1728
|
+
const secrets = withRedis.secrets;
|
|
1729
|
+
const generated = [
|
|
1730
|
+
...withSeed.generated,
|
|
1731
|
+
...withLogs.generated,
|
|
1732
|
+
...withRedis.generated
|
|
1733
|
+
];
|
|
1517
1734
|
if (secrets.seed) ctx.secrets.mask(secrets.seed);
|
|
1518
1735
|
for (const value of Object.values(secrets.custom ?? {})) ctx.secrets.mask(value);
|
|
1519
1736
|
return {
|
|
@@ -1522,13 +1739,36 @@ async function stageSecrets(ctx, manifest) {
|
|
|
1522
1739
|
};
|
|
1523
1740
|
}
|
|
1524
1741
|
/**
|
|
1742
|
+
* The stack with each app's and worker's image pinned to the digest the file
|
|
1743
|
+
* names: the refs it is checked, pulled and recorded by, and the image each
|
|
1744
|
+
* compose service runs.
|
|
1745
|
+
*/
|
|
1746
|
+
function withPinnedImages(stack, file, digests) {
|
|
1747
|
+
const apps = pinImages(file, digests, stack.apps);
|
|
1748
|
+
const workers = pinImages(file, digests, stack.workers);
|
|
1749
|
+
const services = { ...stack.compose.services };
|
|
1750
|
+
for (const image of [...apps, ...workers]) services[image.name] = {
|
|
1751
|
+
...services[image.name],
|
|
1752
|
+
image: image.ref
|
|
1753
|
+
};
|
|
1754
|
+
return {
|
|
1755
|
+
...stack,
|
|
1756
|
+
apps,
|
|
1757
|
+
workers,
|
|
1758
|
+
compose: {
|
|
1759
|
+
...stack.compose,
|
|
1760
|
+
services
|
|
1761
|
+
}
|
|
1762
|
+
};
|
|
1763
|
+
}
|
|
1764
|
+
/**
|
|
1525
1765
|
* A dry run: the stack's files, written so they can be read, and what a real
|
|
1526
1766
|
* run would do — no image is built or pulled, no container touched, nothing
|
|
1527
1767
|
* recorded and no secret kept.
|
|
1528
1768
|
*/
|
|
1529
1769
|
async function planCompose(ctx, run) {
|
|
1530
1770
|
if (run.generated.length > 0) ctx.logger.info(`🔑 "${ctx.stage}" has no ${run.generated.join(", ")} yet; this dry run used values it did not keep.`);
|
|
1531
|
-
run.files = await writeStack(ctx.cwd, run.dir, run.stack
|
|
1771
|
+
run.files = await writeStack(ctx.cwd, run.dir, run.stack);
|
|
1532
1772
|
printPlan(ctx, run);
|
|
1533
1773
|
const planned = (change) => {
|
|
1534
1774
|
run.changes.push(change);
|
|
@@ -1537,7 +1777,7 @@ async function planCompose(ctx, run) {
|
|
|
1537
1777
|
...change
|
|
1538
1778
|
});
|
|
1539
1779
|
};
|
|
1540
|
-
for (const service of run.stack.infra) planned({
|
|
1780
|
+
for (const service of run.push ? [] : run.stack.infra) planned({
|
|
1541
1781
|
key: `service:${service}`,
|
|
1542
1782
|
resourceType: "service",
|
|
1543
1783
|
action: "ensure"
|
|
@@ -1549,6 +1789,7 @@ async function planCompose(ctx, run) {
|
|
|
1549
1789
|
action: run.mode === "build" ? "build" : "reuse",
|
|
1550
1790
|
id: app.ref
|
|
1551
1791
|
});
|
|
1792
|
+
if (run.push) continue;
|
|
1552
1793
|
planned({
|
|
1553
1794
|
key: `service:${app.name}`,
|
|
1554
1795
|
resourceType: "service",
|
|
@@ -1562,13 +1803,14 @@ async function planCompose(ctx, run) {
|
|
|
1562
1803
|
action: run.mode === "build" ? "build" : "reuse",
|
|
1563
1804
|
id: worker.ref
|
|
1564
1805
|
});
|
|
1806
|
+
if (run.push) continue;
|
|
1565
1807
|
planned({
|
|
1566
1808
|
key: `service:${worker.name}`,
|
|
1567
1809
|
resourceType: "service",
|
|
1568
1810
|
action: run.previous[worker.name] ? "reuse" : "create"
|
|
1569
1811
|
});
|
|
1570
1812
|
}
|
|
1571
|
-
planned({
|
|
1813
|
+
if (!run.push) planned({
|
|
1572
1814
|
key: "service:caddy",
|
|
1573
1815
|
resourceType: "service",
|
|
1574
1816
|
action: "ensure"
|
|
@@ -1581,11 +1823,12 @@ async function planCompose(ctx, run) {
|
|
|
1581
1823
|
*/
|
|
1582
1824
|
async function provisionCompose(ctx, run, deps) {
|
|
1583
1825
|
const { stack, ref } = run;
|
|
1826
|
+
if (run.push) return;
|
|
1584
1827
|
if (run.generated.length > 0 && run.secrets) {
|
|
1585
1828
|
await ctx.secrets.write(run.secrets);
|
|
1586
1829
|
ctx.logger.info(`🔑 Generated for "${ctx.stage}" (${ctx.secrets.store}): ${run.generated.join(", ")}`);
|
|
1587
1830
|
}
|
|
1588
|
-
run.files = await writeStack(ctx.cwd, run.dir, stack
|
|
1831
|
+
run.files = await writeStack(ctx.cwd, run.dir, stack);
|
|
1589
1832
|
printPlan(ctx, run);
|
|
1590
1833
|
if (stack.infra.length > 0) {
|
|
1591
1834
|
ctx.logger.info(`\n🗄️ Starting ${stack.infra.join(", ")}…`);
|
|
@@ -1653,6 +1896,10 @@ async function buildCompose(ctx, run, deps) {
|
|
|
1653
1896
|
const { stack, ref } = run;
|
|
1654
1897
|
const images = [...stack.apps, ...stack.workers];
|
|
1655
1898
|
const apps = images.map((image) => image.app);
|
|
1899
|
+
if (run.push) {
|
|
1900
|
+
run.files = await writeStack(ctx.cwd, run.dir, stack);
|
|
1901
|
+
printPlan(ctx, run);
|
|
1902
|
+
}
|
|
1656
1903
|
if (run.mode === "build") {
|
|
1657
1904
|
ctx.logger.info("\n🐳 Building images…");
|
|
1658
1905
|
await deps.docker.build(ref, apps);
|
|
@@ -1660,8 +1907,10 @@ async function buildCompose(ctx, run, deps) {
|
|
|
1660
1907
|
ctx.logger.info(`\n🐳 Pulling ${ctx.tag}…`);
|
|
1661
1908
|
await deps.docker.pull(ref, apps);
|
|
1662
1909
|
}
|
|
1910
|
+
if (run.push) ctx.logger.info("\n📤 Pushing images…");
|
|
1663
1911
|
for (const image of images) {
|
|
1664
|
-
const digest = await deps.docker.digest(image.ref);
|
|
1912
|
+
const digest = run.push ? await deps.docker.push(ref, image.ref) : await deps.docker.digest(image.ref);
|
|
1913
|
+
if (run.push) ctx.logger.info(` ${image.app.padEnd(12)} ${image.ref}@${digest}`);
|
|
1665
1914
|
run.images[image.app] = {
|
|
1666
1915
|
ref: image.ref,
|
|
1667
1916
|
tag: image.tag,
|
|
@@ -1687,6 +1936,7 @@ async function buildCompose(ctx, run, deps) {
|
|
|
1687
1936
|
*/
|
|
1688
1937
|
async function releaseCompose(ctx, run, deps) {
|
|
1689
1938
|
const { stack, ref } = run;
|
|
1939
|
+
if (run.push) return;
|
|
1690
1940
|
ctx.logger.info("\n🚀 Starting the stack…");
|
|
1691
1941
|
await deps.docker.up(ref);
|
|
1692
1942
|
if (stack.local) await deps.docker.copyOut(ref, "caddy", "/data/caddy/pki/authorities/local/root.crt", caFile(run)).catch(() => {});
|
|
@@ -1745,6 +1995,7 @@ async function recordImages(ctx, run) {
|
|
|
1745
1995
|
*/
|
|
1746
1996
|
async function verifyCompose(ctx, run, deps) {
|
|
1747
1997
|
const { stack } = run;
|
|
1998
|
+
if (run.push) return;
|
|
1748
1999
|
const ca = stack.local ? await readFile(caFile(run), "utf-8").catch(() => void 0) : void 0;
|
|
1749
2000
|
ctx.logger.info("\n🩺 Checking each app through Caddy…");
|
|
1750
2001
|
const results = await Promise.all(stack.apps.map((app) => checkApp(ctx, app, deps, {
|
|
@@ -1924,7 +2175,7 @@ function applied(ctx, run, change) {
|
|
|
1924
2175
|
});
|
|
1925
2176
|
}
|
|
1926
2177
|
/** Write the stack's files: owner-only, since the env files hold secrets. */
|
|
1927
|
-
async function writeStack(root, dir, stack
|
|
2178
|
+
async function writeStack(root, dir, stack) {
|
|
1928
2179
|
await mkdir(dir, {
|
|
1929
2180
|
recursive: true,
|
|
1930
2181
|
mode: 448
|
|
@@ -1940,8 +2191,9 @@ async function writeStack(root, dir, stack, credentials = {}) {
|
|
|
1940
2191
|
await write(join(dir, "docker-compose.yml"), composeYaml(stack));
|
|
1941
2192
|
await write(join(dir, "Caddyfile"), stack.caddyfile);
|
|
1942
2193
|
for (const app of [...stack.apps, ...stack.workers]) if (app.env) await write(join(dir, `${app.name}.env`), envFile(app.env), 384);
|
|
1943
|
-
if (stack.logs) await write(join(dir, `${LOGS_SERVICE}.env`), envFile(stack.logs.env), 384);
|
|
1944
|
-
|
|
2194
|
+
if (stack.logs && stack.compose.services["openobserve"]?.env_file) await write(join(dir, `${LOGS_SERVICE}.env`), envFile(stack.logs.env), 384);
|
|
2195
|
+
if (stack.redis && stack.compose.services["redis"]?.env_file) await write(join(dir, `${REDIS_SERVICE}.env`), envFile(stack.redis.env), 384);
|
|
2196
|
+
for (const entry of await readdir(dir)) if (entry.endsWith(".credentials")) await rm(join(dir, entry));
|
|
1945
2197
|
for (const [path, content] of Object.entries(stack.dockerfiles)) await write(join(root, path), content);
|
|
1946
2198
|
await ensureDockerignore(findBuildRoot(root));
|
|
1947
2199
|
return files;
|
|
@@ -1991,13 +2243,16 @@ function printPlan(ctx, run) {
|
|
|
1991
2243
|
* digest in the stage's state
|
|
1992
2244
|
* - `verify`: each app asked through Caddy over HTTPS
|
|
1993
2245
|
*
|
|
2246
|
+
* With `push` (`gkm compose --build --push`), run with `buildOnly`, it builds
|
|
2247
|
+
* every image and pushes it to `deploy.registry`, and does nothing else.
|
|
2248
|
+
*
|
|
1994
2249
|
* `gkm compose` is this target with a few more switches (`--build`, `--pull`,
|
|
1995
2250
|
* `--down`), run through the same `deploy()`.
|
|
1996
2251
|
*/
|
|
1997
2252
|
var compose_exports = /* @__PURE__ */ __exportAll({ composeTarget: () => composeTarget });
|
|
1998
2253
|
/** The compose target, with its Docker, bundler and probe as given. */
|
|
1999
2254
|
function composeTarget(options = {}) {
|
|
2000
|
-
const { mode, report, ...given } = options;
|
|
2255
|
+
const { mode, push, pin, report, ...given } = options;
|
|
2001
2256
|
const deps = {
|
|
2002
2257
|
...defaultDeps,
|
|
2003
2258
|
...given
|
|
@@ -2013,7 +2268,11 @@ function composeTarget(options = {}) {
|
|
|
2013
2268
|
},
|
|
2014
2269
|
credentials: [],
|
|
2015
2270
|
tag: ({ cwd }) => deps.revision(cwd),
|
|
2016
|
-
validate: (ctx) => validateCompose(ctx, deps,
|
|
2271
|
+
validate: (ctx) => validateCompose(ctx, deps, {
|
|
2272
|
+
...mode ? { mode } : {},
|
|
2273
|
+
...push ? { push } : {},
|
|
2274
|
+
...pin ? { pin } : {}
|
|
2275
|
+
}),
|
|
2017
2276
|
plan: (ctx, run) => planCompose(ctx, run),
|
|
2018
2277
|
provision: (ctx, run) => provisionCompose(ctx, run, deps),
|
|
2019
2278
|
build: (ctx, run) => buildCompose(ctx, run, deps),
|
|
@@ -2026,6 +2285,6 @@ function composeTarget(options = {}) {
|
|
|
2026
2285
|
});
|
|
2027
2286
|
}
|
|
2028
2287
|
//#endregion
|
|
2029
|
-
export {
|
|
2288
|
+
export { stackOverrideFile as a, parseDigests as c, ComposePushNeedsBuild as i, pinnedRef as l, compose_exports as n, composeProject as o, ComposePinNeedsPull as r, stackDir as s, composeTarget as t, dockerCompose as u };
|
|
2030
2289
|
|
|
2031
|
-
//# sourceMappingURL=compose-
|
|
2290
|
+
//# sourceMappingURL=compose-DdH2JQJD.mjs.map
|