@geekmidas/cli 10.0.0-alpha.64 → 10.0.0-alpha.66
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/README.md +15 -1
- package/dist/{HostingerProvider-Bx2lHHvu.cjs → HostingerProvider-BznqRC2S.cjs} +5 -5
- package/dist/HostingerProvider-BznqRC2S.cjs.map +1 -0
- package/dist/{HostingerProvider-Cd4aS5Kj.mjs → HostingerProvider-CY_VpVud.mjs} +4 -4
- package/dist/HostingerProvider-CY_VpVud.mjs.map +1 -0
- package/dist/{LegacyStateStore-C2abV49N.cjs → LegacyStateStore-BFcTT2QL.cjs} +2 -2
- package/dist/{LegacyStateStore-C2abV49N.cjs.map → LegacyStateStore-BFcTT2QL.cjs.map} +1 -1
- package/dist/{LegacyStateStore-DIgkGaUP.mjs → LegacyStateStore-CFheFo62.mjs} +2 -2
- package/dist/{LegacyStateStore-DIgkGaUP.mjs.map → LegacyStateStore-CFheFo62.mjs.map} +1 -1
- package/dist/{LocalStateStore-DiJqKyPy.cjs → LocalStateStore-BVUexSvu.cjs} +2 -2
- package/dist/{LocalStateStore-DiJqKyPy.cjs.map → LocalStateStore-BVUexSvu.cjs.map} +1 -1
- package/dist/{LocalStateStore-Cy1ZBW7U.mjs → LocalStateStore-Bm3x9oOg.mjs} +2 -2
- package/dist/{LocalStateStore-Cy1ZBW7U.mjs.map → LocalStateStore-Bm3x9oOg.mjs.map} +1 -1
- package/dist/{OpenApiTsGenerator-BWuDRHe9.cjs → OpenApiTsGenerator-B2zrdyP4.cjs} +2 -2
- package/dist/{OpenApiTsGenerator-BWuDRHe9.cjs.map → OpenApiTsGenerator-B2zrdyP4.cjs.map} +1 -1
- package/dist/{OpenApiTsGenerator-C6v-IeHt.mjs → OpenApiTsGenerator-BmUAcR7d.mjs} +2 -2
- package/dist/{OpenApiTsGenerator-C6v-IeHt.mjs.map → OpenApiTsGenerator-BmUAcR7d.mjs.map} +1 -1
- package/dist/{Route53Provider-DVILBsvV.cjs → Route53Provider-Bllus2NR.cjs} +2 -2
- package/dist/Route53Provider-Bllus2NR.cjs.map +1 -0
- package/dist/{Route53Provider-Cvj40A8I.mjs → Route53Provider-QXxjkZO1.mjs} +2 -2
- package/dist/Route53Provider-QXxjkZO1.mjs.map +1 -0
- package/dist/{S3StateStore-D3x6ivBL.mjs → S3StateStore-BFHuctWM.mjs} +2 -2
- package/dist/{S3StateStore-D3x6ivBL.mjs.map → S3StateStore-BFHuctWM.mjs.map} +1 -1
- package/dist/{S3StateStore-Ds0V1Lgm.cjs → S3StateStore-CoXBhILd.cjs} +2 -2
- package/dist/{S3StateStore-Ds0V1Lgm.cjs.map → S3StateStore-CoXBhILd.cjs.map} +1 -1
- package/dist/{SSMStateStore-CLipvx47.cjs → SSMStateStore-BqCA9TP2.cjs} +2 -2
- package/dist/{SSMStateStore-CLipvx47.cjs.map → SSMStateStore-BqCA9TP2.cjs.map} +1 -1
- package/dist/{SSMStateStore-DfF174xQ.mjs → SSMStateStore-C1kvHScY.mjs} +2 -2
- package/dist/{SSMStateStore-DfF174xQ.mjs.map → SSMStateStore-C1kvHScY.mjs.map} +1 -1
- package/dist/{backup-provisioner-DIVCbfee.mjs → backup-provisioner-C8DnCmxK.mjs} +2 -2
- package/dist/backup-provisioner-C8DnCmxK.mjs.map +1 -0
- package/dist/{backup-provisioner-BmhIO_Yr.cjs → backup-provisioner-oDEsPj02.cjs} +2 -2
- package/dist/backup-provisioner-oDEsPj02.cjs.map +1 -0
- package/dist/{bundler-DYgm9xsd.mjs → bundler-Dvaiud0h.mjs} +4 -4
- package/dist/{bundler-DYgm9xsd.mjs.map → bundler-Dvaiud0h.mjs.map} +1 -1
- package/dist/{bundler-C8uJf324.cjs → bundler-orRqUJNZ.cjs} +4 -4
- package/dist/{bundler-C8uJf324.cjs.map → bundler-orRqUJNZ.cjs.map} +1 -1
- package/dist/{cli-Bogz-zEj.mjs → cli-CfHm0AO7.mjs} +7 -6
- package/dist/cli-CfHm0AO7.mjs.map +1 -0
- package/dist/{cli-D3jofJ6j.cjs → cli-D1JkkOwg.cjs} +8 -7
- package/dist/cli-D1JkkOwg.cjs.map +1 -0
- package/dist/compose-CNQL8BnO.mjs +1398 -0
- package/dist/compose-CNQL8BnO.mjs.map +1 -0
- package/dist/compose-jrLy7KGO.cjs +1427 -0
- package/dist/compose-jrLy7KGO.cjs.map +1 -0
- package/dist/{config-Be-4_I6H.d.cts → config-4XFxX1al.d.cts} +3 -3
- package/dist/{config-Be-4_I6H.d.cts.map → config-4XFxX1al.d.cts.map} +1 -1
- package/dist/{config-DqNRiQOp.d.mts → config-UtKzhpFb.d.mts} +3 -3
- package/dist/{config-DqNRiQOp.d.mts.map → config-UtKzhpFb.d.mts.map} +1 -1
- package/dist/config.cjs +3 -3
- package/dist/config.d.cts +3 -3
- package/dist/config.d.mts +3 -3
- package/dist/config.mjs +3 -3
- package/dist/{credentials-B70vmLcm.cjs → credentials-Bzbg23n8.cjs} +176 -3
- package/dist/credentials-Bzbg23n8.cjs.map +1 -0
- package/dist/{credentials-s0wMeZBU.mjs → credentials-Dq1MnWQf.mjs} +128 -3
- package/dist/credentials-Dq1MnWQf.mjs.map +1 -0
- package/dist/databases-C2p5LGAp.cjs +419 -0
- package/dist/databases-C2p5LGAp.cjs.map +1 -0
- package/dist/databases-hUHTuwhx.mjs +346 -0
- package/dist/databases-hUHTuwhx.mjs.map +1 -0
- package/dist/{define-CYs0PEfZ.mjs → define-DR8xGYfL.mjs} +5 -1
- package/dist/define-DR8xGYfL.mjs.map +1 -0
- package/dist/{define-zUsP72ym.cjs → define-DzRz8FgH.cjs} +5 -1
- package/dist/define-DzRz8FgH.cjs.map +1 -0
- package/dist/deploy/public.cjs +35 -26
- package/dist/deploy/public.cjs.map +1 -1
- package/dist/deploy/public.d.cts +140 -20
- package/dist/deploy/public.d.cts.map +1 -1
- package/dist/deploy/public.d.mts +140 -20
- package/dist/deploy/public.d.mts.map +1 -1
- package/dist/deploy/public.mjs +13 -13
- package/dist/{terminal-Bdv--jhY.cjs → deploy-B7aJi_Ah.cjs} +146 -116
- package/dist/deploy-B7aJi_Ah.cjs.map +1 -0
- package/dist/{terminal-D7Ntz6WE.mjs → deploy-DWn7YfIc.mjs} +137 -89
- package/dist/deploy-DWn7YfIc.mjs.map +1 -0
- package/dist/{discover-LMrqsWSU.mjs → discover-BFoJWKGD.mjs} +4 -2
- package/dist/discover-BFoJWKGD.mjs.map +1 -0
- package/dist/{discover-DHdECueV.cjs → discover-OEfZ9OkA.cjs} +4 -2
- package/dist/discover-OEfZ9OkA.cjs.map +1 -0
- package/dist/dokploy-B4LcYG1E.cjs +38 -0
- package/dist/dokploy-B4LcYG1E.cjs.map +1 -0
- package/dist/dokploy-DalwLML5.mjs +38 -0
- package/dist/dokploy-DalwLML5.mjs.map +1 -0
- package/dist/{encryption-C9X98u2F.cjs → encryption-3KlGS1sl.cjs} +6 -8
- package/dist/{encryption-C9X98u2F.cjs.map → encryption-3KlGS1sl.cjs.map} +1 -1
- package/dist/{encryption-CKHjMpCr.mjs → encryption-CPfENwWO.mjs} +3 -5
- package/dist/{encryption-CKHjMpCr.mjs.map → encryption-CPfENwWO.mjs.map} +1 -1
- package/dist/{deploy-ZRXIPp1c.mjs → engine-CunvOG3s.mjs} +2116 -1855
- package/dist/engine-CunvOG3s.mjs.map +1 -0
- package/dist/{deploy-55rJkdKz.cjs → engine-DR6KurWX.cjs} +2172 -1869
- package/dist/engine-DR6KurWX.cjs.map +1 -0
- package/dist/{github-Dd3tMa1u.cjs → github-BJ5gh9bw.cjs} +7 -7
- package/dist/{github-Dd3tMa1u.cjs.map → github-BJ5gh9bw.cjs.map} +1 -1
- package/dist/{github-D-byNI7T.mjs → github-MXA9v7Ge.mjs} +3 -3
- package/dist/{github-D-byNI7T.mjs.map → github-MXA9v7Ge.mjs.map} +1 -1
- package/dist/{index-CfE9Y885.d.cts → index-CyApaHVe.d.cts} +16 -3
- package/dist/index-CyApaHVe.d.cts.map +1 -0
- package/dist/{index-CJ2qsSpp.d.mts → index-DD1FfUHh.d.mts} +16 -3
- package/dist/index-DD1FfUHh.d.mts.map +1 -0
- package/dist/index.cjs +465 -1691
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +405 -1619
- package/dist/index.mjs.map +1 -1
- package/dist/{local-C7wnrcYK.cjs → local-BdH4XN0E.cjs} +1 -178
- package/dist/local-BdH4XN0E.cjs.map +1 -0
- package/dist/{local-Cd8ZYQG3.mjs → local-CWR4Y_kW.mjs} +3 -126
- package/dist/local-CWR4Y_kW.mjs.map +1 -0
- package/dist/{migrate-B4gngTLe.mjs → migrate-BJBcm4sG.mjs} +7 -6
- package/dist/{migrate-B4gngTLe.mjs.map → migrate-BJBcm4sG.mjs.map} +1 -1
- package/dist/{migrate-tx9-P9uz.cjs → migrate-CF4J4GQS.cjs} +20 -19
- package/dist/{migrate-tx9-P9uz.cjs.map → migrate-CF4J4GQS.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/provider-B3Sv_a74.cjs +173 -0
- package/dist/provider-B3Sv_a74.cjs.map +1 -0
- package/dist/provider-C9_-45d5.cjs +56 -0
- package/dist/provider-C9_-45d5.cjs.map +1 -0
- package/dist/provider-D7rQzzzf.mjs +138 -0
- package/dist/provider-D7rQzzzf.mjs.map +1 -0
- package/dist/provider-q4lsYzYG.mjs +45 -0
- package/dist/provider-q4lsYzYG.mjs.map +1 -0
- package/dist/{ready-bQfu9GES.mjs → ready-2s3-CndS.mjs} +3 -3
- package/dist/{ready-bQfu9GES.mjs.map → ready-2s3-CndS.mjs.map} +1 -1
- package/dist/{ready-brEhh68f.cjs → ready-B6uHr4sm.cjs} +3 -3
- package/dist/{ready-brEhh68f.cjs.map → ready-B6uHr4sm.cjs.map} +1 -1
- package/dist/reconcile/public.cjs +1 -1
- package/dist/reconcile/public.d.cts +1 -1
- package/dist/reconcile/public.d.mts +1 -1
- package/dist/reconcile/public.mjs +1 -1
- package/dist/resolve-CVmch0wI.cjs +315 -0
- package/dist/resolve-CVmch0wI.cjs.map +1 -0
- package/dist/resolve-Db0tNlrW.mjs +262 -0
- package/dist/resolve-Db0tNlrW.mjs.map +1 -0
- package/dist/rollback-DvAo4OKS.mjs +133 -0
- package/dist/rollback-DvAo4OKS.mjs.map +1 -0
- package/dist/rollback-DxJK84hy.cjs +162 -0
- package/dist/rollback-DxJK84hy.cjs.map +1 -0
- package/dist/{runtime-CJEYK4Gz.d.mts → runtime-BRqFOvPM.d.cts} +2 -2
- package/dist/{runtime-CJEYK4Gz.d.mts.map → runtime-BRqFOvPM.d.cts.map} +1 -1
- package/dist/{runtime-Cjq1jlVX.d.cts → runtime-CEk5n4VH.d.mts} +2 -2
- package/dist/{runtime-Cjq1jlVX.d.cts.map → runtime-CEk5n4VH.d.mts.map} +1 -1
- package/dist/sandbox/discover-worker.cjs +1 -1
- package/dist/sandbox/discover-worker.mjs +1 -1
- package/dist/sandbox/migrate-worker.cjs +61 -0
- package/dist/sandbox/migrate-worker.cjs.map +1 -0
- package/dist/sandbox/migrate-worker.d.cts +1 -0
- package/dist/sandbox/migrate-worker.d.mts +1 -0
- package/dist/sandbox/migrate-worker.mjs +62 -0
- package/dist/sandbox/migrate-worker.mjs.map +1 -0
- package/dist/{schemas-D2LCF689.mjs → schemas-Cfhtpdo0.mjs} +10 -2
- package/dist/{schemas-Ckm_aBnF.cjs.map → schemas-Cfhtpdo0.mjs.map} +1 -1
- package/dist/{schemas-Ckm_aBnF.cjs → schemas-RUlKHu27.cjs} +15 -1
- package/dist/{schemas-D2LCF689.mjs.map → schemas-RUlKHu27.cjs.map} +1 -1
- package/dist/sniffer-5xmrkh2a.cjs +1742 -0
- package/dist/sniffer-5xmrkh2a.cjs.map +1 -0
- package/dist/sniffer-BAQhLEsI.mjs +1617 -0
- package/dist/sniffer-BAQhLEsI.mjs.map +1 -0
- package/dist/{sst-Cper040I.mjs → sst-B2HbrDR3.mjs} +4 -4
- package/dist/{sst-Cper040I.mjs.map → sst-B2HbrDR3.mjs.map} +1 -1
- package/dist/{sst-BdALKEas.cjs → sst-DcfsVH6x.cjs} +4 -4
- package/dist/{sst-BdALKEas.cjs.map → sst-DcfsVH6x.cjs.map} +1 -1
- package/dist/{sniffer-ByhbEW5A.cjs → state-Y6ATakMX.cjs} +1769 -2759
- package/dist/state-Y6ATakMX.cjs.map +1 -0
- package/dist/{sniffer-49KPbDgq.mjs → state-uuYqnRVO.mjs} +1763 -2764
- package/dist/state-uuYqnRVO.mjs.map +1 -0
- package/dist/target/public.cjs +6 -6
- package/dist/target/public.d.cts +2 -2
- package/dist/target/public.d.mts +2 -2
- package/dist/target/public.mjs +3 -3
- package/dist/terminal-CHy_MdjG.cjs +84 -0
- package/dist/terminal-CHy_MdjG.cjs.map +1 -0
- package/dist/terminal-DUhcZ1ND.mjs +84 -0
- package/dist/terminal-DUhcZ1ND.mjs.map +1 -0
- package/dist/{types-DkrDOMxw.d.cts → types-CSw2w69a.d.mts} +110 -20
- package/dist/types-CSw2w69a.d.mts.map +1 -0
- package/dist/{types-B_bEQvb9.d.mts → types-D5WIxRDP.d.cts} +111 -19
- package/dist/types-D5WIxRDP.d.cts.map +1 -0
- package/dist/vitest.cjs +1 -1
- package/dist/vitest.mjs +1 -1
- package/dist/workspace/index.cjs +3 -1
- package/dist/workspace/index.d.cts +3 -3
- package/dist/workspace/index.d.mts +3 -3
- package/dist/workspace/index.mjs +2 -2
- package/dist/{workspace-Bwit4q9O.mjs → workspace-Be6uBhlF.mjs} +36 -6
- package/dist/workspace-Be6uBhlF.mjs.map +1 -0
- package/dist/{workspace-BwgWJK7h.cjs → workspace-ON-bqiJz.cjs} +47 -5
- package/dist/workspace-ON-bqiJz.cjs.map +1 -0
- package/package.json +13 -13
- package/dist/HostingerProvider-Bx2lHHvu.cjs.map +0 -1
- package/dist/HostingerProvider-Cd4aS5Kj.mjs.map +0 -1
- package/dist/Route53Provider-Cvj40A8I.mjs.map +0 -1
- package/dist/Route53Provider-DVILBsvV.cjs.map +0 -1
- package/dist/backup-provisioner-BmhIO_Yr.cjs.map +0 -1
- package/dist/backup-provisioner-DIVCbfee.mjs.map +0 -1
- package/dist/cli-Bogz-zEj.mjs.map +0 -1
- package/dist/cli-D3jofJ6j.cjs.map +0 -1
- package/dist/credentials-B70vmLcm.cjs.map +0 -1
- package/dist/credentials-s0wMeZBU.mjs.map +0 -1
- package/dist/define-CYs0PEfZ.mjs.map +0 -1
- package/dist/define-zUsP72ym.cjs.map +0 -1
- package/dist/deploy-55rJkdKz.cjs.map +0 -1
- package/dist/deploy-Dc2LBJaK.mjs +0 -136
- package/dist/deploy-Dc2LBJaK.mjs.map +0 -1
- package/dist/deploy-ZRXIPp1c.mjs.map +0 -1
- package/dist/deploy-yUcR2OlW.cjs +0 -141
- package/dist/deploy-yUcR2OlW.cjs.map +0 -1
- package/dist/discover-DHdECueV.cjs.map +0 -1
- package/dist/discover-LMrqsWSU.mjs.map +0 -1
- package/dist/dokploy-CepdXuMl.mjs +0 -32
- package/dist/dokploy-CepdXuMl.mjs.map +0 -1
- package/dist/dokploy-D1d37rvM.cjs +0 -32
- package/dist/dokploy-D1d37rvM.cjs.map +0 -1
- package/dist/index-CJ2qsSpp.d.mts.map +0 -1
- package/dist/index-CfE9Y885.d.cts.map +0 -1
- package/dist/local-C7wnrcYK.cjs.map +0 -1
- package/dist/local-Cd8ZYQG3.mjs.map +0 -1
- package/dist/resolve-BXsNQjMR.cjs +0 -220
- package/dist/resolve-BXsNQjMR.cjs.map +0 -1
- package/dist/resolve-DZ2IJ0qY.mjs +0 -173
- package/dist/resolve-DZ2IJ0qY.mjs.map +0 -1
- package/dist/runtime-BGNDrHzh.cjs +0 -163
- package/dist/runtime-BGNDrHzh.cjs.map +0 -1
- package/dist/runtime-Cmn-FeYO.mjs +0 -134
- package/dist/runtime-Cmn-FeYO.mjs.map +0 -1
- package/dist/sniffer-49KPbDgq.mjs.map +0 -1
- package/dist/sniffer-ByhbEW5A.cjs.map +0 -1
- package/dist/storage-BzaUxmqe.mjs +0 -124
- package/dist/storage-BzaUxmqe.mjs.map +0 -1
- package/dist/storage-CSGoC3bK.cjs +0 -152
- package/dist/storage-CSGoC3bK.cjs.map +0 -1
- package/dist/store-BaaonhkJ.mjs +0 -415
- package/dist/store-BaaonhkJ.mjs.map +0 -1
- package/dist/store-D_2ByRtW.cjs +0 -510
- package/dist/store-D_2ByRtW.cjs.map +0 -1
- package/dist/terminal-Bdv--jhY.cjs.map +0 -1
- package/dist/terminal-D7Ntz6WE.mjs.map +0 -1
- package/dist/types-B_bEQvb9.d.mts.map +0 -1
- package/dist/types-DkrDOMxw.d.cts.map +0 -1
- package/dist/workspace-BwgWJK7h.cjs.map +0 -1
- package/dist/workspace-Bwit4q9O.mjs.map +0 -1
|
@@ -0,0 +1,1398 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-BhDjJH2R.mjs";
|
|
2
|
+
import { c as runOutput, s as run, t as LocalSandbox } from "./local-CWR4Y_kW.mjs";
|
|
3
|
+
import { r as discover } from "./discover-BFoJWKGD.mjs";
|
|
4
|
+
import { i as activeSandbox } from "./sandbox-Dm247NGx.mjs";
|
|
5
|
+
import { t as appKey } from "./derive-B4vDpzgP.mjs";
|
|
6
|
+
import { t as defineTarget } from "./define-DR8xGYfL.mjs";
|
|
7
|
+
import { B as appEnvKeys, C as validateImageRef, Et as initStageSecrets, G as planFor, H as networkEnv, I as postgresStatements, O as constructGlobs, P as applyPostgres, Q as DEFAULT_IMAGES, R as composeFor, S as generateSlimDockerfile, T as resolveHost, U as envFor, W as localRolePassword, _ as appPackageName, a as recordRelease, b as detectPackageManager, m as withGeneratedSecrets, nt as primaryPortKey, p as DeployJournal, q as hostFor, t as createEmptyState, tt as portKeys, w as isMainFrontendApp, x as generateDockerignore, xt as imageRef, y as siteDockerfile, z as pgClient } from "./state-uuYqnRVO.mjs";
|
|
8
|
+
import { n as DEFAULT_CACHE, r as DEFAULT_EVENTS } from "./fakes-C9RiT6XG.mjs";
|
|
9
|
+
import { u as migrateDatabases } from "./databases-hUHTuwhx.mjs";
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
12
|
+
import { appendFile, chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
13
|
+
import { provideKey, provisionOrder, publicEnvFor } from "@geekmidas/manifest";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
import { spawn } from "node:child_process";
|
|
16
|
+
import { stringify } from "yaml";
|
|
17
|
+
import { request } from "node:https";
|
|
18
|
+
//#region src/compose/docker.ts
|
|
19
|
+
/**
|
|
20
|
+
* The Docker operations `gkm compose` needs, behind an interface.
|
|
21
|
+
*
|
|
22
|
+
* Small on purpose, like reconcile's: the rules about *when* each runs — the
|
|
23
|
+
* registry is asked before anything is pulled, nothing is started for a tag
|
|
24
|
+
* that is missing — are asserted against a fake, and this file is only the
|
|
25
|
+
* wiring to the real CLI.
|
|
26
|
+
*
|
|
27
|
+
* Every call is an argument array. Image refs, tags and paths come from the
|
|
28
|
+
* workspace, and none of them reaches a shell.
|
|
29
|
+
*/
|
|
30
|
+
/** `output` as `spawn` reads it. */
|
|
31
|
+
const STDIO = {
|
|
32
|
+
inherit: "inherit",
|
|
33
|
+
stderr: [
|
|
34
|
+
"ignore",
|
|
35
|
+
2,
|
|
36
|
+
2
|
|
37
|
+
],
|
|
38
|
+
ignore: "ignore"
|
|
39
|
+
};
|
|
40
|
+
/** How a stack's docker commands run: where it is, where output goes. */
|
|
41
|
+
function runOptions(stack, env) {
|
|
42
|
+
return {
|
|
43
|
+
cwd: stack.cwd,
|
|
44
|
+
stdio: STDIO[stack.output ?? "inherit"],
|
|
45
|
+
...env ? { env } : {},
|
|
46
|
+
...stack.signal ? { signal: stack.signal } : {}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** How long a stack has to pass its health checks. */
|
|
50
|
+
const WAIT_TIMEOUT_S = 180;
|
|
51
|
+
/** `docker compose port` printed nothing a port can be read from. */
|
|
52
|
+
var ServicePortUnknown = class extends Error {
|
|
53
|
+
service;
|
|
54
|
+
inside;
|
|
55
|
+
output;
|
|
56
|
+
constructor(service, inside, output) {
|
|
57
|
+
super(`'${service}' publishes no host port for ${inside} (docker said: ${output.trim() || "nothing"}). Is it running? \`docker compose ps\` shows the stack.`);
|
|
58
|
+
this.service = service;
|
|
59
|
+
this.inside = inside;
|
|
60
|
+
this.output = output;
|
|
61
|
+
this.name = "ServicePortUnknown";
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
function compose(stack, args) {
|
|
65
|
+
return [
|
|
66
|
+
"compose",
|
|
67
|
+
"-p",
|
|
68
|
+
stack.project,
|
|
69
|
+
"-f",
|
|
70
|
+
stack.file,
|
|
71
|
+
...args
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
/** Run a program and keep everything it said, whatever it exits with. */
|
|
75
|
+
function capture(command, args, timeoutMs = 6e4) {
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
const child = spawn(command, [...args], {
|
|
78
|
+
stdio: [
|
|
79
|
+
"ignore",
|
|
80
|
+
"pipe",
|
|
81
|
+
"pipe"
|
|
82
|
+
],
|
|
83
|
+
shell: false
|
|
84
|
+
});
|
|
85
|
+
let stdout = "";
|
|
86
|
+
let stderr = "";
|
|
87
|
+
child.stdout.on("data", (chunk) => {
|
|
88
|
+
stdout += chunk.toString();
|
|
89
|
+
});
|
|
90
|
+
child.stderr.on("data", (chunk) => {
|
|
91
|
+
stderr += chunk.toString();
|
|
92
|
+
});
|
|
93
|
+
const timer = setTimeout(() => child.kill("SIGKILL"), timeoutMs);
|
|
94
|
+
timer.unref?.();
|
|
95
|
+
child.on("error", (error) => {
|
|
96
|
+
clearTimeout(timer);
|
|
97
|
+
reject(error);
|
|
98
|
+
});
|
|
99
|
+
child.on("close", (code) => {
|
|
100
|
+
clearTimeout(timer);
|
|
101
|
+
resolve({
|
|
102
|
+
code,
|
|
103
|
+
stdout,
|
|
104
|
+
stderr
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Whether a failed `docker manifest inspect` means the tag is not there, as
|
|
111
|
+
* opposed to the registry not answering.
|
|
112
|
+
*
|
|
113
|
+
* Read off the message because that is all the CLI gives. Anything not
|
|
114
|
+
* recognisably "no such tag" is treated as the registry being unreachable:
|
|
115
|
+
* a registry that refused credentials must not be reported as a tag that was
|
|
116
|
+
* never pushed, or the fix offered would be the wrong one.
|
|
117
|
+
*/
|
|
118
|
+
function isMissingManifest(output) {
|
|
119
|
+
return /no such manifest|manifest unknown|manifest for .* not found|not found: manifest/i.test(output);
|
|
120
|
+
}
|
|
121
|
+
const dockerCompose = {
|
|
122
|
+
async lookup(ref) {
|
|
123
|
+
const { code, stdout, stderr } = await capture("docker", [
|
|
124
|
+
"manifest",
|
|
125
|
+
"inspect",
|
|
126
|
+
ref
|
|
127
|
+
]);
|
|
128
|
+
if (code === 0) return {
|
|
129
|
+
ref,
|
|
130
|
+
status: "found"
|
|
131
|
+
};
|
|
132
|
+
const output = `${stderr}\n${stdout}`.trim();
|
|
133
|
+
return isMissingManifest(output) ? {
|
|
134
|
+
ref,
|
|
135
|
+
status: "missing"
|
|
136
|
+
} : {
|
|
137
|
+
ref,
|
|
138
|
+
status: "unreachable",
|
|
139
|
+
detail: output
|
|
140
|
+
};
|
|
141
|
+
},
|
|
142
|
+
async build(stack, services) {
|
|
143
|
+
await run("docker", compose(stack, ["build", ...services]), runOptions(stack, {
|
|
144
|
+
...process.env,
|
|
145
|
+
DOCKER_BUILDKIT: "1"
|
|
146
|
+
}));
|
|
147
|
+
},
|
|
148
|
+
async pull(stack, services) {
|
|
149
|
+
await run("docker", compose(stack, ["pull", ...services]), runOptions(stack));
|
|
150
|
+
},
|
|
151
|
+
async up(stack, services) {
|
|
152
|
+
await run("docker", compose(stack, [
|
|
153
|
+
"up",
|
|
154
|
+
"-d",
|
|
155
|
+
"--wait",
|
|
156
|
+
`--wait-timeout=${WAIT_TIMEOUT_S}`,
|
|
157
|
+
...services ? [] : ["--remove-orphans"],
|
|
158
|
+
...services ?? []
|
|
159
|
+
]), runOptions(stack));
|
|
160
|
+
},
|
|
161
|
+
async down(stack) {
|
|
162
|
+
await run("docker", compose(stack, ["down"]), runOptions(stack));
|
|
163
|
+
},
|
|
164
|
+
async port(stack, service, inside) {
|
|
165
|
+
const output = await runOutput("docker", compose(stack, [
|
|
166
|
+
"port",
|
|
167
|
+
service,
|
|
168
|
+
String(inside)
|
|
169
|
+
]), {
|
|
170
|
+
cwd: stack.cwd,
|
|
171
|
+
...stack.signal ? { signal: stack.signal } : {}
|
|
172
|
+
});
|
|
173
|
+
const port = Number(output.trim().split(":").pop());
|
|
174
|
+
if (!Number.isInteger(port) || port <= 0) throw new ServicePortUnknown(service, inside, output);
|
|
175
|
+
return port;
|
|
176
|
+
},
|
|
177
|
+
async copyOut(stack, service, from, to) {
|
|
178
|
+
await mkdir(dirname(to), { recursive: true });
|
|
179
|
+
await run("docker", compose(stack, [
|
|
180
|
+
"cp",
|
|
181
|
+
`${service}:${from}`,
|
|
182
|
+
to
|
|
183
|
+
]), runOptions(stack));
|
|
184
|
+
},
|
|
185
|
+
async digest(ref) {
|
|
186
|
+
const { code, stdout } = await capture("docker", [
|
|
187
|
+
"image",
|
|
188
|
+
"inspect",
|
|
189
|
+
"--format={{json .RepoDigests}} {{.Id}}",
|
|
190
|
+
ref
|
|
191
|
+
]);
|
|
192
|
+
if (code !== 0) return void 0;
|
|
193
|
+
const [json = "[]", id] = stdout.trim().split(" ");
|
|
194
|
+
const repository = ref.split("@")[0].replace(/:[\w][\w.-]*$/, "");
|
|
195
|
+
try {
|
|
196
|
+
const digests = JSON.parse(json);
|
|
197
|
+
const match = Array.isArray(digests) ? digests.find((entry) => typeof entry === "string" && entry.startsWith(`${repository}@`)) : void 0;
|
|
198
|
+
if (match) return match.slice(repository.length + 1);
|
|
199
|
+
} catch {}
|
|
200
|
+
return id || void 0;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
//#endregion
|
|
204
|
+
//#region src/compose/images.ts
|
|
205
|
+
/**
|
|
206
|
+
* The tag a site's image carries for a release.
|
|
207
|
+
*
|
|
208
|
+
* A site's public URLs are inlined when it is built, so one bundle serves one
|
|
209
|
+
* stage: `v1.4.0-production` and `v1.4.0-staging` are two images of the same
|
|
210
|
+
* commit. A backend reads its URLs at runtime, so one image serves every stage
|
|
211
|
+
* and its tag is the release's alone.
|
|
212
|
+
*/
|
|
213
|
+
function siteTag(tag, stage) {
|
|
214
|
+
return `${tag}-${stage}`;
|
|
215
|
+
}
|
|
216
|
+
/** Images a tag names that the registry does not have. */
|
|
217
|
+
var ImageTagNotFound = class extends Error {
|
|
218
|
+
tag;
|
|
219
|
+
refs;
|
|
220
|
+
constructor(tag, refs) {
|
|
221
|
+
super(`The registry has no image for ${refs.length === 1 ? "this app" : `${refs.length} apps`} at '${tag}':\n` + refs.map((ref) => ` - ${ref}`).join("\n") + `\nNothing was pulled or started. Push '${tag}' for every app from CI, pick a tag that exists, or run without --tag to build from this checkout.`);
|
|
222
|
+
this.tag = tag;
|
|
223
|
+
this.refs = refs;
|
|
224
|
+
this.name = "ImageTagNotFound";
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
/** The registry did not answer, or did not let this machine in. */
|
|
228
|
+
var RegistryUnreachable = class extends Error {
|
|
229
|
+
ref;
|
|
230
|
+
detail;
|
|
231
|
+
constructor(ref, detail) {
|
|
232
|
+
super(`Could not ask the registry about ${ref}: ${detail || "no answer"}\nNothing was pulled or started. Check the registry is reachable and that this machine is logged in to it (\`docker login <registry>\`).`);
|
|
233
|
+
this.ref = ref;
|
|
234
|
+
this.detail = detail;
|
|
235
|
+
this.name = "RegistryUnreachable";
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
/**
|
|
239
|
+
* Assert every image exists, before anything else happens.
|
|
240
|
+
*
|
|
241
|
+
* Asked all at once and reported all at once: a release missing two images is
|
|
242
|
+
* one problem with two names, and finding the second only after fixing the
|
|
243
|
+
* first is a slower way to learn the same thing.
|
|
244
|
+
*
|
|
245
|
+
* @throws {RegistryUnreachable} when the registry cannot say
|
|
246
|
+
* @throws {ImageTagNotFound} listing every missing image
|
|
247
|
+
*/
|
|
248
|
+
async function assertImagesExist(docker, tag, images) {
|
|
249
|
+
const lookups = await Promise.all(images.map((image) => docker.lookup(image.ref)));
|
|
250
|
+
const unreachable = lookups.find((lookup) => lookup.status === "unreachable");
|
|
251
|
+
if (unreachable?.status === "unreachable") throw new RegistryUnreachable(unreachable.ref, unreachable.detail);
|
|
252
|
+
const missing = lookups.filter((lookup) => lookup.status === "missing").map((lookup) => lookup.ref);
|
|
253
|
+
if (missing.length > 0) throw new ImageTagNotFound(tag, missing);
|
|
254
|
+
}
|
|
255
|
+
//#endregion
|
|
256
|
+
//#region src/compose/caddyfile.ts
|
|
257
|
+
/** The Caddyfile for a stack's edge. */
|
|
258
|
+
function edgeCaddyfile(sites, options) {
|
|
259
|
+
const global = options.local ? `{
|
|
260
|
+
# No ACME and no public DNS: the CA is Caddy's own and the names resolve
|
|
261
|
+
# to this machine.
|
|
262
|
+
local_certs
|
|
263
|
+
auto_https disable_redirects
|
|
264
|
+
}` : options.email ? `{
|
|
265
|
+
email ${options.email}
|
|
266
|
+
}` : "";
|
|
267
|
+
const blocks = sites.map(({ host, upstream }) => `https://${host} {${options.local ? `
|
|
268
|
+
tls internal
|
|
269
|
+
` : ""}
|
|
270
|
+
# Host and X-Forwarded-* are passed through as Caddy does by default, so
|
|
271
|
+
# an app builds redirects, cookies and links on the address a caller used.
|
|
272
|
+
reverse_proxy ${upstream} {
|
|
273
|
+
# Never buffer: a streamed response — server-sent events, a Next.js
|
|
274
|
+
# RSC payload — reaches the client as each chunk is written.
|
|
275
|
+
flush_interval -1
|
|
276
|
+
}
|
|
277
|
+
}`);
|
|
278
|
+
return `${HEADER}${global ? `${global}\n\n` : ""}${blocks.join("\n\n")}\n`;
|
|
279
|
+
}
|
|
280
|
+
const HEADER = `# Generated by gkm compose from the construct manifest — do not edit.
|
|
281
|
+
#
|
|
282
|
+
# One host per API and site, each in front of its own container.
|
|
283
|
+
`;
|
|
284
|
+
//#endregion
|
|
285
|
+
//#region src/compose/stack.ts
|
|
286
|
+
/**
|
|
287
|
+
* The stack a workspace runs as for one stage — pure.
|
|
288
|
+
*
|
|
289
|
+
* Everything `gkm compose` writes is decided here, as data: the compose file,
|
|
290
|
+
* the Caddyfile, one env file per backend, and the image each app runs. The
|
|
291
|
+
* command around it reads the workspace, asks the registry, writes the files
|
|
292
|
+
* and starts Docker; none of those decide anything, so all of it can be
|
|
293
|
+
* asserted without a daemon.
|
|
294
|
+
*
|
|
295
|
+
* The values come from the same derivations every other target uses. The
|
|
296
|
+
* plan is reconcile's `planFor`, the keys an app reads are `appEnvKeys`, and
|
|
297
|
+
* every URL is `envFor`'s — asked twice, once with each app at its public
|
|
298
|
+
* address and once on the compose network, because a browser and a sibling
|
|
299
|
+
* service reach the same API by different names.
|
|
300
|
+
*/
|
|
301
|
+
/** Where a stack's files are written, relative to the workspace root. */
|
|
302
|
+
function stackDir(stage) {
|
|
303
|
+
return `.gkm/compose/${stage}`;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* The compose project a stage's stack runs as.
|
|
307
|
+
*
|
|
308
|
+
* The deploy identity's scope, so two workspaces with one name in different
|
|
309
|
+
* namespaces never share containers or volumes — and the stage, so a
|
|
310
|
+
* machine can run two stages side by side.
|
|
311
|
+
*/
|
|
312
|
+
function composeProject(identity) {
|
|
313
|
+
return `${identity.scope}-${identity.stage}`;
|
|
314
|
+
}
|
|
315
|
+
/** The ports the edge is published on, and the variables that move them. */
|
|
316
|
+
const EDGE_PORT_ENV = {
|
|
317
|
+
https: "GKM_COMPOSE_HTTPS_PORT",
|
|
318
|
+
http: "GKM_COMPOSE_HTTP_PORT"
|
|
319
|
+
};
|
|
320
|
+
/**
|
|
321
|
+
* Containers this target does not run.
|
|
322
|
+
*
|
|
323
|
+
* Object storage is the stage's own — an externally hosted bucket whose URL
|
|
324
|
+
* the stage's secrets hold — and so is a deployed stage's mail. The AWS
|
|
325
|
+
* emulator never applies: a stack is a server target, whose events are
|
|
326
|
+
* pg-boss.
|
|
327
|
+
*/
|
|
328
|
+
const NOT_RUN = {
|
|
329
|
+
minio: true,
|
|
330
|
+
localstack: true,
|
|
331
|
+
caddy: true
|
|
332
|
+
};
|
|
333
|
+
/** Kinds whose `<ID>_URL` a stage must be given, because nothing here runs them. */
|
|
334
|
+
const EXTERNAL_KINDS = {
|
|
335
|
+
objects: true,
|
|
336
|
+
"file-server": true
|
|
337
|
+
};
|
|
338
|
+
/** The S3 client's credentials, read beside a bucket's URL when it has them. */
|
|
339
|
+
const STORAGE_KEYS = [
|
|
340
|
+
"AWS_ACCESS_KEY_ID",
|
|
341
|
+
"AWS_SECRET_ACCESS_KEY",
|
|
342
|
+
"AWS_REGION"
|
|
343
|
+
];
|
|
344
|
+
/** A bucket the stack needs and the stage was not given. */
|
|
345
|
+
var BucketNotConfigured = class extends Error {
|
|
346
|
+
app;
|
|
347
|
+
id;
|
|
348
|
+
key;
|
|
349
|
+
stage;
|
|
350
|
+
constructor(app, id, key, stage) {
|
|
351
|
+
super(`'${app}' uses the bucket '${id}', and gkm compose runs no object storage: the stage needs an externally hosted bucket. Set its URL in the stage's secrets: gkm secrets:set ${key} 's3://…' --stage ${stage}`);
|
|
352
|
+
this.app = app;
|
|
353
|
+
this.id = id;
|
|
354
|
+
this.key = key;
|
|
355
|
+
this.stage = stage;
|
|
356
|
+
this.name = "BucketNotConfigured";
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
/** A value only the stage's secrets can hold, and they do not. */
|
|
360
|
+
var StageSecretMissing = class extends Error {
|
|
361
|
+
app;
|
|
362
|
+
key;
|
|
363
|
+
stage;
|
|
364
|
+
constructor(app, key, stage) {
|
|
365
|
+
super(`'${app}' reads ${key}, and the stage '${stage}' has none. Nothing in the stack can derive it; set it in the stage's secrets: gkm secrets:set ${key} '…' --stage ${stage}`);
|
|
366
|
+
this.app = app;
|
|
367
|
+
this.key = key;
|
|
368
|
+
this.stage = stage;
|
|
369
|
+
this.name = "StageSecretMissing";
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
/** A deployed stage's secrets with no seed to derive its passwords from. */
|
|
373
|
+
var StageSeedMissing = class extends Error {
|
|
374
|
+
stage;
|
|
375
|
+
constructor(stage) {
|
|
376
|
+
super(`The stage '${stage}' has no seed in its secrets, and a deployed stage's database passwords are derived from it. Run gkm compose without --dry-run once, or gkm deploy, to generate it.`);
|
|
377
|
+
this.stage = stage;
|
|
378
|
+
this.name = "StageSeedMissing";
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
/** A workspace with no API and no site to run. */
|
|
382
|
+
var NothingToCompose = class extends Error {
|
|
383
|
+
root;
|
|
384
|
+
constructor(root) {
|
|
385
|
+
super(`${root} declares no RestApi and no site, so a stack would run nothing. Workers and mobile apps are not run by gkm compose yet.`);
|
|
386
|
+
this.root = root;
|
|
387
|
+
this.name = "NothingToCompose";
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
/** A value that cannot be written to an env file a line at a time. */
|
|
391
|
+
var EnvValueMultiline = class extends Error {
|
|
392
|
+
app;
|
|
393
|
+
key;
|
|
394
|
+
constructor(app, key) {
|
|
395
|
+
super(`${key} for '${app}' spans more than one line, and an env file holds one value per line. Store it on one line — base64, or JSON — and decode it where it is read.`);
|
|
396
|
+
this.app = app;
|
|
397
|
+
this.key = key;
|
|
398
|
+
this.name = "EnvValueMultiline";
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
/** The services a stack's apps wait on and the edge waits on in turn. */
|
|
402
|
+
const HEALTHY = { condition: "service_healthy" };
|
|
403
|
+
/**
|
|
404
|
+
* An app's health check: its own server, asked on 127.0.0.1 — not
|
|
405
|
+
* `localhost`, which resolves to `::1` first in an Alpine container while a
|
|
406
|
+
* server may listen on IPv4 alone.
|
|
407
|
+
*/
|
|
408
|
+
function probe(port, path) {
|
|
409
|
+
return {
|
|
410
|
+
test: [
|
|
411
|
+
"CMD",
|
|
412
|
+
"wget",
|
|
413
|
+
"-q",
|
|
414
|
+
"-O",
|
|
415
|
+
"/dev/null",
|
|
416
|
+
`http://127.0.0.1:${port}${path}`
|
|
417
|
+
],
|
|
418
|
+
interval: "5s",
|
|
419
|
+
timeout: "3s",
|
|
420
|
+
retries: 20,
|
|
421
|
+
start_period: "10s"
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
/** The stack for one stage. */
|
|
425
|
+
function composeStack(input) {
|
|
426
|
+
const { workspace, manifest, stage, identity } = input;
|
|
427
|
+
const local = stage === workspace.stages.local;
|
|
428
|
+
const project = composeProject(identity);
|
|
429
|
+
const custom = input.secrets?.custom ?? {};
|
|
430
|
+
const https = input.ports?.https ?? 443;
|
|
431
|
+
const http = input.ports?.http ?? 80;
|
|
432
|
+
const plan = planFor(manifest, stage, provisionOrder(manifest), {
|
|
433
|
+
localStage: workspace.stages.local,
|
|
434
|
+
events: DEFAULT_EVENTS.server,
|
|
435
|
+
cache: DEFAULT_CACHE.server,
|
|
436
|
+
edge: false
|
|
437
|
+
});
|
|
438
|
+
const byId = new Map(plan.resources.map((r) => [r.id, r]));
|
|
439
|
+
const seed = local ? void 0 : input.secrets?.seed;
|
|
440
|
+
if (!local && !seed) throw new StageSeedMissing(stage);
|
|
441
|
+
const credential = {
|
|
442
|
+
master: local ? "geekmidas" : localRolePassword(workspace.name, plan, "master", seed),
|
|
443
|
+
...seed ? { seed } : {}
|
|
444
|
+
};
|
|
445
|
+
const infra = plan.containers.filter((c) => !NOT_RUN[c]).filter((c) => local || c !== "mailpit").sort();
|
|
446
|
+
const apps = stackApps(workspace, manifest, plan, {
|
|
447
|
+
local,
|
|
448
|
+
stage,
|
|
449
|
+
https,
|
|
450
|
+
identity,
|
|
451
|
+
images: input.images
|
|
452
|
+
});
|
|
453
|
+
if (apps.length === 0) throw new NothingToCompose(workspace.root);
|
|
454
|
+
const derivation = {
|
|
455
|
+
project: workspace.name,
|
|
456
|
+
master: credential.master,
|
|
457
|
+
...credential.seed ? { seed: credential.seed } : {}
|
|
458
|
+
};
|
|
459
|
+
const outside = networkEnv(plan, {
|
|
460
|
+
...derivation,
|
|
461
|
+
addresses: Object.fromEntries(apps.map((app) => [app.id, app.url]))
|
|
462
|
+
});
|
|
463
|
+
const inside = networkEnv(plan, {
|
|
464
|
+
...derivation,
|
|
465
|
+
addresses: Object.fromEntries(apps.map((app) => [app.id, `http://${app.name}:${app.port}`]))
|
|
466
|
+
});
|
|
467
|
+
const owners = /* @__PURE__ */ new Map();
|
|
468
|
+
for (const resource of plan.resources) {
|
|
469
|
+
owners.set(resource.envKey, resource);
|
|
470
|
+
if (resource.kind === "email") {
|
|
471
|
+
owners.set(provideKey(resource.id, "from"), resource);
|
|
472
|
+
owners.set(provideKey(resource.id, "inboxUrl"), resource);
|
|
473
|
+
}
|
|
474
|
+
if (resource.kind === "external-api") owners.set(provideKey(resource.id, "credentials"), resource);
|
|
475
|
+
}
|
|
476
|
+
const surfaceUrls = new Map(plan.resources.filter((r) => r.kind === "rest-api" || r.kind === "site").map((r) => [r.envKey, r.id]));
|
|
477
|
+
const appById = new Map(apps.map((app) => [app.id, app]));
|
|
478
|
+
/**
|
|
479
|
+
* What a key resolves to for one app, from the stage's secrets where only
|
|
480
|
+
* they can hold it, and the derivations otherwise.
|
|
481
|
+
*/
|
|
482
|
+
const valueFor = (app, key, owner) => {
|
|
483
|
+
if (owner && EXTERNAL_KINDS[owner.kind] && key === owner.envKey) {
|
|
484
|
+
const value = custom[key];
|
|
485
|
+
if (value === void 0) throw new BucketNotConfigured(app.name, owner.id, key, stage);
|
|
486
|
+
return value;
|
|
487
|
+
}
|
|
488
|
+
if (owner && suppliedBy(owner, key, local)) {
|
|
489
|
+
const value = custom[key];
|
|
490
|
+
if (value === void 0) throw new StageSecretMissing(app.name, key, stage);
|
|
491
|
+
return value;
|
|
492
|
+
}
|
|
493
|
+
if (custom[key] !== void 0) return custom[key];
|
|
494
|
+
const surface = surfaceUrls.get(key);
|
|
495
|
+
if (app.kind === "rest-api" && surface && surface !== app.id) return inside[key];
|
|
496
|
+
return outside[key];
|
|
497
|
+
};
|
|
498
|
+
for (const app of apps) {
|
|
499
|
+
if (app.kind === "site") {
|
|
500
|
+
const site = manifest[app.id];
|
|
501
|
+
if (site?.kind !== "site") continue;
|
|
502
|
+
const args = {};
|
|
503
|
+
for (const [key, source] of Object.entries(publicEnvFor(site, manifest))) {
|
|
504
|
+
const value = valueFor(app, source, owners.get(source));
|
|
505
|
+
if (value !== void 0) args[key] = value;
|
|
506
|
+
}
|
|
507
|
+
if (app.build && Object.keys(args).length > 0) app.build.args = args;
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
const keys = appEnvKeys(manifest, app.name, input.runnables) ?? /* @__PURE__ */ new Set();
|
|
511
|
+
const values = {};
|
|
512
|
+
for (const key of [...keys].sort()) {
|
|
513
|
+
const owner = owners.get(key) ?? storageOwner(key, plan);
|
|
514
|
+
if (!local && owner?.kind === "email" && key.endsWith("_INBOX_URL")) continue;
|
|
515
|
+
if (STORAGE_KEYS.includes(key)) {
|
|
516
|
+
if (custom[key] !== void 0) values[key] = custom[key];
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
const value = valueFor(app, key, owner);
|
|
520
|
+
if (value !== void 0) values[key] = value;
|
|
521
|
+
}
|
|
522
|
+
const trusted = provideKey(app.id, "trustedOrigins");
|
|
523
|
+
if (keys.has(trusted)) {
|
|
524
|
+
const internal = (byId.get(app.id)?.callers ?? []).map((caller) => appById.get(caller)).filter((caller) => caller?.kind === "rest-api").map((caller) => `http://${caller.name}:${caller.port}`);
|
|
525
|
+
const origins = [...(values[trusted] ?? "").split(",").filter(Boolean), ...internal];
|
|
526
|
+
values[trusted] = [...new Set(origins)].join(",");
|
|
527
|
+
}
|
|
528
|
+
app.env = {
|
|
529
|
+
...values,
|
|
530
|
+
NODE_ENV: "production",
|
|
531
|
+
PORT: String(app.port),
|
|
532
|
+
STAGE: stage
|
|
533
|
+
};
|
|
534
|
+
for (const [key, value] of Object.entries(app.env)) if (/[\r\n]/.test(value)) throw new EnvValueMultiline(app.name, key);
|
|
535
|
+
}
|
|
536
|
+
const compose = stackFile({
|
|
537
|
+
project,
|
|
538
|
+
plan,
|
|
539
|
+
infra,
|
|
540
|
+
apps,
|
|
541
|
+
master: credential.master,
|
|
542
|
+
https,
|
|
543
|
+
http
|
|
544
|
+
});
|
|
545
|
+
const caddyfile = edgeCaddyfile(apps.map((app) => ({
|
|
546
|
+
host: app.host,
|
|
547
|
+
upstream: `${app.name}:${app.port}`
|
|
548
|
+
})), { local });
|
|
549
|
+
const dockerfiles = {};
|
|
550
|
+
for (const app of apps) {
|
|
551
|
+
if (!app.build) continue;
|
|
552
|
+
dockerfiles[app.build.dockerfile] = app.kind === "site" ? siteDockerfile(app.name, workspace.apps[app.name], {
|
|
553
|
+
turboPackage: input.packages?.[app.name] ?? app.name,
|
|
554
|
+
packageManager: input.packageManager ?? "pnpm",
|
|
555
|
+
manifest
|
|
556
|
+
}) : generateSlimDockerfile({
|
|
557
|
+
imageName: app.name,
|
|
558
|
+
baseImage: "node:22-alpine",
|
|
559
|
+
port: app.port,
|
|
560
|
+
healthCheckPath: "/health",
|
|
561
|
+
prebuilt: true,
|
|
562
|
+
packageManager: input.packageManager ?? "pnpm",
|
|
563
|
+
bundle: `${app.path}/.gkm/server/dist/server.mjs`
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
return {
|
|
567
|
+
project,
|
|
568
|
+
stage,
|
|
569
|
+
local,
|
|
570
|
+
apps,
|
|
571
|
+
infra,
|
|
572
|
+
compose,
|
|
573
|
+
caddyfile,
|
|
574
|
+
dockerfiles,
|
|
575
|
+
plan,
|
|
576
|
+
credential
|
|
577
|
+
};
|
|
578
|
+
/** Whether only the stage's secrets can supply `key` to `owner`. */
|
|
579
|
+
function suppliedBy(owner, key, isLocal) {
|
|
580
|
+
if (owner.kind === "credential") return true;
|
|
581
|
+
if (owner.kind === "external-api") return key === provideKey(owner.id, "credentials");
|
|
582
|
+
if (isLocal) return false;
|
|
583
|
+
if (owner.kind === "secret" || owner.kind === "encryption") return true;
|
|
584
|
+
return owner.kind === "email" && !key.endsWith("_INBOX_URL");
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
/** Which bucket an S3 credential key belongs to — the first one declared. */
|
|
588
|
+
function storageOwner(key, plan) {
|
|
589
|
+
if (!STORAGE_KEYS.includes(key)) return void 0;
|
|
590
|
+
return plan.resources.find((r) => r.kind === "objects");
|
|
591
|
+
}
|
|
592
|
+
/** The apps a stack runs: every API and every site, at its stage address. */
|
|
593
|
+
function stackApps(workspace, manifest, plan, options) {
|
|
594
|
+
const apps = [];
|
|
595
|
+
const resources = new Map(plan.resources.map((r) => [r.id, r]));
|
|
596
|
+
for (const [id, declaration] of Object.entries(manifest)) {
|
|
597
|
+
if (declaration.kind !== "rest-api" && declaration.kind !== "site") continue;
|
|
598
|
+
const name = appKey(id);
|
|
599
|
+
const app = workspace.apps[name];
|
|
600
|
+
const resource = resources.get(id);
|
|
601
|
+
if (!app || !resource || !app.port) continue;
|
|
602
|
+
const host = hostOf(name, app, workspace, resource, options);
|
|
603
|
+
const tag = declaration.kind === "site" ? siteTag(options.images.tag, options.stage) : options.images.tag;
|
|
604
|
+
const ref = validateImageRef(imageRef(options.identity, name, options.images.registry, tag));
|
|
605
|
+
apps.push({
|
|
606
|
+
app: name,
|
|
607
|
+
name,
|
|
608
|
+
id,
|
|
609
|
+
kind: declaration.kind,
|
|
610
|
+
path: app.path,
|
|
611
|
+
port: app.port,
|
|
612
|
+
host,
|
|
613
|
+
url: `https://${host}${options.local && options.https !== 443 ? `:${options.https}` : ""}`,
|
|
614
|
+
ref,
|
|
615
|
+
tag,
|
|
616
|
+
...options.images.mode === "build" ? { build: { dockerfile: `${stackDir(options.stage)}/Dockerfile.${name}` } } : {}
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
return apps.sort((a, b) => a.name.localeCompare(b.name));
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* The host an app answers on for a stage: the `gkm dev` edge's
|
|
623
|
+
* `*.localhost` names for the local stage, and the stage's domain — by the
|
|
624
|
+
* same rule a deploy follows — for any other.
|
|
625
|
+
*/
|
|
626
|
+
function hostOf(name, app, workspace, resource, options) {
|
|
627
|
+
if (options.local) return hostFor(resource, workspace.name);
|
|
628
|
+
return resolveHost(name, app, options.stage, workspace.deploy?.domains, isMainFrontendApp(name, app, workspace.apps));
|
|
629
|
+
}
|
|
630
|
+
/** The compose document: the infrastructure, the apps, and the edge. */
|
|
631
|
+
function stackFile(options) {
|
|
632
|
+
const { project, plan, infra, apps } = options;
|
|
633
|
+
const derived = composeFor({
|
|
634
|
+
...plan,
|
|
635
|
+
containers: [...infra]
|
|
636
|
+
}, {
|
|
637
|
+
project,
|
|
638
|
+
ports: Object.fromEntries(portKeys(infra, plan.fakes).map((key) => [key, 0]))
|
|
639
|
+
});
|
|
640
|
+
const services = {};
|
|
641
|
+
const volumes = {
|
|
642
|
+
...derived.volumes,
|
|
643
|
+
"caddy-data": {},
|
|
644
|
+
"caddy-config": {}
|
|
645
|
+
};
|
|
646
|
+
for (const name of infra) {
|
|
647
|
+
const { ports: _ports, depends_on, profiles: _profiles, ...service } = derived.services[name];
|
|
648
|
+
services[name] = {
|
|
649
|
+
...service,
|
|
650
|
+
...depends_on?.length ? { depends_on: Object.fromEntries(depends_on.map((dep) => [dep, HEALTHY])) } : {}
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
const postgres = services.postgres;
|
|
654
|
+
if (postgres) {
|
|
655
|
+
postgres.environment = {
|
|
656
|
+
...postgres.environment,
|
|
657
|
+
POSTGRES_PASSWORD: options.master
|
|
658
|
+
};
|
|
659
|
+
postgres.ports = ["127.0.0.1::5432"];
|
|
660
|
+
}
|
|
661
|
+
for (const app of apps) services[app.name] = {
|
|
662
|
+
image: app.ref,
|
|
663
|
+
...app.build ? { build: {
|
|
664
|
+
context: "../../..",
|
|
665
|
+
dockerfile: app.build.dockerfile,
|
|
666
|
+
...app.build.args ? { args: app.build.args } : {}
|
|
667
|
+
} } : {},
|
|
668
|
+
restart: "unless-stopped",
|
|
669
|
+
...app.kind === "rest-api" ? {
|
|
670
|
+
env_file: [{
|
|
671
|
+
path: `./${app.name}.env`,
|
|
672
|
+
format: "raw"
|
|
673
|
+
}],
|
|
674
|
+
...infra.length ? { depends_on: Object.fromEntries(infra.map((name) => [name, HEALTHY])) } : {}
|
|
675
|
+
} : {},
|
|
676
|
+
healthcheck: probe(app.port, app.kind === "site" ? "/" : "/health")
|
|
677
|
+
};
|
|
678
|
+
services.caddy = {
|
|
679
|
+
image: DEFAULT_IMAGES.caddy,
|
|
680
|
+
restart: "unless-stopped",
|
|
681
|
+
ports: [`${options.https}:443`, `${options.http}:80`],
|
|
682
|
+
volumes: [
|
|
683
|
+
"./Caddyfile:/etc/caddy/Caddyfile:ro",
|
|
684
|
+
"caddy-data:/data",
|
|
685
|
+
"caddy-config:/config"
|
|
686
|
+
],
|
|
687
|
+
depends_on: Object.fromEntries(apps.map((app) => [app.name, HEALTHY])),
|
|
688
|
+
healthcheck: {
|
|
689
|
+
test: [
|
|
690
|
+
"CMD",
|
|
691
|
+
"caddy",
|
|
692
|
+
"version"
|
|
693
|
+
],
|
|
694
|
+
interval: "10s",
|
|
695
|
+
timeout: "5s",
|
|
696
|
+
retries: 5
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
return {
|
|
700
|
+
name: project,
|
|
701
|
+
services,
|
|
702
|
+
volumes
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
/** Render an app's env file: one `KEY=value` per line, read raw by compose. */
|
|
706
|
+
function envFile(env) {
|
|
707
|
+
return `${Object.entries(env).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${key}=${value}`).join("\n")}\n`;
|
|
708
|
+
}
|
|
709
|
+
//#endregion
|
|
710
|
+
//#region src/target/compose/health.ts
|
|
711
|
+
/**
|
|
712
|
+
* Asking each app of a stack whether it answers — through Caddy, over HTTPS,
|
|
713
|
+
* the way a browser reaches it.
|
|
714
|
+
*
|
|
715
|
+
* `docker compose up --wait` already knows each container passed its own
|
|
716
|
+
* health check, asked on 127.0.0.1 inside it. What that cannot say is whether
|
|
717
|
+
* the stack is reachable: whether the edge routes the host to the app, and
|
|
718
|
+
* whether the certificate it presents is one a client accepts. So the check
|
|
719
|
+
* goes through the edge, by hostname, and verifies the certificate.
|
|
720
|
+
*/
|
|
721
|
+
/** A request that got no answer in time. */
|
|
722
|
+
var HealthRequestTimedOut = class extends Error {
|
|
723
|
+
url;
|
|
724
|
+
timeoutMs;
|
|
725
|
+
constructor(url, timeoutMs) {
|
|
726
|
+
super(`${url} did not answer within ${timeoutMs}ms. Check the stack is up with \`docker compose ps\`.`);
|
|
727
|
+
this.url = url;
|
|
728
|
+
this.timeoutMs = timeoutMs;
|
|
729
|
+
this.name = "HealthRequestTimedOut";
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
/** The real probe: one HTTPS GET, the certificate verified. */
|
|
733
|
+
const httpsProbe = ({ url, ca, connectTo, timeoutMs, signal }) => {
|
|
734
|
+
const target = new URL(url);
|
|
735
|
+
return new Promise((resolve, reject) => {
|
|
736
|
+
const req = request({
|
|
737
|
+
host: connectTo ?? target.hostname,
|
|
738
|
+
port: target.port || 443,
|
|
739
|
+
servername: target.hostname,
|
|
740
|
+
path: `${target.pathname}${target.search}`,
|
|
741
|
+
method: "GET",
|
|
742
|
+
headers: { host: target.host },
|
|
743
|
+
...ca ? { ca } : {},
|
|
744
|
+
...signal ? { signal } : {},
|
|
745
|
+
timeout: timeoutMs
|
|
746
|
+
}, (res) => {
|
|
747
|
+
res.resume();
|
|
748
|
+
res.on("end", () => resolve(res.statusCode ?? 0));
|
|
749
|
+
res.on("error", reject);
|
|
750
|
+
});
|
|
751
|
+
req.on("timeout", () => req.destroy(new HealthRequestTimedOut(url, timeoutMs)));
|
|
752
|
+
req.on("error", reject);
|
|
753
|
+
req.end();
|
|
754
|
+
});
|
|
755
|
+
};
|
|
756
|
+
/** Whether a status means the app answered as it should. */
|
|
757
|
+
function isHealthy(status) {
|
|
758
|
+
return status >= 200 && status < 400;
|
|
759
|
+
}
|
|
760
|
+
/** Apps that did not answer through the edge. */
|
|
761
|
+
var ComposeAppsUnhealthy = class extends Error {
|
|
762
|
+
project;
|
|
763
|
+
apps;
|
|
764
|
+
constructor(project, apps) {
|
|
765
|
+
super(`${project} started, but ${apps.length === 1 ? "this app does" : "these apps do"} not answer through Caddy:\n` + apps.map(({ url, last }) => ` - ${url}: ${last}`).join("\n") + `\nSee why with \`docker compose -p ${project} logs caddy ${apps.map((a) => a.app).join(" ")}\`.`);
|
|
766
|
+
this.project = project;
|
|
767
|
+
this.apps = apps;
|
|
768
|
+
this.name = "ComposeAppsUnhealthy";
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
//#endregion
|
|
772
|
+
//#region src/target/compose/phases.ts
|
|
773
|
+
/**
|
|
774
|
+
* The compose target's phases: a stage as one Docker Compose stack behind
|
|
775
|
+
* Caddy, on the machine the deploy runs on.
|
|
776
|
+
*
|
|
777
|
+
* The order is the point of this file, and the rules it keeps are these:
|
|
778
|
+
*
|
|
779
|
+
* - A tag is checked before anything happens (`validate`). Every app's image
|
|
780
|
+
* is asked of the registry first; one missing image stops the run naming
|
|
781
|
+
* them all, and nothing is written, pulled or started.
|
|
782
|
+
* - The databases exist before the apps do (`provision`). Postgres comes up
|
|
783
|
+
* alone, its databases, roles and grants are created and its migrations
|
|
784
|
+
* applied from this machine, and only then do the apps start — so an app
|
|
785
|
+
* never boots against a schema that is not there yet.
|
|
786
|
+
* - What a stage runs is recorded (`release`). Each app's image, the tag it
|
|
787
|
+
* was released under and the digest it resolved to are written to the
|
|
788
|
+
* stage's state, so "what is this stage running" has an exact answer.
|
|
789
|
+
* - What was started answers (`verify`): each app is asked through Caddy,
|
|
790
|
+
* over HTTPS, with the certificate verified.
|
|
791
|
+
*
|
|
792
|
+
* Docker, the registry, the bundler, Postgres and the health probe are
|
|
793
|
+
* injected, so the rules are asserted without a daemon; the defaults are the
|
|
794
|
+
* real ones.
|
|
795
|
+
*/
|
|
796
|
+
/** A port variable that does not hold a port. */
|
|
797
|
+
var EdgePortInvalid = class extends Error {
|
|
798
|
+
variable;
|
|
799
|
+
value;
|
|
800
|
+
constructor(variable, value) {
|
|
801
|
+
super(`${variable}=${value} is not a port. Set it to a number between 1 and 65535, or unset it for the default.`);
|
|
802
|
+
this.variable = variable;
|
|
803
|
+
this.value = value;
|
|
804
|
+
this.name = "EdgePortInvalid";
|
|
805
|
+
}
|
|
806
|
+
};
|
|
807
|
+
/** A build with no commit to tag its images with. */
|
|
808
|
+
var NoGitRevision = class extends Error {
|
|
809
|
+
root;
|
|
810
|
+
constructor(root) {
|
|
811
|
+
super(`Images built here are tagged with the commit they were built from, and ${root} is not in a git repository with a commit. Commit, or pass --tag to name the images yourself.`);
|
|
812
|
+
this.root = root;
|
|
813
|
+
this.name = "NoGitRevision";
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
/** A backend's production bundle did not build. */
|
|
817
|
+
var BundleFailed = class extends Error {
|
|
818
|
+
app;
|
|
819
|
+
exitCode;
|
|
820
|
+
stderr;
|
|
821
|
+
constructor(app, exitCode, stderr) {
|
|
822
|
+
super(`Bundling '${app}' for production failed (exit code ${exitCode}).${stderr.trim() ? `\n${stderr.trim().slice(-4e3)}` : ""}\nRun \`gkm build --provider server --production\` in the app's directory to see it again.`);
|
|
823
|
+
this.app = app;
|
|
824
|
+
this.exitCode = exitCode;
|
|
825
|
+
this.stderr = stderr;
|
|
826
|
+
this.name = "BundleFailed";
|
|
827
|
+
}
|
|
828
|
+
};
|
|
829
|
+
/** The CLI's own entry, which the bundle step runs, is not where it should be. */
|
|
830
|
+
var CliEntryNotFound = class extends Error {
|
|
831
|
+
from;
|
|
832
|
+
constructor(from) {
|
|
833
|
+
super(`Could not find bin/gkm.mjs above ${from}, which bundling a backend runs. Reinstall @geekmidas/cli.`);
|
|
834
|
+
this.from = from;
|
|
835
|
+
this.name = "CliEntryNotFound";
|
|
836
|
+
}
|
|
837
|
+
};
|
|
838
|
+
/** The commit HEAD is at, short — and `-dirty` when the tree has changes. */
|
|
839
|
+
async function gitRevision(root) {
|
|
840
|
+
let sha;
|
|
841
|
+
try {
|
|
842
|
+
sha = (await runOutput("git", [
|
|
843
|
+
"rev-parse",
|
|
844
|
+
"--short",
|
|
845
|
+
"HEAD"
|
|
846
|
+
], { cwd: root })).trim();
|
|
847
|
+
} catch {
|
|
848
|
+
throw new NoGitRevision(root);
|
|
849
|
+
}
|
|
850
|
+
if (!sha) throw new NoGitRevision(root);
|
|
851
|
+
return (await runOutput("git", ["status", "--porcelain"], { cwd: root }).catch(() => "")).trim() ? `${sha}-dirty` : sha;
|
|
852
|
+
}
|
|
853
|
+
/** The edge's ports, from the environment — 443 and 80 when unset. */
|
|
854
|
+
function edgePorts(env = process.env) {
|
|
855
|
+
const read = (variable, fallback) => {
|
|
856
|
+
const value = env[variable];
|
|
857
|
+
if (value === void 0 || value === "") return fallback;
|
|
858
|
+
const port = Number(value);
|
|
859
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new EdgePortInvalid(variable, value);
|
|
860
|
+
return port;
|
|
861
|
+
};
|
|
862
|
+
return {
|
|
863
|
+
https: read(EDGE_PORT_ENV.https, 443),
|
|
864
|
+
http: read(EDGE_PORT_ENV.http, 80)
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
/** `bin/gkm.mjs` of the CLI that is running. */
|
|
868
|
+
function cliEntry() {
|
|
869
|
+
const from = dirname(fileURLToPath(import.meta.url));
|
|
870
|
+
for (let dir = from; dirname(dir) !== dir; dir = dirname(dir)) {
|
|
871
|
+
const entry = join(dir, "bin", "gkm.mjs");
|
|
872
|
+
if (existsSync(entry)) return entry;
|
|
873
|
+
}
|
|
874
|
+
throw new CliEntryNotFound(from);
|
|
875
|
+
}
|
|
876
|
+
/** A cold production bundle is seconds; a hung one must not hold the lock. */
|
|
877
|
+
const BUNDLE_TIMEOUT_MS = 6e5;
|
|
878
|
+
/**
|
|
879
|
+
* Bundle a backend with the gkm that is running — the same build a deploy
|
|
880
|
+
* makes, so the image holds one file and no dependencies — in the run's
|
|
881
|
+
* sandbox, because the build imports the project's code.
|
|
882
|
+
*/
|
|
883
|
+
async function bundleInSandbox(appRoot, ctx) {
|
|
884
|
+
const sandbox = activeSandbox() ?? new LocalSandbox({ root: ctx.cwd });
|
|
885
|
+
const result = await sandbox.exec("node", [
|
|
886
|
+
cliEntry(),
|
|
887
|
+
"build",
|
|
888
|
+
"--provider",
|
|
889
|
+
"server",
|
|
890
|
+
"--production"
|
|
891
|
+
], {
|
|
892
|
+
cwd: appRoot,
|
|
893
|
+
env: sandbox.env,
|
|
894
|
+
timeoutMs: BUNDLE_TIMEOUT_MS,
|
|
895
|
+
output: ctx.childOutput,
|
|
896
|
+
signal: ctx.signal
|
|
897
|
+
});
|
|
898
|
+
if (result.exitCode !== 0) throw new BundleFailed(appRoot, result.exitCode, result.stderr);
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* The real ones. Each app is asked for three minutes before it counts as
|
|
902
|
+
* down: a deployed stage's first ACME certificate can take that long.
|
|
903
|
+
*/
|
|
904
|
+
const defaultDeps = {
|
|
905
|
+
docker: dockerCompose,
|
|
906
|
+
bundle: bundleInSandbox,
|
|
907
|
+
revision: gitRevision,
|
|
908
|
+
sql: pgClient,
|
|
909
|
+
migrate: migrateDatabases,
|
|
910
|
+
probe: httpsProbe,
|
|
911
|
+
healthAttempts: 90,
|
|
912
|
+
healthIntervalMs: 2e3,
|
|
913
|
+
env: process.env
|
|
914
|
+
};
|
|
915
|
+
/**
|
|
916
|
+
* The stack the stage would run, and — for a tag — the registry's word that
|
|
917
|
+
* every image it names exists. Nothing is written: a missing image stops the
|
|
918
|
+
* run here, before a file, a secret or a container is touched.
|
|
919
|
+
*/
|
|
920
|
+
async function validateCompose(ctx, deps, forced) {
|
|
921
|
+
const { workspace, stage, identity, tag } = ctx;
|
|
922
|
+
const root = workspace.root;
|
|
923
|
+
const mode = forced ?? (ctx.tagGiven ? "pull" : "build");
|
|
924
|
+
const runnables = {};
|
|
925
|
+
const manifest = await discover({
|
|
926
|
+
patterns: constructGlobs(workspace),
|
|
927
|
+
cwd: root,
|
|
928
|
+
runnables
|
|
929
|
+
});
|
|
930
|
+
const { secrets, generated } = await stageSecrets(ctx, manifest);
|
|
931
|
+
const stack = composeStack({
|
|
932
|
+
workspace,
|
|
933
|
+
manifest,
|
|
934
|
+
runnables,
|
|
935
|
+
stage,
|
|
936
|
+
identity,
|
|
937
|
+
images: {
|
|
938
|
+
mode,
|
|
939
|
+
tag,
|
|
940
|
+
...workspace.deploy?.registry ? { registry: workspace.deploy.registry } : {}
|
|
941
|
+
},
|
|
942
|
+
secrets,
|
|
943
|
+
ports: edgePorts(deps.env),
|
|
944
|
+
packageManager: detectPackageManager(root),
|
|
945
|
+
packages: Object.fromEntries(Object.entries(workspace.apps).map(([name, app]) => [name, appPackageName(root, name, app.path)]))
|
|
946
|
+
});
|
|
947
|
+
if (mode === "pull" && !ctx.dryRun) await assertImagesExist(deps.docker, tag, stack.apps);
|
|
948
|
+
const dir = join(root, stackDir(stage));
|
|
949
|
+
const previous = Object.fromEntries(Object.entries((await ctx.state.read(stage))?.state.releases ?? {}).map(([name, releases]) => {
|
|
950
|
+
const { ref, tag, digest } = releases.current;
|
|
951
|
+
return [name, {
|
|
952
|
+
ref,
|
|
953
|
+
...tag ? { tag } : {},
|
|
954
|
+
...digest ? { digest } : {}
|
|
955
|
+
}];
|
|
956
|
+
}));
|
|
957
|
+
return {
|
|
958
|
+
mode,
|
|
959
|
+
stack,
|
|
960
|
+
dir,
|
|
961
|
+
ref: {
|
|
962
|
+
project: composeProject(identity),
|
|
963
|
+
file: join(dir, "docker-compose.yml"),
|
|
964
|
+
cwd: root,
|
|
965
|
+
output: ctx.childOutput,
|
|
966
|
+
signal: ctx.signal
|
|
967
|
+
},
|
|
968
|
+
secrets,
|
|
969
|
+
generated,
|
|
970
|
+
previous,
|
|
971
|
+
files: [],
|
|
972
|
+
images: {},
|
|
973
|
+
changes: []
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* The stage's secrets — and, for a deployed stage, everything it generates
|
|
978
|
+
* once: its seed, and each declared secret and keyring. Kept only once the
|
|
979
|
+
* run goes ahead (`provision`), so a dry run, or a tag that is not there,
|
|
980
|
+
* leaves the stage's secrets as it found them.
|
|
981
|
+
*/
|
|
982
|
+
async function stageSecrets(ctx, manifest) {
|
|
983
|
+
const stored = await ctx.secrets.read();
|
|
984
|
+
if (ctx.stage === ctx.workspace.stages.local) return {
|
|
985
|
+
secrets: stored,
|
|
986
|
+
generated: []
|
|
987
|
+
};
|
|
988
|
+
const { secrets, generated } = withGeneratedSecrets(stored ?? initStageSecrets(ctx.stage), manifest);
|
|
989
|
+
if (secrets.seed) ctx.secrets.mask(secrets.seed);
|
|
990
|
+
for (const value of Object.values(secrets.custom ?? {})) ctx.secrets.mask(value);
|
|
991
|
+
return {
|
|
992
|
+
secrets,
|
|
993
|
+
generated
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* A dry run: the stack's files, written so they can be read, and what a real
|
|
998
|
+
* run would do — no image is built or pulled, no container touched, nothing
|
|
999
|
+
* recorded and no secret kept.
|
|
1000
|
+
*/
|
|
1001
|
+
async function planCompose(ctx, run) {
|
|
1002
|
+
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.`);
|
|
1003
|
+
run.files = await writeStack(ctx.cwd, run.dir, run.stack);
|
|
1004
|
+
printPlan(ctx, run);
|
|
1005
|
+
const planned = (change) => {
|
|
1006
|
+
run.changes.push(change);
|
|
1007
|
+
ctx.emit({
|
|
1008
|
+
type: "resource.planned",
|
|
1009
|
+
...change
|
|
1010
|
+
});
|
|
1011
|
+
};
|
|
1012
|
+
for (const service of run.stack.infra) planned({
|
|
1013
|
+
key: `service:${service}`,
|
|
1014
|
+
resourceType: "service",
|
|
1015
|
+
action: "ensure"
|
|
1016
|
+
});
|
|
1017
|
+
for (const app of run.stack.apps) {
|
|
1018
|
+
planned({
|
|
1019
|
+
key: `image:${app.name}`,
|
|
1020
|
+
resourceType: "image",
|
|
1021
|
+
action: run.mode === "build" ? "build" : "reuse",
|
|
1022
|
+
id: app.ref
|
|
1023
|
+
});
|
|
1024
|
+
planned({
|
|
1025
|
+
key: `service:${app.name}`,
|
|
1026
|
+
resourceType: "service",
|
|
1027
|
+
action: run.previous[app.name] ? "reuse" : "create"
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
planned({
|
|
1031
|
+
key: "service:caddy",
|
|
1032
|
+
resourceType: "service",
|
|
1033
|
+
action: "ensure"
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* What the apps run on: the stage's generated secrets kept, the stack's files
|
|
1038
|
+
* written, its infrastructure started, and its databases, roles, grants and
|
|
1039
|
+
* migrations applied.
|
|
1040
|
+
*/
|
|
1041
|
+
async function provisionCompose(ctx, run, deps) {
|
|
1042
|
+
const { stack, ref } = run;
|
|
1043
|
+
if (run.generated.length > 0 && run.secrets) {
|
|
1044
|
+
await ctx.secrets.write(run.secrets);
|
|
1045
|
+
ctx.logger.info(`🔑 Generated for "${ctx.stage}" (${ctx.secrets.store}): ${run.generated.join(", ")}`);
|
|
1046
|
+
}
|
|
1047
|
+
run.files = await writeStack(ctx.cwd, run.dir, stack);
|
|
1048
|
+
printPlan(ctx, run);
|
|
1049
|
+
if (stack.infra.length > 0) {
|
|
1050
|
+
ctx.logger.info(`\n🗄️ Starting ${stack.infra.join(", ")}…`);
|
|
1051
|
+
await deps.docker.up(ref, stack.infra);
|
|
1052
|
+
for (const service of stack.infra) applied(ctx, run, {
|
|
1053
|
+
key: `service:${service}`,
|
|
1054
|
+
resourceType: "service",
|
|
1055
|
+
action: "ensure",
|
|
1056
|
+
id: `${stack.project}/${service}`,
|
|
1057
|
+
via: Object.keys(run.previous).length > 0 ? "recorded" : "created"
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
if (stack.infra.includes("postgres")) await prepareDatabases(ctx, run, deps);
|
|
1061
|
+
}
|
|
1062
|
+
/** Create what the plan names in the stack's Postgres, then migrate it. */
|
|
1063
|
+
async function prepareDatabases(ctx, run, deps) {
|
|
1064
|
+
const { stack } = run;
|
|
1065
|
+
const workspace = ctx.workspace;
|
|
1066
|
+
const port = await deps.docker.port(run.ref, "postgres", 5432);
|
|
1067
|
+
const statements = postgresStatements(stack.plan, workspace.name, stack.credential.seed);
|
|
1068
|
+
if (statements.length > 0) {
|
|
1069
|
+
ctx.logger.info("🗄️ Creating databases, roles and grants…");
|
|
1070
|
+
await applyPostgres(deps.sql(port, stack.credential.master), statements);
|
|
1071
|
+
}
|
|
1072
|
+
const sources = {};
|
|
1073
|
+
const manifest = await discover({
|
|
1074
|
+
patterns: constructGlobs(workspace),
|
|
1075
|
+
cwd: workspace.root,
|
|
1076
|
+
sources
|
|
1077
|
+
});
|
|
1078
|
+
const env = envFor(stack.plan, {
|
|
1079
|
+
ports: { [primaryPortKey("postgres")]: port },
|
|
1080
|
+
project: workspace.name,
|
|
1081
|
+
master: stack.credential.master,
|
|
1082
|
+
...stack.credential.seed ? { seed: stack.credential.seed } : {}
|
|
1083
|
+
});
|
|
1084
|
+
const runs = await deps.migrate({
|
|
1085
|
+
root: workspace.root,
|
|
1086
|
+
manifest,
|
|
1087
|
+
sources,
|
|
1088
|
+
env
|
|
1089
|
+
});
|
|
1090
|
+
for (const { target, applied } of runs) if (applied.length > 0) ctx.logger.info(`🗄️ ${target.migrations}: applied ${applied.length}`);
|
|
1091
|
+
}
|
|
1092
|
+
/**
|
|
1093
|
+
* Each app's image: built from this checkout — every backend bundled first,
|
|
1094
|
+
* on the host, with this CLI — or pulled at the tag. Nothing live changes.
|
|
1095
|
+
*/
|
|
1096
|
+
async function buildCompose(ctx, run, deps) {
|
|
1097
|
+
const { stack, ref } = run;
|
|
1098
|
+
const apps = stack.apps.map((app) => app.name);
|
|
1099
|
+
if (run.mode === "build") {
|
|
1100
|
+
for (const app of stack.apps.filter((a) => a.kind === "rest-api")) {
|
|
1101
|
+
ctx.signal.throwIfAborted();
|
|
1102
|
+
ctx.logger.info(`\n📦 Bundling ${app.name}…`);
|
|
1103
|
+
await deps.bundle(join(ctx.cwd, app.path), ctx);
|
|
1104
|
+
}
|
|
1105
|
+
ctx.logger.info("\n🐳 Building images…");
|
|
1106
|
+
await deps.docker.build(ref, apps);
|
|
1107
|
+
} else {
|
|
1108
|
+
ctx.logger.info(`\n🐳 Pulling ${ctx.tag}…`);
|
|
1109
|
+
await deps.docker.pull(ref, apps);
|
|
1110
|
+
}
|
|
1111
|
+
for (const app of stack.apps) {
|
|
1112
|
+
const digest = await deps.docker.digest(app.ref);
|
|
1113
|
+
run.images[app.name] = {
|
|
1114
|
+
ref: app.ref,
|
|
1115
|
+
tag: app.tag,
|
|
1116
|
+
...digest ? { digest } : {}
|
|
1117
|
+
};
|
|
1118
|
+
ctx.emit({
|
|
1119
|
+
type: "artifact.built",
|
|
1120
|
+
app: app.name,
|
|
1121
|
+
imageRef: app.ref,
|
|
1122
|
+
...digest ? { digest } : {}
|
|
1123
|
+
});
|
|
1124
|
+
run.changes.push({
|
|
1125
|
+
key: `image:${app.name}`,
|
|
1126
|
+
resourceType: "image",
|
|
1127
|
+
action: run.mode === "build" ? "build" : "reuse",
|
|
1128
|
+
id: digest ?? app.ref
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
/**
|
|
1133
|
+
* The whole stack up — anything it no longer defines removed — and each
|
|
1134
|
+
* app's image recorded in the stage's state.
|
|
1135
|
+
*/
|
|
1136
|
+
async function releaseCompose(ctx, run, deps) {
|
|
1137
|
+
const { stack, ref } = run;
|
|
1138
|
+
ctx.logger.info("\n🚀 Starting the stack…");
|
|
1139
|
+
await deps.docker.up(ref);
|
|
1140
|
+
if (stack.local) await deps.docker.copyOut(ref, "caddy", "/data/caddy/pki/authorities/local/root.crt", caFile(run)).catch(() => {});
|
|
1141
|
+
await recordImages(ctx, run);
|
|
1142
|
+
for (const app of stack.apps) {
|
|
1143
|
+
const image = run.images[app.name];
|
|
1144
|
+
applied(ctx, run, {
|
|
1145
|
+
key: `service:${app.name}`,
|
|
1146
|
+
resourceType: "service",
|
|
1147
|
+
action: "ensure",
|
|
1148
|
+
id: `${stack.project}/${app.name}`,
|
|
1149
|
+
via: run.previous[app.name] ? "recorded" : "created"
|
|
1150
|
+
});
|
|
1151
|
+
ctx.emit({
|
|
1152
|
+
type: "app.deployed",
|
|
1153
|
+
app: app.name,
|
|
1154
|
+
applicationId: `${stack.project}/${app.name}`,
|
|
1155
|
+
imageRef: image?.ref ?? app.ref,
|
|
1156
|
+
url: app.url
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
ctx.logger.info(`\n✅ ${stack.project} is running:`);
|
|
1160
|
+
for (const app of stack.apps) ctx.logger.info(` ${app.name.padEnd(12)} ${app.url}`);
|
|
1161
|
+
if (stack.local) ctx.logger.info(`\n🔐 Certificates are from Caddy's local CA: NODE_EXTRA_CA_CERTS=${join(stackDir(ctx.stage), "caddy-root.crt")}`);
|
|
1162
|
+
}
|
|
1163
|
+
/**
|
|
1164
|
+
* Each app's image in the stage's state — the same `images` a Dokploy deploy
|
|
1165
|
+
* records: the ref it runs, the tag it was released under, and the digest
|
|
1166
|
+
* the tag resolved to.
|
|
1167
|
+
*/
|
|
1168
|
+
async function recordImages(ctx, run) {
|
|
1169
|
+
const journal = await DeployJournal.open(ctx.state, ctx.stage, () => createEmptyState(ctx.stage, "", ""));
|
|
1170
|
+
for (const app of run.stack.apps) {
|
|
1171
|
+
const image = run.images[app.name] ?? {
|
|
1172
|
+
ref: app.ref,
|
|
1173
|
+
tag: app.tag
|
|
1174
|
+
};
|
|
1175
|
+
recordRelease(journal.state, app.name, image);
|
|
1176
|
+
}
|
|
1177
|
+
journal.state.identity = ctx.identity.key;
|
|
1178
|
+
await journal.save();
|
|
1179
|
+
}
|
|
1180
|
+
/**
|
|
1181
|
+
* Each app asked through Caddy, over HTTPS, the certificate verified: an API
|
|
1182
|
+
* at `/health`, a site at `/`. On the local stage the edge is this machine and
|
|
1183
|
+
* its certificates are Caddy's internal CA's, so the CA `release` copied out
|
|
1184
|
+
* is the one trusted and the request goes to 127.0.0.1.
|
|
1185
|
+
*/
|
|
1186
|
+
async function verifyCompose(ctx, run, deps) {
|
|
1187
|
+
const { stack } = run;
|
|
1188
|
+
const ca = stack.local ? await readFile(caFile(run), "utf-8").catch(() => void 0) : void 0;
|
|
1189
|
+
ctx.logger.info("\n🩺 Checking each app through Caddy…");
|
|
1190
|
+
const down = (await Promise.all(stack.apps.map((app) => checkApp(ctx, app, deps, {
|
|
1191
|
+
ca,
|
|
1192
|
+
local: stack.local
|
|
1193
|
+
})))).filter((result) => !result.healthy).map(({ app, url, last }) => ({
|
|
1194
|
+
app,
|
|
1195
|
+
url,
|
|
1196
|
+
last
|
|
1197
|
+
}));
|
|
1198
|
+
if (down.length > 0) throw new ComposeAppsUnhealthy(stack.project, down);
|
|
1199
|
+
}
|
|
1200
|
+
async function checkApp(ctx, app, deps, options) {
|
|
1201
|
+
const url = `${app.url}${app.kind === "site" ? "/" : "/health"}`;
|
|
1202
|
+
let last = "not asked";
|
|
1203
|
+
for (let attempt = 1; attempt <= deps.healthAttempts; attempt++) {
|
|
1204
|
+
ctx.signal.throwIfAborted();
|
|
1205
|
+
let status;
|
|
1206
|
+
try {
|
|
1207
|
+
status = await deps.probe({
|
|
1208
|
+
url,
|
|
1209
|
+
...options.ca ? { ca: options.ca } : {},
|
|
1210
|
+
...options.local ? { connectTo: "127.0.0.1" } : {},
|
|
1211
|
+
timeoutMs: 1e4,
|
|
1212
|
+
signal: ctx.signal
|
|
1213
|
+
});
|
|
1214
|
+
last = `HTTP ${status}`;
|
|
1215
|
+
} catch (error) {
|
|
1216
|
+
last = error instanceof Error ? error.message : String(error);
|
|
1217
|
+
}
|
|
1218
|
+
const healthy = status !== void 0 && isHealthy(status);
|
|
1219
|
+
ctx.emit({
|
|
1220
|
+
type: "health.checked",
|
|
1221
|
+
app: app.name,
|
|
1222
|
+
url,
|
|
1223
|
+
healthy,
|
|
1224
|
+
...status !== void 0 ? { status } : {},
|
|
1225
|
+
attempt
|
|
1226
|
+
});
|
|
1227
|
+
if (healthy) {
|
|
1228
|
+
ctx.logger.info(` ✓ ${app.name.padEnd(12)} ${url} (${last})`);
|
|
1229
|
+
return {
|
|
1230
|
+
app: app.name,
|
|
1231
|
+
url,
|
|
1232
|
+
last,
|
|
1233
|
+
healthy
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
if (attempt < deps.healthAttempts) await new Promise((resolve) => setTimeout(resolve, deps.healthIntervalMs));
|
|
1237
|
+
}
|
|
1238
|
+
ctx.logger.warn(` ✗ ${app.name.padEnd(12)} ${url} (${last})`);
|
|
1239
|
+
return {
|
|
1240
|
+
app: app.name,
|
|
1241
|
+
url,
|
|
1242
|
+
last,
|
|
1243
|
+
healthy: false
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
function composeResult(ctx, run) {
|
|
1247
|
+
const apps = run.stack.apps.map((app) => {
|
|
1248
|
+
const image = run.images[app.name];
|
|
1249
|
+
return {
|
|
1250
|
+
appName: app.name,
|
|
1251
|
+
type: app.kind === "site" ? "web" : "backend",
|
|
1252
|
+
success: !ctx.dryRun,
|
|
1253
|
+
applicationId: `${run.stack.project}/${app.name}`,
|
|
1254
|
+
imageRef: image?.ref ?? app.ref,
|
|
1255
|
+
...image?.digest ? { digest: image.digest } : {},
|
|
1256
|
+
url: app.url
|
|
1257
|
+
};
|
|
1258
|
+
});
|
|
1259
|
+
return {
|
|
1260
|
+
apps,
|
|
1261
|
+
projectId: run.stack.project,
|
|
1262
|
+
successCount: ctx.dryRun ? 0 : apps.length,
|
|
1263
|
+
failedCount: 0,
|
|
1264
|
+
stage: ctx.stage,
|
|
1265
|
+
identity: ctx.identity.key,
|
|
1266
|
+
tag: ctx.tag,
|
|
1267
|
+
dryRun: ctx.dryRun,
|
|
1268
|
+
environmentId: "",
|
|
1269
|
+
skipped: [...ctx.skipped],
|
|
1270
|
+
urls: Object.fromEntries(run.stack.apps.map((app) => [app.name, app.url])),
|
|
1271
|
+
changes: run.changes
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
function caFile(run) {
|
|
1275
|
+
return join(run.dir, "caddy-root.crt");
|
|
1276
|
+
}
|
|
1277
|
+
function applied(ctx, run, change) {
|
|
1278
|
+
const { via, ...rest } = change;
|
|
1279
|
+
run.changes.push(rest);
|
|
1280
|
+
ctx.emit({
|
|
1281
|
+
type: "resource.applied",
|
|
1282
|
+
...rest,
|
|
1283
|
+
via
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
/** Write the stack's files: owner-only, since the env files hold secrets. */
|
|
1287
|
+
async function writeStack(root, dir, stack) {
|
|
1288
|
+
await mkdir(dir, {
|
|
1289
|
+
recursive: true,
|
|
1290
|
+
mode: 448
|
|
1291
|
+
});
|
|
1292
|
+
await chmod(dir, 448);
|
|
1293
|
+
const files = [];
|
|
1294
|
+
const write = async (path, content, mode = 420) => {
|
|
1295
|
+
await mkdir(dirname(path), { recursive: true });
|
|
1296
|
+
await writeFile(path, content, { mode });
|
|
1297
|
+
await chmod(path, mode);
|
|
1298
|
+
files.push(path);
|
|
1299
|
+
};
|
|
1300
|
+
await write(join(dir, "docker-compose.yml"), composeYaml(stack));
|
|
1301
|
+
await write(join(dir, "Caddyfile"), stack.caddyfile);
|
|
1302
|
+
for (const app of stack.apps) if (app.env) await write(join(dir, `${app.name}.env`), envFile(app.env), 384);
|
|
1303
|
+
for (const [path, content] of Object.entries(stack.dockerfiles)) await write(join(root, path), content);
|
|
1304
|
+
await ignoreStacks(root);
|
|
1305
|
+
return files;
|
|
1306
|
+
}
|
|
1307
|
+
/** Make sure the build context leaves `.gkm/compose` out. */
|
|
1308
|
+
async function ignoreStacks(root) {
|
|
1309
|
+
const path = join(root, ".dockerignore");
|
|
1310
|
+
if (!existsSync(path)) {
|
|
1311
|
+
await writeFile(path, generateDockerignore());
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
const current = await readFile(path, "utf-8");
|
|
1315
|
+
if (/^\/?\.gkm\/compose\/?$|^\/?\.gkm\/?$/m.test(current)) return;
|
|
1316
|
+
await appendFile(path, `${current.endsWith("\n") ? "" : "\n"}\n# gkm compose's stacks: each app's env file holds its stage's secrets\n.gkm/compose\n`);
|
|
1317
|
+
}
|
|
1318
|
+
function composeYaml(stack) {
|
|
1319
|
+
const stage = stack.local ? "" : ` --stage ${stack.stage}`;
|
|
1320
|
+
return `# Generated by gkm compose from the construct manifest — do not edit.
|
|
1321
|
+
# The ${stack.stage} stage's APIs and sites behind one Caddy. Each backend
|
|
1322
|
+
# reads exactly the keys in its own env file beside this one.
|
|
1323
|
+
#
|
|
1324
|
+
# gkm compose${stage} start or update it
|
|
1325
|
+
# gkm compose${stage} --down stop it
|
|
1326
|
+
${stringify(stack.compose, {
|
|
1327
|
+
lineWidth: 0,
|
|
1328
|
+
aliasDuplicateObjects: false
|
|
1329
|
+
})}`;
|
|
1330
|
+
}
|
|
1331
|
+
function printPlan(ctx, run) {
|
|
1332
|
+
const { stack, mode, files } = run;
|
|
1333
|
+
ctx.logger.info(`\n🧱 ${stack.project} — stage ${stack.stage}`);
|
|
1334
|
+
for (const app of stack.apps) ctx.logger.info(` ${app.name.padEnd(12)} ${app.url.padEnd(40)} ${mode} ${app.ref}`);
|
|
1335
|
+
if (stack.infra.length > 0) ctx.logger.info(` infrastructure: ${stack.infra.join(", ")}`);
|
|
1336
|
+
ctx.logger.info(`\n📝 Wrote ${files.length} file(s) under ${stackDir(stack.stage)}/`);
|
|
1337
|
+
}
|
|
1338
|
+
//#endregion
|
|
1339
|
+
//#region src/target/compose/index.ts
|
|
1340
|
+
/**
|
|
1341
|
+
* The built-in `compose` target: a stage as one Docker Compose stack behind
|
|
1342
|
+
* Caddy, on the machine that runs the deploy.
|
|
1343
|
+
*
|
|
1344
|
+
* ```bash
|
|
1345
|
+
* gkm deploy --target compose --stage production --tag v1.4.0 # what CI pushed
|
|
1346
|
+
* gkm deploy --target compose --stage production # built from here
|
|
1347
|
+
* gkm deploy --target compose --stage development # the local stage
|
|
1348
|
+
* ```
|
|
1349
|
+
*
|
|
1350
|
+
* - `validate`: the stack (`composeStack`, pure) and, for a tag, every image
|
|
1351
|
+
* looked up in the registry — `ImageTagNotFound` before anything changes
|
|
1352
|
+
* - `plan`: the stack's files and what a run would create or reuse
|
|
1353
|
+
* - `provision`: generated secrets kept, files written, infrastructure up,
|
|
1354
|
+
* databases, roles, grants and migrations
|
|
1355
|
+
* - `build`: each backend bundled on the host and every image built — or, for
|
|
1356
|
+
* a tag, pulled
|
|
1357
|
+
* - `release`: `up --wait --remove-orphans`, and each app's image, tag and
|
|
1358
|
+
* digest in the stage's state
|
|
1359
|
+
* - `verify`: each app asked through Caddy over HTTPS
|
|
1360
|
+
*
|
|
1361
|
+
* `gkm compose` is this target with a few more switches (`--build`, `--pull`,
|
|
1362
|
+
* `--down`), run through the same `deploy()`.
|
|
1363
|
+
*/
|
|
1364
|
+
var compose_exports = /* @__PURE__ */ __exportAll({ composeTarget: () => composeTarget });
|
|
1365
|
+
/** The compose target, with its Docker, bundler and probe as given. */
|
|
1366
|
+
function composeTarget(options = {}) {
|
|
1367
|
+
const { mode, report, ...given } = options;
|
|
1368
|
+
const deps = {
|
|
1369
|
+
...defaultDeps,
|
|
1370
|
+
...given
|
|
1371
|
+
};
|
|
1372
|
+
return defineTarget({
|
|
1373
|
+
name: "compose",
|
|
1374
|
+
runtime: "server",
|
|
1375
|
+
capabilities: {
|
|
1376
|
+
rollback: false,
|
|
1377
|
+
migrations: "target",
|
|
1378
|
+
images: true,
|
|
1379
|
+
localStage: true
|
|
1380
|
+
},
|
|
1381
|
+
credentials: [],
|
|
1382
|
+
tag: ({ cwd }) => deps.revision(cwd),
|
|
1383
|
+
validate: (ctx) => validateCompose(ctx, deps, mode),
|
|
1384
|
+
plan: (ctx, run) => planCompose(ctx, run),
|
|
1385
|
+
provision: (ctx, run) => provisionCompose(ctx, run, deps),
|
|
1386
|
+
build: (ctx, run) => buildCompose(ctx, run, deps),
|
|
1387
|
+
release: (ctx, run) => releaseCompose(ctx, run, deps),
|
|
1388
|
+
verify: (ctx, run) => verifyCompose(ctx, run, deps),
|
|
1389
|
+
result: (ctx, run) => {
|
|
1390
|
+
report?.(run);
|
|
1391
|
+
return composeResult(ctx, run);
|
|
1392
|
+
}
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
//#endregion
|
|
1396
|
+
export { dockerCompose as a, stackDir as i, compose_exports as n, composeProject as r, composeTarget as t };
|
|
1397
|
+
|
|
1398
|
+
//# sourceMappingURL=compose-CNQL8BnO.mjs.map
|