@geekmidas/cli 10.0.0-alpha.69 → 10.0.0-alpha.71
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-LS_uTO8D.cjs → LegacyStateStore-Dd5IwiyC.cjs} +2 -2
- package/dist/{LegacyStateStore-LS_uTO8D.cjs.map → LegacyStateStore-Dd5IwiyC.cjs.map} +1 -1
- package/dist/{LegacyStateStore-BE1Et8RS.mjs → LegacyStateStore-NSHEinQi.mjs} +2 -2
- package/dist/{LegacyStateStore-BE1Et8RS.mjs.map → LegacyStateStore-NSHEinQi.mjs.map} +1 -1
- package/dist/{LocalStateStore-CV6phNMZ.cjs → LocalStateStore-DTylG4T4.cjs} +2 -2
- package/dist/{LocalStateStore-CV6phNMZ.cjs.map → LocalStateStore-DTylG4T4.cjs.map} +1 -1
- package/dist/{LocalStateStore-DAnFc708.mjs → LocalStateStore-DUoG2wA_.mjs} +2 -2
- package/dist/{LocalStateStore-DAnFc708.mjs.map → LocalStateStore-DUoG2wA_.mjs.map} +1 -1
- package/dist/{OpenApiTsGenerator-CSaXs-dB.mjs → OpenApiTsGenerator-Bhr_XWWN.mjs} +57 -638
- package/dist/OpenApiTsGenerator-Bhr_XWWN.mjs.map +1 -0
- package/dist/{OpenApiTsGenerator-CxB8mqXz.cjs → OpenApiTsGenerator-DZZwHdDC.cjs} +57 -638
- package/dist/OpenApiTsGenerator-DZZwHdDC.cjs.map +1 -0
- package/dist/{S3StateStore-dA34LAYm.cjs → S3StateStore-D7hrU-sy.cjs} +2 -2
- package/dist/{S3StateStore-dA34LAYm.cjs.map → S3StateStore-D7hrU-sy.cjs.map} +1 -1
- package/dist/{S3StateStore-8kJEGnTh.mjs → S3StateStore-Dxob3PNA.mjs} +2 -2
- package/dist/{S3StateStore-8kJEGnTh.mjs.map → S3StateStore-Dxob3PNA.mjs.map} +1 -1
- package/dist/{SSMStateStore-DBzlfc-S.cjs → SSMStateStore-DGIKJ4NN.cjs} +2 -2
- package/dist/{SSMStateStore-DBzlfc-S.cjs.map → SSMStateStore-DGIKJ4NN.cjs.map} +1 -1
- package/dist/{SSMStateStore-Dkdw-fPL.mjs → SSMStateStore-Dvl4A8Up.mjs} +2 -2
- package/dist/{SSMStateStore-Dkdw-fPL.mjs.map → SSMStateStore-Dvl4A8Up.mjs.map} +1 -1
- package/dist/{add-CoIr2wgv.cjs → add-DeC2Kyyv.cjs} +3 -3
- package/dist/{add-CoIr2wgv.cjs.map → add-DeC2Kyyv.cjs.map} +1 -1
- package/dist/{add-Dzk23476.mjs → add-XTLU19_E.mjs} +3 -3
- package/dist/{add-Dzk23476.mjs.map → add-XTLU19_E.mjs.map} +1 -1
- package/dist/{bundler-CSR2nM4m.cjs → bundler-CEaR6Dpb.cjs} +6 -6
- package/dist/bundler-CEaR6Dpb.cjs.map +1 -0
- package/dist/{bundler-Cn0JF5G0.mjs → bundler-CNTVLCCA.mjs} +6 -6
- package/dist/bundler-CNTVLCCA.mjs.map +1 -0
- package/dist/{cli-ij8Lwlww.mjs → cli-D1arS0p1.mjs} +3 -3
- package/dist/{cli-ij8Lwlww.mjs.map → cli-D1arS0p1.mjs.map} +1 -1
- package/dist/{cli-IoM2-x48.cjs → cli-MMgBrHx7.cjs} +3 -3
- package/dist/{cli-IoM2-x48.cjs.map → cli-MMgBrHx7.cjs.map} +1 -1
- package/dist/{compose-DfIioFX0.cjs → compose-B-MME-4C.cjs} +358 -16
- package/dist/compose-B-MME-4C.cjs.map +1 -0
- package/dist/{compose-CZtinmtw.mjs → compose-T3hnnjKZ.mjs} +353 -17
- package/dist/compose-T3hnnjKZ.mjs.map +1 -0
- package/dist/{config-CxEp_g7j.d.mts → config-BN6iXvyR.d.mts} +3 -3
- package/dist/{config-BnA0kPTY.d.cts.map → config-BN6iXvyR.d.mts.map} +1 -1
- package/dist/{config-BnA0kPTY.d.cts → config-j9zBKaEG.d.cts} +3 -3
- package/dist/{config-CxEp_g7j.d.mts.map → config-j9zBKaEG.d.cts.map} +1 -1
- package/dist/config.cjs +1 -1
- package/dist/config.d.cts +3 -3
- package/dist/config.d.mts +3 -3
- package/dist/config.mjs +1 -1
- package/dist/deploy/public.cjs +6 -6
- package/dist/deploy/public.d.cts +3 -3
- package/dist/deploy/public.d.cts.map +1 -1
- package/dist/deploy/public.d.mts +3 -3
- package/dist/deploy/public.d.mts.map +1 -1
- package/dist/deploy/public.mjs +6 -6
- package/dist/{deploy-Dpz9raPM.cjs → deploy-CZE0m6jd.cjs} +5 -5
- package/dist/{deploy-Dpz9raPM.cjs.map → deploy-CZE0m6jd.cjs.map} +1 -1
- package/dist/{deploy-BzqbkJ3o.mjs → deploy-ClC53rc6.mjs} +5 -5
- package/dist/{deploy-BzqbkJ3o.mjs.map → deploy-ClC53rc6.mjs.map} +1 -1
- package/dist/{dokploy-DkwcORbv.mjs → dokploy-D1UwQKm3.mjs} +2 -2
- package/dist/{dokploy-DkwcORbv.mjs.map → dokploy-D1UwQKm3.mjs.map} +1 -1
- package/dist/{dokploy-B6O-J1fb.cjs → dokploy-lwmH05zr.cjs} +2 -2
- package/dist/{dokploy-B6O-J1fb.cjs.map → dokploy-lwmH05zr.cjs.map} +1 -1
- package/dist/{encryption-C8Ic5t0C.cjs → encryption-D05sNwED.cjs} +3 -3
- package/dist/{encryption-C8Ic5t0C.cjs.map → encryption-D05sNwED.cjs.map} +1 -1
- package/dist/{encryption-7m1mKcRy.mjs → encryption-DcrxWl1s.mjs} +3 -3
- package/dist/{encryption-7m1mKcRy.mjs.map → encryption-DcrxWl1s.mjs.map} +1 -1
- package/dist/{engine-BR9qlS7A.cjs → engine-Cry1vwMz.cjs} +6 -5
- package/dist/engine-Cry1vwMz.cjs.map +1 -0
- package/dist/{engine-62Kl70Pj.mjs → engine-DmQS3XHX.mjs} +6 -5
- package/dist/{engine-62Kl70Pj.mjs.map → engine-DmQS3XHX.mjs.map} +1 -1
- package/dist/{github-CmGIZjhu.cjs → github-BchWoiVx.cjs} +3 -3
- package/dist/{github-CmGIZjhu.cjs.map → github-BchWoiVx.cjs.map} +1 -1
- package/dist/{github-DtymyOcH.mjs → github-CqYOP8cI.mjs} +3 -3
- package/dist/{github-DtymyOcH.mjs.map → github-CqYOP8cI.mjs.map} +1 -1
- package/dist/{index-CkX89m6S.d.mts → index-Bk3NY6DN.d.cts} +2 -2
- package/dist/{index-CkX89m6S.d.mts.map → index-Bk3NY6DN.d.cts.map} +1 -1
- package/dist/{index-EvZBkycu.d.cts → index-DvoxuTqR.d.mts} +2 -2
- package/dist/{index-EvZBkycu.d.cts.map → index-DvoxuTqR.d.mts.map} +1 -1
- package/dist/index.cjs +53 -52
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +53 -52
- package/dist/index.mjs.map +1 -1
- package/dist/{migrate-CEzq6DjL.mjs → migrate-guck0NJK.mjs} +3 -3
- package/dist/{migrate-CEzq6DjL.mjs.map → migrate-guck0NJK.mjs.map} +1 -1
- package/dist/{migrate-CYC-hBOU.cjs → migrate-qKeGYq21.cjs} +3 -3
- package/dist/{migrate-CYC-hBOU.cjs.map → migrate-qKeGYq21.cjs.map} +1 -1
- package/dist/openapi.cjs +1 -1
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.mjs +1 -1
- package/dist/{ready-QGgjmcuN.cjs → ready-Dp6arkI4.cjs} +5 -5
- package/dist/ready-Dp6arkI4.cjs.map +1 -0
- package/dist/{ready-DeYaSoNN.mjs → ready-xpoX11uu.mjs} +5 -5
- package/dist/ready-xpoX11uu.mjs.map +1 -0
- package/dist/reconcile/public.d.cts +1 -1
- package/dist/reconcile/public.d.mts +1 -1
- package/dist/{resolve-Dn5wGf1r.mjs → resolve-BsXEXAUz.mjs} +3 -3
- package/dist/{resolve-Dn5wGf1r.mjs.map → resolve-BsXEXAUz.mjs.map} +1 -1
- package/dist/{resolve-BDsMffvz.cjs → resolve-wEjEvbJ5.cjs} +3 -3
- package/dist/{resolve-BDsMffvz.cjs.map → resolve-wEjEvbJ5.cjs.map} +1 -1
- package/dist/{rollback-DDfaqv7P.mjs → rollback-Bk91csFg.mjs} +5 -5
- package/dist/{rollback-DDfaqv7P.mjs.map → rollback-Bk91csFg.mjs.map} +1 -1
- package/dist/{rollback-5XHaBDxW.cjs → rollback-DmcU-efZ.cjs} +5 -5
- package/dist/{rollback-5XHaBDxW.cjs.map → rollback-DmcU-efZ.cjs.map} +1 -1
- package/dist/{runtime-B3jNgeP-.d.mts → runtime-CYAO4iZG.d.cts} +2 -2
- package/dist/{runtime-B3jNgeP-.d.mts.map → runtime-CYAO4iZG.d.cts.map} +1 -1
- package/dist/{runtime-DWThEP-r.d.cts → runtime-DHJhe4QI.d.mts} +2 -2
- package/dist/{runtime-DWThEP-r.d.cts.map → runtime-DHJhe4QI.d.mts.map} +1 -1
- package/dist/{sniffer-Bc0nNBDF.cjs → sniffer-DpHpFZBD.cjs} +6 -6
- package/dist/sniffer-DpHpFZBD.cjs.map +1 -0
- package/dist/{sniffer-DMqi2b1q.mjs → sniffer-vAXsU_4G.mjs} +6 -6
- package/dist/sniffer-vAXsU_4G.mjs.map +1 -0
- package/dist/{state-DClnPZt5.mjs → state-BJFherj1.mjs} +60 -5
- package/dist/state-BJFherj1.mjs.map +1 -0
- package/dist/{state-8RgXeX5Y.cjs → state-CCuTJNRH.cjs} +71 -4
- package/dist/state-CCuTJNRH.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-CZdCxUCK.d.cts → types-BD1jgA85.d.cts} +70 -2
- package/dist/types-BD1jgA85.d.cts.map +1 -0
- package/dist/{types-Da0E3Qv7.d.mts → types-DkG5WW8l.d.mts} +70 -2
- package/dist/types-DkG5WW8l.d.mts.map +1 -0
- package/dist/vitest.cjs +1 -1
- package/dist/vitest.mjs +1 -1
- package/dist/workspace/index.cjs +1 -1
- package/dist/workspace/index.d.cts +3 -3
- package/dist/workspace/index.d.mts +3 -3
- package/dist/workspace/index.mjs +1 -1
- package/dist/{workspace-B2UK_rVP.mjs → workspace-C7NqCsQF.mjs} +104 -2
- package/dist/workspace-C7NqCsQF.mjs.map +1 -0
- package/dist/{workspace-8NuAjaXJ.cjs → workspace-vdrcRI-m.cjs} +115 -1
- package/dist/workspace-vdrcRI-m.cjs.map +1 -0
- package/package.json +15 -15
- package/dist/OpenApiTsGenerator-CSaXs-dB.mjs.map +0 -1
- package/dist/OpenApiTsGenerator-CxB8mqXz.cjs.map +0 -1
- package/dist/bundler-CSR2nM4m.cjs.map +0 -1
- package/dist/bundler-Cn0JF5G0.mjs.map +0 -1
- package/dist/compose-CZtinmtw.mjs.map +0 -1
- package/dist/compose-DfIioFX0.cjs.map +0 -1
- package/dist/engine-BR9qlS7A.cjs.map +0 -1
- package/dist/ready-DeYaSoNN.mjs.map +0 -1
- package/dist/ready-QGgjmcuN.cjs.map +0 -1
- package/dist/sniffer-Bc0nNBDF.cjs.map +0 -1
- package/dist/sniffer-DMqi2b1q.mjs.map +0 -1
- package/dist/state-8RgXeX5Y.cjs.map +0 -1
- package/dist/state-DClnPZt5.mjs.map +0 -1
- package/dist/types-CZdCxUCK.d.cts.map +0 -1
- package/dist/types-Da0E3Qv7.d.mts.map +0 -1
- package/dist/workspace-8NuAjaXJ.cjs.map +0 -1
- package/dist/workspace-B2UK_rVP.mjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli-
|
|
1
|
+
{"version":3,"file":"cli-MMgBrHx7.cjs","names":["targetForProvider","formatError","parseDevServices","deploy","storedCredentials","terminalCredentials"],"sources":["../src/deploy/cli.ts"],"sourcesContent":["/**\n * `gkm deploy`: the terminal around `deploy()`. CLI layer only.\n *\n * Everything that belongs to a person at a terminal lives here and nowhere\n * below it: prompting for a missing login (and storing it), printing progress,\n * and turning the outcome into an exit code. The deploy itself is the same\n * `deploy()` a host calls.\n */\n\nimport { formatError } from '../debug';\nimport { targetForProvider } from '../target/provider';\nimport { storedCredentials } from './credentials';\nimport { deploy } from './deploy';\nimport { type DevService, parseDevServices } from './devServices';\nimport { terminalCredentials } from './terminal';\n\nexport interface DeployCliOptions {\n\t/** The project directory: `--cwd`, else where the command was run. */\n\tcwd: string;\n\t/** `--target`: where to deploy. Defaults to `deploy.default`. */\n\ttarget?: string;\n\t/** `--provider`, deprecated: `dokploy` means `--target dokploy`. */\n\tprovider?: string;\n\tstage: string;\n\ttag?: string;\n\t/**\n\t * Print each event as one JSON object per line on stdout, instead of the\n\t * progress lines. Nothing prompts: a missing credential fails the run.\n\t */\n\tjson?: boolean;\n\t/** Plan only: create, change, build and push nothing. */\n\tdryRun?: boolean;\n\t/** On a failed release, roll back every app, not only the failed ones. */\n\tatomic?: boolean;\n\t/** `--allow-dev-services minio,mailpit`, as typed. */\n\tallowDevServices?: string;\n}\n\n/** Where the command writes. The process's own streams, outside tests. */\nexport interface DeployCliStreams {\n\tstdout: Pick<NodeJS.WriteStream, 'write'>;\n}\n\n/**\n * Run `gkm deploy` and resolve with its exit code: 0 when the run finished,\n * 1 when anything stopped it.\n */\nexport async function deployCli(\n\toptions: DeployCliOptions,\n\tstreams: DeployCliStreams = { stdout: process.stdout },\n): Promise<number> {\n\tlet target = options.target;\n\tif (options.provider !== undefined && target === undefined) {\n\t\ttry {\n\t\t\t// On stderr, so a `--json` run's stdout still carries only events.\n\t\t\ttarget = targetForProvider(options.provider, (message) =>\n\t\t\t\tconsole.warn(message),\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tconsole.error(formatError(error));\n\t\t\treturn 1;\n\t\t}\n\t}\n\n\tlet allowDevServices: DevService[];\n\ttry {\n\t\tallowDevServices = parseDevServices(options.allowDevServices);\n\t} catch (error) {\n\t\tconsole.error(formatError(error));\n\t\treturn 1;\n\t}\n\n\tconst input = {\n\t\tcwd: options.cwd,\n\t\tstage: options.stage,\n\t\t...(target ? { target } : {}),\n\t\t...(options.tag ? { tag: options.tag } : {}),\n\t\t...(options.dryRun ? { dryRun: true } : {}),\n\t\t...(options.atomic ? { atomic: true } : {}),\n\t\t...(allowDevServices.length > 0 ? { allowDevServices } : {}),\n\t};\n\n\tif (options.json) {\n\t\t// stdout carries the events and nothing else: docker's own output goes\n\t\t// to stderr, and nobody is asked anything — a prompt would be written\n\t\t// into the stream a program is parsing.\n\t\tconst run = deploy({\n\t\t\t...input,\n\t\t\tcredentials: storedCredentials(),\n\t\t\tchildOutput: 'stderr',\n\t\t});\n\t\tfor await (const event of run) {\n\t\t\tstreams.stdout.write(`${JSON.stringify(event)}\\n`);\n\t\t}\n\t\treturn run.result.then(\n\t\t\t() => 0,\n\t\t\t(error) => {\n\t\t\t\tconsole.error(formatError(error));\n\t\t\t\treturn 1;\n\t\t\t},\n\t\t);\n\t}\n\n\t// The lines arrive as they are written, rather than as events read back a\n\t// tick later, so they interleave with a prompt exactly as they always did.\n\tconst run = deploy({\n\t\t...input,\n\t\tcredentials: terminalCredentials(storedCredentials()),\n\t\tlogger: {\n\t\t\tinfo: (message) => console.log(message),\n\t\t\twarn: (message) => console.warn(message),\n\t\t\terror: (message) => console.error(message),\n\t\t},\n\t});\n\n\ttry {\n\t\tawait run.result;\n\t\treturn 0;\n\t} catch (error) {\n\t\tconsole.error(formatError(error));\n\t\treturn 1;\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AA+CA,eAAsB,UACrB,SACA,UAA4B,EAAE,QAAQ,QAAQ,OAAO,GACnC;CAClB,IAAI,SAAS,QAAQ;CACrB,IAAI,QAAQ,aAAa,KAAA,KAAa,WAAW,KAAA,GAChD,IAAI;EAEH,SAASA,iBAAAA,kBAAkB,QAAQ,WAAW,YAC7C,QAAQ,KAAK,OAAO,CACrB;CACD,SAAS,OAAO;EACf,QAAQ,MAAMC,cAAAA,YAAY,KAAK,CAAC;EAChC,OAAO;CACR;CAGD,IAAI;CACJ,IAAI;EACH,mBAAmBC,cAAAA,iBAAiB,QAAQ,gBAAgB;CAC7D,SAAS,OAAO;EACf,QAAQ,MAAMD,cAAAA,YAAY,KAAK,CAAC;EAChC,OAAO;CACR;CAEA,MAAM,QAAQ;EACb,KAAK,QAAQ;EACb,OAAO,QAAQ;EACf,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;EAC3B,GAAI,QAAQ,MAAM,EAAE,KAAK,QAAQ,IAAI,IAAI,CAAC;EAC1C,GAAI,QAAQ,SAAS,EAAE,QAAQ,KAAK,IAAI,CAAC;EACzC,GAAI,QAAQ,SAAS,EAAE,QAAQ,KAAK,IAAI,CAAC;EACzC,GAAI,iBAAiB,SAAS,IAAI,EAAE,iBAAiB,IAAI,CAAC;CAC3D;CAEA,IAAI,QAAQ,MAAM;EAIjB,MAAM,MAAME,eAAAA,OAAO;GAClB,GAAG;GACH,aAAaC,oBAAAA,kBAAkB;GAC/B,aAAa;EACd,CAAC;EACD,WAAW,MAAM,SAAS,KACzB,QAAQ,OAAO,MAAM,GAAG,KAAK,UAAU,KAAK,EAAE,GAAG;EAElD,OAAO,IAAI,OAAO,WACX,IACL,UAAU;GACV,QAAQ,MAAMH,cAAAA,YAAY,KAAK,CAAC;GAChC,OAAO;EACR,CACD;CACD;CAIA,MAAM,MAAME,eAAAA,OAAO;EAClB,GAAG;EACH,aAAaE,iBAAAA,oBAAoBD,oBAAAA,kBAAkB,CAAC;EACpD,QAAQ;GACP,OAAO,YAAY,QAAQ,IAAI,OAAO;GACtC,OAAO,YAAY,QAAQ,KAAK,OAAO;GACvC,QAAQ,YAAY,QAAQ,MAAM,OAAO;EAC1C;CACD,CAAC;CAED,IAAI;EACH,MAAM,IAAI;EACV,OAAO;CACR,SAAS,OAAO;EACf,QAAQ,MAAMH,cAAAA,YAAY,KAAK,CAAC;EAChC,OAAO;CACR;AACD"}
|
|
@@ -2,15 +2,19 @@ const require_rolldown_runtime = require("./rolldown-runtime-C0BPl7ul.cjs");
|
|
|
2
2
|
const require_run = require("./run-fVZ-rLnc.cjs");
|
|
3
3
|
const require_discover = require("./discover-B4VjaFJk.cjs");
|
|
4
4
|
const require_derive = require("./derive-DAbo49sm.cjs");
|
|
5
|
+
const require_workspace = require("./workspace-vdrcRI-m.cjs");
|
|
5
6
|
const require_define = require("./define-DzRz8FgH.cjs");
|
|
6
|
-
const require_state = require("./state-
|
|
7
|
+
const require_state = require("./state-CCuTJNRH.cjs");
|
|
7
8
|
const require_fakes = require("./fakes-zW-jNAbQ.cjs");
|
|
8
9
|
const require_databases = require("./databases-C2p5LGAp.cjs");
|
|
9
10
|
const require_credentialSchemas = require("./credentialSchemas-BcNL1N5R.cjs");
|
|
10
11
|
let node_path = require("node:path");
|
|
12
|
+
let node_fs = require("node:fs");
|
|
11
13
|
let node_fs_promises = require("node:fs/promises");
|
|
14
|
+
let node_os = require("node:os");
|
|
12
15
|
let _geekmidas_manifest = require("@geekmidas/manifest");
|
|
13
16
|
let node_child_process = require("node:child_process");
|
|
17
|
+
let node_crypto = require("node:crypto");
|
|
14
18
|
let yaml = require("yaml");
|
|
15
19
|
let node_https = require("node:https");
|
|
16
20
|
//#region src/compose/docker.ts
|
|
@@ -66,6 +70,7 @@ function compose(stack, args) {
|
|
|
66
70
|
stack.project,
|
|
67
71
|
"-f",
|
|
68
72
|
stack.file,
|
|
73
|
+
...(stack.overrides ?? []).flatMap((file) => ["-f", file]),
|
|
69
74
|
...args
|
|
70
75
|
];
|
|
71
76
|
}
|
|
@@ -172,6 +177,16 @@ const dockerCompose = {
|
|
|
172
177
|
if (!Number.isInteger(port) || port <= 0) throw new ServicePortUnknown(service, inside, output);
|
|
173
178
|
return port;
|
|
174
179
|
},
|
|
180
|
+
async health(stack, service) {
|
|
181
|
+
const { code, stdout } = await capture("docker", [...compose(stack, [
|
|
182
|
+
"ps",
|
|
183
|
+
"--format",
|
|
184
|
+
"{{.Health}}",
|
|
185
|
+
service
|
|
186
|
+
])]);
|
|
187
|
+
if (code !== 0) return void 0;
|
|
188
|
+
return stdout.trim().split("\n")[0] || void 0;
|
|
189
|
+
},
|
|
175
190
|
async copyOut(stack, service, from, to) {
|
|
176
191
|
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(to), { recursive: true });
|
|
177
192
|
await require_run.run("docker", compose(stack, [
|
|
@@ -251,6 +266,184 @@ async function assertImagesExist(docker, tag, images) {
|
|
|
251
266
|
if (missing.length > 0) throw new ImageTagNotFound(tag, missing);
|
|
252
267
|
}
|
|
253
268
|
//#endregion
|
|
269
|
+
//#region src/compose/logs.ts
|
|
270
|
+
/**
|
|
271
|
+
* The stack's log UI: OpenObserve, run beside the apps when
|
|
272
|
+
* `deploy.compose.logs` asks for it, and every backend's OpenTelemetry
|
|
273
|
+
* pointed at it.
|
|
274
|
+
*
|
|
275
|
+
* The production server already exports its traces and pino logs over
|
|
276
|
+
* OTLP/HTTP when `OTEL_EXPORTER_OTLP_ENDPOINT` is set. OpenObserve takes OTLP
|
|
277
|
+
* at `/api/<org>/v1/{logs,traces}` with HTTP basic auth, so wiring an app is
|
|
278
|
+
* two variables: the endpoint, `http://openobserve:5080/api/default`, and
|
|
279
|
+
* the header that signs in as the root user.
|
|
280
|
+
*
|
|
281
|
+
* It is reached, by default, through an SSH tunnel: published on 127.0.0.1
|
|
282
|
+
* only, because a port Docker publishes on every interface is opened in the
|
|
283
|
+
* firewall by Docker itself, past ufw's rules. Served publicly, it is a host
|
|
284
|
+
* on the stack's Caddy that refuses every address not allowed.
|
|
285
|
+
*
|
|
286
|
+
* Pure: the stack decides it, and the phases print and check it.
|
|
287
|
+
*/
|
|
288
|
+
/**
|
|
289
|
+
* The image, pinned. A log store's on-disk format is the thing that must not
|
|
290
|
+
* move under a running stack, so a new version is a change to this line.
|
|
291
|
+
*/
|
|
292
|
+
const OPENOBSERVE_IMAGE = "public.ecr.aws/zinclabs/openobserve:v1.0.4";
|
|
293
|
+
/** Its compose service name — the host the apps send to. */
|
|
294
|
+
const LOGS_SERVICE = "openobserve";
|
|
295
|
+
/** The organisation every app's telemetry lands in. */
|
|
296
|
+
const LOGS_ORG = "default";
|
|
297
|
+
/** The stage secrets that sign in as OpenObserve's root user. */
|
|
298
|
+
const LOGS_EMAIL_KEY = "ZO_ROOT_USER_EMAIL";
|
|
299
|
+
const LOGS_PASSWORD_KEY = "ZO_ROOT_USER_PASSWORD";
|
|
300
|
+
/** The stage sends its telemetry somewhere already, and logs are on too. */
|
|
301
|
+
var LogsEndpointConflict = class extends Error {
|
|
302
|
+
stage;
|
|
303
|
+
keys;
|
|
304
|
+
constructor(stage, keys) {
|
|
305
|
+
super(`The stage '${stage}' sets ${keys.join(", ")} in its secrets, and deploy.compose.logs also runs OpenObserve for it — each backend can send its telemetry to one place. Either keep your own OTLP backend: remove logs from deploy.compose in gkm.config.ts; or use the bundled OpenObserve: remove ${keys.join(", ")} from the stage's secrets, and every backend is pointed at it.`);
|
|
306
|
+
this.stage = stage;
|
|
307
|
+
this.keys = keys;
|
|
308
|
+
this.name = "LogsEndpointConflict";
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
/** A root password OpenObserve would refuse to start with. */
|
|
312
|
+
var LogsPasswordWeak = class extends Error {
|
|
313
|
+
stage;
|
|
314
|
+
constructor(stage) {
|
|
315
|
+
super(`The stage '${stage}' sets ${LOGS_PASSWORD_KEY}, and OpenObserve refuses it: a root password must be 8 to 128 characters with a lowercase letter, an uppercase letter, a digit and a symbol. Set another with gkm secrets:set ${LOGS_PASSWORD_KEY} '…' --stage ${stage}, or unset it to have one generated.`);
|
|
316
|
+
this.stage = stage;
|
|
317
|
+
this.name = "LogsPasswordWeak";
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
/** A deployed stage with logs on and no root password generated yet. */
|
|
321
|
+
var LogsPasswordMissing = class extends Error {
|
|
322
|
+
stage;
|
|
323
|
+
constructor(stage) {
|
|
324
|
+
super(`The stage '${stage}' runs OpenObserve and has no ${LOGS_PASSWORD_KEY} in its secrets. Run gkm compose --stage ${stage} without --dry-run once to generate it, or set one: gkm secrets:set ${LOGS_PASSWORD_KEY} '…' --stage ${stage}`);
|
|
325
|
+
this.stage = stage;
|
|
326
|
+
this.name = "LogsPasswordMissing";
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
/** Whether OpenObserve accepts `password` for its root user. */
|
|
330
|
+
function isStrongLogsPassword(password) {
|
|
331
|
+
return password.length >= 8 && password.length <= 128 && /[a-z]/.test(password) && /[A-Z]/.test(password) && /\d/.test(password) && /[^A-Za-z0-9]/.test(password);
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* A root password: 192 random bits, with a fixed tail so every class
|
|
335
|
+
* OpenObserve requires is present whatever the random part drew.
|
|
336
|
+
*/
|
|
337
|
+
function generateLogsPassword() {
|
|
338
|
+
return `${(0, node_crypto.randomBytes)(24).toString("base64url")}-Zo1`;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* The stage's secrets with a generated root password, where it has none.
|
|
342
|
+
* Pure, like `withGeneratedSecrets`: the caller keeps the result once the
|
|
343
|
+
* run goes ahead.
|
|
344
|
+
*/
|
|
345
|
+
function withLogsPassword(secrets) {
|
|
346
|
+
if (secrets.custom?.["ZO_ROOT_USER_PASSWORD"]) return {
|
|
347
|
+
secrets,
|
|
348
|
+
generated: []
|
|
349
|
+
};
|
|
350
|
+
return {
|
|
351
|
+
secrets: {
|
|
352
|
+
...secrets,
|
|
353
|
+
custom: {
|
|
354
|
+
...secrets.custom,
|
|
355
|
+
[LOGS_PASSWORD_KEY]: generateLogsPassword()
|
|
356
|
+
},
|
|
357
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
358
|
+
},
|
|
359
|
+
generated: [LOGS_PASSWORD_KEY]
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* `OTEL_EXPORTER_OTLP_HEADERS` signing in as `email`.
|
|
364
|
+
*
|
|
365
|
+
* The variable's values are percent-decoded by the SDK (they follow the W3C
|
|
366
|
+
* baggage format), so the space after `Basic` is written `%20`. Base64's own
|
|
367
|
+
* `+`, `/` and `=` pass through decoding unchanged.
|
|
368
|
+
*/
|
|
369
|
+
function otlpHeaders(email, password) {
|
|
370
|
+
return `Authorization=Basic%20${Buffer.from(`${email}:${password}`).toString("base64")}`;
|
|
371
|
+
}
|
|
372
|
+
/** The stack's OpenObserve for one stage. */
|
|
373
|
+
function stackLogs(options) {
|
|
374
|
+
const { config, stage, local, custom } = options;
|
|
375
|
+
const conflicting = Object.keys(custom).filter(require_state.isOtlpDestination).sort();
|
|
376
|
+
if (conflicting.length > 0) throw new LogsEndpointConflict(stage, conflicting);
|
|
377
|
+
if (!local && !options.domain) throw new require_state.NoDomainForStage(stage);
|
|
378
|
+
const email = custom["ZO_ROOT_USER_EMAIL"] ?? (local ? "admin@gkm.localhost" : `admin@${options.domain}`);
|
|
379
|
+
const set = custom[LOGS_PASSWORD_KEY];
|
|
380
|
+
if (!set && !local) throw new LogsPasswordMissing(stage);
|
|
381
|
+
const password = set ?? "Geekmidas-1";
|
|
382
|
+
if (!isStrongLogsPassword(password)) throw new LogsPasswordWeak(stage);
|
|
383
|
+
const host = config.public ? local ? `logs.${options.project}.localhost` : `logs.${options.domain}` : void 0;
|
|
384
|
+
const url = host ? `https://${host}${local && options.https !== 443 ? `:${options.https}` : ""}` : `http://localhost:${config.port}`;
|
|
385
|
+
return {
|
|
386
|
+
...config,
|
|
387
|
+
email,
|
|
388
|
+
password,
|
|
389
|
+
passwordFromSecrets: Boolean(set),
|
|
390
|
+
url,
|
|
391
|
+
...host ? { host } : {},
|
|
392
|
+
env: {
|
|
393
|
+
[LOGS_EMAIL_KEY]: email,
|
|
394
|
+
[LOGS_PASSWORD_KEY]: password,
|
|
395
|
+
ZO_DATA_DIR: "/data",
|
|
396
|
+
ZO_COMPACT_DATA_RETENTION_DAYS: String(config.retentionDays),
|
|
397
|
+
ZO_TELEMETRY: "false"
|
|
398
|
+
},
|
|
399
|
+
appEnv: {
|
|
400
|
+
OTEL_EXPORTER_OTLP_ENDPOINT: `http://${LOGS_SERVICE}:${require_workspace.LOGS_PORT}/api/${LOGS_ORG}`,
|
|
401
|
+
OTEL_EXPORTER_OTLP_HEADERS: otlpHeaders(email, password)
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* OpenObserve's service. The image has no shell, `wget` or `curl`, so its
|
|
407
|
+
* health check is its own binary asking the running server for its status —
|
|
408
|
+
* which exits non-zero when nothing answers on its port.
|
|
409
|
+
*/
|
|
410
|
+
function logsService(logs) {
|
|
411
|
+
return {
|
|
412
|
+
image: OPENOBSERVE_IMAGE,
|
|
413
|
+
restart: "unless-stopped",
|
|
414
|
+
env_file: [{
|
|
415
|
+
path: `./${LOGS_SERVICE}.env`,
|
|
416
|
+
format: "raw"
|
|
417
|
+
}],
|
|
418
|
+
volumes: ["openobserve-data:/data"],
|
|
419
|
+
...logs.public ? {} : { ports: [`127.0.0.1:${logs.port}:${require_workspace.LOGS_PORT}`] },
|
|
420
|
+
healthcheck: {
|
|
421
|
+
test: [
|
|
422
|
+
"CMD",
|
|
423
|
+
"/openobserve",
|
|
424
|
+
"node",
|
|
425
|
+
"status"
|
|
426
|
+
],
|
|
427
|
+
interval: "10s",
|
|
428
|
+
timeout: "5s",
|
|
429
|
+
retries: 12,
|
|
430
|
+
start_period: "20s"
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
/** What `gkm compose` prints once the stack is up: how to open the logs. */
|
|
435
|
+
function logsAccess(logs, ctx) {
|
|
436
|
+
const login = `login: ${logs.email}, password: ${ctx.local && !logs.passwordFromSecrets ? logs.password : `gkm secrets:show --stage ${ctx.stage} --reveal → ${LOGS_PASSWORD_KEY}`}`;
|
|
437
|
+
if (logs.public) return [`📜 Logs (OpenObserve): ${logs.url} (${login})`, ` answered only for ${logs.public.allow.join(", ")}; every other address gets 403`];
|
|
438
|
+
return [
|
|
439
|
+
`📜 Logs (OpenObserve) on 127.0.0.1:${logs.port} — from your computer:`,
|
|
440
|
+
...ctx.local ? [` on this machine, open http://localhost:${logs.port} directly; from another:`] : [],
|
|
441
|
+
` ssh -N -L ${logs.port}:localhost:${logs.port} ${ctx.user}@${ctx.hostname} (user and host are guesses: this machine's)`,
|
|
442
|
+
` then open http://localhost:${logs.port} (${login})`,
|
|
443
|
+
`⚠️ Docker-published ports bypass ufw, so OpenObserve is bound to 127.0.0.1 only — reach it through the tunnel, not by opening the port.`
|
|
444
|
+
];
|
|
445
|
+
}
|
|
446
|
+
//#endregion
|
|
254
447
|
//#region src/compose/caddyfile.ts
|
|
255
448
|
/** The Caddyfile for a stack's edge. */
|
|
256
449
|
function edgeCaddyfile(sites, options) {
|
|
@@ -262,8 +455,13 @@ function edgeCaddyfile(sites, options) {
|
|
|
262
455
|
}` : options.email ? `{
|
|
263
456
|
email ${options.email}
|
|
264
457
|
}` : "";
|
|
265
|
-
const blocks = sites.map(({ host, upstream, rewrite }) => `https://${host} {${options.local ? `
|
|
458
|
+
const blocks = sites.map(({ host, upstream, rewrite, allow }) => `https://${host} {${options.local ? `
|
|
266
459
|
tls internal
|
|
460
|
+
` : ""}${allow ? `
|
|
461
|
+
# Only these addresses — the peer Caddy sees, never a header a client
|
|
462
|
+
# could set. Everything else is refused before it reaches the upstream.
|
|
463
|
+
@denied not remote_ip ${allow.join(" ")}
|
|
464
|
+
respond @denied 403
|
|
267
465
|
` : ""}${rewrite ? `
|
|
268
466
|
# A bucket, served at a host of its own: the bucket is a prefix on MinIO,
|
|
269
467
|
# which routes and signs on the Host header — so it is sent the upstream's.
|
|
@@ -283,8 +481,9 @@ function edgeCaddyfile(sites, options) {
|
|
|
283
481
|
}
|
|
284
482
|
const HEADER = `# Generated by gkm compose from the construct manifest — do not edit.
|
|
285
483
|
#
|
|
286
|
-
# One host per API and site, each in front of its own container,
|
|
287
|
-
# file server in front of the stack's MinIO
|
|
484
|
+
# One host per API and site, each in front of its own container, one per
|
|
485
|
+
# file server in front of the stack's MinIO, and — when its logs are served
|
|
486
|
+
# publicly — one for OpenObserve, to the addresses it allows.
|
|
288
487
|
`;
|
|
289
488
|
//#endregion
|
|
290
489
|
//#region src/compose/stack.ts
|
|
@@ -351,6 +550,20 @@ const STORAGE_KEYS = [
|
|
|
351
550
|
"AWS_SECRET_ACCESS_KEY",
|
|
352
551
|
"AWS_REGION"
|
|
353
552
|
];
|
|
553
|
+
/**
|
|
554
|
+
* Every service's Docker logs, rotated. The json-file driver keeps a
|
|
555
|
+
* container's output in one file that grows until the disk is full — on a
|
|
556
|
+
* small server, in weeks. Three files of 10 MB each per container is enough
|
|
557
|
+
* to see what happened and never fills anything. A project's own override
|
|
558
|
+
* file that sets `logging` wins over it.
|
|
559
|
+
*/
|
|
560
|
+
const LOG_ROTATION = {
|
|
561
|
+
driver: "json-file",
|
|
562
|
+
options: {
|
|
563
|
+
"max-size": "10m",
|
|
564
|
+
"max-file": "3"
|
|
565
|
+
}
|
|
566
|
+
};
|
|
354
567
|
/** A value only the stage's secrets can hold, and they do not. */
|
|
355
568
|
var StageSecretMissing = class extends Error {
|
|
356
569
|
app;
|
|
@@ -460,6 +673,16 @@ function composeStack(input) {
|
|
|
460
673
|
reads.set(app.name, site?.kind === "site" ? Object.values((0, _geekmidas_manifest.publicEnvFor)(site, manifest)) : [...require_state.appEnvKeys(manifest, app.name, input.runnables) ?? []]);
|
|
461
674
|
}
|
|
462
675
|
const domain = workspace.deploy?.domains?.[stage];
|
|
676
|
+
const logsConfig = require_workspace.resolveLogs(workspace.deploy?.compose?.logs);
|
|
677
|
+
const logs = logsConfig ? stackLogs({
|
|
678
|
+
config: logsConfig,
|
|
679
|
+
stage,
|
|
680
|
+
local,
|
|
681
|
+
project: workspace.name,
|
|
682
|
+
...domain ? { domain } : {},
|
|
683
|
+
custom,
|
|
684
|
+
https
|
|
685
|
+
}) : void 0;
|
|
463
686
|
const services = local ? {
|
|
464
687
|
missing: [],
|
|
465
688
|
minio: plan.resources.filter((r) => r.kind === "objects").map((r) => r.id),
|
|
@@ -471,7 +694,7 @@ function composeStack(input) {
|
|
|
471
694
|
allow: input.allowDevServices ?? [],
|
|
472
695
|
...domain ? { domain } : {}
|
|
473
696
|
});
|
|
474
|
-
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).sort();
|
|
697
|
+
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), ...logs ? [LOGS_SERVICE] : []].sort();
|
|
475
698
|
const storage = infra.includes("minio") ? (() => {
|
|
476
699
|
const buckets = services.minio.map((id) => byId.get(id)?.name).filter((name) => Boolean(name)).sort();
|
|
477
700
|
return {
|
|
@@ -572,6 +795,10 @@ function composeStack(input) {
|
|
|
572
795
|
}
|
|
573
796
|
app.env = {
|
|
574
797
|
...values,
|
|
798
|
+
...require_state.otelEnv({
|
|
799
|
+
...custom,
|
|
800
|
+
...logs?.appEnv
|
|
801
|
+
}, app.name),
|
|
575
802
|
NODE_ENV: "production",
|
|
576
803
|
PORT: String(app.port),
|
|
577
804
|
STAGE: stage
|
|
@@ -586,16 +813,25 @@ function composeStack(input) {
|
|
|
586
813
|
apps,
|
|
587
814
|
master: credential.master,
|
|
588
815
|
...storage ? { storage } : {},
|
|
816
|
+
...logs ? { logs } : {},
|
|
589
817
|
https,
|
|
590
818
|
http,
|
|
591
819
|
composeDir: (0, node_path.join)(workspace.root, stackDir(stage)),
|
|
592
820
|
buildRoot: layout.buildRoot,
|
|
593
821
|
workspaceRoot: workspace.root
|
|
594
822
|
});
|
|
595
|
-
const caddyfile = edgeCaddyfile([
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
823
|
+
const caddyfile = edgeCaddyfile([
|
|
824
|
+
...apps.map((app) => ({
|
|
825
|
+
host: app.host,
|
|
826
|
+
upstream: `${app.name}:${app.port}`
|
|
827
|
+
})),
|
|
828
|
+
...[...fileServers.values()].map(({ site }) => site),
|
|
829
|
+
...logs?.host && logs.public ? [{
|
|
830
|
+
host: logs.host,
|
|
831
|
+
upstream: `${LOGS_SERVICE}:${require_workspace.LOGS_PORT}`,
|
|
832
|
+
allow: logs.public.allow
|
|
833
|
+
}] : []
|
|
834
|
+
], { local });
|
|
599
835
|
const dockerfiles = {};
|
|
600
836
|
for (const app of apps) {
|
|
601
837
|
if (!app.build) continue;
|
|
@@ -617,7 +853,8 @@ function composeStack(input) {
|
|
|
617
853
|
plan,
|
|
618
854
|
credential,
|
|
619
855
|
...storage ? { storage } : {},
|
|
620
|
-
devServices: local ? [] : require_state.devServicesUsed(services)
|
|
856
|
+
devServices: local ? [] : require_state.devServicesUsed(services),
|
|
857
|
+
...logs ? { logs } : {}
|
|
621
858
|
};
|
|
622
859
|
}
|
|
623
860
|
/**
|
|
@@ -704,7 +941,8 @@ function hostOf(name, app, workspace, resource, options) {
|
|
|
704
941
|
}
|
|
705
942
|
/** The compose document: the infrastructure, the apps, and the edge. */
|
|
706
943
|
function stackFile(options) {
|
|
707
|
-
const { project, plan,
|
|
944
|
+
const { project, plan, apps } = options;
|
|
945
|
+
const infra = options.infra.filter((name) => name !== LOGS_SERVICE);
|
|
708
946
|
const derived = require_state.composeFor({
|
|
709
947
|
...plan,
|
|
710
948
|
containers: [...infra]
|
|
@@ -716,7 +954,8 @@ function stackFile(options) {
|
|
|
716
954
|
const volumes = {
|
|
717
955
|
...derived.volumes,
|
|
718
956
|
"caddy-data": {},
|
|
719
|
-
"caddy-config": {}
|
|
957
|
+
"caddy-config": {},
|
|
958
|
+
...options.logs ? { "openobserve-data": {} } : {}
|
|
720
959
|
};
|
|
721
960
|
for (const name of infra) {
|
|
722
961
|
const { ports: _ports, depends_on, profiles: _profiles, ...service } = derived.services[name];
|
|
@@ -742,6 +981,7 @@ function stackFile(options) {
|
|
|
742
981
|
};
|
|
743
982
|
minio.ports = ["127.0.0.1::9000"];
|
|
744
983
|
}
|
|
984
|
+
if (options.logs) services[LOGS_SERVICE] = logsService(options.logs);
|
|
745
985
|
for (const app of apps) services[app.name] = {
|
|
746
986
|
image: app.ref,
|
|
747
987
|
...app.build ? { build: {
|
|
@@ -784,6 +1024,10 @@ function stackFile(options) {
|
|
|
784
1024
|
retries: 5
|
|
785
1025
|
}
|
|
786
1026
|
};
|
|
1027
|
+
for (const service of Object.values(services)) service.logging = {
|
|
1028
|
+
driver: LOG_ROTATION.driver,
|
|
1029
|
+
options: { ...LOG_ROTATION.options }
|
|
1030
|
+
};
|
|
787
1031
|
return {
|
|
788
1032
|
name: project,
|
|
789
1033
|
services,
|
|
@@ -979,6 +1223,15 @@ var NoGitRevision = class extends Error {
|
|
|
979
1223
|
this.name = "NoGitRevision";
|
|
980
1224
|
}
|
|
981
1225
|
};
|
|
1226
|
+
/**
|
|
1227
|
+
* The project's own file merged over a stage's stack: `docker-compose.<stage>.yml`
|
|
1228
|
+
* at the workspace root. What the generated file cannot know — a port bound
|
|
1229
|
+
* to a tailnet address, another log driver — goes there, and is never
|
|
1230
|
+
* overwritten.
|
|
1231
|
+
*/
|
|
1232
|
+
function stackOverrideFile(root, stage) {
|
|
1233
|
+
return (0, node_path.join)(root, `docker-compose.${stage}.yml`);
|
|
1234
|
+
}
|
|
982
1235
|
/** The commit HEAD is at, short — and `-dirty` when the tree has changes. */
|
|
983
1236
|
async function gitRevision(root) {
|
|
984
1237
|
let sha;
|
|
@@ -1084,7 +1337,14 @@ async function validateCompose(ctx, deps, forced) {
|
|
|
1084
1337
|
}
|
|
1085
1338
|
const stack = withBuildCredentials(composed, masterKeys);
|
|
1086
1339
|
if (mode === "pull" && !ctx.dryRun) await assertImagesExist(deps.docker, tag, stack.apps);
|
|
1340
|
+
if (stack.logs && (!stack.local || stack.logs.passwordFromSecrets)) {
|
|
1341
|
+
ctx.secrets.mask(stack.logs.password);
|
|
1342
|
+
ctx.secrets.mask(stack.logs.appEnv.OTEL_EXPORTER_OTLP_HEADERS);
|
|
1343
|
+
}
|
|
1087
1344
|
const dir = (0, node_path.join)(root, stackDir(stage));
|
|
1345
|
+
const override = stackOverrideFile(root, stage);
|
|
1346
|
+
const overrides = (0, node_fs.existsSync)(override) ? [override] : [];
|
|
1347
|
+
if (overrides.length > 0) ctx.logger.info(`🧩 Merging docker-compose.${stage}.yml over the generated stack`);
|
|
1088
1348
|
const previous = Object.fromEntries(Object.entries((await ctx.state.read(stage))?.state.releases ?? {}).map(([name, releases]) => {
|
|
1089
1349
|
const { ref, tag, digest } = releases.current;
|
|
1090
1350
|
return [name, {
|
|
@@ -1100,6 +1360,7 @@ async function validateCompose(ctx, deps, forced) {
|
|
|
1100
1360
|
ref: {
|
|
1101
1361
|
project: composeProject(identity),
|
|
1102
1362
|
file: (0, node_path.join)(dir, "docker-compose.yml"),
|
|
1363
|
+
...overrides.length > 0 ? { overrides } : {},
|
|
1103
1364
|
cwd: root,
|
|
1104
1365
|
output: ctx.childOutput,
|
|
1105
1366
|
signal: ctx.signal
|
|
@@ -1125,7 +1386,13 @@ async function stageSecrets(ctx, manifest) {
|
|
|
1125
1386
|
secrets: stored,
|
|
1126
1387
|
generated: []
|
|
1127
1388
|
};
|
|
1128
|
-
const
|
|
1389
|
+
const withSeed = require_state.withGeneratedSecrets(stored ?? require_state.initStageSecrets(ctx.stage), manifest);
|
|
1390
|
+
const withLogs = require_workspace.resolveLogs(ctx.workspace.deploy?.compose?.logs) ? withLogsPassword(withSeed.secrets) : {
|
|
1391
|
+
secrets: withSeed.secrets,
|
|
1392
|
+
generated: []
|
|
1393
|
+
};
|
|
1394
|
+
const secrets = withLogs.secrets;
|
|
1395
|
+
const generated = [...withSeed.generated, ...withLogs.generated];
|
|
1129
1396
|
if (secrets.seed) ctx.secrets.mask(secrets.seed);
|
|
1130
1397
|
for (const value of Object.values(secrets.custom ?? {})) ctx.secrets.mask(value);
|
|
1131
1398
|
return {
|
|
@@ -1337,15 +1604,83 @@ async function verifyCompose(ctx, run, deps) {
|
|
|
1337
1604
|
const { stack } = run;
|
|
1338
1605
|
const ca = stack.local ? await (0, node_fs_promises.readFile)(caFile(run), "utf-8").catch(() => void 0) : void 0;
|
|
1339
1606
|
ctx.logger.info("\n🩺 Checking each app through Caddy…");
|
|
1340
|
-
const
|
|
1607
|
+
const results = await Promise.all(stack.apps.map((app) => checkApp(ctx, app, deps, {
|
|
1341
1608
|
ca,
|
|
1342
1609
|
local: stack.local
|
|
1343
|
-
})))
|
|
1610
|
+
})));
|
|
1611
|
+
if (stack.logs) results.push(await checkLogs(ctx, run, deps));
|
|
1612
|
+
const down = results.filter((result) => !result.healthy).map(({ app, url, last }) => ({
|
|
1344
1613
|
app,
|
|
1345
1614
|
url,
|
|
1346
1615
|
last
|
|
1347
1616
|
}));
|
|
1348
1617
|
if (down.length > 0) throw new ComposeAppsUnhealthy(stack.project, down);
|
|
1618
|
+
if (stack.logs) reportLogs(ctx, stack.logs, stack.local);
|
|
1619
|
+
}
|
|
1620
|
+
/**
|
|
1621
|
+
* OpenObserve's own health check, as Docker reports it. It is not asked
|
|
1622
|
+
* through the edge: by default it has no host there, and served publicly it
|
|
1623
|
+
* answers only the addresses it allows — which need not include this one.
|
|
1624
|
+
*/
|
|
1625
|
+
async function checkLogs(ctx, run, deps) {
|
|
1626
|
+
const url = `docker:${LOGS_SERVICE}`;
|
|
1627
|
+
let last = "not asked";
|
|
1628
|
+
for (let attempt = 1; attempt <= deps.healthAttempts; attempt++) {
|
|
1629
|
+
ctx.signal.throwIfAborted();
|
|
1630
|
+
const health = await deps.docker.health(run.ref, LOGS_SERVICE).catch((error) => error instanceof Error ? error.message : String(error));
|
|
1631
|
+
last = health ?? "not running";
|
|
1632
|
+
const healthy = health === "healthy";
|
|
1633
|
+
ctx.emit({
|
|
1634
|
+
type: "health.checked",
|
|
1635
|
+
app: LOGS_SERVICE,
|
|
1636
|
+
url,
|
|
1637
|
+
healthy,
|
|
1638
|
+
attempt
|
|
1639
|
+
});
|
|
1640
|
+
if (healthy) {
|
|
1641
|
+
ctx.logger.info(` ✓ ${LOGS_SERVICE.padEnd(12)} ${url} (${last})`);
|
|
1642
|
+
return {
|
|
1643
|
+
app: LOGS_SERVICE,
|
|
1644
|
+
url,
|
|
1645
|
+
last,
|
|
1646
|
+
healthy
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
if (attempt < deps.healthAttempts) await new Promise((resolve) => setTimeout(resolve, deps.healthIntervalMs));
|
|
1650
|
+
}
|
|
1651
|
+
ctx.logger.warn(` ✗ ${LOGS_SERVICE.padEnd(12)} ${url} (${last})`);
|
|
1652
|
+
return {
|
|
1653
|
+
app: LOGS_SERVICE,
|
|
1654
|
+
url,
|
|
1655
|
+
last,
|
|
1656
|
+
healthy: false
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1659
|
+
/** How to open the logs, printed — and as an event, for a headless caller. */
|
|
1660
|
+
function reportLogs(ctx, logs, local) {
|
|
1661
|
+
const lines = logsAccess(logs, {
|
|
1662
|
+
stage: ctx.stage,
|
|
1663
|
+
local,
|
|
1664
|
+
user: currentUser(),
|
|
1665
|
+
hostname: (0, node_os.hostname)()
|
|
1666
|
+
});
|
|
1667
|
+
ctx.logger.info(`\n${lines.join("\n")}`);
|
|
1668
|
+
ctx.emit({
|
|
1669
|
+
type: "logs.ready",
|
|
1670
|
+
service: LOGS_SERVICE,
|
|
1671
|
+
access: logs.public ? "public" : "tunnel",
|
|
1672
|
+
url: logs.url,
|
|
1673
|
+
...logs.public ? { allow: [...logs.public.allow] } : { port: logs.port },
|
|
1674
|
+
email: logs.email
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1677
|
+
/** This machine's user — a guess at who would SSH in. */
|
|
1678
|
+
function currentUser() {
|
|
1679
|
+
try {
|
|
1680
|
+
return (0, node_os.userInfo)().username;
|
|
1681
|
+
} catch {
|
|
1682
|
+
return process.env.USER ?? "user";
|
|
1683
|
+
}
|
|
1349
1684
|
}
|
|
1350
1685
|
async function checkApp(ctx, app, deps, options) {
|
|
1351
1686
|
const url = `${app.url}${app.kind === "site" ? "/" : "/health"}`;
|
|
@@ -1450,6 +1785,7 @@ async function writeStack(root, dir, stack, credentials = {}) {
|
|
|
1450
1785
|
await write((0, node_path.join)(dir, "docker-compose.yml"), composeYaml(stack));
|
|
1451
1786
|
await write((0, node_path.join)(dir, "Caddyfile"), stack.caddyfile);
|
|
1452
1787
|
for (const app of stack.apps) if (app.env) await write((0, node_path.join)(dir, `${app.name}.env`), envFile(app.env), 384);
|
|
1788
|
+
if (stack.logs) await write((0, node_path.join)(dir, `${LOGS_SERVICE}.env`), envFile(stack.logs.env), 384);
|
|
1453
1789
|
for (const [name, payload] of Object.entries(credentials)) await write((0, node_path.join)(dir, credentialsFile(name)), require_state.credentialsFileContent(payload), 384);
|
|
1454
1790
|
for (const [path, content] of Object.entries(stack.dockerfiles)) await write((0, node_path.join)(root, path), content);
|
|
1455
1791
|
await require_state.ensureDockerignore(require_state.findBuildRoot(root));
|
|
@@ -1563,5 +1899,11 @@ Object.defineProperty(exports, "stackDir", {
|
|
|
1563
1899
|
return stackDir;
|
|
1564
1900
|
}
|
|
1565
1901
|
});
|
|
1902
|
+
Object.defineProperty(exports, "stackOverrideFile", {
|
|
1903
|
+
enumerable: true,
|
|
1904
|
+
get: function() {
|
|
1905
|
+
return stackOverrideFile;
|
|
1906
|
+
}
|
|
1907
|
+
});
|
|
1566
1908
|
|
|
1567
|
-
//# sourceMappingURL=compose-
|
|
1909
|
+
//# sourceMappingURL=compose-B-MME-4C.cjs.map
|