@geekmidas/cli 10.0.0-alpha.93 → 10.0.0-alpha.95
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/{GoDaddyProvider-Mi3vptSR.mjs → GoDaddyProvider-BwW51hPm.mjs} +3 -3
- package/dist/{GoDaddyProvider-Mi3vptSR.mjs.map → GoDaddyProvider-BwW51hPm.mjs.map} +1 -1
- package/dist/{GoDaddyProvider-BZb-V14e.cjs → GoDaddyProvider-C_DgIkMv.cjs} +3 -3
- package/dist/{GoDaddyProvider-BZb-V14e.cjs.map → GoDaddyProvider-C_DgIkMv.cjs.map} +1 -1
- package/dist/{HostingerProvider-1mqD6aG8.mjs → HostingerProvider-BNpS9aRJ.mjs} +9 -4
- package/dist/{HostingerProvider-1mqD6aG8.mjs.map → HostingerProvider-BNpS9aRJ.mjs.map} +1 -1
- package/dist/{HostingerProvider-C6t-PSNe.cjs → HostingerProvider-DR1iws1P.cjs} +9 -4
- package/dist/HostingerProvider-DR1iws1P.cjs.map +1 -0
- package/dist/{LegacyStateStore-BlYXQQSE.cjs → LegacyStateStore-BawjE_5n.cjs} +10 -9
- package/dist/{LegacyStateStore-BlYXQQSE.cjs.map → LegacyStateStore-BawjE_5n.cjs.map} +1 -1
- package/dist/{LegacyStateStore-CtwO7fwy.mjs → LegacyStateStore-D5DarNA2.mjs} +3 -2
- package/dist/{LegacyStateStore-CtwO7fwy.mjs.map → LegacyStateStore-D5DarNA2.mjs.map} +1 -1
- package/dist/{LocalStateStore-oX_zxj9V.cjs → LocalStateStore-D8fu5WiF.cjs} +8 -8
- package/dist/{LocalStateStore-oX_zxj9V.cjs.map → LocalStateStore-D8fu5WiF.cjs.map} +1 -1
- package/dist/{LocalStateStore-D40_SDmJ.mjs → LocalStateStore-YjIqlgAz.mjs} +2 -2
- package/dist/{LocalStateStore-D40_SDmJ.mjs.map → LocalStateStore-YjIqlgAz.mjs.map} +1 -1
- package/dist/{S3StateStore-BzS0IeSt.mjs → S3StateStore-CjTAVsWh.mjs} +2 -2
- package/dist/{S3StateStore-BzS0IeSt.mjs.map → S3StateStore-CjTAVsWh.mjs.map} +1 -1
- package/dist/{S3StateStore-DeI7jtvi.cjs → S3StateStore-Cm-v2IPI.cjs} +5 -5
- package/dist/{S3StateStore-DeI7jtvi.cjs.map → S3StateStore-Cm-v2IPI.cjs.map} +1 -1
- package/dist/{SSMStateStore-CaCtrzFL.cjs → SSMStateStore-BS4BiZCN.cjs} +9 -9
- package/dist/{SSMStateStore-CaCtrzFL.cjs.map → SSMStateStore-BS4BiZCN.cjs.map} +1 -1
- package/dist/{SSMStateStore-k_Ew7TWx.mjs → SSMStateStore-BhtM6jOh.mjs} +2 -2
- package/dist/{SSMStateStore-k_Ew7TWx.mjs.map → SSMStateStore-BhtM6jOh.mjs.map} +1 -1
- package/dist/{add-VcyJnsPX.cjs → add-D-3IKT7W.cjs} +24 -53
- package/dist/add-D-3IKT7W.cjs.map +1 -0
- package/dist/{add-CP1podDH.mjs → add-ylx8dejw.mjs} +23 -52
- package/dist/add-ylx8dejw.mjs.map +1 -0
- package/dist/{bundler-Dfsr6KXU.cjs → bundler-CI3elfZS.cjs} +5 -5
- package/dist/{bundler-Dfsr6KXU.cjs.map → bundler-CI3elfZS.cjs.map} +1 -1
- package/dist/{bundler-BWrh-AJH.mjs → bundler-ywR3C1B_.mjs} +5 -5
- package/dist/{bundler-BWrh-AJH.mjs.map → bundler-ywR3C1B_.mjs.map} +1 -1
- package/dist/{cli-B8otnZfX.mjs → cli-BCSuCfQk.mjs} +9 -5
- package/dist/cli-BCSuCfQk.mjs.map +1 -0
- package/dist/{cli-DL19w4OE.cjs → cli-Dewt2T21.cjs} +9 -5
- package/dist/cli-Dewt2T21.cjs.map +1 -0
- package/dist/{compose-DrEB_4F-.mjs → compose-C4js1JDv.mjs} +82 -47
- package/dist/compose-C4js1JDv.mjs.map +1 -0
- package/dist/{compose-BHVKLluX.cjs → compose-Dd0XphvF.cjs} +94 -59
- package/dist/compose-Dd0XphvF.cjs.map +1 -0
- package/dist/{config-DCCcrOSo.d.cts → config-4yirvxS8.d.cts} +4 -4
- package/dist/{config-CHX5GOo3.d.mts.map → config-4yirvxS8.d.cts.map} +1 -1
- package/dist/{config-CHX5GOo3.d.mts → config-C8NPCn1l.d.mts} +4 -4
- package/dist/{config-DCCcrOSo.d.cts.map → config-C8NPCn1l.d.mts.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/{credentials-C4KII3PD.mjs → credentials-B5N5yKk6.mjs} +12 -134
- package/dist/credentials-B5N5yKk6.mjs.map +1 -0
- package/dist/{credentials-QBu7VgaQ.cjs → credentials-Zqr3QQYB.cjs} +11 -175
- package/dist/credentials-Zqr3QQYB.cjs.map +1 -0
- package/dist/deploy/public.cjs +19 -18
- package/dist/deploy/public.cjs.map +1 -1
- package/dist/deploy/public.d.cts +24 -9
- package/dist/deploy/public.d.cts.map +1 -1
- package/dist/deploy/public.d.mts +24 -9
- package/dist/deploy/public.d.mts.map +1 -1
- package/dist/deploy/public.mjs +11 -10
- package/dist/deploy/public.mjs.map +1 -1
- package/dist/deploy-B0drCD6s.mjs +1854 -0
- package/dist/deploy-B0drCD6s.mjs.map +1 -0
- package/dist/deploy-Bw1FjX_x.cjs +1903 -0
- package/dist/deploy-Bw1FjX_x.cjs.map +1 -0
- package/dist/{discover-vk2r5giK.d.cts → discover-BtFofyQB.d.cts} +2 -2
- package/dist/{discover-DqwlBVyY.d.mts.map → discover-BtFofyQB.d.cts.map} +1 -1
- package/dist/{discover-DqwlBVyY.d.mts → discover-D-MdXZgm.d.mts} +2 -2
- package/dist/{discover-vk2r5giK.d.cts.map → discover-D-MdXZgm.d.mts.map} +1 -1
- package/dist/dns-Bmk7P9c2.cjs +663 -0
- package/dist/dns-Bmk7P9c2.cjs.map +1 -0
- package/dist/dns-CyMIq6to.cjs +55 -0
- package/dist/dns-CyMIq6to.cjs.map +1 -0
- package/dist/dns-X3oqEV4C.mjs +44 -0
- package/dist/dns-X3oqEV4C.mjs.map +1 -0
- package/dist/dns-idXfyCay.mjs +610 -0
- package/dist/dns-idXfyCay.mjs.map +1 -0
- package/dist/{dokploy-o4I6vb9W.mjs → dokploy-CZYxXeMa.mjs} +2 -2
- package/dist/{dokploy-o4I6vb9W.mjs.map → dokploy-CZYxXeMa.mjs.map} +1 -1
- package/dist/{dokploy-rivb1RFt.cjs → dokploy-DaT0mgLU.cjs} +2 -2
- package/dist/{dokploy-rivb1RFt.cjs.map → dokploy-DaT0mgLU.cjs.map} +1 -1
- package/dist/{encryption-B6ilrT7p.mjs → encryption-D0ucY4O7.mjs} +3 -3
- package/dist/{encryption-B6ilrT7p.mjs.map → encryption-D0ucY4O7.mjs.map} +1 -1
- package/dist/{encryption-D7zmwzWz.cjs → encryption-Dp0IvLVX.cjs} +3 -3
- package/dist/{encryption-D7zmwzWz.cjs.map → encryption-Dp0IvLVX.cjs.map} +1 -1
- package/dist/{engine-DjEcovm-.mjs → engine-BcuhnyQO.mjs} +14 -31
- package/dist/{engine-DjEcovm-.mjs.map → engine-BcuhnyQO.mjs.map} +1 -1
- package/dist/{engine-Cfx8fEZU.cjs → engine-WY6j_kvA.cjs} +18 -35
- package/dist/engine-WY6j_kvA.cjs.map +1 -0
- package/dist/{github-CzBbs43W.mjs → github-DUGA57du.mjs} +96 -6
- package/dist/github-DUGA57du.mjs.map +1 -0
- package/dist/{github-zneTMvnO.cjs → github-DgmW83OP.cjs} +96 -6
- package/dist/github-DgmW83OP.cjs.map +1 -0
- package/dist/{index-BiYU7cUl.d.mts → index-5eL8rM6H.d.mts} +2 -2
- package/dist/{index-BiYU7cUl.d.mts.map → index-5eL8rM6H.d.mts.map} +1 -1
- package/dist/{index-Dm6V9bwh.d.cts → index-Dz-gyjm_.d.cts} +2 -2
- package/dist/{index-Dm6V9bwh.d.cts.map → index-Dz-gyjm_.d.cts.map} +1 -1
- package/dist/index.cjs +150 -118
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +144 -112
- package/dist/index.mjs.map +1 -1
- package/dist/{local-Otp74ak0.mjs → local-BwC2v2Y8.mjs} +2 -2
- package/dist/{local-Otp74ak0.mjs.map → local-BwC2v2Y8.mjs.map} +1 -1
- package/dist/{local-B61kUh0w.cjs → local-DNhnkugT.cjs} +3 -3
- package/dist/{local-B61kUh0w.cjs.map → local-DNhnkugT.cjs.map} +1 -1
- package/dist/{migrate-DFjbGnSr.cjs → migrate-BBcG1dhS.cjs} +3 -3
- package/dist/{migrate-DFjbGnSr.cjs.map → migrate-BBcG1dhS.cjs.map} +1 -1
- package/dist/{migrate-CsBZKALY.mjs → migrate-CZ-v5WV0.mjs} +3 -3
- package/dist/{migrate-CsBZKALY.mjs.map → migrate-CZ-v5WV0.mjs.map} +1 -1
- package/dist/{migrate-BaC4w0sb.mjs → migrate-D6PlNv2o.mjs} +3 -3
- package/dist/{migrate-BaC4w0sb.mjs.map → migrate-D6PlNv2o.mjs.map} +1 -1
- package/dist/{migrate-C8wTpakT.cjs → migrate-afgRL3Hg.cjs} +3 -3
- package/dist/{migrate-C8wTpakT.cjs.map → migrate-afgRL3Hg.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/reconcile/public.d.cts +2 -2
- package/dist/reconcile/public.d.mts +2 -2
- package/dist/{resolve-g8htuR80.cjs → resolve-CFb4Navi.cjs} +4 -4
- package/dist/{resolve-g8htuR80.cjs.map → resolve-CFb4Navi.cjs.map} +1 -1
- package/dist/{resolve-Ccjlpnd9.mjs → resolve-DakDhr6Z.mjs} +4 -4
- package/dist/{resolve-Ccjlpnd9.mjs.map → resolve-DakDhr6Z.mjs.map} +1 -1
- package/dist/{rollback--ympG9ss.cjs → rollback-B-1dPP0m.cjs} +9 -8
- package/dist/{rollback--ympG9ss.cjs.map → rollback-B-1dPP0m.cjs.map} +1 -1
- package/dist/{rollback-Drbg0OeR.mjs → rollback-B1TIpjdE.mjs} +7 -6
- package/dist/{rollback-Drbg0OeR.mjs.map → rollback-B1TIpjdE.mjs.map} +1 -1
- package/dist/run-BJLIerMM.mjs +133 -0
- package/dist/run-BJLIerMM.mjs.map +1 -0
- package/dist/run-fVZ-rLnc.cjs +174 -0
- package/dist/run-fVZ-rLnc.cjs.map +1 -0
- package/dist/{runtime-_wEVtshH.d.mts → runtime-DUQdM-mW.d.cts} +2 -2
- package/dist/{runtime-D_J3ExJn.d.cts.map → runtime-DUQdM-mW.d.cts.map} +1 -1
- package/dist/{runtime-D_J3ExJn.d.cts → runtime-Vr6mleo2.d.mts} +2 -2
- package/dist/{runtime-_wEVtshH.d.mts.map → runtime-Vr6mleo2.d.mts.map} +1 -1
- package/dist/{sandbox-CPJq1Kia.d.mts → sandbox-BbJmh2rI.d.cts} +2 -2
- package/dist/{sandbox-CPJq1Kia.d.mts.map → sandbox-BbJmh2rI.d.cts.map} +1 -1
- package/dist/{sandbox-JwuPAl7I.d.cts → sandbox-DGOsMPMf.d.mts} +2 -2
- package/dist/{sandbox-JwuPAl7I.d.cts.map → sandbox-DGOsMPMf.d.mts.map} +1 -1
- package/dist/{secrets-DJteijZo.cjs → secrets-CPM0Qffe.cjs} +3 -3
- package/dist/secrets-CPM0Qffe.cjs.map +1 -0
- package/dist/{secrets-DbPmAS4k.mjs → secrets-oI9ClAPR.mjs} +3 -3
- package/dist/secrets-oI9ClAPR.mjs.map +1 -0
- package/dist/{seeds-c-ByX9je.cjs → seeds-CnXkgKUv.cjs} +1339 -3594
- package/dist/seeds-CnXkgKUv.cjs.map +1 -0
- package/dist/{seeds-BSL55F6q.mjs → seeds-CoKkrSny.mjs} +1126 -3266
- package/dist/seeds-CoKkrSny.mjs.map +1 -0
- package/dist/{sniffer-BTtGAtm3.mjs → sniffer-CFRzIrby.mjs} +496 -5
- package/dist/sniffer-CFRzIrby.mjs.map +1 -0
- package/dist/{sniffer-Dq5BS98f.cjs → sniffer-vbiQ2Isn.cjs} +572 -3
- package/dist/sniffer-vbiQ2Isn.cjs.map +1 -0
- package/dist/{sst-BaRXp2Dd.mjs → sst-CGdjHn2A.mjs} +4 -3
- package/dist/{sst-BaRXp2Dd.mjs.map → sst-CGdjHn2A.mjs.map} +1 -1
- package/dist/{sst-CnKY-CSh.cjs → sst-DGuHDxVU.cjs} +5 -4
- package/dist/{sst-CnKY-CSh.cjs.map → sst-DGuHDxVU.cjs.map} +1 -1
- package/dist/{stack-1PeLJS2j.cjs → stack-1g57OfOw.cjs} +14 -52
- package/dist/stack-1g57OfOw.cjs.map +1 -0
- package/dist/{stack-Co4uZkAo.mjs → stack-B7O975TR.mjs} +14 -52
- package/dist/stack-B7O975TR.mjs.map +1 -0
- package/dist/{stages-DY-fQKmo.mjs → stages-Bt4OggMm.mjs} +17 -3
- package/dist/stages-Bt4OggMm.mjs.map +1 -0
- package/dist/{stages-BI8JvPzI.cjs → stages-BujUB4we.cjs} +17 -3
- package/dist/stages-BujUB4we.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/{terminal-8XngdSTo.cjs → terminal-CPHuH4S1.cjs} +2 -2
- package/dist/{terminal-8XngdSTo.cjs.map → terminal-CPHuH4S1.cjs.map} +1 -1
- package/dist/{terminal-C4XRdwnU.mjs → terminal-iNmAXXjs.mjs} +2 -2
- package/dist/{terminal-C4XRdwnU.mjs.map → terminal-iNmAXXjs.mjs.map} +1 -1
- package/dist/{types-BibpiHKB.d.cts → types-bkqVscdN.d.mts} +43 -11
- package/dist/types-bkqVscdN.d.mts.map +1 -0
- package/dist/{types-B6lmD3en.d.mts → types-dipJRmKV.d.cts} +44 -10
- package/dist/types-dipJRmKV.d.cts.map +1 -0
- package/dist/workspace/index.cjs +1 -1
- package/dist/workspace/index.d.cts +2 -2
- package/dist/workspace/index.d.mts +2 -2
- package/dist/workspace/index.mjs +1 -1
- package/dist/{workspace-Bzw7Zs82.mjs → workspace-DWK1PTXP.mjs} +4 -4
- package/dist/workspace-DWK1PTXP.mjs.map +1 -0
- package/dist/{workspace-CHOB2Xg3.cjs → workspace-cSyi5nb1.cjs} +4 -4
- package/dist/workspace-cSyi5nb1.cjs.map +1 -0
- package/package.json +15 -15
- package/dist/HostingerProvider-C6t-PSNe.cjs.map +0 -1
- package/dist/add-CP1podDH.mjs.map +0 -1
- package/dist/add-VcyJnsPX.cjs.map +0 -1
- package/dist/cli-B8otnZfX.mjs.map +0 -1
- package/dist/cli-DL19w4OE.cjs.map +0 -1
- package/dist/compose-BHVKLluX.cjs.map +0 -1
- package/dist/compose-DrEB_4F-.mjs.map +0 -1
- package/dist/credentials-C4KII3PD.mjs.map +0 -1
- package/dist/credentials-QBu7VgaQ.cjs.map +0 -1
- package/dist/deploy-C3Mhnmus.cjs +0 -556
- package/dist/deploy-C3Mhnmus.cjs.map +0 -1
- package/dist/deploy-DYIyIUYd.mjs +0 -527
- package/dist/deploy-DYIyIUYd.mjs.map +0 -1
- package/dist/engine-Cfx8fEZU.cjs.map +0 -1
- package/dist/github-CzBbs43W.mjs.map +0 -1
- package/dist/github-zneTMvnO.cjs.map +0 -1
- package/dist/secrets-DJteijZo.cjs.map +0 -1
- package/dist/secrets-DbPmAS4k.mjs.map +0 -1
- package/dist/seeds-BSL55F6q.mjs.map +0 -1
- package/dist/seeds-c-ByX9je.cjs.map +0 -1
- package/dist/sniffer-BTtGAtm3.mjs.map +0 -1
- package/dist/sniffer-Dq5BS98f.cjs.map +0 -1
- package/dist/stack-1PeLJS2j.cjs.map +0 -1
- package/dist/stack-Co4uZkAo.mjs.map +0 -1
- package/dist/stages-BI8JvPzI.cjs.map +0 -1
- package/dist/stages-DY-fQKmo.mjs.map +0 -1
- package/dist/types-B6lmD3en.d.mts.map +0 -1
- package/dist/types-BibpiHKB.d.cts.map +0 -1
- package/dist/workspace-Bzw7Zs82.mjs.map +0 -1
- package/dist/workspace-CHOB2Xg3.cjs.map +0 -1
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-BhDjJH2R.mjs";
|
|
2
2
|
import { t as GkmError } from "./errors-D8Gwp5kU.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { i as run$1, s as gkmHome } from "./run-BJLIerMM.mjs";
|
|
4
4
|
import { d as output, r as discover, u as ConstructGenerator } from "./discover-qjFapetL.mjs";
|
|
5
5
|
import { t as appKey } from "./derive-B4vDpzgP.mjs";
|
|
6
|
-
import {
|
|
6
|
+
import { B as LOGS_PORT, H as provisioningOf, R as stageSampleRate, U as stageProvider, _ as PUBLIC_ENV_PREFIXES, a as allConstructGlobs, at as SERVER_IPV6_KEY, it as SERVER_IPV4_KEY, j as TEST_STAGE, lt as UnknownSecretsStoreProvider, st as isReservedStageKey, v as getPublicEnvPrefix, z as telemetryEnv } from "./workspace-DWK1PTXP.mjs";
|
|
7
7
|
import { ConfigNotFound, NotInAnApp, loadWorkspaceAppInfo, loadWorkspaceConfig } from "./config.mjs";
|
|
8
8
|
import { a as telemetryArgs, c as startTelemetryCode, n as runtimeFor, u as writeTelemetryModule } from "./EndpointGenerator-Bor2tZCm.mjs";
|
|
9
9
|
import { a as DEFAULT_STORAGE, i as DEFAULT_EVENTS$1, n as readFakes, o as holdsEveryCache, r as DEFAULT_CACHE, t as SubscriberGenerator } from "./SubscriberGenerator-CyN2VF5X.mjs";
|
|
10
|
-
import { c as runtimeOf } from "./resolve-
|
|
10
|
+
import { c as runtimeOf } from "./resolve-DakDhr6Z.mjs";
|
|
11
11
|
import { basename, dirname, isAbsolute, join, parse, relative, resolve, sep } from "node:path";
|
|
12
12
|
import { constants, existsSync, readFileSync } from "node:fs";
|
|
13
13
|
import { appendFile, chmod, copyFile, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
@@ -16,9 +16,9 @@ import { promisify } from "node:util";
|
|
|
16
16
|
import { Cron } from "@geekmidas/constructs/crons";
|
|
17
17
|
import { Queue } from "@geekmidas/constructs/queue";
|
|
18
18
|
import { CLIENT_IP_HEADER, DEFAULT_POSTGRES_VERSION, TELEMETRY_KEYS, cacheTable, cookieDomain, dependenciesOf, dependentsOf, externalApiUrl, kebabCase, mobileOrigins, provideKey, providedKeyFor, provisionOrder, publicEnvFor, schemeBase, scopedName } from "@geekmidas/manifest";
|
|
19
|
-
import { createServer
|
|
19
|
+
import { createServer } from "node:net";
|
|
20
20
|
import { execFile, spawn } from "node:child_process";
|
|
21
|
-
import { createCipheriv, createDecipheriv, createHash, randomBytes, randomInt
|
|
21
|
+
import { createCipheriv, createDecipheriv, createHash, randomBytes, randomInt } from "node:crypto";
|
|
22
22
|
import { config } from "dotenv";
|
|
23
23
|
import { stringify } from "yaml";
|
|
24
24
|
import { formatKeyring, generateKeyring } from "@geekmidas/constructs/encryption";
|
|
@@ -31,7 +31,6 @@ import { CreateQueueCommand, GetQueueUrlCommand, SQSClient } from "@aws-sdk/clie
|
|
|
31
31
|
import { Client } from "pg";
|
|
32
32
|
import { cacheTableStatements } from "@geekmidas/cache/postgres";
|
|
33
33
|
import { lookup } from "node:dns/promises";
|
|
34
|
-
import * as s3Url from "@geekmidas/storage/s3-url";
|
|
35
34
|
//#region src/workspace/rootSite.ts
|
|
36
35
|
/**
|
|
37
36
|
* Which site the base domain points at — deployed and locally alike.
|
|
@@ -4924,7 +4923,7 @@ function bucketPolicies(plan) {
|
|
|
4924
4923
|
* client, can be admitted by the policy where the client would have said no.
|
|
4925
4924
|
* Prefer `**` where crossing segments is what you meant.
|
|
4926
4925
|
*/
|
|
4927
|
-
function bucketPolicy
|
|
4926
|
+
function bucketPolicy(bucket, open) {
|
|
4928
4927
|
return JSON.stringify({
|
|
4929
4928
|
Version: "2012-10-17",
|
|
4930
4929
|
Statement: [{
|
|
@@ -5006,7 +5005,7 @@ async function databasesExist(client, names) {
|
|
|
5006
5005
|
async function applyPolicies(client, policies) {
|
|
5007
5006
|
const applied = [];
|
|
5008
5007
|
for (const { bucket, open } of policies) {
|
|
5009
|
-
const wanted = bucketPolicy
|
|
5008
|
+
const wanted = bucketPolicy(bucket, open);
|
|
5010
5009
|
const describe = `open paths on ${bucket}`;
|
|
5011
5010
|
if (equivalent(await client.policy(bucket), wanted)) {
|
|
5012
5011
|
applied.push({
|
|
@@ -6646,1295 +6645,385 @@ function isUnder(file, dir) {
|
|
|
6646
6645
|
return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel) ? !rel.startsWith(`..${sep}`) : false;
|
|
6647
6646
|
}
|
|
6648
6647
|
//#endregion
|
|
6649
|
-
//#region src/
|
|
6648
|
+
//#region src/target/dokploy/dns/DnsProvider.ts
|
|
6650
6649
|
/**
|
|
6651
|
-
*
|
|
6652
|
-
* someone asked — the dev services the local stage runs.
|
|
6653
|
-
*
|
|
6654
|
-
* Locally a declared `Email` is Mailpit and a declared bucket is MinIO, and
|
|
6655
|
-
* nobody has to configure either. Deployed, the same two containers would be
|
|
6656
|
-
* a stage that silently delivers no mail and keeps its files on one
|
|
6657
|
-
* container's disk. So a deployed stage on a server target takes both from
|
|
6658
|
-
* its secrets — the mail server's URL and sending address, and each bucket's
|
|
6659
|
-
* URL — and a stage missing any of them fails before anything is built,
|
|
6660
|
-
* provisioned or written, naming every key at once.
|
|
6661
|
-
*
|
|
6662
|
-
* A bucket's credentials are not checked: they are optional, and come from
|
|
6663
|
-
* either of two places. The bucket's URL may carry a key of its own
|
|
6664
|
-
* (`s3://KEY:SECRET@uploads`), which wins; otherwise the S3 client's default
|
|
6665
|
-
* chain applies — the stage's shared `AWS_ACCESS_KEY_ID`/
|
|
6666
|
-
* `AWS_SECRET_ACCESS_KEY` when it set them, or a role.
|
|
6667
|
-
*
|
|
6668
|
-
* `--allow-dev-services` is the way out for a stage that is not production —
|
|
6669
|
-
* a preview, a demo: every construct the stage does not account for gets the
|
|
6670
|
-
* dev service the local stage runs for it — a bucket MinIO, mail Mailpit —
|
|
6671
|
-
* and the target derives the keys from it. What the stage does account for
|
|
6672
|
-
* still wins: a key set in its secrets, or a provider (`deploy.objects`)
|
|
6673
|
-
* that backs the kind. Every run that uses one says so, loudly.
|
|
6674
|
-
*
|
|
6675
|
-
* Pure: what each target runs is decided here from declarations and the
|
|
6676
|
-
* stage's values, and the targets do the running.
|
|
6650
|
+
* Check if value is a DnsProvider implementation.
|
|
6677
6651
|
*/
|
|
6652
|
+
function isDnsProvider(value) {
|
|
6653
|
+
return typeof value === "object" && value !== null && typeof value.name === "string" && typeof value.getRecords === "function" && typeof value.upsertRecords === "function" && typeof value.deleteRecords === "function";
|
|
6654
|
+
}
|
|
6678
6655
|
/**
|
|
6679
|
-
*
|
|
6680
|
-
*
|
|
6681
|
-
*
|
|
6656
|
+
* Create a DNS provider based on configuration.
|
|
6657
|
+
*
|
|
6658
|
+
* - 'hostinger': HostingerProvider
|
|
6659
|
+
* - 'route53': Route53Provider
|
|
6660
|
+
* - 'godaddy': GoDaddyProvider
|
|
6661
|
+
* - 'manual': Returns null (user handles DNS)
|
|
6662
|
+
* - Custom: Use provided DnsProvider implementation
|
|
6682
6663
|
*/
|
|
6683
|
-
|
|
6664
|
+
async function createDnsProvider(options) {
|
|
6665
|
+
const { config } = options;
|
|
6666
|
+
if (config.provider === "manual") return null;
|
|
6667
|
+
if (isDnsProvider(config.provider)) return config.provider;
|
|
6668
|
+
const provider = config.provider;
|
|
6669
|
+
if (provider === "hostinger") {
|
|
6670
|
+
const { HostingerProvider } = await import("./HostingerProvider-BNpS9aRJ.mjs");
|
|
6671
|
+
return new HostingerProvider(options.hostinger);
|
|
6672
|
+
}
|
|
6673
|
+
if (provider === "route53") {
|
|
6674
|
+
const { Route53Provider } = await import("./Route53Provider-QXxjkZO1.mjs");
|
|
6675
|
+
const route53Config = config;
|
|
6676
|
+
return new Route53Provider({
|
|
6677
|
+
region: route53Config.region,
|
|
6678
|
+
profile: route53Config.profile,
|
|
6679
|
+
hostedZoneId: route53Config.hostedZoneId
|
|
6680
|
+
});
|
|
6681
|
+
}
|
|
6682
|
+
if (provider === "godaddy") {
|
|
6683
|
+
const { GoDaddyProvider } = await import("./GoDaddyProvider-BwW51hPm.mjs");
|
|
6684
|
+
return new GoDaddyProvider(options.godaddy ?? {});
|
|
6685
|
+
}
|
|
6686
|
+
if (provider === "cloudflare") throw new DnsProviderNotImplemented("cloudflare");
|
|
6687
|
+
throw new DnsProviderUnknown(config);
|
|
6688
|
+
}
|
|
6684
6689
|
/**
|
|
6685
|
-
* The
|
|
6686
|
-
*
|
|
6687
|
-
*
|
|
6688
|
-
* chain finds — a role.
|
|
6690
|
+
* The provider's credentials can write records and not read them — a key
|
|
6691
|
+
* scoped to updates only. What reads them falls back to writing every record,
|
|
6692
|
+
* which is idempotent, without a diff.
|
|
6689
6693
|
*/
|
|
6690
|
-
|
|
6691
|
-
|
|
6692
|
-
|
|
6693
|
-
|
|
6694
|
-
constructor(
|
|
6695
|
-
super(
|
|
6696
|
-
this.
|
|
6697
|
-
this.
|
|
6694
|
+
var DnsRecordsUnreadable = class extends GkmError {
|
|
6695
|
+
provider;
|
|
6696
|
+
domain;
|
|
6697
|
+
reason;
|
|
6698
|
+
constructor(provider, domain, reason) {
|
|
6699
|
+
super(`The ${provider} key cannot read ${domain}'s DNS records (${reason}), so changes cannot be compared with what is there: every record is written, which leaves one that already has its value as it was.`);
|
|
6700
|
+
this.provider = provider;
|
|
6701
|
+
this.domain = domain;
|
|
6702
|
+
this.reason = reason;
|
|
6703
|
+
this.name = "DnsRecordsUnreadable";
|
|
6698
6704
|
}
|
|
6699
6705
|
};
|
|
6700
|
-
/**
|
|
6701
|
-
|
|
6702
|
-
|
|
6703
|
-
|
|
6704
|
-
|
|
6705
|
-
|
|
6706
|
-
|
|
6707
|
-
|
|
6708
|
-
|
|
6709
|
-
|
|
6710
|
-
|
|
6706
|
+
/** A DNS provider gkm names and does not have yet. */
|
|
6707
|
+
var DnsProviderNotImplemented = class extends Error {
|
|
6708
|
+
provider;
|
|
6709
|
+
constructor(provider) {
|
|
6710
|
+
super(`The ${provider} DNS provider is not implemented yet. Use 'route53', 'godaddy', 'hostinger' or 'manual' for this domain in dns, or pass your own DnsProvider object.`);
|
|
6711
|
+
this.provider = provider;
|
|
6712
|
+
this.name = "DnsProviderNotImplemented";
|
|
6713
|
+
}
|
|
6714
|
+
};
|
|
6715
|
+
/** A `provider:` that is none of the built-ins, nor a DnsProvider object. */
|
|
6716
|
+
var DnsProviderUnknown = class extends GkmError {
|
|
6717
|
+
config;
|
|
6718
|
+
constructor(config) {
|
|
6719
|
+
super(`Unknown DNS provider: ${JSON.stringify(config)}. dns takes 'route53', 'godaddy', 'hostinger', 'manual', or an object implementing DnsProvider.`);
|
|
6720
|
+
this.config = config;
|
|
6721
|
+
this.name = "DnsProviderUnknown";
|
|
6711
6722
|
}
|
|
6712
6723
|
};
|
|
6724
|
+
//#endregion
|
|
6725
|
+
//#region src/deploy/state.ts
|
|
6713
6726
|
/**
|
|
6714
|
-
*
|
|
6715
|
-
* a command line, with the reason, before the parser calls the value an
|
|
6716
|
-
* excess argument. `gkm deploy` and `gkm compose` take no positional
|
|
6717
|
-
* argument, so whatever follows the flag and is not a flag was meant for it.
|
|
6718
|
-
*
|
|
6719
|
-
* @throws {AllowDevServicesTakesNoValue}
|
|
6727
|
+
* Create a new empty state for a stage
|
|
6720
6728
|
*/
|
|
6721
|
-
function
|
|
6722
|
-
|
|
6723
|
-
|
|
6724
|
-
|
|
6725
|
-
|
|
6726
|
-
|
|
6727
|
-
|
|
6729
|
+
function createEmptyState(stage, projectId, environmentId) {
|
|
6730
|
+
return {
|
|
6731
|
+
provider: "dokploy",
|
|
6732
|
+
stage,
|
|
6733
|
+
projectId,
|
|
6734
|
+
environmentId,
|
|
6735
|
+
applications: {},
|
|
6736
|
+
services: {},
|
|
6737
|
+
lastDeployedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6738
|
+
};
|
|
6739
|
+
}
|
|
6740
|
+
/** A new compose stage's state. */
|
|
6741
|
+
function createComposeState(stage) {
|
|
6742
|
+
return {
|
|
6743
|
+
provider: "compose",
|
|
6744
|
+
stage,
|
|
6745
|
+
lastDeployedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6746
|
+
};
|
|
6728
6747
|
}
|
|
6729
6748
|
/**
|
|
6730
|
-
*
|
|
6731
|
-
*
|
|
6749
|
+
* `state` as the Dokploy target keeps it. A stage whose state another target
|
|
6750
|
+
* wrote — one moved from compose to Dokploy — keeps its core (releases,
|
|
6751
|
+
* identity) and starts with no Dokploy ids, as a first deploy would.
|
|
6732
6752
|
*/
|
|
6733
|
-
|
|
6734
|
-
|
|
6735
|
-
|
|
6736
|
-
|
|
6737
|
-
|
|
6738
|
-
|
|
6739
|
-
|
|
6740
|
-
|
|
6741
|
-
}
|
|
6742
|
-
|
|
6743
|
-
|
|
6744
|
-
|
|
6745
|
-
this.name = "ExternalServicesNotConfigured";
|
|
6746
|
-
}
|
|
6747
|
-
};
|
|
6753
|
+
function asDokployState(state) {
|
|
6754
|
+
if (state.provider === "dokploy") return state;
|
|
6755
|
+
const { provider: _provider, ...core } = state;
|
|
6756
|
+
return {
|
|
6757
|
+
...core,
|
|
6758
|
+
provider: "dokploy",
|
|
6759
|
+
projectId: "",
|
|
6760
|
+
environmentId: "",
|
|
6761
|
+
applications: {},
|
|
6762
|
+
services: {}
|
|
6763
|
+
};
|
|
6764
|
+
}
|
|
6748
6765
|
/**
|
|
6749
|
-
*
|
|
6750
|
-
* depend on each — what a target that provisions the whole manifest checks.
|
|
6766
|
+
* Set application ID in state (mutates state)
|
|
6751
6767
|
*/
|
|
6752
|
-
function
|
|
6753
|
-
|
|
6754
|
-
|
|
6755
|
-
|
|
6756
|
-
|
|
6757
|
-
|
|
6758
|
-
|
|
6759
|
-
|
|
6760
|
-
|
|
6761
|
-
|
|
6762
|
-
kind,
|
|
6763
|
-
...declaration.kind === "file-server" ? { of: declaration.of } : {},
|
|
6764
|
-
...apps.length ? { apps } : {}
|
|
6765
|
-
}];
|
|
6766
|
-
});
|
|
6768
|
+
function setApplicationId(state, appName, applicationId) {
|
|
6769
|
+
state.applications[appName] = applicationId;
|
|
6770
|
+
}
|
|
6771
|
+
/** Two records naming the same image: by digest when both have one. */
|
|
6772
|
+
function sameImage(a, b) {
|
|
6773
|
+
return a.digest && b.digest ? a.digest === b.digest : a.ref === b.ref;
|
|
6774
|
+
}
|
|
6775
|
+
/** The newest release in `history` after `index` that a rollback may restore. */
|
|
6776
|
+
function restorable(history, index, current) {
|
|
6777
|
+
return history.slice(index + 1).find((entry) => !entry.rolledBack && !sameImage(entry, current));
|
|
6767
6778
|
}
|
|
6768
6779
|
/**
|
|
6769
|
-
*
|
|
6770
|
-
*
|
|
6771
|
-
*
|
|
6772
|
-
*
|
|
6773
|
-
* - Deployed, mail is its URL and its sending address, a bucket its URL, and
|
|
6774
|
-
* a file server its public URL. Locally all of it is Mailpit and MinIO.
|
|
6775
|
-
* - On every stage, a third party's credentials: nobody but the third party
|
|
6776
|
-
* can issue them.
|
|
6777
|
-
*
|
|
6778
|
-
* Never a derived value: a database URL, a generated secret, the seed.
|
|
6780
|
+
* Record that `image` went live for `appName` (mutates state): it becomes
|
|
6781
|
+
* `current`, and what was current becomes `previous`. Releasing what already
|
|
6782
|
+
* runs — a redeploy of the same digest — changes nothing.
|
|
6779
6783
|
*/
|
|
6780
|
-
function
|
|
6781
|
-
const
|
|
6782
|
-
|
|
6783
|
-
const
|
|
6784
|
-
|
|
6784
|
+
function recordRelease(state, appName, image, by, releasedAt = /* @__PURE__ */ new Date()) {
|
|
6785
|
+
const releases = state.releases?.[appName];
|
|
6786
|
+
if (releases && sameImage(releases.current, image)) return;
|
|
6787
|
+
const entry = {
|
|
6788
|
+
...image,
|
|
6789
|
+
releasedAt: releasedAt.toISOString(),
|
|
6790
|
+
releasedBy: by
|
|
6791
|
+
};
|
|
6792
|
+
const history = [entry, ...releases?.history ?? []].slice(0, 10);
|
|
6793
|
+
const previous = releases?.current;
|
|
6794
|
+
state.releases = {
|
|
6795
|
+
...state.releases,
|
|
6796
|
+
[appName]: {
|
|
6797
|
+
current: entry,
|
|
6798
|
+
...previous ? { previous } : {},
|
|
6799
|
+
history
|
|
6800
|
+
}
|
|
6785
6801
|
};
|
|
6786
|
-
const byId = (list) => [...list ?? []].sort((a, b) => a.id.localeCompare(b.id));
|
|
6787
|
-
const apps = (d) => d.apps?.length ? { apps: [...d.apps].sort() } : {};
|
|
6788
|
-
if (!input.local) {
|
|
6789
|
-
const services = byId(input.services);
|
|
6790
|
-
const objects = input.providers?.objects;
|
|
6791
|
-
const storage = objects?.accounted ? objects.hint ? { hint: objects.hint } : {} : { service: "minio" };
|
|
6792
|
-
for (const d of services.filter((d) => d.kind === "email")) {
|
|
6793
|
-
add({
|
|
6794
|
-
key: provideKey(d.id, "url"),
|
|
6795
|
-
id: d.id,
|
|
6796
|
-
kind: "email",
|
|
6797
|
-
what: `where '${d.id}' sends mail — any SMTP server`,
|
|
6798
|
-
example: `smtp://user:password@smtp.${domain}:587`,
|
|
6799
|
-
service: "mailpit",
|
|
6800
|
-
...apps(d)
|
|
6801
|
-
});
|
|
6802
|
-
add({
|
|
6803
|
-
key: provideKey(d.id, "from"),
|
|
6804
|
-
id: d.id,
|
|
6805
|
-
kind: "email",
|
|
6806
|
-
what: `the address '${d.id}' sends from, on a domain the mail server has verified`,
|
|
6807
|
-
example: `noreply@${domain}`,
|
|
6808
|
-
service: "mailpit",
|
|
6809
|
-
...apps(d)
|
|
6810
|
-
});
|
|
6811
|
-
}
|
|
6812
|
-
for (const d of services.filter((d) => d.kind === "objects")) add({
|
|
6813
|
-
key: provideKey(d.id, "url"),
|
|
6814
|
-
id: d.id,
|
|
6815
|
-
kind: "bucket",
|
|
6816
|
-
what: `the bucket '${d.id}' — S3, R2, or any S3-compatible store (add &endpoint=… for one that is not S3)`,
|
|
6817
|
-
note: `credentials are optional: a key for this bucket alone in the URL (s3://KEY:SECRET@${appKey(d.id)}?…) wins; without one, the shared AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY or a role signs`,
|
|
6818
|
-
example: `s3://${appKey(d.id)}?region=eu-west-1`,
|
|
6819
|
-
...storage,
|
|
6820
|
-
...apps(d)
|
|
6821
|
-
});
|
|
6822
|
-
for (const d of services.filter((d) => d.kind === "file-server")) add({
|
|
6823
|
-
key: provideKey(d.id, "url"),
|
|
6824
|
-
id: d.id,
|
|
6825
|
-
kind: "file-server",
|
|
6826
|
-
what: `the public address '${d.id}' serves its bucket on — a CDN or the bucket's own domain`,
|
|
6827
|
-
example: `https://${appKey(d.id)}.${domain}`,
|
|
6828
|
-
...storage,
|
|
6829
|
-
...d.of ? { of: d.of } : {},
|
|
6830
|
-
...apps(d)
|
|
6831
|
-
});
|
|
6832
|
-
}
|
|
6833
|
-
if (!input.local && input.server) add({
|
|
6834
|
-
key: SERVER_IPV4_KEY,
|
|
6835
|
-
id: "server",
|
|
6836
|
-
kind: "server",
|
|
6837
|
-
what: "the public IPv4 address of the server the stage's stack runs on — its hosts' DNS records point at it, and every deploy checks they do",
|
|
6838
|
-
note: `${SERVER_IPV6_KEY} adds its IPv6 address (AAAA records); neither is ever handed to an app`,
|
|
6839
|
-
example: "203.0.113.10"
|
|
6840
|
-
});
|
|
6841
|
-
for (const d of byId(input.credentials)) add({
|
|
6842
|
-
key: provideKey(d.id, "credentials"),
|
|
6843
|
-
id: d.id,
|
|
6844
|
-
kind: d.kind,
|
|
6845
|
-
what: d.kind === "external-api" ? `the credentials '${d.id}' was issued for this stage, as its schema describes them` : `the credential '${d.id}', as its schema describes it`,
|
|
6846
|
-
example: "{\"apiKey\":\"…\"}",
|
|
6847
|
-
...apps(d)
|
|
6848
|
-
});
|
|
6849
|
-
return keys;
|
|
6850
6802
|
}
|
|
6851
6803
|
/**
|
|
6852
|
-
*
|
|
6853
|
-
*
|
|
6854
|
-
*
|
|
6855
|
-
* A third party's credentials, always. Deployed, also each generated secret
|
|
6856
|
-
* and keyring: the stage generated each one once, and every run reads the
|
|
6857
|
-
* same value back.
|
|
6804
|
+
* Record that `appName` was rolled back to `to` (mutates state): every
|
|
6805
|
+
* release newer than it is marked rolled back, so neither this rollback nor
|
|
6806
|
+
* a later one restores it, and `previous` moves to the release before `to`.
|
|
6858
6807
|
*/
|
|
6859
|
-
function
|
|
6860
|
-
|
|
6861
|
-
if (
|
|
6862
|
-
|
|
6863
|
-
|
|
6808
|
+
function recordRollback(state, appName, to) {
|
|
6809
|
+
const releases = state.releases?.[appName];
|
|
6810
|
+
if (!releases) return;
|
|
6811
|
+
const index = releases.history.findIndex((entry) => !entry.rolledBack && sameImage(entry, to));
|
|
6812
|
+
if (index === -1) return;
|
|
6813
|
+
const history = releases.history.map((entry, i) => i < index ? {
|
|
6814
|
+
...entry,
|
|
6815
|
+
rolledBack: true
|
|
6816
|
+
} : entry);
|
|
6817
|
+
const current = history[index];
|
|
6818
|
+
const previous = restorable(history, index, current);
|
|
6819
|
+
state.releases = {
|
|
6820
|
+
...state.releases,
|
|
6821
|
+
[appName]: {
|
|
6822
|
+
current,
|
|
6823
|
+
...previous ? { previous } : {},
|
|
6824
|
+
history
|
|
6825
|
+
}
|
|
6826
|
+
};
|
|
6864
6827
|
}
|
|
6865
6828
|
/**
|
|
6866
|
-
*
|
|
6867
|
-
*
|
|
6868
|
-
* - Mail is external once its URL is set, and then needs its sending
|
|
6869
|
-
* address too. Unset, it is Mailpit where allowed — the address derived
|
|
6870
|
-
* unless set — and missing otherwise.
|
|
6871
|
-
* - A bucket is external once its URL is set; its credentials, in the URL or
|
|
6872
|
-
* the shared pair, are optional. Unset, it is MinIO where allowed, and
|
|
6873
|
-
* missing otherwise.
|
|
6874
|
-
* - A file server's URL is derived where its bucket is MinIO, unless set;
|
|
6875
|
-
* over an external bucket the stage must set it.
|
|
6876
|
-
*
|
|
6877
|
-
* The keys themselves are {@link requiredStageKeys}'s.
|
|
6829
|
+
* Get a generated secret for an app
|
|
6878
6830
|
*/
|
|
6879
|
-
function
|
|
6880
|
-
|
|
6881
|
-
const has = (key) => supplied[key] !== void 0;
|
|
6882
|
-
const required = requiredStageKeys({
|
|
6883
|
-
local: false,
|
|
6884
|
-
services: input.declarations,
|
|
6885
|
-
...input.domain ? { domain: input.domain } : {},
|
|
6886
|
-
...input.providers ? { providers: input.providers } : {}
|
|
6887
|
-
});
|
|
6888
|
-
const standIn = (k) => allow && k.service !== void 0 && !has(k.key);
|
|
6889
|
-
const mailpit = required.filter((k) => k.kind === "email" && k.key === provideKey(k.id, "url")).filter(standIn).map((k) => k.id);
|
|
6890
|
-
const minio = required.filter((k) => k.kind === "bucket").filter(standIn).map((k) => k.id);
|
|
6891
|
-
const covered = (k) => k.kind === "email" && mailpit.includes(k.id) || k.kind === "bucket" && minio.includes(k.id) || k.kind === "file-server" && k.of !== void 0 && minio.includes(k.of);
|
|
6892
|
-
return {
|
|
6893
|
-
missing: required.filter((k) => !has(k.key) && !covered(k)).map(({ key, id, what, note, example, service, apps, hint }) => ({
|
|
6894
|
-
key,
|
|
6895
|
-
id,
|
|
6896
|
-
what,
|
|
6897
|
-
...note ? { note } : {},
|
|
6898
|
-
example,
|
|
6899
|
-
...service ? { service } : {},
|
|
6900
|
-
...apps ? { apps } : {},
|
|
6901
|
-
...hint ? { hint } : {}
|
|
6902
|
-
})),
|
|
6903
|
-
minio,
|
|
6904
|
-
mailpit
|
|
6905
|
-
};
|
|
6831
|
+
function getGeneratedSecret(state, appName, secretName) {
|
|
6832
|
+
return state?.generatedSecrets?.[appName]?.[secretName];
|
|
6906
6833
|
}
|
|
6907
6834
|
/**
|
|
6908
|
-
*
|
|
6909
|
-
*
|
|
6910
|
-
* @throws {ExternalServicesNotConfigured} naming every missing key
|
|
6835
|
+
* Set a generated secret for an app (mutates state)
|
|
6911
6836
|
*/
|
|
6912
|
-
function
|
|
6913
|
-
|
|
6914
|
-
if (
|
|
6915
|
-
|
|
6916
|
-
}
|
|
6917
|
-
/** The dev services a run uses, one entry per service it runs. */
|
|
6918
|
-
function devServicesUsed(services) {
|
|
6919
|
-
const used = [];
|
|
6920
|
-
if (services.mailpit.length > 0) used.push({
|
|
6921
|
-
service: "mailpit",
|
|
6922
|
-
ids: services.mailpit
|
|
6923
|
-
});
|
|
6924
|
-
if (services.minio.length > 0) used.push({
|
|
6925
|
-
service: "minio",
|
|
6926
|
-
ids: services.minio
|
|
6927
|
-
});
|
|
6928
|
-
return used;
|
|
6837
|
+
function setGeneratedSecret(state, appName, secretName, value) {
|
|
6838
|
+
if (!state.generatedSecrets) state.generatedSecrets = {};
|
|
6839
|
+
if (!state.generatedSecrets[appName]) state.generatedSecrets[appName] = {};
|
|
6840
|
+
state.generatedSecrets[appName][secretName] = value;
|
|
6929
6841
|
}
|
|
6930
|
-
/**
|
|
6931
|
-
|
|
6932
|
-
|
|
6842
|
+
/**
|
|
6843
|
+
* Set DNS verification record for a hostname (mutates state)
|
|
6844
|
+
*/
|
|
6845
|
+
function setDnsVerification(state, hostname, serverIp) {
|
|
6846
|
+
if (!state.dnsVerified) state.dnsVerified = {};
|
|
6847
|
+
state.dnsVerified[hostname] = {
|
|
6848
|
+
serverIp,
|
|
6849
|
+
verifiedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6850
|
+
};
|
|
6933
6851
|
}
|
|
6934
6852
|
/**
|
|
6935
|
-
*
|
|
6936
|
-
* event a host can act on.
|
|
6853
|
+
* Check if a hostname is already verified with the given IP
|
|
6937
6854
|
*/
|
|
6938
|
-
function
|
|
6939
|
-
|
|
6940
|
-
ctx.logger.warn(`\n${devServiceWarning(ctx.stage, use)}`);
|
|
6941
|
-
ctx.emit({
|
|
6942
|
-
type: "dev-service.used",
|
|
6943
|
-
service: use.service,
|
|
6944
|
-
stage: ctx.stage,
|
|
6945
|
-
constructs: use.ids
|
|
6946
|
-
});
|
|
6947
|
-
}
|
|
6855
|
+
function isDnsVerified(state, hostname, serverIp) {
|
|
6856
|
+
return (state?.dnsVerified?.[hostname])?.serverIp === serverIp;
|
|
6948
6857
|
}
|
|
6949
|
-
//#endregion
|
|
6950
|
-
//#region src/deploy/actor.ts
|
|
6951
6858
|
/**
|
|
6952
|
-
*
|
|
6953
|
-
* machine. Recorded on every state write (`updatedBy`), on every resource
|
|
6954
|
-
* record and on every release (`releasedBy`), so "who put this here" has an
|
|
6955
|
-
* answer that does not depend on anyone's memory.
|
|
6859
|
+
* Get the key for a DNS record in state
|
|
6956
6860
|
*/
|
|
6957
|
-
|
|
6958
|
-
|
|
6959
|
-
return env.GITHUB_ACTIONS === "true" || env.CI === "true";
|
|
6861
|
+
function getDnsRecordKey(name, type) {
|
|
6862
|
+
return `${name}:${type}`;
|
|
6960
6863
|
}
|
|
6961
|
-
/**
|
|
6962
|
-
|
|
6963
|
-
|
|
6964
|
-
|
|
6965
|
-
|
|
6966
|
-
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
|
|
6970
|
-
};
|
|
6971
|
-
}
|
|
6972
|
-
let user = "unknown";
|
|
6973
|
-
try {
|
|
6974
|
-
user = userInfo().username;
|
|
6975
|
-
} catch {}
|
|
6976
|
-
return {
|
|
6977
|
-
kind: "local",
|
|
6978
|
-
user,
|
|
6979
|
-
host: hostname()
|
|
6864
|
+
/**
|
|
6865
|
+
* Set a created DNS record in state (mutates state)
|
|
6866
|
+
*/
|
|
6867
|
+
function setDnsRecord(state, record) {
|
|
6868
|
+
if (!state.dnsRecords) state.dnsRecords = {};
|
|
6869
|
+
const key = getDnsRecordKey(record.name, record.type);
|
|
6870
|
+
state.dnsRecords[key] = {
|
|
6871
|
+
...record,
|
|
6872
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6980
6873
|
};
|
|
6981
6874
|
}
|
|
6982
|
-
/**
|
|
6983
|
-
|
|
6984
|
-
|
|
6985
|
-
|
|
6986
|
-
return
|
|
6987
|
-
}
|
|
6988
|
-
//#endregion
|
|
6989
|
-
//#region src/deploy/dnsResources.ts
|
|
6990
|
-
/** The resource type of a DNS record gkm wrote. */
|
|
6991
|
-
const DNS_RECORD_RESOURCE = "dns-record";
|
|
6992
|
-
/** A record's full name: `api.example.com`, or the domain for `@`. */
|
|
6993
|
-
function dnsRecordFqdn(domain, name) {
|
|
6994
|
-
return name === "@" || name === "" ? domain : `${name}.${domain}`;
|
|
6875
|
+
/**
|
|
6876
|
+
* Get backup state from state
|
|
6877
|
+
*/
|
|
6878
|
+
function getBackupState(state) {
|
|
6879
|
+
return state?.backups;
|
|
6995
6880
|
}
|
|
6996
|
-
/**
|
|
6997
|
-
|
|
6998
|
-
|
|
6881
|
+
/**
|
|
6882
|
+
* Set backup state (mutates state)
|
|
6883
|
+
*/
|
|
6884
|
+
function setBackupState(state, backupState) {
|
|
6885
|
+
state.backups = backupState;
|
|
6999
6886
|
}
|
|
7000
|
-
/**
|
|
7001
|
-
|
|
7002
|
-
|
|
7003
|
-
|
|
7004
|
-
|
|
7005
|
-
domain: record.domain,
|
|
7006
|
-
name: record.name,
|
|
7007
|
-
value: record.value,
|
|
7008
|
-
ttl: record.ttl,
|
|
7009
|
-
...record.provider ? { provider: record.provider } : {}
|
|
7010
|
-
};
|
|
7011
|
-
return {
|
|
7012
|
-
key: dnsResourceKey(fqdn, type),
|
|
7013
|
-
type: DNS_RECORD_RESOURCE,
|
|
7014
|
-
id: `${fqdn} ${type}`,
|
|
7015
|
-
status: "ready",
|
|
7016
|
-
data
|
|
7017
|
-
};
|
|
6887
|
+
/**
|
|
6888
|
+
* Set postgres backup ID in state (mutates state)
|
|
6889
|
+
*/
|
|
6890
|
+
function setPostgresBackupId(state, backupId) {
|
|
6891
|
+
if (state.backups) state.backups.postgresBackupId = backupId;
|
|
7018
6892
|
}
|
|
7019
|
-
|
|
7020
|
-
|
|
7021
|
-
|
|
7022
|
-
|
|
7023
|
-
|
|
7024
|
-
|
|
7025
|
-
|
|
7026
|
-
|
|
7027
|
-
|
|
7028
|
-
|
|
7029
|
-
|
|
7030
|
-
|
|
6893
|
+
//#endregion
|
|
6894
|
+
//#region src/target/dokploy/dns/index.ts
|
|
6895
|
+
/**
|
|
6896
|
+
* DNS orchestration for deployments
|
|
6897
|
+
*
|
|
6898
|
+
* Handles automatic DNS record creation for deployed applications.
|
|
6899
|
+
*/
|
|
6900
|
+
const logger$1 = output;
|
|
6901
|
+
/**
|
|
6902
|
+
* Check if DNS config is legacy format (single domain with `domain` property)
|
|
6903
|
+
*/
|
|
6904
|
+
function isLegacyDnsConfig(config) {
|
|
6905
|
+
return typeof config === "object" && config !== null && "provider" in config && "domain" in config;
|
|
7031
6906
|
}
|
|
7032
6907
|
/**
|
|
7033
|
-
*
|
|
7034
|
-
* recorded, what it deleted is forgotten. A record it found already right is
|
|
7035
|
-
* recorded only if gkm wrote it before; one it left to a person is not.
|
|
6908
|
+
* Normalize DNS config to new multi-domain format
|
|
7036
6909
|
*/
|
|
7037
|
-
|
|
7038
|
-
|
|
7039
|
-
|
|
7040
|
-
|
|
7041
|
-
continue;
|
|
7042
|
-
}
|
|
7043
|
-
const written = change.action === "create" || change.action === "update" || change.action === "write";
|
|
7044
|
-
const known = change.action === "unchanged" && journal.record(dnsResourceKey(dnsRecordFqdn(change.domain, change.name), change.type)) !== void 0;
|
|
7045
|
-
if (!written && !known || change.value === void 0) continue;
|
|
7046
|
-
const provider = providerOf(change.domain);
|
|
7047
|
-
await recordDnsResource(journal, {
|
|
7048
|
-
domain: change.domain,
|
|
7049
|
-
name: change.name,
|
|
7050
|
-
type: change.type,
|
|
7051
|
-
value: change.value,
|
|
7052
|
-
ttl: change.ttl ?? 0,
|
|
7053
|
-
...provider ? { provider } : {}
|
|
7054
|
-
});
|
|
6910
|
+
function normalizeDnsConfig(config) {
|
|
6911
|
+
if (isLegacyDnsConfig(config)) {
|
|
6912
|
+
const { domain, ...providerConfig } = config;
|
|
6913
|
+
return { [domain]: providerConfig };
|
|
7055
6914
|
}
|
|
6915
|
+
return config;
|
|
7056
6916
|
}
|
|
7057
|
-
/** Every DNS record gkm wrote for the stage, from its resource records. */
|
|
7058
|
-
function dnsResources(resources) {
|
|
7059
|
-
return Object.values(resources).filter((record) => record.type === DNS_RECORD_RESOURCE).map((record) => {
|
|
7060
|
-
const data = record.data ?? {};
|
|
7061
|
-
const [fqdn = "", type = ""] = (record.id ?? "").split(" ");
|
|
7062
|
-
return {
|
|
7063
|
-
...data,
|
|
7064
|
-
type,
|
|
7065
|
-
fqdn
|
|
7066
|
-
};
|
|
7067
|
-
}).sort((a, b) => a.fqdn.localeCompare(b.fqdn) || a.type.localeCompare(b.type));
|
|
7068
|
-
}
|
|
7069
|
-
//#endregion
|
|
7070
|
-
//#region src/deploy/StateProvider.ts
|
|
7071
6917
|
/**
|
|
7072
|
-
*
|
|
6918
|
+
* Find the root domain for a hostname from available DNS configs
|
|
6919
|
+
*
|
|
6920
|
+
* @example
|
|
6921
|
+
* findRootDomain('api.geekmidas.com', { 'geekmidas.com': {...}, 'geekmidas.dev': {...} })
|
|
6922
|
+
* // Returns 'geekmidas.com'
|
|
7073
6923
|
*/
|
|
7074
|
-
function
|
|
7075
|
-
|
|
6924
|
+
function findRootDomain(hostname, dnsConfig) {
|
|
6925
|
+
const domains = Object.keys(dnsConfig).sort((a, b) => b.length - a.length);
|
|
6926
|
+
for (const domain of domains) if (hostname === domain || hostname.endsWith(`.${domain}`)) return domain;
|
|
6927
|
+
return null;
|
|
7076
6928
|
}
|
|
7077
|
-
var StateLocked = class extends GkmError {
|
|
7078
|
-
stage;
|
|
7079
|
-
holder;
|
|
7080
|
-
location;
|
|
7081
|
-
constructor(stage, holder, location) {
|
|
7082
|
-
const by = holder ? `${holder.owner}@${holder.host} (pid ${holder.pid}) since ${holder.acquiredAt}${holder.operation ? ` for '${holder.operation}'` : ""}` : "a run that did not record who it was";
|
|
7083
|
-
super(`Deploy state for stage '${stage}' is locked by ${by} (${location}). Wait for that run to finish; if it crashed, release the lock with \`gkm state:unlock --stage ${stage}\`.`);
|
|
7084
|
-
this.stage = stage;
|
|
7085
|
-
this.holder = holder;
|
|
7086
|
-
this.location = location;
|
|
7087
|
-
this.name = "StateLocked";
|
|
7088
|
-
}
|
|
7089
|
-
};
|
|
7090
|
-
var StateVersionConflict = class extends GkmError {
|
|
7091
|
-
stage;
|
|
7092
|
-
expectedVersion;
|
|
7093
|
-
actualVersion;
|
|
7094
|
-
constructor(stage, expectedVersion, actualVersion) {
|
|
7095
|
-
super(`Deploy state for stage '${stage}' changed since it was read (expected ${expectedVersion ? `version ${expectedVersion}` : "no state"}, found ${actualVersion ? `version ${actualVersion}` : "no state"}). Another run wrote it: read the state again and re-apply the change, and hold the stage's lock so runs take turns.`);
|
|
7096
|
-
this.stage = stage;
|
|
7097
|
-
this.expectedVersion = expectedVersion;
|
|
7098
|
-
this.actualVersion = actualVersion;
|
|
7099
|
-
this.name = "StateVersionConflict";
|
|
7100
|
-
}
|
|
7101
|
-
};
|
|
7102
|
-
var StageStateMissing = class extends GkmError {
|
|
7103
|
-
stage;
|
|
7104
|
-
constructor(stage) {
|
|
7105
|
-
super(`There is no deploy state for stage '${stage}' to record a resource in. Write the stage's state first (write with expectedVersion: null).`);
|
|
7106
|
-
this.stage = stage;
|
|
7107
|
-
this.name = "StageStateMissing";
|
|
7108
|
-
}
|
|
7109
|
-
};
|
|
7110
|
-
var StateUnreadable = class extends GkmError {
|
|
7111
|
-
stage;
|
|
7112
|
-
location;
|
|
7113
|
-
constructor(stage, location, cause) {
|
|
7114
|
-
super(`Deploy state for stage '${stage}' at ${location} is not valid JSON. Restore it from a backup (or the provider's history) rather than deploying without it — a deploy without state recreates every resource.`, { cause });
|
|
7115
|
-
this.stage = stage;
|
|
7116
|
-
this.location = location;
|
|
7117
|
-
this.name = "StateUnreadable";
|
|
7118
|
-
}
|
|
7119
|
-
};
|
|
7120
|
-
var StateSchemaTooNew = class extends GkmError {
|
|
7121
|
-
stage;
|
|
7122
|
-
schemaVersion;
|
|
7123
|
-
constructor(stage, schemaVersion) {
|
|
7124
|
-
super(`Deploy state for stage '${stage}' has schema version ${String(schemaVersion)}, newer than this @geekmidas/cli understands (3). Upgrade @geekmidas/cli to the version that wrote it.`);
|
|
7125
|
-
this.stage = stage;
|
|
7126
|
-
this.schemaVersion = schemaVersion;
|
|
7127
|
-
this.name = "StateSchemaTooNew";
|
|
7128
|
-
}
|
|
7129
|
-
};
|
|
7130
6929
|
/**
|
|
7131
|
-
*
|
|
7132
|
-
* in for a store, so a deploy that cannot be protected from a concurrent one
|
|
7133
|
-
* says so.
|
|
6930
|
+
* Group hostnames by their root domain
|
|
7134
6931
|
*/
|
|
7135
|
-
|
|
7136
|
-
|
|
7137
|
-
|
|
7138
|
-
|
|
7139
|
-
|
|
7140
|
-
|
|
7141
|
-
|
|
7142
|
-
}
|
|
7143
|
-
|
|
7144
|
-
|
|
7145
|
-
constructor(provider) {
|
|
7146
|
-
super(`The '${provider}' state provider keys state by workspace name. Set "name" in gkm.config.ts.`);
|
|
7147
|
-
this.provider = provider;
|
|
7148
|
-
this.name = "StateStoreNeedsWorkspaceName";
|
|
7149
|
-
}
|
|
7150
|
-
};
|
|
7151
|
-
var UnknownStateProvider = class extends GkmError {
|
|
7152
|
-
config;
|
|
7153
|
-
constructor(config) {
|
|
7154
|
-
super(`Unknown state provider ${JSON.stringify(config)}. Use 'local', 'ssm', 's3', or an object implementing StateStore or StateProvider.`);
|
|
7155
|
-
this.config = config;
|
|
7156
|
-
this.name = "UnknownStateProvider";
|
|
6932
|
+
function groupHostnamesByDomain(appHostnames, dnsConfig) {
|
|
6933
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
6934
|
+
for (const [appName, hostname] of appHostnames) {
|
|
6935
|
+
const rootDomain = findRootDomain(hostname, dnsConfig);
|
|
6936
|
+
if (!rootDomain) {
|
|
6937
|
+
logger$1.log(` ⚠ No DNS config found for hostname: ${hostname}`);
|
|
6938
|
+
continue;
|
|
6939
|
+
}
|
|
6940
|
+
if (!grouped.has(rootDomain)) grouped.set(rootDomain, /* @__PURE__ */ new Map());
|
|
6941
|
+
grouped.get(rootDomain).set(appName, hostname);
|
|
7157
6942
|
}
|
|
7158
|
-
|
|
6943
|
+
return grouped;
|
|
6944
|
+
}
|
|
7159
6945
|
/**
|
|
7160
|
-
*
|
|
7161
|
-
* the runner — which is discarded when the job ends.
|
|
6946
|
+
* Resolve IP address from a hostname
|
|
7162
6947
|
*/
|
|
7163
|
-
|
|
7164
|
-
stage;
|
|
7165
|
-
operation;
|
|
7166
|
-
region;
|
|
7167
|
-
constructor(stage, operation, region) {
|
|
7168
|
-
super(`\`gkm ${operation}\` would keep stage '${stage}'s deploy state in .gkm/ on this CI runner, and the runner is discarded when the job ends: the next run would start without it — no releases to roll back to, no record of what was created. Keep the state in AWS — in gkm.config.ts:\n\n state: { provider: 'ssm', region: '${region}' },\n\nthen move the state you already have, from the machine that holds it:\n\n gkm state:push --stage ${stage}\n`);
|
|
7169
|
-
this.stage = stage;
|
|
7170
|
-
this.operation = operation;
|
|
7171
|
-
this.region = region;
|
|
7172
|
-
this.name = "LocalStateInCi";
|
|
7173
|
-
}
|
|
7174
|
-
};
|
|
7175
|
-
/** A lock or write that could not get at the state backend in time. */
|
|
7176
|
-
var StateStoreBusy = class extends GkmError {
|
|
7177
|
-
stage;
|
|
7178
|
-
location;
|
|
7179
|
-
constructor(stage, location) {
|
|
7180
|
-
super(`Timed out waiting to write deploy state for stage '${stage}' (${location}). If no other gkm process is running, remove ${location}.`);
|
|
7181
|
-
this.stage = stage;
|
|
7182
|
-
this.location = location;
|
|
7183
|
-
this.name = "StateStoreBusy";
|
|
7184
|
-
}
|
|
7185
|
-
};
|
|
7186
|
-
function decodeDocument(stage, body, location) {
|
|
7187
|
-
let parsed;
|
|
6948
|
+
async function resolveHostnameToIp(hostname) {
|
|
7188
6949
|
try {
|
|
7189
|
-
|
|
6950
|
+
return (await lookup(hostname, { family: 4 })).address;
|
|
7190
6951
|
} catch (error) {
|
|
7191
|
-
throw new
|
|
6952
|
+
throw new Error(`Failed to resolve IP for ${hostname}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
7192
6953
|
}
|
|
7193
|
-
if (typeof parsed !== "object" || parsed === null) throw new StateUnreadable(stage, location, parsed);
|
|
7194
|
-
const schemaVersion = parsed.schemaVersion;
|
|
7195
|
-
if (schemaVersion === void 0) return {
|
|
7196
|
-
schemaVersion: 1,
|
|
7197
|
-
state: parsed
|
|
7198
|
-
};
|
|
7199
|
-
if (schemaVersion === 2) return {
|
|
7200
|
-
schemaVersion: 2,
|
|
7201
|
-
document: parsed
|
|
7202
|
-
};
|
|
7203
|
-
if (schemaVersion === 3) return {
|
|
7204
|
-
schemaVersion: 3,
|
|
7205
|
-
document: parsed
|
|
7206
|
-
};
|
|
7207
|
-
throw new StateSchemaTooNew(stage, schemaVersion);
|
|
7208
|
-
}
|
|
7209
|
-
function encodeDocument(document) {
|
|
7210
|
-
return JSON.stringify(document, null, 2);
|
|
7211
6954
|
}
|
|
7212
6955
|
/**
|
|
7213
|
-
*
|
|
7214
|
-
*
|
|
7215
|
-
*
|
|
6956
|
+
* Extract subdomain from full hostname relative to root domain
|
|
6957
|
+
*
|
|
6958
|
+
* @example
|
|
6959
|
+
* extractSubdomain('api.joemoer.traflabs.io', 'traflabs.io') => 'api.joemoer'
|
|
6960
|
+
* extractSubdomain('joemoer.traflabs.io', 'traflabs.io') => 'joemoer'
|
|
7216
6961
|
*/
|
|
7217
|
-
function
|
|
7218
|
-
|
|
7219
|
-
|
|
7220
|
-
const seed = (type, name, id) => {
|
|
7221
|
-
if (!id) return;
|
|
7222
|
-
const key = name ? `${type}:${name}` : type;
|
|
7223
|
-
resources[key] = {
|
|
7224
|
-
key,
|
|
7225
|
-
type,
|
|
7226
|
-
id,
|
|
7227
|
-
status: "ready",
|
|
7228
|
-
updatedAt: now
|
|
7229
|
-
};
|
|
7230
|
-
};
|
|
7231
|
-
seed("project", null, state.projectId);
|
|
7232
|
-
seed("environment", null, state.environmentId);
|
|
7233
|
-
for (const [app, id] of Object.entries(state.applications ?? {})) seed("application", app, id);
|
|
7234
|
-
seed("postgres", null, state.services?.postgresId);
|
|
7235
|
-
seed("redis", null, state.services?.redisId);
|
|
7236
|
-
seed("backup-destination", null, state.backups?.destinationId);
|
|
7237
|
-
return {
|
|
7238
|
-
schemaVersion: 2,
|
|
7239
|
-
stage,
|
|
7240
|
-
serial: 1,
|
|
7241
|
-
state,
|
|
7242
|
-
resources,
|
|
7243
|
-
updatedAt: now
|
|
7244
|
-
};
|
|
6962
|
+
function extractSubdomain(hostname, rootDomain) {
|
|
6963
|
+
if (!hostname.endsWith(rootDomain)) throw new Error(`Hostname ${hostname} is not under root domain ${rootDomain}`);
|
|
6964
|
+
return hostname.slice(0, -(rootDomain.length + 1)) || "@";
|
|
7245
6965
|
}
|
|
7246
|
-
const isEmpty = (value) => !value || Object.keys(value).length === 0;
|
|
7247
6966
|
/**
|
|
7248
|
-
*
|
|
7249
|
-
*
|
|
7250
|
-
* Before v3 every target wrote the Dokploy shape, and compose filled none of
|
|
7251
|
-
* its Dokploy fields. So the rule is the shape's content, not the target the
|
|
7252
|
-
* workspace names now: a state whose every Dokploy-only field is empty — no
|
|
7253
|
-
* project or environment id, no application or service ids, no credentials,
|
|
7254
|
-
* generated secrets, backups or registry — holds nothing only Dokploy could
|
|
7255
|
-
* use, and reading it as compose loses nothing. One with any of them was
|
|
7256
|
-
* written by Dokploy and is kept exactly as it is.
|
|
7257
|
-
*
|
|
7258
|
-
* The configured target is not used: a document decodes the same wherever it
|
|
7259
|
-
* is read (`state:show`, `state:push`) without a workspace, a stage can be
|
|
7260
|
-
* moved between targets, and `--target` can override a run.
|
|
6967
|
+
* Generate required DNS records for a deployment
|
|
7261
6968
|
*/
|
|
7262
|
-
function
|
|
7263
|
-
|
|
6969
|
+
function generateRequiredRecords(appHostnames, rootDomain, serverIp) {
|
|
6970
|
+
const records = [];
|
|
6971
|
+
for (const [appName, hostname] of appHostnames) {
|
|
6972
|
+
const subdomain = extractSubdomain(hostname, rootDomain);
|
|
6973
|
+
records.push({
|
|
6974
|
+
hostname,
|
|
6975
|
+
subdomain,
|
|
6976
|
+
type: "A",
|
|
6977
|
+
value: serverIp,
|
|
6978
|
+
appName
|
|
6979
|
+
});
|
|
6980
|
+
}
|
|
6981
|
+
return records;
|
|
7264
6982
|
}
|
|
7265
6983
|
/**
|
|
7266
|
-
*
|
|
7267
|
-
* becomes the compose shape: its `dnsRecords` become `dns-record` resources,
|
|
7268
|
-
* and `dnsVerified` and the empty Dokploy fields go — the DNS check is made
|
|
7269
|
-
* on every deploy. A Dokploy state is returned as it is.
|
|
6984
|
+
* Print DNS records table
|
|
7270
6985
|
*/
|
|
7271
|
-
function
|
|
7272
|
-
|
|
7273
|
-
|
|
7274
|
-
|
|
7275
|
-
|
|
7276
|
-
const
|
|
7277
|
-
|
|
7278
|
-
|
|
7279
|
-
|
|
7280
|
-
|
|
7281
|
-
|
|
7282
|
-
|
|
7283
|
-
|
|
7284
|
-
|
|
7285
|
-
|
|
7286
|
-
upgraded[resource.key] = resource;
|
|
6986
|
+
function printDnsRecordsTable(records, rootDomain) {
|
|
6987
|
+
logger$1.log(`\n 📋 DNS Records for ${rootDomain}:`);
|
|
6988
|
+
logger$1.log(" ┌─────────────────────────────────────┬──────┬─────────────────┬────────┐");
|
|
6989
|
+
logger$1.log(" │ Subdomain │ Type │ Value │ Status │");
|
|
6990
|
+
logger$1.log(" ├─────────────────────────────────────┼──────┼─────────────────┼────────┤");
|
|
6991
|
+
for (const record of records) {
|
|
6992
|
+
const subdomain = record.subdomain.padEnd(35);
|
|
6993
|
+
const type = record.type.padEnd(4);
|
|
6994
|
+
const value = record.value.padEnd(15);
|
|
6995
|
+
let status;
|
|
6996
|
+
if (record.error) status = "✗";
|
|
6997
|
+
else if (record.created) status = "✓ new";
|
|
6998
|
+
else if (record.existed) status = "✓";
|
|
6999
|
+
else status = "?";
|
|
7000
|
+
logger$1.log(` │ ${subdomain} │ ${type} │ ${value} │ ${status.padEnd(6)} │`);
|
|
7287
7001
|
}
|
|
7288
|
-
|
|
7289
|
-
state: compose,
|
|
7290
|
-
resources: upgraded
|
|
7291
|
-
};
|
|
7292
|
-
}
|
|
7293
|
-
/** A v2 `dnsRecords` entry as the `dns-record` resource v3 keeps. */
|
|
7294
|
-
function migratedDnsResource(record) {
|
|
7295
|
-
return {
|
|
7296
|
-
...dnsRecordResource({
|
|
7297
|
-
domain: record.domain,
|
|
7298
|
-
name: record.name,
|
|
7299
|
-
type: record.type,
|
|
7300
|
-
value: record.value,
|
|
7301
|
-
ttl: record.ttl
|
|
7302
|
-
}),
|
|
7303
|
-
updatedAt: record.createdAt
|
|
7304
|
-
};
|
|
7305
|
-
}
|
|
7306
|
-
/** A v2 document as v3 — not written until the next write. */
|
|
7307
|
-
function migrateV2(document) {
|
|
7308
|
-
const { state, resources } = upgradeV2State(document.state, document.resources);
|
|
7309
|
-
return {
|
|
7310
|
-
schemaVersion: 3,
|
|
7311
|
-
stage: document.stage,
|
|
7312
|
-
serial: document.serial,
|
|
7313
|
-
state,
|
|
7314
|
-
resources,
|
|
7315
|
-
updatedAt: document.updatedAt,
|
|
7316
|
-
history: []
|
|
7317
|
-
};
|
|
7318
|
-
}
|
|
7319
|
-
function newLockHolder(options) {
|
|
7320
|
-
let owner = "unknown";
|
|
7321
|
-
try {
|
|
7322
|
-
owner = userInfo().username;
|
|
7323
|
-
} catch {}
|
|
7324
|
-
return {
|
|
7325
|
-
id: randomUUID(),
|
|
7326
|
-
owner,
|
|
7327
|
-
host: hostname(),
|
|
7328
|
-
pid: process.pid,
|
|
7329
|
-
acquiredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7330
|
-
operation: options?.operation
|
|
7331
|
-
};
|
|
7332
|
-
}
|
|
7333
|
-
function contentVersion(body) {
|
|
7334
|
-
return createHash("sha256").update(body).digest("hex").slice(0, 16);
|
|
7002
|
+
logger$1.log(" └─────────────────────────────────────┴──────┴─────────────────┴────────┘");
|
|
7335
7003
|
}
|
|
7336
7004
|
/**
|
|
7337
|
-
*
|
|
7338
|
-
* resource it journals — so a run is one entry: a write by the run that
|
|
7339
|
-
* wrote the newest entry (the same lock) replaces it, and any other is
|
|
7340
|
-
* added. Without a lock, each write is its own entry.
|
|
7005
|
+
* Print DNS records in a simple format for manual setup
|
|
7341
7006
|
*/
|
|
7342
|
-
function
|
|
7343
|
-
|
|
7007
|
+
function printDnsRecordsSimple(records, rootDomain) {
|
|
7008
|
+
logger$1.log("\n 📋 Required DNS Records:");
|
|
7009
|
+
logger$1.log(` Add these A records to your DNS provider (${rootDomain}):\n`);
|
|
7010
|
+
for (const record of records) logger$1.log(` ${record.subdomain} → ${record.value} (A record)`);
|
|
7011
|
+
logger$1.log("");
|
|
7344
7012
|
}
|
|
7345
7013
|
/**
|
|
7346
|
-
*
|
|
7347
|
-
* stage, and create a lock object only if it does not exist. Migration,
|
|
7348
|
-
* resource records and history are the same for every such backend, so they
|
|
7349
|
-
* live here.
|
|
7014
|
+
* Create DNS records for a single domain using its configured provider
|
|
7350
7015
|
*/
|
|
7351
|
-
|
|
7352
|
-
|
|
7353
|
-
|
|
7354
|
-
|
|
7355
|
-
|
|
7356
|
-
|
|
7357
|
-
|
|
7358
|
-
|
|
7359
|
-
|
|
7360
|
-
|
|
7361
|
-
|
|
7362
|
-
await this.createLock(stage, holder);
|
|
7363
|
-
this.held.set(stage, holder);
|
|
7364
|
-
let released = false;
|
|
7365
|
-
return {
|
|
7366
|
-
stage,
|
|
7367
|
-
holder,
|
|
7368
|
-
release: async () => {
|
|
7369
|
-
if (released) return;
|
|
7370
|
-
released = true;
|
|
7371
|
-
if (this.held.get(stage)?.id === holder.id) this.held.delete(stage);
|
|
7372
|
-
await this.removeLock(stage, holder);
|
|
7373
|
-
}
|
|
7374
|
-
};
|
|
7375
|
-
}
|
|
7376
|
-
async forceUnlock(stage) {
|
|
7377
|
-
const holder = await this.readLock(stage);
|
|
7378
|
-
await this.removeLock(stage, null);
|
|
7379
|
-
return holder;
|
|
7380
|
-
}
|
|
7381
|
-
async read(stage) {
|
|
7382
|
-
const current = await this.readDocument(stage);
|
|
7383
|
-
if (!current) return null;
|
|
7384
|
-
return {
|
|
7385
|
-
state: current.document.state,
|
|
7386
|
-
resources: current.document.resources,
|
|
7387
|
-
history: current.document.history,
|
|
7388
|
-
version: current.version
|
|
7389
|
-
};
|
|
7390
|
-
}
|
|
7391
|
-
async write(stage, state, options) {
|
|
7392
|
-
if (options.expectedVersion === null) {
|
|
7393
|
-
const document = this.stamp(stage, {
|
|
7394
|
-
schemaVersion: 3,
|
|
7395
|
-
stage,
|
|
7396
|
-
serial: 1,
|
|
7397
|
-
state,
|
|
7398
|
-
resources: {},
|
|
7399
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7400
|
-
history: []
|
|
7401
|
-
});
|
|
7402
|
-
return this.writeRaw(stage, encodeDocument(document), null);
|
|
7403
|
-
}
|
|
7404
|
-
return this.mutate(stage, options.expectedVersion, (document) => ({
|
|
7405
|
-
...document,
|
|
7406
|
-
state
|
|
7407
|
-
}));
|
|
7408
|
-
}
|
|
7409
|
-
async putResource(stage, record, options) {
|
|
7410
|
-
return this.mutate(stage, options?.expectedVersion, (document) => ({
|
|
7411
|
-
...document,
|
|
7412
|
-
resources: {
|
|
7413
|
-
...document.resources,
|
|
7414
|
-
[record.key]: {
|
|
7415
|
-
...record,
|
|
7416
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7417
|
-
updatedBy: this.actor()
|
|
7418
|
-
}
|
|
7419
|
-
}
|
|
7420
|
-
}));
|
|
7421
|
-
}
|
|
7422
|
-
async deleteResource(stage, key, options) {
|
|
7423
|
-
return this.mutate(stage, options?.expectedVersion, (document) => {
|
|
7424
|
-
const { [key]: _removed, ...resources } = document.resources;
|
|
7425
|
-
return {
|
|
7426
|
-
...document,
|
|
7427
|
-
resources
|
|
7428
|
-
};
|
|
7429
|
-
});
|
|
7430
|
-
}
|
|
7431
|
-
/** `document` as this write leaves it: who wrote it, in its history. */
|
|
7432
|
-
stamp(stage, document) {
|
|
7433
|
-
const by = this.actor();
|
|
7434
|
-
const holder = this.held.get(stage);
|
|
7435
|
-
const operation = holder?.operation ?? "write";
|
|
7436
|
-
const history = document.history ?? [];
|
|
7437
|
-
const sameRun = holder !== void 0 && this.logged.get(stage) === holder.id && history[0]?.operation === operation;
|
|
7438
|
-
if (holder) this.logged.set(stage, holder.id);
|
|
7439
|
-
return {
|
|
7440
|
-
...document,
|
|
7441
|
-
updatedBy: by,
|
|
7442
|
-
history: appendHistory(history, {
|
|
7443
|
-
serial: document.serial,
|
|
7444
|
-
at: document.updatedAt,
|
|
7445
|
-
by,
|
|
7446
|
-
operation
|
|
7447
|
-
}, sameRun)
|
|
7448
|
-
};
|
|
7449
|
-
}
|
|
7450
|
-
async mutate(stage, expectedVersion, change) {
|
|
7451
|
-
const current = await this.readDocument(stage);
|
|
7452
|
-
if (!current) {
|
|
7453
|
-
if (expectedVersion !== void 0) throw new StateVersionConflict(stage, expectedVersion, null);
|
|
7454
|
-
throw new StageStateMissing(stage);
|
|
7455
|
-
}
|
|
7456
|
-
if (expectedVersion !== void 0 && current.version !== expectedVersion) throw new StateVersionConflict(stage, expectedVersion, current.version);
|
|
7457
|
-
const next = change(current.document);
|
|
7458
|
-
next.schemaVersion = 3;
|
|
7459
|
-
next.serial = current.document.serial + 1;
|
|
7460
|
-
next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7461
|
-
return this.writeRaw(stage, encodeDocument(this.stamp(stage, next)), current.version);
|
|
7462
|
-
}
|
|
7463
|
-
/**
|
|
7464
|
-
* Reads the stage's document as v3. A v1 document is migrated in place on
|
|
7465
|
-
* the way — itself a conditional write: if another run migrated (or wrote)
|
|
7466
|
-
* first, its result is read instead. A v2 document is upgraded in memory
|
|
7467
|
-
* and written as v3 by the next write.
|
|
7468
|
-
*/
|
|
7469
|
-
async readDocument(stage, attempt = 0) {
|
|
7470
|
-
const raw = await this.readRaw(stage);
|
|
7471
|
-
if (!raw) return null;
|
|
7472
|
-
const decoded = decodeDocument(stage, raw.body, this.location(stage));
|
|
7473
|
-
if (decoded.schemaVersion === 3) return {
|
|
7474
|
-
document: decoded.document,
|
|
7475
|
-
version: raw.version
|
|
7476
|
-
};
|
|
7477
|
-
if (decoded.schemaVersion === 2) return {
|
|
7478
|
-
document: migrateV2(decoded.document),
|
|
7479
|
-
version: raw.version
|
|
7480
|
-
};
|
|
7481
|
-
await this.writeV1Backup(stage, raw.body);
|
|
7482
|
-
const document = this.stamp(stage, migrateV2(migrateV1(stage, decoded.state)));
|
|
7483
|
-
try {
|
|
7484
|
-
return {
|
|
7485
|
-
document,
|
|
7486
|
-
version: await this.writeRaw(stage, encodeDocument(document), raw.version)
|
|
7487
|
-
};
|
|
7488
|
-
} catch (error) {
|
|
7489
|
-
if (error instanceof StateVersionConflict && attempt < 2) return this.readDocument(stage, attempt + 1);
|
|
7490
|
-
throw error;
|
|
7491
|
-
}
|
|
7492
|
-
}
|
|
7493
|
-
};
|
|
7494
|
-
/** Whether `config` keeps state on this machine — `local`, or none given. */
|
|
7495
|
-
function isLocalState(config) {
|
|
7496
|
-
return !config || config.provider === "local";
|
|
7497
|
-
}
|
|
7498
|
-
/**
|
|
7499
|
-
* Refuses, with `LocalStateInCi`, a run that would write a deployed stage's
|
|
7500
|
-
* state to a CI runner's disk — the local stage, which runs on the runner
|
|
7501
|
-
* itself, is exempt. Local use is untouched. The region the message
|
|
7502
|
-
* suggests is the secrets store's, when the workspace keeps them in AWS.
|
|
7503
|
-
*/
|
|
7504
|
-
function assertStateOutlivesRun(workspace, stage, operation, env = process.env) {
|
|
7505
|
-
if (stage === workspace.stages?.local) return;
|
|
7506
|
-
if (!isCi(env) || !isLocalState(workspace.state)) return;
|
|
7507
|
-
const store = workspace.secrets?.store;
|
|
7508
|
-
throw new LocalStateInCi(stage, operation, typeof store === "object" && store && typeof store.region === "string" ? store.region : "<region>");
|
|
7509
|
-
}
|
|
7510
|
-
function isStateStore(value) {
|
|
7511
|
-
return typeof value === "object" && value !== null && typeof value.lock === "function" && typeof value.read === "function" && typeof value.write === "function" && typeof value.putResource === "function";
|
|
7512
|
-
}
|
|
7513
|
-
/**
|
|
7514
|
-
* The store for a workspace's `state` config:
|
|
7515
|
-
*
|
|
7516
|
-
* - none / `'local'`: `LocalStateStore` under `.gkm/`
|
|
7517
|
-
* - `'ssm'`: `SSMStateStore`, with no local cache — SSM is read directly, so
|
|
7518
|
-
* a stale local copy can never win over it
|
|
7519
|
-
* - `'s3'`: `S3StateStore`
|
|
7520
|
-
* - a `StateStore` object: used as is
|
|
7521
|
-
* - a `StateProvider` object: wrapped by `LegacyStateStore`
|
|
7522
|
-
*/
|
|
7523
|
-
async function createStateStore(options) {
|
|
7524
|
-
const { config, workspaceRoot, workspaceName } = options;
|
|
7525
|
-
if (!config || config.provider === "local") {
|
|
7526
|
-
const { LocalStateStore } = await import("./LocalStateStore-D40_SDmJ.mjs").then((n) => n.n);
|
|
7527
|
-
return new LocalStateStore(workspaceRoot);
|
|
7528
|
-
}
|
|
7529
|
-
if (isStateStore(config.provider)) return config.provider;
|
|
7530
|
-
if (isStateProvider(config.provider)) {
|
|
7531
|
-
const { LegacyStateStore } = await import("./LegacyStateStore-CtwO7fwy.mjs");
|
|
7532
|
-
return new LegacyStateStore(config.provider, { warn: options.warn });
|
|
7533
|
-
}
|
|
7534
|
-
if (config.provider === "ssm") {
|
|
7535
|
-
if (!workspaceName) throw new StateStoreNeedsWorkspaceName("ssm");
|
|
7536
|
-
const { SSMStateStore } = await import("./SSMStateStore-k_Ew7TWx.mjs");
|
|
7537
|
-
const ssm = config;
|
|
7538
|
-
return SSMStateStore.create({
|
|
7539
|
-
workspaceName,
|
|
7540
|
-
region: ssm.region,
|
|
7541
|
-
profile: ssm.profile
|
|
7542
|
-
});
|
|
7543
|
-
}
|
|
7544
|
-
if (config.provider === "s3") {
|
|
7545
|
-
if (!workspaceName) throw new StateStoreNeedsWorkspaceName("s3");
|
|
7546
|
-
const { S3StateStore } = await import("./S3StateStore-BzS0IeSt.mjs");
|
|
7547
|
-
const s3 = config;
|
|
7548
|
-
return S3StateStore.create({
|
|
7549
|
-
workspaceName,
|
|
7550
|
-
bucket: s3.bucket,
|
|
7551
|
-
prefix: s3.prefix,
|
|
7552
|
-
region: s3.region,
|
|
7553
|
-
profile: s3.profile
|
|
7554
|
-
});
|
|
7555
|
-
}
|
|
7556
|
-
throw new UnknownStateProvider(config);
|
|
7557
|
-
}
|
|
7558
|
-
//#endregion
|
|
7559
|
-
//#region src/target/dokploy/dns/DnsProvider.ts
|
|
7560
|
-
/**
|
|
7561
|
-
* Check if value is a DnsProvider implementation.
|
|
7562
|
-
*/
|
|
7563
|
-
function isDnsProvider(value) {
|
|
7564
|
-
return typeof value === "object" && value !== null && typeof value.name === "string" && typeof value.getRecords === "function" && typeof value.upsertRecords === "function" && typeof value.deleteRecords === "function";
|
|
7565
|
-
}
|
|
7566
|
-
/**
|
|
7567
|
-
* Create a DNS provider based on configuration.
|
|
7568
|
-
*
|
|
7569
|
-
* - 'hostinger': HostingerProvider
|
|
7570
|
-
* - 'route53': Route53Provider
|
|
7571
|
-
* - 'godaddy': GoDaddyProvider
|
|
7572
|
-
* - 'manual': Returns null (user handles DNS)
|
|
7573
|
-
* - Custom: Use provided DnsProvider implementation
|
|
7574
|
-
*/
|
|
7575
|
-
async function createDnsProvider(options) {
|
|
7576
|
-
const { config } = options;
|
|
7577
|
-
if (config.provider === "manual") return null;
|
|
7578
|
-
if (isDnsProvider(config.provider)) return config.provider;
|
|
7579
|
-
const provider = config.provider;
|
|
7580
|
-
if (provider === "hostinger") {
|
|
7581
|
-
const { HostingerProvider } = await import("./HostingerProvider-1mqD6aG8.mjs");
|
|
7582
|
-
return new HostingerProvider();
|
|
7583
|
-
}
|
|
7584
|
-
if (provider === "route53") {
|
|
7585
|
-
const { Route53Provider } = await import("./Route53Provider-QXxjkZO1.mjs");
|
|
7586
|
-
const route53Config = config;
|
|
7587
|
-
return new Route53Provider({
|
|
7588
|
-
region: route53Config.region,
|
|
7589
|
-
profile: route53Config.profile,
|
|
7590
|
-
hostedZoneId: route53Config.hostedZoneId
|
|
7591
|
-
});
|
|
7592
|
-
}
|
|
7593
|
-
if (provider === "godaddy") {
|
|
7594
|
-
const { GoDaddyProvider } = await import("./GoDaddyProvider-Mi3vptSR.mjs");
|
|
7595
|
-
return new GoDaddyProvider(options.godaddy ?? {});
|
|
7596
|
-
}
|
|
7597
|
-
if (provider === "cloudflare") throw new DnsProviderNotImplemented("cloudflare");
|
|
7598
|
-
throw new DnsProviderUnknown(config);
|
|
7599
|
-
}
|
|
7600
|
-
/**
|
|
7601
|
-
* The provider's credentials can write records and not read them — a key
|
|
7602
|
-
* scoped to updates only. What reads them falls back to writing every record,
|
|
7603
|
-
* which is idempotent, without a diff.
|
|
7604
|
-
*/
|
|
7605
|
-
var DnsRecordsUnreadable = class extends GkmError {
|
|
7606
|
-
provider;
|
|
7607
|
-
domain;
|
|
7608
|
-
reason;
|
|
7609
|
-
constructor(provider, domain, reason) {
|
|
7610
|
-
super(`The ${provider} key cannot read ${domain}'s DNS records (${reason}), so changes cannot be compared with what is there: every record is written, which leaves one that already has its value as it was.`);
|
|
7611
|
-
this.provider = provider;
|
|
7612
|
-
this.domain = domain;
|
|
7613
|
-
this.reason = reason;
|
|
7614
|
-
this.name = "DnsRecordsUnreadable";
|
|
7615
|
-
}
|
|
7616
|
-
};
|
|
7617
|
-
/** A DNS provider gkm names and does not have yet. */
|
|
7618
|
-
var DnsProviderNotImplemented = class extends Error {
|
|
7619
|
-
provider;
|
|
7620
|
-
constructor(provider) {
|
|
7621
|
-
super(`The ${provider} DNS provider is not implemented yet. Use 'route53', 'godaddy', 'hostinger' or 'manual' for this domain in dns, or pass your own DnsProvider object.`);
|
|
7622
|
-
this.provider = provider;
|
|
7623
|
-
this.name = "DnsProviderNotImplemented";
|
|
7624
|
-
}
|
|
7625
|
-
};
|
|
7626
|
-
/** A `provider:` that is none of the built-ins, nor a DnsProvider object. */
|
|
7627
|
-
var DnsProviderUnknown = class extends GkmError {
|
|
7628
|
-
config;
|
|
7629
|
-
constructor(config) {
|
|
7630
|
-
super(`Unknown DNS provider: ${JSON.stringify(config)}. dns takes 'route53', 'godaddy', 'hostinger', 'manual', or an object implementing DnsProvider.`);
|
|
7631
|
-
this.config = config;
|
|
7632
|
-
this.name = "DnsProviderUnknown";
|
|
7633
|
-
}
|
|
7634
|
-
};
|
|
7635
|
-
//#endregion
|
|
7636
|
-
//#region src/deploy/state.ts
|
|
7637
|
-
/**
|
|
7638
|
-
* Create a new empty state for a stage
|
|
7639
|
-
*/
|
|
7640
|
-
function createEmptyState(stage, projectId, environmentId) {
|
|
7641
|
-
return {
|
|
7642
|
-
provider: "dokploy",
|
|
7643
|
-
stage,
|
|
7644
|
-
projectId,
|
|
7645
|
-
environmentId,
|
|
7646
|
-
applications: {},
|
|
7647
|
-
services: {},
|
|
7648
|
-
lastDeployedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7649
|
-
};
|
|
7650
|
-
}
|
|
7651
|
-
/** A new compose stage's state. */
|
|
7652
|
-
function createComposeState(stage) {
|
|
7653
|
-
return {
|
|
7654
|
-
provider: "compose",
|
|
7655
|
-
stage,
|
|
7656
|
-
lastDeployedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7657
|
-
};
|
|
7658
|
-
}
|
|
7659
|
-
/**
|
|
7660
|
-
* `state` as the Dokploy target keeps it. A stage whose state another target
|
|
7661
|
-
* wrote — one moved from compose to Dokploy — keeps its core (releases,
|
|
7662
|
-
* identity) and starts with no Dokploy ids, as a first deploy would.
|
|
7663
|
-
*/
|
|
7664
|
-
function asDokployState(state) {
|
|
7665
|
-
if (state.provider === "dokploy") return state;
|
|
7666
|
-
const { provider: _provider, ...core } = state;
|
|
7667
|
-
return {
|
|
7668
|
-
...core,
|
|
7669
|
-
provider: "dokploy",
|
|
7670
|
-
projectId: "",
|
|
7671
|
-
environmentId: "",
|
|
7672
|
-
applications: {},
|
|
7673
|
-
services: {}
|
|
7674
|
-
};
|
|
7675
|
-
}
|
|
7676
|
-
/**
|
|
7677
|
-
* Set application ID in state (mutates state)
|
|
7678
|
-
*/
|
|
7679
|
-
function setApplicationId(state, appName, applicationId) {
|
|
7680
|
-
state.applications[appName] = applicationId;
|
|
7681
|
-
}
|
|
7682
|
-
/** Two records naming the same image: by digest when both have one. */
|
|
7683
|
-
function sameImage(a, b) {
|
|
7684
|
-
return a.digest && b.digest ? a.digest === b.digest : a.ref === b.ref;
|
|
7685
|
-
}
|
|
7686
|
-
/** The newest release in `history` after `index` that a rollback may restore. */
|
|
7687
|
-
function restorable(history, index, current) {
|
|
7688
|
-
return history.slice(index + 1).find((entry) => !entry.rolledBack && !sameImage(entry, current));
|
|
7689
|
-
}
|
|
7690
|
-
/**
|
|
7691
|
-
* Record that `image` went live for `appName` (mutates state): it becomes
|
|
7692
|
-
* `current`, and what was current becomes `previous`. Releasing what already
|
|
7693
|
-
* runs — a redeploy of the same digest — changes nothing.
|
|
7694
|
-
*/
|
|
7695
|
-
function recordRelease(state, appName, image, by, releasedAt = /* @__PURE__ */ new Date()) {
|
|
7696
|
-
const releases = state.releases?.[appName];
|
|
7697
|
-
if (releases && sameImage(releases.current, image)) return;
|
|
7698
|
-
const entry = {
|
|
7699
|
-
...image,
|
|
7700
|
-
releasedAt: releasedAt.toISOString(),
|
|
7701
|
-
releasedBy: by
|
|
7702
|
-
};
|
|
7703
|
-
const history = [entry, ...releases?.history ?? []].slice(0, 10);
|
|
7704
|
-
const previous = releases?.current;
|
|
7705
|
-
state.releases = {
|
|
7706
|
-
...state.releases,
|
|
7707
|
-
[appName]: {
|
|
7708
|
-
current: entry,
|
|
7709
|
-
...previous ? { previous } : {},
|
|
7710
|
-
history
|
|
7711
|
-
}
|
|
7712
|
-
};
|
|
7713
|
-
}
|
|
7714
|
-
/**
|
|
7715
|
-
* Record that `appName` was rolled back to `to` (mutates state): every
|
|
7716
|
-
* release newer than it is marked rolled back, so neither this rollback nor
|
|
7717
|
-
* a later one restores it, and `previous` moves to the release before `to`.
|
|
7718
|
-
*/
|
|
7719
|
-
function recordRollback(state, appName, to) {
|
|
7720
|
-
const releases = state.releases?.[appName];
|
|
7721
|
-
if (!releases) return;
|
|
7722
|
-
const index = releases.history.findIndex((entry) => !entry.rolledBack && sameImage(entry, to));
|
|
7723
|
-
if (index === -1) return;
|
|
7724
|
-
const history = releases.history.map((entry, i) => i < index ? {
|
|
7725
|
-
...entry,
|
|
7726
|
-
rolledBack: true
|
|
7727
|
-
} : entry);
|
|
7728
|
-
const current = history[index];
|
|
7729
|
-
const previous = restorable(history, index, current);
|
|
7730
|
-
state.releases = {
|
|
7731
|
-
...state.releases,
|
|
7732
|
-
[appName]: {
|
|
7733
|
-
current,
|
|
7734
|
-
...previous ? { previous } : {},
|
|
7735
|
-
history
|
|
7736
|
-
}
|
|
7737
|
-
};
|
|
7738
|
-
}
|
|
7739
|
-
/**
|
|
7740
|
-
* Get a generated secret for an app
|
|
7741
|
-
*/
|
|
7742
|
-
function getGeneratedSecret(state, appName, secretName) {
|
|
7743
|
-
return state?.generatedSecrets?.[appName]?.[secretName];
|
|
7744
|
-
}
|
|
7745
|
-
/**
|
|
7746
|
-
* Set a generated secret for an app (mutates state)
|
|
7747
|
-
*/
|
|
7748
|
-
function setGeneratedSecret(state, appName, secretName, value) {
|
|
7749
|
-
if (!state.generatedSecrets) state.generatedSecrets = {};
|
|
7750
|
-
if (!state.generatedSecrets[appName]) state.generatedSecrets[appName] = {};
|
|
7751
|
-
state.generatedSecrets[appName][secretName] = value;
|
|
7752
|
-
}
|
|
7753
|
-
/**
|
|
7754
|
-
* Set DNS verification record for a hostname (mutates state)
|
|
7755
|
-
*/
|
|
7756
|
-
function setDnsVerification(state, hostname, serverIp) {
|
|
7757
|
-
if (!state.dnsVerified) state.dnsVerified = {};
|
|
7758
|
-
state.dnsVerified[hostname] = {
|
|
7759
|
-
serverIp,
|
|
7760
|
-
verifiedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7761
|
-
};
|
|
7762
|
-
}
|
|
7763
|
-
/**
|
|
7764
|
-
* Check if a hostname is already verified with the given IP
|
|
7765
|
-
*/
|
|
7766
|
-
function isDnsVerified(state, hostname, serverIp) {
|
|
7767
|
-
return (state?.dnsVerified?.[hostname])?.serverIp === serverIp;
|
|
7768
|
-
}
|
|
7769
|
-
/**
|
|
7770
|
-
* Get the key for a DNS record in state
|
|
7771
|
-
*/
|
|
7772
|
-
function getDnsRecordKey(name, type) {
|
|
7773
|
-
return `${name}:${type}`;
|
|
7774
|
-
}
|
|
7775
|
-
/**
|
|
7776
|
-
* Set a created DNS record in state (mutates state)
|
|
7777
|
-
*/
|
|
7778
|
-
function setDnsRecord(state, record) {
|
|
7779
|
-
if (!state.dnsRecords) state.dnsRecords = {};
|
|
7780
|
-
const key = getDnsRecordKey(record.name, record.type);
|
|
7781
|
-
state.dnsRecords[key] = {
|
|
7782
|
-
...record,
|
|
7783
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7784
|
-
};
|
|
7785
|
-
}
|
|
7786
|
-
/**
|
|
7787
|
-
* Get backup state from state
|
|
7788
|
-
*/
|
|
7789
|
-
function getBackupState(state) {
|
|
7790
|
-
return state?.backups;
|
|
7791
|
-
}
|
|
7792
|
-
/**
|
|
7793
|
-
* Set backup state (mutates state)
|
|
7794
|
-
*/
|
|
7795
|
-
function setBackupState(state, backupState) {
|
|
7796
|
-
state.backups = backupState;
|
|
7797
|
-
}
|
|
7798
|
-
/**
|
|
7799
|
-
* Set postgres backup ID in state (mutates state)
|
|
7800
|
-
*/
|
|
7801
|
-
function setPostgresBackupId(state, backupId) {
|
|
7802
|
-
if (state.backups) state.backups.postgresBackupId = backupId;
|
|
7803
|
-
}
|
|
7804
|
-
//#endregion
|
|
7805
|
-
//#region src/target/dokploy/dns/index.ts
|
|
7806
|
-
/**
|
|
7807
|
-
* DNS orchestration for deployments
|
|
7808
|
-
*
|
|
7809
|
-
* Handles automatic DNS record creation for deployed applications.
|
|
7810
|
-
*/
|
|
7811
|
-
const logger$1 = output;
|
|
7812
|
-
/**
|
|
7813
|
-
* Check if DNS config is legacy format (single domain with `domain` property)
|
|
7814
|
-
*/
|
|
7815
|
-
function isLegacyDnsConfig(config) {
|
|
7816
|
-
return typeof config === "object" && config !== null && "provider" in config && "domain" in config;
|
|
7817
|
-
}
|
|
7818
|
-
/**
|
|
7819
|
-
* Normalize DNS config to new multi-domain format
|
|
7820
|
-
*/
|
|
7821
|
-
function normalizeDnsConfig(config) {
|
|
7822
|
-
if (isLegacyDnsConfig(config)) {
|
|
7823
|
-
const { domain, ...providerConfig } = config;
|
|
7824
|
-
return { [domain]: providerConfig };
|
|
7825
|
-
}
|
|
7826
|
-
return config;
|
|
7827
|
-
}
|
|
7828
|
-
/**
|
|
7829
|
-
* Find the root domain for a hostname from available DNS configs
|
|
7830
|
-
*
|
|
7831
|
-
* @example
|
|
7832
|
-
* findRootDomain('api.geekmidas.com', { 'geekmidas.com': {...}, 'geekmidas.dev': {...} })
|
|
7833
|
-
* // Returns 'geekmidas.com'
|
|
7834
|
-
*/
|
|
7835
|
-
function findRootDomain(hostname, dnsConfig) {
|
|
7836
|
-
const domains = Object.keys(dnsConfig).sort((a, b) => b.length - a.length);
|
|
7837
|
-
for (const domain of domains) if (hostname === domain || hostname.endsWith(`.${domain}`)) return domain;
|
|
7838
|
-
return null;
|
|
7839
|
-
}
|
|
7840
|
-
/**
|
|
7841
|
-
* Group hostnames by their root domain
|
|
7842
|
-
*/
|
|
7843
|
-
function groupHostnamesByDomain(appHostnames, dnsConfig) {
|
|
7844
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
7845
|
-
for (const [appName, hostname] of appHostnames) {
|
|
7846
|
-
const rootDomain = findRootDomain(hostname, dnsConfig);
|
|
7847
|
-
if (!rootDomain) {
|
|
7848
|
-
logger$1.log(` ⚠ No DNS config found for hostname: ${hostname}`);
|
|
7849
|
-
continue;
|
|
7850
|
-
}
|
|
7851
|
-
if (!grouped.has(rootDomain)) grouped.set(rootDomain, /* @__PURE__ */ new Map());
|
|
7852
|
-
grouped.get(rootDomain).set(appName, hostname);
|
|
7853
|
-
}
|
|
7854
|
-
return grouped;
|
|
7855
|
-
}
|
|
7856
|
-
/**
|
|
7857
|
-
* Resolve IP address from a hostname
|
|
7858
|
-
*/
|
|
7859
|
-
async function resolveHostnameToIp(hostname) {
|
|
7860
|
-
try {
|
|
7861
|
-
return (await lookup(hostname, { family: 4 })).address;
|
|
7862
|
-
} catch (error) {
|
|
7863
|
-
throw new Error(`Failed to resolve IP for ${hostname}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
7864
|
-
}
|
|
7865
|
-
}
|
|
7866
|
-
/**
|
|
7867
|
-
* Extract subdomain from full hostname relative to root domain
|
|
7868
|
-
*
|
|
7869
|
-
* @example
|
|
7870
|
-
* extractSubdomain('api.joemoer.traflabs.io', 'traflabs.io') => 'api.joemoer'
|
|
7871
|
-
* extractSubdomain('joemoer.traflabs.io', 'traflabs.io') => 'joemoer'
|
|
7872
|
-
*/
|
|
7873
|
-
function extractSubdomain(hostname, rootDomain) {
|
|
7874
|
-
if (!hostname.endsWith(rootDomain)) throw new Error(`Hostname ${hostname} is not under root domain ${rootDomain}`);
|
|
7875
|
-
return hostname.slice(0, -(rootDomain.length + 1)) || "@";
|
|
7876
|
-
}
|
|
7877
|
-
/**
|
|
7878
|
-
* Generate required DNS records for a deployment
|
|
7879
|
-
*/
|
|
7880
|
-
function generateRequiredRecords(appHostnames, rootDomain, serverIp) {
|
|
7881
|
-
const records = [];
|
|
7882
|
-
for (const [appName, hostname] of appHostnames) {
|
|
7883
|
-
const subdomain = extractSubdomain(hostname, rootDomain);
|
|
7884
|
-
records.push({
|
|
7885
|
-
hostname,
|
|
7886
|
-
subdomain,
|
|
7887
|
-
type: "A",
|
|
7888
|
-
value: serverIp,
|
|
7889
|
-
appName
|
|
7890
|
-
});
|
|
7891
|
-
}
|
|
7892
|
-
return records;
|
|
7893
|
-
}
|
|
7894
|
-
/**
|
|
7895
|
-
* Print DNS records table
|
|
7896
|
-
*/
|
|
7897
|
-
function printDnsRecordsTable(records, rootDomain) {
|
|
7898
|
-
logger$1.log(`\n 📋 DNS Records for ${rootDomain}:`);
|
|
7899
|
-
logger$1.log(" ┌─────────────────────────────────────┬──────┬─────────────────┬────────┐");
|
|
7900
|
-
logger$1.log(" │ Subdomain │ Type │ Value │ Status │");
|
|
7901
|
-
logger$1.log(" ├─────────────────────────────────────┼──────┼─────────────────┼────────┤");
|
|
7902
|
-
for (const record of records) {
|
|
7903
|
-
const subdomain = record.subdomain.padEnd(35);
|
|
7904
|
-
const type = record.type.padEnd(4);
|
|
7905
|
-
const value = record.value.padEnd(15);
|
|
7906
|
-
let status;
|
|
7907
|
-
if (record.error) status = "✗";
|
|
7908
|
-
else if (record.created) status = "✓ new";
|
|
7909
|
-
else if (record.existed) status = "✓";
|
|
7910
|
-
else status = "?";
|
|
7911
|
-
logger$1.log(` │ ${subdomain} │ ${type} │ ${value} │ ${status.padEnd(6)} │`);
|
|
7912
|
-
}
|
|
7913
|
-
logger$1.log(" └─────────────────────────────────────┴──────┴─────────────────┴────────┘");
|
|
7914
|
-
}
|
|
7915
|
-
/**
|
|
7916
|
-
* Print DNS records in a simple format for manual setup
|
|
7917
|
-
*/
|
|
7918
|
-
function printDnsRecordsSimple(records, rootDomain) {
|
|
7919
|
-
logger$1.log("\n 📋 Required DNS Records:");
|
|
7920
|
-
logger$1.log(` Add these A records to your DNS provider (${rootDomain}):\n`);
|
|
7921
|
-
for (const record of records) logger$1.log(` ${record.subdomain} → ${record.value} (A record)`);
|
|
7922
|
-
logger$1.log("");
|
|
7923
|
-
}
|
|
7924
|
-
/**
|
|
7925
|
-
* Create DNS records for a single domain using its configured provider
|
|
7926
|
-
*/
|
|
7927
|
-
async function createDnsRecordsForDomain(records, rootDomain, providerConfig) {
|
|
7928
|
-
const ttl = "ttl" in providerConfig && providerConfig.ttl ? providerConfig.ttl : 300;
|
|
7929
|
-
let provider;
|
|
7930
|
-
try {
|
|
7931
|
-
provider = await createDnsProvider({ config: providerConfig });
|
|
7932
|
-
} catch (error) {
|
|
7933
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
7934
|
-
logger$1.log(` ⚠ Failed to create DNS provider for ${rootDomain}: ${message}`);
|
|
7935
|
-
return records.map((r) => ({
|
|
7936
|
-
...r,
|
|
7937
|
-
error: message
|
|
7016
|
+
async function createDnsRecordsForDomain(records, rootDomain, providerConfig) {
|
|
7017
|
+
const ttl = "ttl" in providerConfig && providerConfig.ttl ? providerConfig.ttl : 300;
|
|
7018
|
+
let provider;
|
|
7019
|
+
try {
|
|
7020
|
+
provider = await createDnsProvider({ config: providerConfig });
|
|
7021
|
+
} catch (error) {
|
|
7022
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
7023
|
+
logger$1.log(` ⚠ Failed to create DNS provider for ${rootDomain}: ${message}`);
|
|
7024
|
+
return records.map((r) => ({
|
|
7025
|
+
...r,
|
|
7026
|
+
error: message
|
|
7938
7027
|
}));
|
|
7939
7028
|
}
|
|
7940
7029
|
if (!provider) return records.map((r) => ({
|
|
@@ -8146,2172 +7235,943 @@ async function verifyDnsRecords(appHostnames, serverIp, state) {
|
|
|
8146
7235
|
return results;
|
|
8147
7236
|
}
|
|
8148
7237
|
//#endregion
|
|
8149
|
-
//#region src/
|
|
8150
|
-
|
|
8151
|
-
|
|
8152
|
-
* they resolve to its server before a certificate is asked for.
|
|
8153
|
-
*
|
|
8154
|
-
* - **The server** (`serverAddress`): the stage's own secrets hold its public
|
|
8155
|
-
* addresses — `GKM_SERVER_IPV4`, and `GKM_SERVER_IPV6` where it has one.
|
|
8156
|
-
* Reserved for gkm: never handed to an app.
|
|
8157
|
-
* - **The plan** (`planStackDns`, pure): every public host the stack serves —
|
|
8158
|
-
* its apex, each app, each file server, the public log UI — pointed at the
|
|
8159
|
-
* server. An A record per host (and an AAAA with an IPv6 address), or, where
|
|
8160
|
-
* the domain's `dns` entry says `records: { mode: 'cname', target }`, one A
|
|
8161
|
-
* record for the target and a CNAME to it for every other host but the
|
|
8162
|
-
* apex, which DNS forbids a CNAME at.
|
|
8163
|
-
* - **Applying it** (`applyStackDns`): through the domain's provider in `dns`,
|
|
8164
|
-
* a record set at a time. A record with the right value is left alone; one
|
|
8165
|
-
* with another value is replaced and the change printed. A `manual` domain
|
|
8166
|
-
* is printed and nothing else. A dry run reads, prints and writes nothing.
|
|
8167
|
-
* - **The check** (`checkStackDns`): each host resolved with the system
|
|
8168
|
-
* resolver. One that is not the server stops the deploy with
|
|
8169
|
-
* {@link HostNotPointingAtServer}, before Caddy or Traefik ask Let's Encrypt
|
|
8170
|
-
* for a certificate it cannot get — and use up the rate limit trying.
|
|
8171
|
-
*/
|
|
8172
|
-
/** The line that says how to set the server's address. */
|
|
8173
|
-
function serverAddressHint(stage) {
|
|
8174
|
-
return `gkm secrets:set ${SERVER_IPV4_KEY} '<ip>' --stage ${stage}`;
|
|
8175
|
-
}
|
|
8176
|
-
/** A server address in the stage's secrets that is not an address. */
|
|
8177
|
-
var ServerAddressInvalid = class extends GkmError {
|
|
7238
|
+
//#region src/deploy/journal.ts
|
|
7239
|
+
var DeployJournal = class DeployJournal {
|
|
7240
|
+
store;
|
|
8178
7241
|
stage;
|
|
8179
|
-
|
|
8180
|
-
|
|
8181
|
-
|
|
8182
|
-
|
|
8183
|
-
|
|
7242
|
+
state;
|
|
7243
|
+
resources;
|
|
7244
|
+
version;
|
|
7245
|
+
existed;
|
|
7246
|
+
constructor(store, stage, state, resources, version, existed) {
|
|
7247
|
+
this.store = store;
|
|
8184
7248
|
this.stage = stage;
|
|
8185
|
-
this.
|
|
8186
|
-
this.
|
|
8187
|
-
this.
|
|
7249
|
+
this.state = state;
|
|
7250
|
+
this.resources = resources;
|
|
7251
|
+
this.version = version;
|
|
7252
|
+
this.existed = existed;
|
|
8188
7253
|
}
|
|
8189
|
-
|
|
8190
|
-
|
|
8191
|
-
|
|
8192
|
-
|
|
8193
|
-
|
|
8194
|
-
|
|
8195
|
-
stage;
|
|
8196
|
-
domain;
|
|
8197
|
-
constructor(stage, domain) {
|
|
8198
|
-
super(`The '${stage}' stage serves ${domain} and its secrets hold no ${SERVER_IPV4_KEY} — the public address of the server its stack runs on, which its hosts' DNS records point at and every deploy checks before asking for certificates. Set it: ${serverAddressHint(stage)} (and ${SERVER_IPV6_KEY} for an IPv6 address).`);
|
|
8199
|
-
this.stage = stage;
|
|
8200
|
-
this.domain = domain;
|
|
8201
|
-
this.name = "ServerAddressMissing";
|
|
7254
|
+
static async open(store, stage, initial, adopt = (stored) => stored) {
|
|
7255
|
+
const stored = await store.read(stage);
|
|
7256
|
+
if (stored) return new DeployJournal(store, stage, adopt(stored.state), stored.resources, stored.version, true);
|
|
7257
|
+
const state = initial();
|
|
7258
|
+
const version = await store.write(stage, state, { expectedVersion: null });
|
|
7259
|
+
return new DeployJournal(store, stage, state, {}, version, false);
|
|
8202
7260
|
}
|
|
8203
|
-
|
|
8204
|
-
|
|
8205
|
-
* The server's address from the stage's secrets, checked — `undefined` when
|
|
8206
|
-
* `GKM_SERVER_IPV4` is not set.
|
|
8207
|
-
*
|
|
8208
|
-
* @throws {ServerAddressInvalid} for a value that is not an address
|
|
8209
|
-
*/
|
|
8210
|
-
function serverAddress(stage, custom) {
|
|
8211
|
-
const ipv4 = custom?.[SERVER_IPV4_KEY]?.trim();
|
|
8212
|
-
const ipv6 = custom?.[SERVER_IPV6_KEY]?.trim();
|
|
8213
|
-
if (ipv6 && !isIPv6(ipv6)) throw new ServerAddressInvalid(stage, SERVER_IPV6_KEY, ipv6);
|
|
8214
|
-
if (!ipv4) return void 0;
|
|
8215
|
-
if (!isIPv4(ipv4)) throw new ServerAddressInvalid(stage, SERVER_IPV4_KEY, ipv4);
|
|
8216
|
-
return {
|
|
8217
|
-
ipv4,
|
|
8218
|
-
...ipv6 ? { ipv6 } : {}
|
|
8219
|
-
};
|
|
8220
|
-
}
|
|
8221
|
-
/**
|
|
8222
|
-
* A name that resolves to this machine by definition — `localhost` and every
|
|
8223
|
-
* `*.localhost` — and is in nobody's DNS.
|
|
8224
|
-
*/
|
|
8225
|
-
function isLocalHost(host) {
|
|
8226
|
-
const name = host.toLowerCase().replace(/\.$/, "");
|
|
8227
|
-
return name === "localhost" || name.endsWith(".localhost");
|
|
8228
|
-
}
|
|
8229
|
-
/**
|
|
8230
|
-
* The server's address, required where the stage serves real domains — not
|
|
8231
|
-
* `*.localhost` ones, which point at this machine already.
|
|
8232
|
-
*
|
|
8233
|
-
* @throws {ServerAddressMissing} when the stage has a domain and no address
|
|
8234
|
-
* @throws {ServerAddressInvalid} for a value that is not an address
|
|
8235
|
-
*/
|
|
8236
|
-
function requiredServerAddress(stage, domain, custom) {
|
|
8237
|
-
const server = serverAddress(stage, custom);
|
|
8238
|
-
if (!server && domain && !isLocalHost(domain)) throw new ServerAddressMissing(stage, domain);
|
|
8239
|
-
return server;
|
|
8240
|
-
}
|
|
8241
|
-
const MANAGED = [
|
|
8242
|
-
"A",
|
|
8243
|
-
"AAAA",
|
|
8244
|
-
"CNAME"
|
|
8245
|
-
];
|
|
8246
|
-
/** Every public host the stack serves, once each, sorted. */
|
|
8247
|
-
function stackHosts(stack) {
|
|
8248
|
-
return [...new Set(stack.routes.map((route) => route.host))].sort();
|
|
8249
|
-
}
|
|
8250
|
-
/** The TTL a domain's records are written with. */
|
|
8251
|
-
function recordTtl(config) {
|
|
8252
|
-
const ttl = "ttl" in config ? config.ttl : void 0;
|
|
8253
|
-
if (ttl) return ttl;
|
|
8254
|
-
return config.provider === "godaddy" ? 600 : 300;
|
|
8255
|
-
}
|
|
8256
|
-
function providerName(config) {
|
|
8257
|
-
return typeof config.provider === "string" ? config.provider : "custom";
|
|
8258
|
-
}
|
|
8259
|
-
/** The records a stage's hosts need — pure. */
|
|
8260
|
-
function planStackDns(input) {
|
|
8261
|
-
const { stage, server } = input;
|
|
8262
|
-
const dns = input.dns ? normalizeDnsConfig(input.dns) : {};
|
|
8263
|
-
const records = /* @__PURE__ */ new Map();
|
|
8264
|
-
const uncovered = [];
|
|
8265
|
-
const add = (host, domain, type, value) => {
|
|
8266
|
-
const name = extractSubdomain(host, domain);
|
|
8267
|
-
records.set(`${domain} ${name} ${type}`, {
|
|
8268
|
-
host,
|
|
8269
|
-
domain,
|
|
8270
|
-
name,
|
|
8271
|
-
type,
|
|
8272
|
-
value,
|
|
8273
|
-
ttl: recordTtl(dns[domain])
|
|
8274
|
-
});
|
|
8275
|
-
};
|
|
8276
|
-
const pointAt = (host, domain) => {
|
|
8277
|
-
add(host, domain, "A", server.ipv4);
|
|
8278
|
-
if (server.ipv6) add(host, domain, "AAAA", server.ipv6);
|
|
8279
|
-
};
|
|
8280
|
-
for (const raw of input.hosts) {
|
|
8281
|
-
const host = raw.toLowerCase();
|
|
8282
|
-
const domain = findRootDomain(host, dns);
|
|
8283
|
-
if (!domain) {
|
|
8284
|
-
uncovered.push(host);
|
|
8285
|
-
continue;
|
|
8286
|
-
}
|
|
8287
|
-
const target = cnameTarget(dns[domain].records, stage);
|
|
8288
|
-
if (target) pointAt(target, domain);
|
|
8289
|
-
if (target && host !== domain && host !== target) add(host, domain, "CNAME", target);
|
|
8290
|
-
else pointAt(host, domain);
|
|
8291
|
-
}
|
|
8292
|
-
const domains = /* @__PURE__ */ new Map();
|
|
8293
|
-
for (const record of [...records.values()].sort((a, b) => a.domain.localeCompare(b.domain) || a.name.localeCompare(b.name) || a.type.localeCompare(b.type))) {
|
|
8294
|
-
const config = dns[record.domain];
|
|
8295
|
-
const plan = domains.get(record.domain) ?? {
|
|
8296
|
-
domain: record.domain,
|
|
8297
|
-
provider: providerName(config),
|
|
8298
|
-
config,
|
|
8299
|
-
records: []
|
|
8300
|
-
};
|
|
8301
|
-
plan.records.push(record);
|
|
8302
|
-
domains.set(record.domain, plan);
|
|
7261
|
+
record(key) {
|
|
7262
|
+
return this.resources[key];
|
|
8303
7263
|
}
|
|
8304
|
-
|
|
8305
|
-
|
|
8306
|
-
|
|
8307
|
-
domains: [...domains.values()],
|
|
8308
|
-
uncovered: uncovered.sort()
|
|
8309
|
-
};
|
|
8310
|
-
}
|
|
8311
|
-
/** `api.example.com A 203.0.113.10 (TTL 600)` */
|
|
8312
|
-
function describe(record) {
|
|
8313
|
-
return `${record.host.padEnd(32)} ${record.type.padEnd(5)} ${record.value} (TTL ${record.ttl})`;
|
|
8314
|
-
}
|
|
8315
|
-
function printManual(plan, log, why) {
|
|
8316
|
-
log(` ${plan.domain}: ${why} — create these records at its DNS host:`);
|
|
8317
|
-
for (const record of plan.records) log(` ${describe(record)}`);
|
|
8318
|
-
return plan.records.map((r) => ({
|
|
8319
|
-
domain: r.domain,
|
|
8320
|
-
name: r.name,
|
|
8321
|
-
type: r.type,
|
|
8322
|
-
action: "manual",
|
|
8323
|
-
value: r.value,
|
|
8324
|
-
ttl: r.ttl
|
|
8325
|
-
}));
|
|
8326
|
-
}
|
|
8327
|
-
/** The existing records of every planned name, for the types gkm manages. */
|
|
8328
|
-
async function existingRecords(provider, plan) {
|
|
8329
|
-
const names = [...new Set(plan.records.map((r) => r.name))];
|
|
8330
|
-
const wanted = names.flatMap((name) => MANAGED.map((type) => ({
|
|
8331
|
-
name,
|
|
8332
|
-
type
|
|
8333
|
-
})));
|
|
8334
|
-
if (provider.readRecords) return provider.readRecords(plan.domain, wanted);
|
|
8335
|
-
return (await provider.getRecords(plan.domain)).filter((r) => names.includes(r.name) && MANAGED.includes(r.type));
|
|
8336
|
-
}
|
|
8337
|
-
const same = (a, b) => a.trim().replace(/\.$/, "").toLowerCase() === b.trim().replace(/\.$/, "").toLowerCase();
|
|
8338
|
-
/**
|
|
8339
|
-
* What each domain's records need, compared with what is there: creates,
|
|
8340
|
-
* updates, and the deletes a change of mode needs — a name moving from an A
|
|
8341
|
-
* record to a CNAME, or back, cannot hold both. Nothing else is deleted.
|
|
8342
|
-
*/
|
|
8343
|
-
function diffDomainDns(plan, existing) {
|
|
8344
|
-
const changes = [];
|
|
8345
|
-
const find = (name, type) => existing.find((r) => r.name === name && r.type === type);
|
|
8346
|
-
const planned = /* @__PURE__ */ new Map();
|
|
8347
|
-
for (const record of plan.records) {
|
|
8348
|
-
const types = planned.get(record.name) ?? /* @__PURE__ */ new Set();
|
|
8349
|
-
types.add(record.type);
|
|
8350
|
-
planned.set(record.name, types);
|
|
8351
|
-
}
|
|
8352
|
-
for (const [name, types] of planned) {
|
|
8353
|
-
const conflicting = types.has("CNAME") ? ["A", "AAAA"] : ["CNAME"];
|
|
8354
|
-
for (const type of conflicting) {
|
|
8355
|
-
const found = find(name, type);
|
|
8356
|
-
if (!found || found.values.length === 0) continue;
|
|
8357
|
-
changes.push({
|
|
8358
|
-
domain: plan.domain,
|
|
8359
|
-
name,
|
|
8360
|
-
type,
|
|
8361
|
-
action: "delete",
|
|
8362
|
-
previous: found.values.join(", ")
|
|
8363
|
-
});
|
|
8364
|
-
}
|
|
7264
|
+
/** The keys a previous run marked `pending` and never saw created. */
|
|
7265
|
+
unfinished() {
|
|
7266
|
+
return Object.values(this.resources).filter((record) => record.status === "pending").map((record) => record.key);
|
|
8365
7267
|
}
|
|
8366
|
-
|
|
8367
|
-
|
|
8368
|
-
|
|
8369
|
-
|
|
8370
|
-
|
|
8371
|
-
type: record.type,
|
|
8372
|
-
value: record.value,
|
|
8373
|
-
ttl: record.ttl
|
|
8374
|
-
};
|
|
8375
|
-
if (!found || found.values.length === 0) changes.push({
|
|
8376
|
-
...base,
|
|
8377
|
-
action: "create"
|
|
8378
|
-
});
|
|
8379
|
-
else if (found.values.length === 1 && same(found.values[0], record.value)) changes.push({
|
|
8380
|
-
...base,
|
|
8381
|
-
action: "unchanged"
|
|
7268
|
+
/** Records that `entry` is about to be created. */
|
|
7269
|
+
async pending(entry) {
|
|
7270
|
+
await this.put({
|
|
7271
|
+
...entry,
|
|
7272
|
+
status: "pending"
|
|
8382
7273
|
});
|
|
8383
|
-
|
|
8384
|
-
|
|
8385
|
-
|
|
8386
|
-
|
|
7274
|
+
}
|
|
7275
|
+
/**
|
|
7276
|
+
* Records `entry` as existing with `id`; a no-op if it already is, with
|
|
7277
|
+
* the same data.
|
|
7278
|
+
*/
|
|
7279
|
+
async ready(entry, id) {
|
|
7280
|
+
const current = this.resources[entry.key];
|
|
7281
|
+
if (current?.status === "ready" && current.id === id && JSON.stringify(current.data) === JSON.stringify(entry.data)) return;
|
|
7282
|
+
await this.put({
|
|
7283
|
+
...entry,
|
|
7284
|
+
status: "ready",
|
|
7285
|
+
id
|
|
8387
7286
|
});
|
|
8388
7287
|
}
|
|
8389
|
-
|
|
8390
|
-
|
|
8391
|
-
|
|
8392
|
-
|
|
8393
|
-
|
|
8394
|
-
|
|
8395
|
-
|
|
8396
|
-
|
|
8397
|
-
|
|
8398
|
-
|
|
8399
|
-
|
|
8400
|
-
|
|
8401
|
-
|
|
8402
|
-
|
|
8403
|
-
|
|
8404
|
-
|
|
8405
|
-
|
|
8406
|
-
|
|
8407
|
-
|
|
8408
|
-
break;
|
|
8409
|
-
case "write":
|
|
8410
|
-
log(dryRun ? ` ? ${host} ${type} ${change.value} (TTL ${change.ttl}) — would write; current value unknown (key cannot read DNS)` : ` ! ${host} ${type} ${change.value} (TTL ${change.ttl}) — written (not diffed)`);
|
|
8411
|
-
break;
|
|
8412
|
-
case "delete": log(` - ${host} ${type} ${change.previous} — ${would}delete (a name cannot hold a CNAME and an A record)`);
|
|
8413
|
-
}
|
|
8414
|
-
}
|
|
8415
|
-
/**
|
|
8416
|
-
* Each domain's records, through its provider. Returns every change, the
|
|
8417
|
-
* unchanged included; a dry run makes none of them.
|
|
8418
|
-
*/
|
|
8419
|
-
async function applyStackDns(plan, options = {}) {
|
|
8420
|
-
const dryRun = options.dryRun === true;
|
|
8421
|
-
const log = options.log ?? ((line) => console.log(line));
|
|
8422
|
-
const providerFor = options.providerFor ?? ((config) => createDnsProvider({
|
|
8423
|
-
config,
|
|
8424
|
-
...options.credentials ? { godaddy: options.credentials } : {}
|
|
8425
|
-
}));
|
|
8426
|
-
const changes = [];
|
|
8427
|
-
for (const host of plan.uncovered) log(` ⚠ ${host} is under no domain in dns — point it at ${plan.server.ipv4} yourself`);
|
|
8428
|
-
for (const domain of plan.domains) {
|
|
8429
|
-
if (domain.provider === "manual") {
|
|
8430
|
-
changes.push(...printManual(domain, log, "manual"));
|
|
8431
|
-
continue;
|
|
8432
|
-
}
|
|
8433
|
-
const provider = await providerFor(domain.config);
|
|
8434
|
-
if (!provider) {
|
|
8435
|
-
changes.push(...printManual(domain, log, "manual"));
|
|
8436
|
-
continue;
|
|
8437
|
-
}
|
|
8438
|
-
log(` ${domain.domain} (${provider.name})${dryRun ? " — dry run" : ""}`);
|
|
8439
|
-
let existing;
|
|
8440
|
-
let diff;
|
|
8441
|
-
try {
|
|
8442
|
-
existing = await existingRecords(provider, domain);
|
|
8443
|
-
diff = diffDomainDns(domain, existing);
|
|
8444
|
-
} catch (error) {
|
|
8445
|
-
if (!(error instanceof DnsRecordsUnreadable)) throw error;
|
|
8446
|
-
log(` ℹ The ${provider.name} key cannot read ${domain.domain}'s DNS records, so changes cannot be diffed: every record is written (idempotent).`);
|
|
8447
|
-
existing = [];
|
|
8448
|
-
diff = domain.records.map((r) => ({
|
|
8449
|
-
domain: r.domain,
|
|
8450
|
-
name: r.name,
|
|
8451
|
-
type: r.type,
|
|
8452
|
-
action: "write",
|
|
8453
|
-
value: r.value,
|
|
8454
|
-
ttl: r.ttl
|
|
8455
|
-
}));
|
|
8456
|
-
}
|
|
8457
|
-
for (const change of diff) printChange(change, dryRun, log);
|
|
8458
|
-
if (!plan.server.ipv6) for (const name of new Set(domain.records.map((r) => r.name))) {
|
|
8459
|
-
const aaaa = existing.find((r) => r.name === name && r.type === "AAAA");
|
|
8460
|
-
const planned = domain.records.find((r) => r.name === name && r.type === "A");
|
|
8461
|
-
if (aaaa && planned && aaaa.values.length > 0) log(` ⚠ ${fullName(domain.domain, name)} also has AAAA ${aaaa.values.join(", ")}: set ${SERVER_IPV6_KEY} in the '${plan.stage}' stage's secrets to the server's IPv6 address, or remove that record, or IPv6 clients go elsewhere`);
|
|
7288
|
+
/**
|
|
7289
|
+
* The resource `entry` names: by its recorded id, else by looking it up,
|
|
7290
|
+
* else created — with `pending` written before the create and `ready`
|
|
7291
|
+
* after, so a run that dies in between leaves a record the next run
|
|
7292
|
+
* resolves by looking the resource up rather than creating another.
|
|
7293
|
+
*/
|
|
7294
|
+
async ensure(entry, steps) {
|
|
7295
|
+
const record = this.resources[entry.key];
|
|
7296
|
+
let staleId;
|
|
7297
|
+
if (record?.status === "ready" && record.id && steps.get) {
|
|
7298
|
+
const known = await steps.get(record.id);
|
|
7299
|
+
if (known) {
|
|
7300
|
+
await this.ready(entry, steps.id(known));
|
|
7301
|
+
return {
|
|
7302
|
+
resource: known,
|
|
7303
|
+
via: "recorded"
|
|
7304
|
+
};
|
|
7305
|
+
}
|
|
7306
|
+
staleId = record.id;
|
|
8462
7307
|
}
|
|
8463
|
-
|
|
8464
|
-
if (
|
|
8465
|
-
|
|
8466
|
-
|
|
8467
|
-
|
|
8468
|
-
|
|
8469
|
-
|
|
8470
|
-
|
|
8471
|
-
if (writes.length > 0) await provider.upsertRecords(domain.domain, writes.map((c) => ({
|
|
8472
|
-
name: c.name,
|
|
8473
|
-
type: c.type,
|
|
8474
|
-
value: c.value,
|
|
8475
|
-
ttl: c.ttl
|
|
8476
|
-
})));
|
|
8477
|
-
}
|
|
8478
|
-
return changes;
|
|
8479
|
-
}
|
|
8480
|
-
/** The system resolver: what the server's own clients would be told. */
|
|
8481
|
-
const systemLookup = async (host) => {
|
|
8482
|
-
return (await lookup(host, { all: true })).map((a) => a.address);
|
|
8483
|
-
};
|
|
8484
|
-
/** Hosts that do not resolve to the stage's server. */
|
|
8485
|
-
var HostNotPointingAtServer = class extends GkmError {
|
|
8486
|
-
stage;
|
|
8487
|
-
hosts;
|
|
8488
|
-
expected;
|
|
8489
|
-
fix;
|
|
8490
|
-
constructor(stage, hosts, expected, fix) {
|
|
8491
|
-
const want = expected.ipv6 ? `${expected.ipv4} (and ${expected.ipv6})` : expected.ipv4;
|
|
8492
|
-
const lines = hosts.map((h) => ` - ${h.host} → ${h.resolved.length > 0 ? h.resolved.join(", ") : `nothing${h.error ? ` (${h.error})` : ""}`}, expected ${want}`);
|
|
8493
|
-
super(`${hosts.length === 1 ? "A host" : `${hosts.length} hosts`} of '${stage}' do${hosts.length === 1 ? "es" : ""} not resolve to its server, so a certificate for ${hosts.length === 1 ? "it" : "them"} cannot be issued:\n${lines.join("\n")}\n${fix}\nRecords can take a few minutes to propagate. With a CDN or proxy in front of the server, pass --skip-dns-check.`);
|
|
8494
|
-
this.stage = stage;
|
|
8495
|
-
this.hosts = hosts;
|
|
8496
|
-
this.expected = expected;
|
|
8497
|
-
this.fix = fix;
|
|
8498
|
-
this.name = "HostNotPointingAtServer";
|
|
8499
|
-
}
|
|
8500
|
-
};
|
|
8501
|
-
/**
|
|
8502
|
-
* Every host resolved: each must resolve to the server's IPv4 address and to
|
|
8503
|
-
* nothing but the server's addresses.
|
|
8504
|
-
*
|
|
8505
|
-
* @throws {HostNotPointingAtServer} naming every host that does not
|
|
8506
|
-
*/
|
|
8507
|
-
async function checkStackDns(input) {
|
|
8508
|
-
const lookup = input.lookup ?? systemLookup;
|
|
8509
|
-
const { server } = input;
|
|
8510
|
-
const ours = new Set([server.ipv4, server.ipv6].filter(Boolean).map((a) => a.toLowerCase()));
|
|
8511
|
-
const bad = [];
|
|
8512
|
-
const ok = [];
|
|
8513
|
-
for (const host of input.hosts) {
|
|
8514
|
-
let resolved;
|
|
8515
|
-
try {
|
|
8516
|
-
resolved = [...new Set(await lookup(host))];
|
|
8517
|
-
} catch (error) {
|
|
8518
|
-
const code = error && typeof error === "object" && "code" in error ? String(error.code) : error instanceof Error ? error.message : String(error);
|
|
8519
|
-
bad.push({
|
|
8520
|
-
host,
|
|
8521
|
-
resolved: [],
|
|
8522
|
-
error: code
|
|
8523
|
-
});
|
|
8524
|
-
continue;
|
|
7308
|
+
const existing = await steps.find();
|
|
7309
|
+
if (existing) {
|
|
7310
|
+
await this.ready(entry, steps.id(existing));
|
|
7311
|
+
return {
|
|
7312
|
+
resource: existing,
|
|
7313
|
+
via: record?.status === "pending" ? "resumed" : "found",
|
|
7314
|
+
...staleId ? { staleId } : {}
|
|
7315
|
+
};
|
|
8525
7316
|
}
|
|
8526
|
-
|
|
8527
|
-
|
|
8528
|
-
|
|
8529
|
-
|
|
8530
|
-
|
|
8531
|
-
|
|
8532
|
-
|
|
8533
|
-
ipv4: server.ipv4,
|
|
8534
|
-
...server.ipv6 ? { ipv6: server.ipv6 } : {}
|
|
8535
|
-
}, dnsFix(input, bad));
|
|
8536
|
-
return ok;
|
|
8537
|
-
}
|
|
8538
|
-
/** How to fix it: `gkm setup`, or the records to create by hand. */
|
|
8539
|
-
function dnsFix(input, bad) {
|
|
8540
|
-
const hosts = bad.map((h) => h.host);
|
|
8541
|
-
let plan;
|
|
8542
|
-
try {
|
|
8543
|
-
plan = planStackDns({
|
|
8544
|
-
stage: input.stage,
|
|
8545
|
-
hosts,
|
|
8546
|
-
server: input.server,
|
|
8547
|
-
dns: input.dns
|
|
8548
|
-
});
|
|
8549
|
-
} catch {
|
|
8550
|
-
plan = {
|
|
8551
|
-
stage: input.stage,
|
|
8552
|
-
server: input.server,
|
|
8553
|
-
domains: [],
|
|
8554
|
-
uncovered: hosts
|
|
7317
|
+
await this.pending(entry);
|
|
7318
|
+
const created = await steps.create();
|
|
7319
|
+
await this.ready(entry, steps.id(created));
|
|
7320
|
+
return {
|
|
7321
|
+
resource: created,
|
|
7322
|
+
via: "created",
|
|
7323
|
+
...staleId ? { staleId } : {}
|
|
8555
7324
|
};
|
|
8556
7325
|
}
|
|
8557
|
-
|
|
8558
|
-
|
|
8559
|
-
|
|
8560
|
-
|
|
8561
|
-
|
|
8562
|
-
|
|
8563
|
-
}
|
|
8564
|
-
//#endregion
|
|
8565
|
-
//#region src/docker/imageRef.ts
|
|
8566
|
-
/**
|
|
8567
|
-
* An image reference, checked before it reaches `docker`.
|
|
8568
|
-
*
|
|
8569
|
-
* The parts come from the workspace — a registry in `gkm.config.ts`, an image
|
|
8570
|
-
* name from the project, a tag from `--tag` — and an argument array already
|
|
8571
|
-
* keeps them out of a shell. What it cannot do is stop `docker` reading a value
|
|
8572
|
-
* that starts with `-` as one of its own flags, so a ref is held to Docker's
|
|
8573
|
-
* own grammar (distribution/reference) and anything else is refused by name
|
|
8574
|
-
* before a build starts.
|
|
8575
|
-
*/
|
|
8576
|
-
const DOMAIN_COMPONENT = "(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])";
|
|
8577
|
-
const DOMAIN = `${DOMAIN_COMPONENT}(?:\\.${DOMAIN_COMPONENT})*(?::[0-9]+)?`;
|
|
8578
|
-
const PATH_COMPONENT = "[a-z0-9]+(?:(?:[._]|__|-+)[a-z0-9]+)*";
|
|
8579
|
-
const REFERENCE = new RegExp(`^(?:${DOMAIN}/)?${PATH_COMPONENT}(?:/${PATH_COMPONENT})*(?::[\\w][\\w.-]{0,127})?(?:@[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*:[0-9a-fA-F]{32,})?$`);
|
|
8580
|
-
/** Docker refuses a repository name longer than this. */
|
|
8581
|
-
const MAX_NAME_LENGTH = 255;
|
|
8582
|
-
/** A value that is not an image reference was about to be built or pushed. */
|
|
8583
|
-
var ImageRefInvalid = class extends GkmError {
|
|
8584
|
-
ref;
|
|
8585
|
-
constructor(ref) {
|
|
8586
|
-
super(`'${ref}' is not a valid image reference. An image is \`[registry/]name[:tag]\`: the name lowercase letters, digits and . _ - separators, the tag up to 128 of letters, digits, _ . and -, not starting with . or -. Check docker.registry in gkm.config.ts and the --tag passed.`);
|
|
8587
|
-
this.ref = ref;
|
|
8588
|
-
this.name = "ImageRefInvalid";
|
|
7326
|
+
/** Removes `key`'s record — a resource that no longer exists. */
|
|
7327
|
+
async forget(key) {
|
|
7328
|
+
if (!(key in this.resources)) return;
|
|
7329
|
+
this.version = await this.store.deleteResource(this.stage, key, { expectedVersion: this.version });
|
|
7330
|
+
const { [key]: _removed, ...resources } = this.resources;
|
|
7331
|
+
this.resources = resources;
|
|
8589
7332
|
}
|
|
8590
|
-
|
|
8591
|
-
|
|
8592
|
-
|
|
8593
|
-
const name = ref.split("@")[0].replace(/:[\w][\w.-]*$/, "");
|
|
8594
|
-
if (!REFERENCE.test(ref) || name.length > MAX_NAME_LENGTH) throw new ImageRefInvalid(ref);
|
|
8595
|
-
return ref;
|
|
8596
|
-
}
|
|
8597
|
-
//#endregion
|
|
8598
|
-
//#region src/docker/index.ts
|
|
8599
|
-
const logger = output;
|
|
8600
|
-
/** `docker build` failed; what docker printed above says why. */
|
|
8601
|
-
var DockerBuildFailed = class extends Error {
|
|
8602
|
-
imageRef;
|
|
8603
|
-
constructor(imageRef, cause) {
|
|
8604
|
-
super(`Failed to build Docker image: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
8605
|
-
this.imageRef = imageRef;
|
|
8606
|
-
this.name = "DockerBuildFailed";
|
|
7333
|
+
/** Every record, as the journal last wrote or read it. */
|
|
7334
|
+
records() {
|
|
7335
|
+
return this.resources;
|
|
8607
7336
|
}
|
|
8608
|
-
|
|
8609
|
-
|
|
8610
|
-
|
|
8611
|
-
|
|
8612
|
-
|
|
8613
|
-
|
|
8614
|
-
this.
|
|
8615
|
-
this.
|
|
7337
|
+
/** Writes the stage's state, keeping its resource records. */
|
|
7338
|
+
async save() {
|
|
7339
|
+
this.state.lastDeployedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7340
|
+
this.version = await this.store.write(this.stage, this.state, { expectedVersion: this.version });
|
|
7341
|
+
}
|
|
7342
|
+
async put(record) {
|
|
7343
|
+
this.version = await this.store.putResource(this.stage, record, { expectedVersion: this.version });
|
|
7344
|
+
this.resources = {
|
|
7345
|
+
...this.resources,
|
|
7346
|
+
[record.key]: {
|
|
7347
|
+
...record,
|
|
7348
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7349
|
+
}
|
|
7350
|
+
};
|
|
8616
7351
|
}
|
|
8617
7352
|
};
|
|
8618
|
-
|
|
8619
|
-
|
|
8620
|
-
|
|
8621
|
-
|
|
8622
|
-
|
|
8623
|
-
|
|
8624
|
-
|
|
7353
|
+
//#endregion
|
|
7354
|
+
//#region src/providers/types.ts
|
|
7355
|
+
/** The manifest kinds a stage can name a provider for. */
|
|
7356
|
+
const PROVIDER_KINDS = ["objects"];
|
|
7357
|
+
//#endregion
|
|
7358
|
+
//#region src/providers/notes.ts
|
|
7359
|
+
/** The constructs of a kind: `objects` is every bucket. */
|
|
7360
|
+
function constructsOf(manifest, kind) {
|
|
7361
|
+
return Object.entries(manifest).filter(([, d]) => d.kind === kind).map(([id]) => id).sort();
|
|
7362
|
+
}
|
|
7363
|
+
/** A stage that names `false` for a kind, and declares constructs of it. */
|
|
7364
|
+
var StageProviderDisabled = class extends GkmError {
|
|
7365
|
+
kind;
|
|
7366
|
+
stage;
|
|
7367
|
+
ids;
|
|
7368
|
+
constructor(kind, stage, ids) {
|
|
7369
|
+
super(`deploy.${kind}.${stage} is false — the stage has no ${kind} — and ${ids.join(", ")} ${ids.length === 1 ? "is" : "are"} declared. Name what backs ${ids.length === 1 ? "it" : "them"} on '${stage}' (deploy.${kind}.${stage}: 'external', or a provider), or remove the ${ids.length === 1 ? "construct" : "constructs"}.`);
|
|
7370
|
+
this.kind = kind;
|
|
7371
|
+
this.stage = stage;
|
|
7372
|
+
this.ids = ids;
|
|
7373
|
+
this.name = "StageProviderDisabled";
|
|
8625
7374
|
}
|
|
8626
7375
|
};
|
|
7376
|
+
/** The command a stage's providers are run with: every deploy runs them. */
|
|
7377
|
+
function provisionCommand(stage) {
|
|
7378
|
+
return `gkm deploy --stage ${stage}`;
|
|
7379
|
+
}
|
|
8627
7380
|
/**
|
|
8628
|
-
*
|
|
8629
|
-
*
|
|
8630
|
-
*
|
|
8631
|
-
* `.dockerignore` and `docker-compose.constructs.yml`.
|
|
7381
|
+
* The line a stage missing a key a provider would write is given: what
|
|
7382
|
+
* creates it, and with which credentials. Undefined when no provider backs
|
|
7383
|
+
* the kind on the stage.
|
|
8632
7384
|
*/
|
|
8633
|
-
|
|
8634
|
-
const
|
|
8635
|
-
|
|
8636
|
-
const
|
|
8637
|
-
|
|
8638
|
-
|
|
8639
|
-
if (options.push) for (const app of result.apps) await pushDockerImage(app.imageName, options, result.buildRoot);
|
|
8640
|
-
return result;
|
|
7385
|
+
function provisionHint(workspace, kind, stage) {
|
|
7386
|
+
const choice = stageProvider(workspace, kind, stage);
|
|
7387
|
+
if (choice.mode !== "provider") return void 0;
|
|
7388
|
+
const provisioning = provisioningOf(kind, choice.name);
|
|
7389
|
+
if (!provisioning) return void 0;
|
|
7390
|
+
return `deploy.${kind}.${stage} is ${choice.name}: the deploy (${provisionCommand(stage)}) creates it and writes this key, with ${provisioning.describe}` + (provisioning.login ? `, or gkm login --provider ${provisioning.login}` : "");
|
|
8641
7391
|
}
|
|
8642
7392
|
/**
|
|
8643
|
-
*
|
|
8644
|
-
*
|
|
7393
|
+
* What a deploy's checks read off the stage's providers: whether each kind is
|
|
7394
|
+
* accounted for — a provider, or `false` — so no dev service stands in for
|
|
7395
|
+
* it, and what creates a missing key of it.
|
|
8645
7396
|
*/
|
|
8646
|
-
|
|
8647
|
-
|
|
8648
|
-
const
|
|
8649
|
-
|
|
8650
|
-
|
|
8651
|
-
|
|
8652
|
-
|
|
8653
|
-
"build",
|
|
8654
|
-
`--file=${relative(buildRoot, app.dockerfile)}`,
|
|
8655
|
-
`--tag=${fullImageName}`,
|
|
8656
|
-
"."
|
|
8657
|
-
], {
|
|
8658
|
-
cwd: buildRoot,
|
|
8659
|
-
env: {
|
|
8660
|
-
...process.env,
|
|
8661
|
-
DOCKER_BUILDKIT: "1"
|
|
8662
|
-
}
|
|
8663
|
-
});
|
|
8664
|
-
logger.log(`✅ Docker image built: ${fullImageName}`);
|
|
8665
|
-
} catch (error) {
|
|
8666
|
-
throw new DockerBuildFailed(fullImageName, error);
|
|
8667
|
-
}
|
|
7397
|
+
function stageProviderNotes(workspace, stage) {
|
|
7398
|
+
if (stageProvider(workspace, "objects", stage).mode === "external") return {};
|
|
7399
|
+
const hint = provisionHint(workspace, "objects", stage);
|
|
7400
|
+
return { objects: {
|
|
7401
|
+
accounted: true,
|
|
7402
|
+
...hint ? { hint } : {}
|
|
7403
|
+
} };
|
|
8668
7404
|
}
|
|
8669
7405
|
/**
|
|
8670
|
-
*
|
|
7406
|
+
* A kind the stage set to `false`, declared anyway — refused before anything
|
|
7407
|
+
* else is checked. Pure: what a stack is composed from.
|
|
7408
|
+
*
|
|
7409
|
+
* @throws {StageProviderDisabled}
|
|
8671
7410
|
*/
|
|
8672
|
-
|
|
8673
|
-
const
|
|
8674
|
-
|
|
8675
|
-
|
|
8676
|
-
|
|
8677
|
-
logger.log(`\n🚀 Pushing Docker image: ${fullImageName}`);
|
|
8678
|
-
try {
|
|
8679
|
-
await run$1("docker", ["push", fullImageName], { cwd });
|
|
8680
|
-
logger.log(`✅ Docker image pushed: ${fullImageName}`);
|
|
8681
|
-
} catch (error) {
|
|
8682
|
-
throw new DockerPushFailed(fullImageName, error);
|
|
7411
|
+
function assertStageProvidersEnabled(workspace, manifest, stage) {
|
|
7412
|
+
for (const kind of PROVIDER_KINDS) {
|
|
7413
|
+
const ids = constructsOf(manifest, kind);
|
|
7414
|
+
if (ids.length === 0) continue;
|
|
7415
|
+
if (stageProvider(workspace, kind, stage).mode === "none") throw new StageProviderDisabled(kind, stage, ids);
|
|
8683
7416
|
}
|
|
8684
7417
|
}
|
|
7418
|
+
//#endregion
|
|
7419
|
+
//#region src/deploy/devServices.ts
|
|
8685
7420
|
/**
|
|
8686
|
-
*
|
|
8687
|
-
*
|
|
7421
|
+
* Mail and object storage on a deployed stage: real ones, or — only when
|
|
7422
|
+
* someone asked — the dev services the local stage runs.
|
|
7423
|
+
*
|
|
7424
|
+
* Locally a declared `Email` is Mailpit and a declared bucket is MinIO, and
|
|
7425
|
+
* nobody has to configure either. Deployed, the same two containers would be
|
|
7426
|
+
* a stage that silently delivers no mail and keeps its files on one
|
|
7427
|
+
* container's disk. So a deployed stage on a server target takes both from
|
|
7428
|
+
* its secrets — the mail server's URL and sending address, and each bucket's
|
|
7429
|
+
* URL — and a stage missing any of them fails before anything is built,
|
|
7430
|
+
* provisioned or written, naming every key at once.
|
|
7431
|
+
*
|
|
7432
|
+
* A bucket's credentials are not checked: they are optional, and come from
|
|
7433
|
+
* either of two places. The bucket's URL may carry a key of its own
|
|
7434
|
+
* (`s3://KEY:SECRET@uploads`), which wins; otherwise the S3 client's default
|
|
7435
|
+
* chain applies — the stage's shared `AWS_ACCESS_KEY_ID`/
|
|
7436
|
+
* `AWS_SECRET_ACCESS_KEY` when it set them, or a role.
|
|
7437
|
+
*
|
|
7438
|
+
* `--allow-dev-services` is the way out for a stage that is not production —
|
|
7439
|
+
* a preview, a demo: every construct the stage does not account for gets the
|
|
7440
|
+
* dev service the local stage runs for it — a bucket MinIO, mail Mailpit —
|
|
7441
|
+
* and the target derives the keys from it. What the stage does account for
|
|
7442
|
+
* still wins: a key set in its secrets, or a provider (`deploy.objects`)
|
|
7443
|
+
* that backs the kind. Every run that uses one says so, loudly.
|
|
7444
|
+
*
|
|
7445
|
+
* Pure: what each target runs is decided here from declarations and the
|
|
7446
|
+
* stage's values, and the targets do the running.
|
|
8688
7447
|
*/
|
|
8689
|
-
|
|
8690
|
-
|
|
8691
|
-
|
|
8692
|
-
|
|
8693
|
-
|
|
8694
|
-
|
|
8695
|
-
|
|
8696
|
-
|
|
8697
|
-
|
|
8698
|
-
|
|
8699
|
-
|
|
8700
|
-
|
|
8701
|
-
|
|
8702
|
-
|
|
8703
|
-
|
|
8704
|
-
|
|
8705
|
-
|
|
8706
|
-
|
|
8707
|
-
|
|
8708
|
-
|
|
8709
|
-
});
|
|
8710
|
-
const path = dockerfileOf(appName, app.path);
|
|
8711
|
-
const dockerfilePath = join(workspace.root, path);
|
|
8712
|
-
await mkdir(dirname(dockerfilePath), { recursive: true });
|
|
8713
|
-
await writeFile(dockerfilePath, dockerfile);
|
|
8714
|
-
logger.log(` Generated: ${path}`);
|
|
8715
|
-
results.push({
|
|
8716
|
-
appName,
|
|
8717
|
-
type: app.type,
|
|
8718
|
-
dockerfile: dockerfilePath,
|
|
8719
|
-
imageName: imageNames[appName] ?? appName
|
|
8720
|
-
});
|
|
8721
|
-
}
|
|
8722
|
-
for (const worker of manifest ? workerUnits(workspace, manifest, background) : []) {
|
|
8723
|
-
logger.log(`\n 📄 Generating Dockerfile for ${worker.name} (worker ${worker.id}, built from ${worker.app})`);
|
|
8724
|
-
const dockerfile = workerDockerfile(worker, workspace.apps[worker.app], {
|
|
8725
|
-
layout,
|
|
8726
|
-
workspaceRoot: workspace.root
|
|
8727
|
-
});
|
|
8728
|
-
const path = workerDockerfileOf(worker.name);
|
|
8729
|
-
const dockerfilePath = join(workspace.root, path);
|
|
8730
|
-
await mkdir(dirname(dockerfilePath), { recursive: true });
|
|
8731
|
-
await writeFile(dockerfilePath, dockerfile);
|
|
8732
|
-
logger.log(` Generated: ${path}`);
|
|
8733
|
-
results.push({
|
|
8734
|
-
appName: worker.name,
|
|
8735
|
-
type: "backend",
|
|
8736
|
-
dockerfile: dockerfilePath,
|
|
8737
|
-
imageName: imageNames[worker.name] ?? worker.name
|
|
8738
|
-
});
|
|
7448
|
+
/**
|
|
7449
|
+
* The dev services a deployed stage can run — one per kind that has one on
|
|
7450
|
+
* every server target: a bucket's MinIO and mail's Mailpit. A cache or a
|
|
7451
|
+
* database is not here because a server stage runs its own anyway.
|
|
7452
|
+
*/
|
|
7453
|
+
const DEV_SERVICES = ["minio", "mailpit"];
|
|
7454
|
+
/**
|
|
7455
|
+
* The S3 client's shared key pair. Optional: a bucket whose URL carries its own
|
|
7456
|
+
* key (`s3://KEY:SECRET@bucket`) signs with that, and one without signs with
|
|
7457
|
+
* this pair when the stage set it, or with whatever else the SDK's default
|
|
7458
|
+
* chain finds — a role.
|
|
7459
|
+
*/
|
|
7460
|
+
const STORAGE_KEY_PAIR = ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"];
|
|
7461
|
+
/** Dev services asked of a target that runs no containers. */
|
|
7462
|
+
var DevServicesNeedServerTarget = class extends GkmError {
|
|
7463
|
+
target;
|
|
7464
|
+
constructor(target) {
|
|
7465
|
+
super(`--allow-dev-services applies to server targets only, and '${target}' deploys to AWS, where a bucket is S3 and mail is the stage's own. Drop --allow-dev-services.`);
|
|
7466
|
+
this.target = target;
|
|
7467
|
+
this.name = "DevServicesNeedServerTarget";
|
|
8739
7468
|
}
|
|
8740
|
-
|
|
8741
|
-
|
|
8742
|
-
|
|
8743
|
-
|
|
8744
|
-
|
|
8745
|
-
|
|
8746
|
-
|
|
8747
|
-
|
|
7469
|
+
};
|
|
7470
|
+
/**
|
|
7471
|
+
* `--allow-dev-services` given a value — the list it used to take. It is a
|
|
7472
|
+
* switch now: every construct the stage does not account for gets its dev
|
|
7473
|
+
* service.
|
|
7474
|
+
*/
|
|
7475
|
+
var AllowDevServicesTakesNoValue = class extends GkmError {
|
|
7476
|
+
value;
|
|
7477
|
+
constructor(value) {
|
|
7478
|
+
super(`--allow-dev-services takes no value (it was given '${value}'). It runs the dev service for every bucket and mail the stage does not account for — no key in its secrets, no provider in deploy.<kind>.<stage> — so there is nothing to list. Pass --allow-dev-services on its own.`);
|
|
7479
|
+
this.value = value;
|
|
7480
|
+
this.name = "AllowDevServicesTakesNoValue";
|
|
8748
7481
|
}
|
|
8749
|
-
|
|
8750
|
-
return {
|
|
8751
|
-
apps: results,
|
|
8752
|
-
dockerCompose: composePath,
|
|
8753
|
-
dockerignore: dockerignorePath,
|
|
8754
|
-
buildRoot: layout.buildRoot
|
|
8755
|
-
};
|
|
8756
|
-
}
|
|
7482
|
+
};
|
|
8757
7483
|
/**
|
|
8758
|
-
*
|
|
8759
|
-
* a
|
|
8760
|
-
*
|
|
7484
|
+
* Refuses `--allow-dev-services <value>` and `--allow-dev-services=<value>` on
|
|
7485
|
+
* a command line, with the reason, before the parser calls the value an
|
|
7486
|
+
* excess argument. `gkm deploy` and `gkm compose` take no positional
|
|
7487
|
+
* argument, so whatever follows the flag and is not a flag was meant for it.
|
|
7488
|
+
*
|
|
7489
|
+
* @throws {AllowDevServicesTakesNoValue}
|
|
8761
7490
|
*/
|
|
8762
|
-
function
|
|
8763
|
-
|
|
8764
|
-
|
|
8765
|
-
|
|
8766
|
-
|
|
8767
|
-
|
|
8768
|
-
}
|
|
8769
|
-
if (app.entry) return generateEntryDockerfile({
|
|
8770
|
-
...image,
|
|
8771
|
-
entry: app.entry,
|
|
8772
|
-
healthCheckPath: "/health"
|
|
8773
|
-
});
|
|
8774
|
-
return generateBackendDockerfile({
|
|
8775
|
-
...image,
|
|
8776
|
-
healthCheckPath: "/health",
|
|
8777
|
-
...options.cache ? { cache: options.cache } : {}
|
|
8778
|
-
});
|
|
8779
|
-
}
|
|
8780
|
-
/** Where `gkm docker` writes a worker's Dockerfile. */
|
|
8781
|
-
function workerDockerfileOf(worker) {
|
|
8782
|
-
return `.gkm/docker/Dockerfile.${worker}`;
|
|
7491
|
+
function assertDevServicesFlag(argv) {
|
|
7492
|
+
for (let i = 0; i < argv.length; i++) {
|
|
7493
|
+
const arg = argv[i];
|
|
7494
|
+
if (arg.startsWith("--allow-dev-services=")) throw new AllowDevServicesTakesNoValue(arg.slice(arg.indexOf("=") + 1));
|
|
7495
|
+
const next = argv[i + 1];
|
|
7496
|
+
if (arg === "--allow-dev-services" && next && !next.startsWith("-")) throw new AllowDevServicesTakesNoValue(next);
|
|
7497
|
+
}
|
|
8783
7498
|
}
|
|
8784
7499
|
/**
|
|
8785
|
-
* A
|
|
8786
|
-
*
|
|
7500
|
+
* A deployed stage with mail or object storage that its secrets do not
|
|
7501
|
+
* configure — every missing key, across every app, at once.
|
|
8787
7502
|
*/
|
|
8788
|
-
|
|
8789
|
-
|
|
8790
|
-
|
|
8791
|
-
|
|
8792
|
-
|
|
8793
|
-
|
|
8794
|
-
|
|
8795
|
-
|
|
8796
|
-
|
|
8797
|
-
|
|
8798
|
-
|
|
7503
|
+
var ExternalServicesNotConfigured = class extends GkmError {
|
|
7504
|
+
stage;
|
|
7505
|
+
missing;
|
|
7506
|
+
constructor(stage, missing) {
|
|
7507
|
+
const standIns = missing.some((m) => m.service !== void 0);
|
|
7508
|
+
const lines = missing.map((m) => {
|
|
7509
|
+
const by = m.apps?.length ? `, read by ${m.apps.join(", ")}` : "";
|
|
7510
|
+
return ` gkm secrets:set ${m.key} '${m.example}' --stage ${stage}\n ${m.what}${by}` + (m.note ? `\n ${m.note}` : "") + (m.hint ? `\n ${m.hint}` : "");
|
|
7511
|
+
});
|
|
7512
|
+
super(`The stage '${stage}' is deployed, and a deployed stage's mail and object storage are real services: gkm runs no Mailpit or MinIO for it. Set ${missing.length === 1 ? "this key" : `these ${missing.length} keys`} in the stage's secrets:\n\n${lines.join("\n")}\n\n` + (standIns ? "For a stage that is not production — a preview, a demo — the dev services can run instead, with --allow-dev-services. Mailpit delivers no mail, and MinIO keeps every object on one container's disk.\n\n" : "") + `Or run: gkm secrets:add --stage ${stage}`);
|
|
7513
|
+
this.stage = stage;
|
|
7514
|
+
this.missing = missing;
|
|
7515
|
+
this.name = "ExternalServicesNotConfigured";
|
|
7516
|
+
}
|
|
7517
|
+
};
|
|
7518
|
+
/** The kinds a deployed stage takes from its secrets, or a dev service. */
|
|
7519
|
+
const SERVICE_KINDS = {
|
|
7520
|
+
email: "email",
|
|
7521
|
+
objects: "objects",
|
|
7522
|
+
"file-server": "file-server"
|
|
7523
|
+
};
|
|
8799
7524
|
/**
|
|
8800
|
-
*
|
|
7525
|
+
* Every mail and storage construct a manifest declares, each with the apps
|
|
7526
|
+
* and workers that read it — a process's environment, a site's build args. A
|
|
7527
|
+
* file server's reader reads its bucket too.
|
|
8801
7528
|
*
|
|
8802
|
-
* The
|
|
8803
|
-
*
|
|
8804
|
-
*
|
|
8805
|
-
* leaving the bundle with an empty URL.
|
|
7529
|
+
* The one list a deploy's readiness check refuses a stage by, every server
|
|
7530
|
+
* target decides its dev services from, and `gkm secrets:add` offers — so
|
|
7531
|
+
* none of them can disagree.
|
|
8806
7532
|
*/
|
|
8807
|
-
function
|
|
8808
|
-
const
|
|
8809
|
-
const
|
|
8810
|
-
const
|
|
8811
|
-
|
|
8812
|
-
|
|
7533
|
+
function stageServiceDeclarations(input) {
|
|
7534
|
+
const { manifest } = input;
|
|
7535
|
+
const runnables = input.runnables ?? {};
|
|
7536
|
+
const owners = /* @__PURE__ */ new Map();
|
|
7537
|
+
for (const [id, declaration] of Object.entries(manifest)) {
|
|
7538
|
+
if (!SERVICE_KINDS[declaration.kind]) continue;
|
|
7539
|
+
owners.set(provideKey(id, "url"), id);
|
|
7540
|
+
if (declaration.kind === "email") owners.set(provideKey(id, "from"), id);
|
|
7541
|
+
}
|
|
7542
|
+
const readers = /* @__PURE__ */ new Map();
|
|
7543
|
+
const read = (process, keys) => {
|
|
7544
|
+
for (const key of keys) {
|
|
7545
|
+
const id = owners.get(key);
|
|
7546
|
+
if (!id) continue;
|
|
7547
|
+
const declaration = manifest[id];
|
|
7548
|
+
const of = declaration?.kind === "file-server" ? declaration.of : void 0;
|
|
7549
|
+
for (const owner of of ? [id, of] : [id]) {
|
|
7550
|
+
const set = readers.get(owner) ?? /* @__PURE__ */ new Set();
|
|
7551
|
+
set.add(process);
|
|
7552
|
+
readers.set(owner, set);
|
|
7553
|
+
}
|
|
7554
|
+
}
|
|
8813
7555
|
};
|
|
8814
|
-
|
|
8815
|
-
|
|
8816
|
-
|
|
8817
|
-
|
|
8818
|
-
|
|
7556
|
+
for (const [id, declaration] of Object.entries(manifest)) {
|
|
7557
|
+
const name = appKey(id);
|
|
7558
|
+
if (declaration.kind === "rest-api" || declaration.kind === "site" || declaration.kind === "mobile-app") read(name, appEnvKeys(manifest, name, runnables) ?? []);
|
|
7559
|
+
if (declaration.kind === "site") read(name, Object.values(publicEnvFor(declaration, manifest)));
|
|
7560
|
+
if (declaration.kind === "worker") read(name, workerEnvKeys(manifest, id, runnables) ?? []);
|
|
8819
7561
|
}
|
|
7562
|
+
return Object.entries(manifest).flatMap(([id, declaration]) => {
|
|
7563
|
+
const kind = SERVICE_KINDS[declaration.kind];
|
|
7564
|
+
if (!kind) return [];
|
|
7565
|
+
const apps = [...readers.get(id) ?? []].sort();
|
|
7566
|
+
return [{
|
|
7567
|
+
id,
|
|
7568
|
+
kind,
|
|
7569
|
+
...declaration.kind === "file-server" && declaration.of ? { of: declaration.of } : {},
|
|
7570
|
+
...apps.length ? { apps } : {}
|
|
7571
|
+
}];
|
|
7572
|
+
});
|
|
8820
7573
|
}
|
|
8821
7574
|
/**
|
|
8822
|
-
*
|
|
8823
|
-
*
|
|
8824
|
-
*
|
|
8825
|
-
*
|
|
7575
|
+
* Every key a stage must be given rather than derive — the one list both a
|
|
7576
|
+
* deploy's checks and `gkm secrets:add` read, so what a deploy refuses and
|
|
7577
|
+
* what the builder offers cannot drift.
|
|
7578
|
+
*
|
|
7579
|
+
* - Deployed, mail is its URL and its sending address, a bucket its URL, and
|
|
7580
|
+
* a file server its public URL. Locally all of it is Mailpit and MinIO.
|
|
7581
|
+
* - On every stage, a third party's credentials: nobody but the third party
|
|
7582
|
+
* can issue them.
|
|
7583
|
+
*
|
|
7584
|
+
* Never a derived value: a database URL, a generated secret, the seed.
|
|
8826
7585
|
*/
|
|
8827
|
-
|
|
8828
|
-
"
|
|
8829
|
-
|
|
8830
|
-
|
|
8831
|
-
|
|
8832
|
-
|
|
8833
|
-
|
|
8834
|
-
|
|
8835
|
-
|
|
7586
|
+
function requiredStageKeys(input) {
|
|
7587
|
+
const domain = input.domain ?? "example.com";
|
|
7588
|
+
const keys = [];
|
|
7589
|
+
const add = (entry) => {
|
|
7590
|
+
if (!keys.some((k) => k.key === entry.key)) keys.push(entry);
|
|
7591
|
+
};
|
|
7592
|
+
const byId = (list) => [...list ?? []].sort((a, b) => a.id.localeCompare(b.id));
|
|
7593
|
+
const apps = (d) => d.apps?.length ? { apps: [...d.apps].sort() } : {};
|
|
7594
|
+
if (!input.local) {
|
|
7595
|
+
const services = byId(input.services);
|
|
7596
|
+
const objects = input.providers?.objects;
|
|
7597
|
+
const storage = objects?.accounted ? objects.hint ? { hint: objects.hint } : {} : { service: "minio" };
|
|
7598
|
+
for (const d of services.filter((d) => d.kind === "email")) {
|
|
7599
|
+
add({
|
|
7600
|
+
key: provideKey(d.id, "url"),
|
|
7601
|
+
id: d.id,
|
|
7602
|
+
kind: "email",
|
|
7603
|
+
what: `where '${d.id}' sends mail — any SMTP server`,
|
|
7604
|
+
example: `smtp://user:password@smtp.${domain}:587`,
|
|
7605
|
+
service: "mailpit",
|
|
7606
|
+
...apps(d)
|
|
7607
|
+
});
|
|
7608
|
+
add({
|
|
7609
|
+
key: provideKey(d.id, "from"),
|
|
7610
|
+
id: d.id,
|
|
7611
|
+
kind: "email",
|
|
7612
|
+
what: `the address '${d.id}' sends from, on a domain the mail server has verified`,
|
|
7613
|
+
example: `noreply@${domain}`,
|
|
7614
|
+
service: "mailpit",
|
|
7615
|
+
...apps(d)
|
|
7616
|
+
});
|
|
7617
|
+
}
|
|
7618
|
+
for (const d of services.filter((d) => d.kind === "objects")) add({
|
|
7619
|
+
key: provideKey(d.id, "url"),
|
|
7620
|
+
id: d.id,
|
|
7621
|
+
kind: "bucket",
|
|
7622
|
+
what: `the bucket '${d.id}' — S3, R2, or any S3-compatible store (add &endpoint=… for one that is not S3)`,
|
|
7623
|
+
note: `credentials are optional: a key for this bucket alone in the URL (s3://KEY:SECRET@${appKey(d.id)}?…) wins; without one, the shared AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY or a role signs`,
|
|
7624
|
+
example: `s3://${appKey(d.id)}?region=eu-west-1`,
|
|
7625
|
+
...storage,
|
|
7626
|
+
...apps(d)
|
|
7627
|
+
});
|
|
7628
|
+
for (const d of services.filter((d) => d.kind === "file-server")) add({
|
|
7629
|
+
key: provideKey(d.id, "url"),
|
|
7630
|
+
id: d.id,
|
|
7631
|
+
kind: "file-server",
|
|
7632
|
+
what: `the public address '${d.id}' serves its bucket on — a CDN or the bucket's own domain`,
|
|
7633
|
+
example: `https://${appKey(d.id)}.${domain}`,
|
|
7634
|
+
...storage,
|
|
7635
|
+
...d.of ? { of: d.of } : {},
|
|
7636
|
+
...apps(d)
|
|
7637
|
+
});
|
|
7638
|
+
}
|
|
7639
|
+
if (!input.local && input.server) add({
|
|
7640
|
+
key: SERVER_IPV4_KEY,
|
|
7641
|
+
id: "server",
|
|
7642
|
+
kind: "server",
|
|
7643
|
+
what: "the public IPv4 address of the server the stage's stack runs on — its hosts' DNS records point at it, and every deploy checks they do",
|
|
7644
|
+
note: `${SERVER_IPV6_KEY} adds its IPv6 address (AAAA records); neither is ever handed to an app`,
|
|
7645
|
+
example: "203.0.113.10"
|
|
7646
|
+
});
|
|
7647
|
+
for (const d of byId(input.credentials)) add({
|
|
7648
|
+
key: provideKey(d.id, "credentials"),
|
|
7649
|
+
id: d.id,
|
|
7650
|
+
kind: d.kind,
|
|
7651
|
+
what: d.kind === "external-api" ? `the credentials '${d.id}' was issued for this stage, as its schema describes them` : `the credential '${d.id}', as its schema describes it`,
|
|
7652
|
+
example: "{\"apiKey\":\"…\"}",
|
|
7653
|
+
...apps(d)
|
|
7654
|
+
});
|
|
7655
|
+
return keys;
|
|
7656
|
+
}
|
|
8836
7657
|
/**
|
|
8837
|
-
*
|
|
8838
|
-
*
|
|
8839
|
-
*
|
|
7658
|
+
* Whether only the stage's secrets can supply `key` to a construct of `kind`
|
|
7659
|
+
* — what nothing on the stage derives.
|
|
7660
|
+
*
|
|
7661
|
+
* A third party's credentials, always. Deployed, also each generated secret
|
|
7662
|
+
* and keyring: the stage generated each one once, and every run reads the
|
|
7663
|
+
* same value back.
|
|
8840
7664
|
*/
|
|
8841
|
-
|
|
8842
|
-
|
|
8843
|
-
if (
|
|
8844
|
-
|
|
8845
|
-
|
|
8846
|
-
}
|
|
8847
|
-
const current = await readFile(path, "utf-8");
|
|
8848
|
-
const lines = new Set(current.split(/\r?\n/).map((line) => line.trim()));
|
|
8849
|
-
const missing = REQUIRED_IGNORES.filter((line) => !lines.has(line));
|
|
8850
|
-
if (missing.length === 0) return path;
|
|
8851
|
-
await appendFile(path, `${current.endsWith("\n") || current === "" ? "" : "\n"}\n# gkm: images build from source, and a stack's env files hold its secrets\n${missing.join("\n")}\n`);
|
|
8852
|
-
return path;
|
|
7665
|
+
function suppliedOnly(kind, id, key, local) {
|
|
7666
|
+
if (kind === "credential") return true;
|
|
7667
|
+
if (kind === "external-api") return key === provideKey(id, "credentials");
|
|
7668
|
+
if (local) return false;
|
|
7669
|
+
return kind === "secret" || kind === "encryption";
|
|
8853
7670
|
}
|
|
8854
7671
|
/**
|
|
8855
|
-
*
|
|
8856
|
-
* the apps beside the containers their constructs derive.
|
|
7672
|
+
* Where a deployed stage's mail and storage come from, and what it lacks.
|
|
8857
7673
|
*
|
|
8858
|
-
*
|
|
8859
|
-
*
|
|
7674
|
+
* - Mail is external once its URL is set, and then needs its sending
|
|
7675
|
+
* address too. Unset, it is Mailpit where allowed — the address derived
|
|
7676
|
+
* unless set — and missing otherwise.
|
|
7677
|
+
* - A bucket is external once its URL is set; its credentials, in the URL or
|
|
7678
|
+
* the shared pair, are optional. Unset, it is MinIO where allowed, and
|
|
7679
|
+
* missing otherwise.
|
|
7680
|
+
* - A file server's URL is derived where its bucket is MinIO, unless set;
|
|
7681
|
+
* over an external bucket the stage must set it.
|
|
7682
|
+
*
|
|
7683
|
+
* The keys themselves are {@link requiredStageKeys}'s.
|
|
8860
7684
|
*/
|
|
8861
|
-
|
|
8862
|
-
|
|
8863
|
-
|
|
8864
|
-
|
|
7685
|
+
function externalServices(input) {
|
|
7686
|
+
const { supplied, allow } = input;
|
|
7687
|
+
const has = (key) => supplied[key] !== void 0;
|
|
7688
|
+
const required = requiredStageKeys({
|
|
7689
|
+
local: false,
|
|
7690
|
+
services: input.declarations,
|
|
7691
|
+
...input.domain ? { domain: input.domain } : {},
|
|
7692
|
+
...input.providers ? { providers: input.providers } : {}
|
|
8865
7693
|
});
|
|
8866
|
-
|
|
8867
|
-
|
|
8868
|
-
|
|
8869
|
-
|
|
8870
|
-
|
|
8871
|
-
|
|
8872
|
-
|
|
8873
|
-
|
|
8874
|
-
|
|
8875
|
-
|
|
8876
|
-
|
|
8877
|
-
|
|
8878
|
-
|
|
8879
|
-
|
|
8880
|
-
|
|
8881
|
-
|
|
8882
|
-
|
|
8883
|
-
|
|
8884
|
-
}
|
|
8885
|
-
/** A stage that names `false` for a kind, and declares constructs of it. */
|
|
8886
|
-
var StageProviderDisabled = class extends GkmError {
|
|
8887
|
-
kind;
|
|
8888
|
-
stage;
|
|
8889
|
-
ids;
|
|
8890
|
-
constructor(kind, stage, ids) {
|
|
8891
|
-
super(`deploy.${kind}.${stage} is false — the stage has no ${kind} — and ${ids.join(", ")} ${ids.length === 1 ? "is" : "are"} declared. Name what backs ${ids.length === 1 ? "it" : "them"} on '${stage}' (deploy.${kind}.${stage}: 'external', or a provider), or remove the ${ids.length === 1 ? "construct" : "constructs"}.`);
|
|
8892
|
-
this.kind = kind;
|
|
8893
|
-
this.stage = stage;
|
|
8894
|
-
this.ids = ids;
|
|
8895
|
-
this.name = "StageProviderDisabled";
|
|
8896
|
-
}
|
|
8897
|
-
};
|
|
8898
|
-
/** The command a stage's providers are run with. */
|
|
8899
|
-
function provisionCommand(stage) {
|
|
8900
|
-
return `gkm setup --stage ${stage}`;
|
|
7694
|
+
const standIn = (k) => allow && k.service !== void 0 && !has(k.key);
|
|
7695
|
+
const mailpit = required.filter((k) => k.kind === "email" && k.key === provideKey(k.id, "url")).filter(standIn).map((k) => k.id);
|
|
7696
|
+
const minio = required.filter((k) => k.kind === "bucket").filter(standIn).map((k) => k.id);
|
|
7697
|
+
const covered = (k) => k.kind === "email" && mailpit.includes(k.id) || k.kind === "bucket" && minio.includes(k.id) || k.kind === "file-server" && k.of !== void 0 && minio.includes(k.of);
|
|
7698
|
+
return {
|
|
7699
|
+
missing: required.filter((k) => !has(k.key) && !covered(k)).map(({ key, id, what, note, example, service, apps, hint }) => ({
|
|
7700
|
+
key,
|
|
7701
|
+
id,
|
|
7702
|
+
what,
|
|
7703
|
+
...note ? { note } : {},
|
|
7704
|
+
example,
|
|
7705
|
+
...service ? { service } : {},
|
|
7706
|
+
...apps ? { apps } : {},
|
|
7707
|
+
...hint ? { hint } : {}
|
|
7708
|
+
})),
|
|
7709
|
+
minio,
|
|
7710
|
+
mailpit
|
|
7711
|
+
};
|
|
8901
7712
|
}
|
|
8902
|
-
/**
|
|
8903
|
-
|
|
8904
|
-
|
|
8905
|
-
|
|
8906
|
-
|
|
8907
|
-
|
|
8908
|
-
|
|
8909
|
-
if (
|
|
8910
|
-
|
|
8911
|
-
|
|
8912
|
-
|
|
7713
|
+
/** The dev services a run uses, one entry per service it runs. */
|
|
7714
|
+
function devServicesUsed(services) {
|
|
7715
|
+
const used = [];
|
|
7716
|
+
if (services.mailpit.length > 0) used.push({
|
|
7717
|
+
service: "mailpit",
|
|
7718
|
+
ids: services.mailpit
|
|
7719
|
+
});
|
|
7720
|
+
if (services.minio.length > 0) used.push({
|
|
7721
|
+
service: "minio",
|
|
7722
|
+
ids: services.minio
|
|
7723
|
+
});
|
|
7724
|
+
return used;
|
|
8913
7725
|
}
|
|
8914
|
-
/**
|
|
8915
|
-
|
|
8916
|
-
|
|
8917
|
-
* it, and what creates a missing key of it.
|
|
8918
|
-
*/
|
|
8919
|
-
function stageProviderNotes(workspace, stage) {
|
|
8920
|
-
if (stageProvider(workspace, "objects", stage).mode === "external") return {};
|
|
8921
|
-
const hint = provisionHint(workspace, "objects", stage);
|
|
8922
|
-
return { objects: {
|
|
8923
|
-
accounted: true,
|
|
8924
|
-
...hint ? { hint } : {}
|
|
8925
|
-
} };
|
|
7726
|
+
/** The line a run prints for each dev service a deployed stage uses. */
|
|
7727
|
+
function devServiceWarning(stage, use) {
|
|
7728
|
+
return `⚠️ DEV SERVICE ON A DEPLOYED STAGE (${stage}): ${use.service === "mailpit" ? `Mailpit is catching mail for ${use.ids.join(", ")}. It delivers NO mail: nobody receives a sign-in link, a receipt or a reset` : `MinIO is storing ${use.ids.join(", ")}. Every object is on one container's disk, with no replication and no backup`}. It is not production-grade — set the stage's own keys in its secrets for that.`;
|
|
8926
7729
|
}
|
|
8927
7730
|
/**
|
|
8928
|
-
*
|
|
8929
|
-
*
|
|
8930
|
-
*
|
|
8931
|
-
* @throws {StageProviderDisabled}
|
|
7731
|
+
* Report each dev service a run uses: a loud line, and a `dev-service.used`
|
|
7732
|
+
* event a host can act on.
|
|
8932
7733
|
*/
|
|
8933
|
-
function
|
|
8934
|
-
for (const
|
|
8935
|
-
|
|
8936
|
-
|
|
8937
|
-
|
|
7734
|
+
function reportDevServices(ctx, uses) {
|
|
7735
|
+
for (const use of uses) {
|
|
7736
|
+
ctx.logger.warn(`\n${devServiceWarning(ctx.stage, use)}`);
|
|
7737
|
+
ctx.emit({
|
|
7738
|
+
type: "dev-service.used",
|
|
7739
|
+
service: use.service,
|
|
7740
|
+
stage: ctx.stage,
|
|
7741
|
+
constructs: use.ids
|
|
7742
|
+
});
|
|
8938
7743
|
}
|
|
8939
7744
|
}
|
|
8940
7745
|
//#endregion
|
|
8941
|
-
//#region src/deploy/
|
|
8942
|
-
|
|
8943
|
-
|
|
8944
|
-
|
|
8945
|
-
|
|
8946
|
-
|
|
8947
|
-
|
|
8948
|
-
|
|
8949
|
-
|
|
8950
|
-
|
|
8951
|
-
|
|
8952
|
-
|
|
8953
|
-
|
|
8954
|
-
|
|
8955
|
-
|
|
8956
|
-
}
|
|
8957
|
-
static async open(store, stage, initial, adopt = (stored) => stored) {
|
|
8958
|
-
const stored = await store.read(stage);
|
|
8959
|
-
if (stored) return new DeployJournal(store, stage, adopt(stored.state), stored.resources, stored.version, true);
|
|
8960
|
-
const state = initial();
|
|
8961
|
-
const version = await store.write(stage, state, { expectedVersion: null });
|
|
8962
|
-
return new DeployJournal(store, stage, state, {}, version, false);
|
|
8963
|
-
}
|
|
8964
|
-
record(key) {
|
|
8965
|
-
return this.resources[key];
|
|
8966
|
-
}
|
|
8967
|
-
/** The keys a previous run marked `pending` and never saw created. */
|
|
8968
|
-
unfinished() {
|
|
8969
|
-
return Object.values(this.resources).filter((record) => record.status === "pending").map((record) => record.key);
|
|
8970
|
-
}
|
|
8971
|
-
/** Records that `entry` is about to be created. */
|
|
8972
|
-
async pending(entry) {
|
|
8973
|
-
await this.put({
|
|
8974
|
-
...entry,
|
|
8975
|
-
status: "pending"
|
|
8976
|
-
});
|
|
8977
|
-
}
|
|
8978
|
-
/**
|
|
8979
|
-
* Records `entry` as existing with `id`; a no-op if it already is, with
|
|
8980
|
-
* the same data.
|
|
8981
|
-
*/
|
|
8982
|
-
async ready(entry, id) {
|
|
8983
|
-
const current = this.resources[entry.key];
|
|
8984
|
-
if (current?.status === "ready" && current.id === id && JSON.stringify(current.data) === JSON.stringify(entry.data)) return;
|
|
8985
|
-
await this.put({
|
|
8986
|
-
...entry,
|
|
8987
|
-
status: "ready",
|
|
8988
|
-
id
|
|
8989
|
-
});
|
|
8990
|
-
}
|
|
8991
|
-
/**
|
|
8992
|
-
* The resource `entry` names: by its recorded id, else by looking it up,
|
|
8993
|
-
* else created — with `pending` written before the create and `ready`
|
|
8994
|
-
* after, so a run that dies in between leaves a record the next run
|
|
8995
|
-
* resolves by looking the resource up rather than creating another.
|
|
8996
|
-
*/
|
|
8997
|
-
async ensure(entry, steps) {
|
|
8998
|
-
const record = this.resources[entry.key];
|
|
8999
|
-
let staleId;
|
|
9000
|
-
if (record?.status === "ready" && record.id && steps.get) {
|
|
9001
|
-
const known = await steps.get(record.id);
|
|
9002
|
-
if (known) {
|
|
9003
|
-
await this.ready(entry, steps.id(known));
|
|
9004
|
-
return {
|
|
9005
|
-
resource: known,
|
|
9006
|
-
via: "recorded"
|
|
9007
|
-
};
|
|
9008
|
-
}
|
|
9009
|
-
staleId = record.id;
|
|
9010
|
-
}
|
|
9011
|
-
const existing = await steps.find();
|
|
9012
|
-
if (existing) {
|
|
9013
|
-
await this.ready(entry, steps.id(existing));
|
|
9014
|
-
return {
|
|
9015
|
-
resource: existing,
|
|
9016
|
-
via: record?.status === "pending" ? "resumed" : "found",
|
|
9017
|
-
...staleId ? { staleId } : {}
|
|
9018
|
-
};
|
|
9019
|
-
}
|
|
9020
|
-
await this.pending(entry);
|
|
9021
|
-
const created = await steps.create();
|
|
9022
|
-
await this.ready(entry, steps.id(created));
|
|
7746
|
+
//#region src/deploy/actor.ts
|
|
7747
|
+
/**
|
|
7748
|
+
* Who wrote a stage's deploy state: a GitHub Actions run, or a person at a
|
|
7749
|
+
* machine. Recorded on every state write (`updatedBy`), on every resource
|
|
7750
|
+
* record and on every release (`releasedBy`), so "who put this here" has an
|
|
7751
|
+
* answer that does not depend on anyone's memory.
|
|
7752
|
+
*/
|
|
7753
|
+
/** Whether `env` is a CI job — GitHub Actions or any runner that sets `CI`. */
|
|
7754
|
+
function isCi(env = process.env) {
|
|
7755
|
+
return env.GITHUB_ACTIONS === "true" || env.CI === "true";
|
|
7756
|
+
}
|
|
7757
|
+
/** Who this process is writing as. */
|
|
7758
|
+
function currentActor(env = process.env) {
|
|
7759
|
+
if (env.GITHUB_ACTIONS === "true") {
|
|
7760
|
+
const server = env.GITHUB_SERVER_URL ?? "https://github.com";
|
|
9023
7761
|
return {
|
|
9024
|
-
|
|
9025
|
-
|
|
9026
|
-
|
|
9027
|
-
|
|
9028
|
-
}
|
|
9029
|
-
/** Removes `key`'s record — a resource that no longer exists. */
|
|
9030
|
-
async forget(key) {
|
|
9031
|
-
if (!(key in this.resources)) return;
|
|
9032
|
-
this.version = await this.store.deleteResource(this.stage, key, { expectedVersion: this.version });
|
|
9033
|
-
const { [key]: _removed, ...resources } = this.resources;
|
|
9034
|
-
this.resources = resources;
|
|
9035
|
-
}
|
|
9036
|
-
/** Every record, as the journal last wrote or read it. */
|
|
9037
|
-
records() {
|
|
9038
|
-
return this.resources;
|
|
9039
|
-
}
|
|
9040
|
-
/** Writes the stage's state, keeping its resource records. */
|
|
9041
|
-
async save() {
|
|
9042
|
-
this.state.lastDeployedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
9043
|
-
this.version = await this.store.write(this.stage, this.state, { expectedVersion: this.version });
|
|
9044
|
-
}
|
|
9045
|
-
async put(record) {
|
|
9046
|
-
this.version = await this.store.putResource(this.stage, record, { expectedVersion: this.version });
|
|
9047
|
-
this.resources = {
|
|
9048
|
-
...this.resources,
|
|
9049
|
-
[record.key]: {
|
|
9050
|
-
...record,
|
|
9051
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
9052
|
-
}
|
|
7762
|
+
kind: "github",
|
|
7763
|
+
actor: env.GITHUB_ACTOR ?? "unknown",
|
|
7764
|
+
run: `${server}/${env.GITHUB_REPOSITORY ?? "unknown"}/actions/runs/${env.GITHUB_RUN_ID ?? "unknown"}`,
|
|
7765
|
+
...env.GITHUB_WORKFLOW ? { workflow: env.GITHUB_WORKFLOW } : {}
|
|
9053
7766
|
};
|
|
9054
7767
|
}
|
|
9055
|
-
|
|
7768
|
+
let user = "unknown";
|
|
7769
|
+
try {
|
|
7770
|
+
user = userInfo().username;
|
|
7771
|
+
} catch {}
|
|
7772
|
+
return {
|
|
7773
|
+
kind: "local",
|
|
7774
|
+
user,
|
|
7775
|
+
host: hostname()
|
|
7776
|
+
};
|
|
7777
|
+
}
|
|
7778
|
+
/** `octocat (deploy, https://github.com/…/runs/1)` or `ada@laptop`. */
|
|
7779
|
+
function describeActor(actor) {
|
|
7780
|
+
if (!actor) return "unknown";
|
|
7781
|
+
if (actor.kind === "local") return `${actor.user}@${actor.host}`;
|
|
7782
|
+
return `${actor.actor} (${actor.workflow ? `${actor.workflow}, ` : ""}${actor.run})`;
|
|
7783
|
+
}
|
|
9056
7784
|
//#endregion
|
|
9057
|
-
//#region src/
|
|
9058
|
-
/**
|
|
9059
|
-
|
|
9060
|
-
|
|
7785
|
+
//#region src/deploy/dnsResources.ts
|
|
7786
|
+
/** The resource type of a DNS record gkm wrote. */
|
|
7787
|
+
const DNS_RECORD_RESOURCE = "dns-record";
|
|
7788
|
+
/** A record's full name: `api.example.com`, or the domain for `@`. */
|
|
7789
|
+
function dnsRecordFqdn(domain, name) {
|
|
7790
|
+
return name === "@" || name === "" ? domain : `${name}.${domain}`;
|
|
9061
7791
|
}
|
|
9062
|
-
/**
|
|
9063
|
-
|
|
9064
|
-
|
|
9065
|
-
*
|
|
9066
|
-
* @throws {ServerAddressMissing} for a compose stage with a domain and none
|
|
9067
|
-
* @throws {ServerAddressInvalid} for a value that is not an address
|
|
9068
|
-
*/
|
|
9069
|
-
function assertStageServer(workspace, stage, secrets) {
|
|
9070
|
-
if (stage === workspace.stages.local) return void 0;
|
|
9071
|
-
if (!deploysWithCompose(workspace)) return serverAddress(stage, secrets?.custom);
|
|
9072
|
-
return requiredServerAddress(stage, workspace.domains?.[stage], secrets?.custom);
|
|
7792
|
+
/** `dns-record:<fqdn>:<type>` */
|
|
7793
|
+
function dnsResourceKey(fqdn, type) {
|
|
7794
|
+
return `${DNS_RECORD_RESOURCE}:${fqdn.toLowerCase()}:${type.toUpperCase()}`;
|
|
9073
7795
|
}
|
|
9074
|
-
/**
|
|
9075
|
-
|
|
9076
|
-
|
|
9077
|
-
|
|
9078
|
-
|
|
9079
|
-
|
|
9080
|
-
|
|
9081
|
-
|
|
9082
|
-
|
|
9083
|
-
|
|
9084
|
-
|
|
9085
|
-
|
|
9086
|
-
|
|
9087
|
-
|
|
9088
|
-
|
|
9089
|
-
|
|
9090
|
-
|
|
9091
|
-
|
|
9092
|
-
|
|
9093
|
-
|
|
9094
|
-
|
|
9095
|
-
const {
|
|
9096
|
-
|
|
9097
|
-
|
|
9098
|
-
|
|
9099
|
-
|
|
9100
|
-
|
|
9101
|
-
|
|
9102
|
-
|
|
9103
|
-
|
|
9104
|
-
|
|
9105
|
-
tag: "dns",
|
|
9106
|
-
...workspace.deploy?.registry ? { registry: workspace.deploy.registry } : {}
|
|
9107
|
-
},
|
|
9108
|
-
secrets,
|
|
9109
|
-
buildOnly: true,
|
|
9110
|
-
allowDevServices: true
|
|
9111
|
-
}));
|
|
7796
|
+
/** The resource record a written DNS record is kept as. */
|
|
7797
|
+
function dnsRecordResource(record) {
|
|
7798
|
+
const fqdn = dnsRecordFqdn(record.domain, record.name).toLowerCase();
|
|
7799
|
+
const type = record.type.toUpperCase();
|
|
7800
|
+
const data = {
|
|
7801
|
+
domain: record.domain,
|
|
7802
|
+
name: record.name,
|
|
7803
|
+
value: record.value,
|
|
7804
|
+
ttl: record.ttl,
|
|
7805
|
+
...record.provider ? { provider: record.provider } : {}
|
|
7806
|
+
};
|
|
7807
|
+
return {
|
|
7808
|
+
key: dnsResourceKey(fqdn, type),
|
|
7809
|
+
type: DNS_RECORD_RESOURCE,
|
|
7810
|
+
id: `${fqdn} ${type}`,
|
|
7811
|
+
status: "ready",
|
|
7812
|
+
data
|
|
7813
|
+
};
|
|
7814
|
+
}
|
|
7815
|
+
/** Records that gkm wrote `record` — or updates the value it holds. */
|
|
7816
|
+
async function recordDnsResource(journal, record) {
|
|
7817
|
+
const { key, type, id, data } = dnsRecordResource(record);
|
|
7818
|
+
await journal.ready({
|
|
7819
|
+
key,
|
|
7820
|
+
type,
|
|
7821
|
+
...data ? { data } : {}
|
|
7822
|
+
}, id);
|
|
7823
|
+
}
|
|
7824
|
+
/** Forgets a DNS record gkm deleted. */
|
|
7825
|
+
async function forgetDnsResource(journal, record) {
|
|
7826
|
+
await journal.forget(dnsResourceKey(dnsRecordFqdn(record.domain, record.name), record.type));
|
|
9112
7827
|
}
|
|
9113
7828
|
/**
|
|
9114
|
-
*
|
|
9115
|
-
*
|
|
9116
|
-
*
|
|
7829
|
+
* The changes a DNS step made, kept: what it created, updated or wrote is
|
|
7830
|
+
* recorded, what it deleted is forgotten. A record it found already right is
|
|
7831
|
+
* recorded only if gkm wrote it before; one it left to a person is not.
|
|
9117
7832
|
*/
|
|
9118
|
-
async function
|
|
9119
|
-
const
|
|
9120
|
-
|
|
9121
|
-
|
|
9122
|
-
|
|
9123
|
-
hosts: [],
|
|
9124
|
-
changes: []
|
|
9125
|
-
};
|
|
9126
|
-
const server = serverAddress(stage, (await (await secretsStoreFor(workspace, stage, {
|
|
9127
|
-
...options.profile ? { profile: options.profile } : {},
|
|
9128
|
-
...options.home ? { home: options.home } : {}
|
|
9129
|
-
})).read(stage))?.custom);
|
|
9130
|
-
if (!server) {
|
|
9131
|
-
log(`\n🌐 DNS for '${stage}': no GKM_SERVER_IPV4 in its secrets, so no records — ${serverAddressHint(stage)}`);
|
|
9132
|
-
return {
|
|
9133
|
-
mode: "none",
|
|
9134
|
-
hosts: [],
|
|
9135
|
-
changes: []
|
|
9136
|
-
};
|
|
9137
|
-
}
|
|
9138
|
-
const hosts = [...options.hosts ?? await composeStageHosts({
|
|
9139
|
-
workspace,
|
|
9140
|
-
stage,
|
|
9141
|
-
...options.profile ? { profile: options.profile } : {},
|
|
9142
|
-
...options.home ? { home: options.home } : {},
|
|
9143
|
-
...options.manifest ? { manifest: options.manifest } : {},
|
|
9144
|
-
...options.runnables ? { runnables: options.runnables } : {},
|
|
9145
|
-
...options.background ? { background: options.background } : {}
|
|
9146
|
-
})];
|
|
9147
|
-
log(`\n🌐 DNS for '${stage}' → ${server.ipv4}${server.ipv6 ? ` / ${server.ipv6}` : ""}${options.dryRun ? " (dry run — nothing is written)" : ""}`);
|
|
9148
|
-
const plan = planStackDns({
|
|
9149
|
-
stage,
|
|
9150
|
-
hosts,
|
|
9151
|
-
server,
|
|
9152
|
-
dns: workspace.dns
|
|
9153
|
-
});
|
|
9154
|
-
try {
|
|
9155
|
-
const changes = await applyStackDns(plan, {
|
|
9156
|
-
...options.dryRun ? { dryRun: true } : {},
|
|
9157
|
-
log,
|
|
9158
|
-
credentials: {
|
|
9159
|
-
env: options.env ?? process.env,
|
|
9160
|
-
...options.home ? { home: options.home } : {},
|
|
9161
|
-
log
|
|
9162
|
-
},
|
|
9163
|
-
...options.providerFor ? { providerFor: options.providerFor } : {}
|
|
9164
|
-
});
|
|
9165
|
-
if (options.journal && !options.dryRun) {
|
|
9166
|
-
const providers = new Map(plan.domains.map((domain) => [domain.domain, domain.provider]));
|
|
9167
|
-
await recordDnsChanges(options.journal, changes, (domain) => providers.get(domain));
|
|
9168
|
-
}
|
|
9169
|
-
return {
|
|
9170
|
-
mode: "records",
|
|
9171
|
-
hosts,
|
|
9172
|
-
changes
|
|
9173
|
-
};
|
|
9174
|
-
} catch (error) {
|
|
9175
|
-
if (error instanceof MissingCredential) {
|
|
9176
|
-
log(` ⚠ ${error.message}`);
|
|
9177
|
-
log(" Until then, create these records yourself:");
|
|
9178
|
-
for (const domain of plan.domains) for (const r of domain.records) log(` ${r.host.padEnd(32)} ${r.type.padEnd(5)} ${r.value} (TTL ${r.ttl})`);
|
|
9179
|
-
return {
|
|
9180
|
-
mode: "records",
|
|
9181
|
-
hosts,
|
|
9182
|
-
changes: []
|
|
9183
|
-
};
|
|
7833
|
+
async function recordDnsChanges(journal, changes, providerOf) {
|
|
7834
|
+
for (const change of changes) {
|
|
7835
|
+
if (change.action === "delete") {
|
|
7836
|
+
await forgetDnsResource(journal, change);
|
|
7837
|
+
continue;
|
|
9184
7838
|
}
|
|
9185
|
-
|
|
7839
|
+
const written = change.action === "create" || change.action === "update" || change.action === "write";
|
|
7840
|
+
const known = change.action === "unchanged" && journal.record(dnsResourceKey(dnsRecordFqdn(change.domain, change.name), change.type)) !== void 0;
|
|
7841
|
+
if (!written && !known || change.value === void 0) continue;
|
|
7842
|
+
const provider = providerOf(change.domain);
|
|
7843
|
+
await recordDnsResource(journal, {
|
|
7844
|
+
domain: change.domain,
|
|
7845
|
+
name: change.name,
|
|
7846
|
+
type: change.type,
|
|
7847
|
+
value: change.value,
|
|
7848
|
+
ttl: change.ttl ?? 0,
|
|
7849
|
+
...provider ? { provider } : {}
|
|
7850
|
+
});
|
|
9186
7851
|
}
|
|
9187
7852
|
}
|
|
9188
|
-
|
|
9189
|
-
|
|
9190
|
-
|
|
9191
|
-
|
|
9192
|
-
|
|
9193
|
-
*
|
|
9194
|
-
* 1. `--profile`, resolved on its own: exported keys left over from another
|
|
9195
|
-
* account never win over a profile someone named.
|
|
9196
|
-
* 2. `AWS_PROFILE`, else `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY` — the
|
|
9197
|
-
* deploy's own `aws` credential kind (`storedCredentials`).
|
|
9198
|
-
* 3. The SDK's default chain: `~/.aws` default profile, SSO, a role. In CI,
|
|
9199
|
-
* whatever `aws-actions/configure-aws-credentials` exported.
|
|
9200
|
-
*
|
|
9201
|
-
* None of them: the provider has no provisioning credentials, and the stage
|
|
9202
|
-
* is treated as `external`.
|
|
9203
|
-
*/
|
|
9204
|
-
async function awsProvisioningCredentials(lookup) {
|
|
9205
|
-
const region = lookup.env.AWS_REGION ?? lookup.env.AWS_DEFAULT_REGION;
|
|
9206
|
-
const where = region ? { region } : {};
|
|
9207
|
-
if (lookup.profile) return {
|
|
9208
|
-
profile: lookup.profile,
|
|
9209
|
-
...where
|
|
9210
|
-
};
|
|
9211
|
-
const stored = await storedCredentials({
|
|
9212
|
-
env: lookup.env,
|
|
9213
|
-
...lookup.home ? { home: lookup.home } : {}
|
|
9214
|
-
}).get({
|
|
9215
|
-
kind: "aws",
|
|
9216
|
-
stage: lookup.stage
|
|
9217
|
-
});
|
|
9218
|
-
if (stored) return stored;
|
|
9219
|
-
try {
|
|
9220
|
-
const { fromNodeProviderChain } = await import("@aws-sdk/credential-providers");
|
|
9221
|
-
await fromNodeProviderChain({
|
|
9222
|
-
timeout: 1e3,
|
|
9223
|
-
maxRetries: 0
|
|
9224
|
-
})();
|
|
7853
|
+
/** Every DNS record gkm wrote for the stage, from its resource records. */
|
|
7854
|
+
function dnsResources(resources) {
|
|
7855
|
+
return Object.values(resources).filter((record) => record.type === DNS_RECORD_RESOURCE).map((record) => {
|
|
7856
|
+
const data = record.data ?? {};
|
|
7857
|
+
const [fqdn = "", type = ""] = (record.id ?? "").split(" ");
|
|
9225
7858
|
return {
|
|
9226
|
-
|
|
9227
|
-
|
|
7859
|
+
...data,
|
|
7860
|
+
type,
|
|
7861
|
+
fqdn
|
|
9228
7862
|
};
|
|
9229
|
-
}
|
|
9230
|
-
return;
|
|
9231
|
-
}
|
|
7863
|
+
}).sort((a, b) => a.fqdn.localeCompare(b.fqdn) || a.type.localeCompare(b.type));
|
|
9232
7864
|
}
|
|
7865
|
+
//#endregion
|
|
7866
|
+
//#region src/docker/imageRef.ts
|
|
9233
7867
|
/**
|
|
9234
|
-
* An
|
|
9235
|
-
*
|
|
9236
|
-
*
|
|
9237
|
-
*
|
|
7868
|
+
* An image reference, checked before it reaches `docker`.
|
|
7869
|
+
*
|
|
7870
|
+
* The parts come from the workspace — a registry in `gkm.config.ts`, an image
|
|
7871
|
+
* name from the project, a tag from `--tag` — and an argument array already
|
|
7872
|
+
* keeps them out of a shell. What it cannot do is stop `docker` reading a value
|
|
7873
|
+
* that starts with `-` as one of its own flags, so a ref is held to Docker's
|
|
7874
|
+
* own grammar (distribution/reference) and anything else is refused by name
|
|
7875
|
+
* before a build starts.
|
|
9238
7876
|
*/
|
|
9239
|
-
|
|
9240
|
-
|
|
9241
|
-
|
|
9242
|
-
|
|
9243
|
-
|
|
9244
|
-
|
|
9245
|
-
|
|
9246
|
-
|
|
9247
|
-
|
|
9248
|
-
|
|
9249
|
-
};
|
|
7877
|
+
const DOMAIN_COMPONENT = "(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])";
|
|
7878
|
+
const DOMAIN = `${DOMAIN_COMPONENT}(?:\\.${DOMAIN_COMPONENT})*(?::[0-9]+)?`;
|
|
7879
|
+
const PATH_COMPONENT = "[a-z0-9]+(?:(?:[._]|__|-+)[a-z0-9]+)*";
|
|
7880
|
+
const REFERENCE = new RegExp(`^(?:${DOMAIN}/)?${PATH_COMPONENT}(?:/${PATH_COMPONENT})*(?::[\\w][\\w.-]{0,127})?(?:@[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*:[0-9a-fA-F]{32,})?$`);
|
|
7881
|
+
/** Docker refuses a repository name longer than this. */
|
|
7882
|
+
const MAX_NAME_LENGTH = 255;
|
|
7883
|
+
/** A value that is not an image reference was about to be built or pushed. */
|
|
7884
|
+
var ImageRefInvalid = class extends GkmError {
|
|
7885
|
+
ref;
|
|
7886
|
+
constructor(ref) {
|
|
7887
|
+
super(`'${ref}' is not a valid image reference. An image is \`[registry/]name[:tag]\`: the name lowercase letters, digits and . _ - separators, the tag up to 128 of letters, digits, _ . and -, not starting with . or -. Check docker.registry in gkm.config.ts and the --tag passed.`);
|
|
7888
|
+
this.ref = ref;
|
|
7889
|
+
this.name = "ImageRefInvalid";
|
|
9250
7890
|
}
|
|
9251
|
-
|
|
9252
|
-
|
|
9253
|
-
|
|
9254
|
-
|
|
9255
|
-
|
|
9256
|
-
|
|
9257
|
-
}
|
|
9258
|
-
};
|
|
9259
|
-
return { region };
|
|
7891
|
+
};
|
|
7892
|
+
/** Throw {@link ImageRefInvalid} unless `ref` is a Docker image reference. */
|
|
7893
|
+
function validateImageRef(ref) {
|
|
7894
|
+
const name = ref.split("@")[0].replace(/:[\w][\w.-]*$/, "");
|
|
7895
|
+
if (!REFERENCE.test(ref) || name.length > MAX_NAME_LENGTH) throw new ImageRefInvalid(ref);
|
|
7896
|
+
return ref;
|
|
9260
7897
|
}
|
|
9261
|
-
|
|
9262
|
-
|
|
9263
|
-
|
|
9264
|
-
|
|
9265
|
-
|
|
9266
|
-
|
|
9267
|
-
|
|
9268
|
-
|
|
9269
|
-
this.
|
|
9270
|
-
this.name = "
|
|
7898
|
+
//#endregion
|
|
7899
|
+
//#region src/docker/index.ts
|
|
7900
|
+
const logger = output;
|
|
7901
|
+
/** `docker build` failed; what docker printed above says why. */
|
|
7902
|
+
var DockerBuildFailed = class extends Error {
|
|
7903
|
+
imageRef;
|
|
7904
|
+
constructor(imageRef, cause) {
|
|
7905
|
+
super(`Failed to build Docker image: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
7906
|
+
this.imageRef = imageRef;
|
|
7907
|
+
this.name = "DockerBuildFailed";
|
|
9271
7908
|
}
|
|
9272
7909
|
};
|
|
9273
|
-
/**
|
|
9274
|
-
|
|
9275
|
-
|
|
9276
|
-
|
|
9277
|
-
|
|
9278
|
-
|
|
9279
|
-
|
|
9280
|
-
* short stage name does, and every part keeps its start.
|
|
9281
|
-
*/
|
|
9282
|
-
function fitParts(parts, max) {
|
|
9283
|
-
const kept = parts.map(part).filter(Boolean);
|
|
9284
|
-
const length = () => kept.reduce((sum, p) => sum + p.length, 0) + kept.length - 1;
|
|
9285
|
-
while (length() > max) {
|
|
9286
|
-
let longest = 0;
|
|
9287
|
-
for (let i = 1; i < kept.length; i++) if (kept[i].length > kept[longest].length) longest = i;
|
|
9288
|
-
if (kept[longest].length <= 1) break;
|
|
9289
|
-
kept[longest] = kept[longest].slice(0, -1).replace(/-+$/, "");
|
|
9290
|
-
}
|
|
9291
|
-
return kept.join("-");
|
|
9292
|
-
}
|
|
9293
|
-
/** Why `name` is not a valid bucket name, or undefined when it is. */
|
|
9294
|
-
function bucketNameProblem(name) {
|
|
9295
|
-
if (name.length < 3 || name.length > 63) return `it is ${name.length} characters, and S3 takes 3 to 63`;
|
|
9296
|
-
if (!/^[a-z0-9-]+$/.test(name)) return "it may hold only lowercase letters, digits and -";
|
|
9297
|
-
if (!/^[a-z0-9].*[a-z0-9]$/.test(name)) return "it must start and end with a letter or a digit";
|
|
9298
|
-
if (name.startsWith("xn--") || name.startsWith("sthree-")) return "it starts with a prefix S3 reserves ('xn--', 'sthree-')";
|
|
9299
|
-
if (name.endsWith("-s3alias") || name.endsWith("--ol-s3")) return "it ends with a suffix S3 reserves ('-s3alias', '--ol-s3')";
|
|
9300
|
-
}
|
|
9301
|
-
/**
|
|
9302
|
-
* The good name: `<namespace>-<project>-<stage>-<bucket id>`, lowercased, no
|
|
9303
|
-
* dots, shortened to leave room for a clash's suffix.
|
|
9304
|
-
*
|
|
9305
|
-
* @throws {InvalidBucketName} when nothing usable is left
|
|
9306
|
-
*/
|
|
9307
|
-
function bucketName(input) {
|
|
9308
|
-
const name = fitParts([
|
|
9309
|
-
input.scope,
|
|
9310
|
-
input.stage,
|
|
9311
|
-
input.id
|
|
9312
|
-
], 56);
|
|
9313
|
-
const problem = bucketNameProblem(name);
|
|
9314
|
-
if (problem) throw new InvalidBucketName(name, problem);
|
|
9315
|
-
return name;
|
|
9316
|
-
}
|
|
9317
|
-
/** Six random `[a-z0-9]` characters. */
|
|
9318
|
-
function randomSuffix(random = randomInt) {
|
|
9319
|
-
let suffix = "";
|
|
9320
|
-
for (let i = 0; i < 6; i++) suffix += SUFFIX_ALPHABET[random(36)];
|
|
9321
|
-
return suffix;
|
|
9322
|
-
}
|
|
9323
|
-
/** The good name with a clash's suffix. */
|
|
9324
|
-
function suffixedBucketName(good, suffix) {
|
|
9325
|
-
return `${good}-${suffix}`;
|
|
9326
|
-
}
|
|
9327
|
-
/**
|
|
9328
|
-
* The IAM user a bucket is reached with on a stage:
|
|
9329
|
-
* `gkm-<project>-<stage>-<bucket id>`, at most 64 characters.
|
|
9330
|
-
*/
|
|
9331
|
-
function iamUserName(input) {
|
|
9332
|
-
return fitParts([
|
|
9333
|
-
"gkm",
|
|
9334
|
-
input.scope,
|
|
9335
|
-
input.stage,
|
|
9336
|
-
input.id
|
|
9337
|
-
], 64);
|
|
9338
|
-
}
|
|
9339
|
-
/** The path every user gkm creates is under, so it finds its own. */
|
|
9340
|
-
const IAM_USER_PATH = "/gkm/";
|
|
9341
|
-
/** The inline policy each user is given. */
|
|
9342
|
-
const IAM_POLICY_NAME = "gkm-bucket";
|
|
9343
|
-
//#endregion
|
|
9344
|
-
//#region src/providers/s3/origins.ts
|
|
9345
|
-
/**
|
|
9346
|
-
* Which browser origins may call a bucket: the stage's sites that depend on
|
|
9347
|
-
* an API using it.
|
|
9348
|
-
*
|
|
9349
|
-
* The same reading of the graph an API's own CORS origins are — its callers —
|
|
9350
|
-
* taken one step further: a site that calls an API which hands it a presigned
|
|
9351
|
-
* upload URL is a site whose browser PUTs to the bucket. A site that reads the
|
|
9352
|
-
* bucket (or a file server over it) itself is one too. Nothing is written
|
|
9353
|
-
* down: add a site that calls the API and the next provision run allows it.
|
|
9354
|
-
*/
|
|
9355
|
-
/** The keys a reader of this bucket holds: its URL, and each server's over it. */
|
|
9356
|
-
function bucketKeys(manifest, bucket) {
|
|
9357
|
-
return [provideKey(bucket, "url"), ...Object.entries(manifest).filter(([, d]) => d.kind === "file-server" && d.of === bucket).map(([id]) => provideKey(id, "url"))];
|
|
9358
|
-
}
|
|
9359
|
-
function bucketOrigins(input) {
|
|
9360
|
-
const { manifest, workspace, stage } = input;
|
|
9361
|
-
const keys = bucketKeys(manifest, input.bucket);
|
|
9362
|
-
const readers = Object.entries(manifest).filter(([, d]) => d.kind === "rest-api" || d.kind === "site").filter(([id]) => {
|
|
9363
|
-
const env = appEnvKeys(manifest, appKey(id), input.runnables);
|
|
9364
|
-
return env !== void 0 && keys.some((key) => env.has(key));
|
|
9365
|
-
});
|
|
9366
|
-
const sites = /* @__PURE__ */ new Set();
|
|
9367
|
-
for (const [id, declaration] of readers) {
|
|
9368
|
-
if (declaration.kind === "site") {
|
|
9369
|
-
sites.add(id);
|
|
9370
|
-
continue;
|
|
9371
|
-
}
|
|
9372
|
-
for (const caller of dependentsOf(manifest, id)) if (manifest[caller]?.kind === "site") sites.add(caller);
|
|
7910
|
+
/** `docker push` failed — usually the registry login or its permissions. */
|
|
7911
|
+
var DockerPushFailed = class extends Error {
|
|
7912
|
+
imageRef;
|
|
7913
|
+
constructor(imageRef, cause) {
|
|
7914
|
+
super(`Failed to push Docker image: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
7915
|
+
this.imageRef = imageRef;
|
|
7916
|
+
this.name = "DockerPushFailed";
|
|
9373
7917
|
}
|
|
9374
|
-
|
|
9375
|
-
|
|
9376
|
-
|
|
9377
|
-
|
|
9378
|
-
|
|
9379
|
-
|
|
9380
|
-
|
|
9381
|
-
|
|
9382
|
-
}
|
|
9383
|
-
try {
|
|
9384
|
-
const host = resolveHost(name, app, stage, workspace.domains, isMainFrontendApp(name, app, workspace.apps));
|
|
9385
|
-
origins.add(`https://${host}`);
|
|
9386
|
-
} catch (error) {
|
|
9387
|
-
if (!(error instanceof NoDomainForStage)) throw error;
|
|
9388
|
-
unresolved.push(name);
|
|
9389
|
-
}
|
|
7918
|
+
};
|
|
7919
|
+
/** `gkm docker --push` was asked for with nowhere to push to. */
|
|
7920
|
+
var PushNeedsRegistry = class extends GkmError {
|
|
7921
|
+
imageName;
|
|
7922
|
+
constructor(imageName) {
|
|
7923
|
+
super(`Registry is required to push Docker image '${imageName}'. Use --registry or configure docker.registry in gkm.config.ts`);
|
|
7924
|
+
this.imageName = imageName;
|
|
7925
|
+
this.name = "PushNeedsRegistry";
|
|
9390
7926
|
}
|
|
9391
|
-
|
|
9392
|
-
origins: [...origins].sort(),
|
|
9393
|
-
unresolved
|
|
9394
|
-
};
|
|
9395
|
-
}
|
|
9396
|
-
//#endregion
|
|
9397
|
-
//#region src/providers/s3/policy.ts
|
|
9398
|
-
const bucketArn = (bucket) => `arn:aws:s3:::${bucket}`;
|
|
9399
|
-
/**
|
|
9400
|
-
* What the app's key may do: objects in this bucket, and listing it — and
|
|
9401
|
-
* nothing else in the account.
|
|
9402
|
-
*
|
|
9403
|
-
* Multipart uploads are in it because a presigned upload of a large file is
|
|
9404
|
-
* one, and an abandoned one is cleaned up by the app. Reading old versions is
|
|
9405
|
-
* added only when the bucket keeps them.
|
|
9406
|
-
*/
|
|
9407
|
-
function userPolicy(bucket, options = {}) {
|
|
9408
|
-
return {
|
|
9409
|
-
Version: "2012-10-17",
|
|
9410
|
-
Statement: [{
|
|
9411
|
-
Sid: "GkmObjects",
|
|
9412
|
-
Effect: "Allow",
|
|
9413
|
-
Action: [
|
|
9414
|
-
"s3:GetObject",
|
|
9415
|
-
"s3:PutObject",
|
|
9416
|
-
"s3:DeleteObject",
|
|
9417
|
-
"s3:AbortMultipartUpload",
|
|
9418
|
-
"s3:ListMultipartUploadParts",
|
|
9419
|
-
...options.versioning ? ["s3:GetObjectVersion"] : []
|
|
9420
|
-
],
|
|
9421
|
-
Resource: `${bucketArn(bucket)}/*`
|
|
9422
|
-
}, {
|
|
9423
|
-
Sid: "GkmBucket",
|
|
9424
|
-
Effect: "Allow",
|
|
9425
|
-
Action: [
|
|
9426
|
-
"s3:ListBucket",
|
|
9427
|
-
"s3:GetBucketLocation",
|
|
9428
|
-
"s3:ListBucketMultipartUploads"
|
|
9429
|
-
],
|
|
9430
|
-
Resource: bucketArn(bucket)
|
|
9431
|
-
}]
|
|
9432
|
-
};
|
|
9433
|
-
}
|
|
7927
|
+
};
|
|
9434
7928
|
/**
|
|
9435
|
-
*
|
|
9436
|
-
*
|
|
9437
|
-
*
|
|
9438
|
-
*
|
|
7929
|
+
* Docker command implementation: one Dockerfile per deployable app, each
|
|
7930
|
+
* building its image inside Docker from a turbo-pruned slice of the build
|
|
7931
|
+
* root — nothing is built on the host — plus the build root's
|
|
7932
|
+
* `.dockerignore` and `docker-compose.constructs.yml`.
|
|
9439
7933
|
*/
|
|
9440
|
-
function
|
|
9441
|
-
const
|
|
9442
|
-
|
|
7934
|
+
async function dockerCommand(options) {
|
|
7935
|
+
const cwd = options.cwd ?? process.cwd();
|
|
7936
|
+
const loaded = await loadWorkspaceConfig(cwd);
|
|
7937
|
+
const names = loaded.type === "single" ? Object.fromEntries(Object.keys(loaded.workspace.apps).map((name) => [name, resolveDockerConfig(loaded.raw).imageName])) : {};
|
|
7938
|
+
const result = await workspaceDockerCommand(loaded.workspace, loaded.manifest, names, loaded.background, options.stage);
|
|
7939
|
+
if (options.build) for (const app of result.apps) await buildDockerImage(app, result.buildRoot, options);
|
|
7940
|
+
if (options.push) for (const app of result.apps) await pushDockerImage(app.imageName, options, result.buildRoot);
|
|
7941
|
+
return result;
|
|
9443
7942
|
}
|
|
9444
7943
|
/**
|
|
9445
|
-
*
|
|
9446
|
-
*
|
|
9447
|
-
* on exactly those prefixes.
|
|
7944
|
+
* Build one app's image, from the build root.
|
|
7945
|
+
* Uses BuildKit for cache mount support
|
|
9448
7946
|
*/
|
|
9449
|
-
function
|
|
9450
|
-
const
|
|
9451
|
-
|
|
9452
|
-
|
|
9453
|
-
|
|
9454
|
-
|
|
9455
|
-
|
|
9456
|
-
|
|
9457
|
-
|
|
9458
|
-
|
|
9459
|
-
|
|
9460
|
-
|
|
9461
|
-
|
|
9462
|
-
|
|
9463
|
-
|
|
9464
|
-
|
|
9465
|
-
|
|
9466
|
-
|
|
9467
|
-
|
|
9468
|
-
}
|
|
7947
|
+
async function buildDockerImage(app, buildRoot, options) {
|
|
7948
|
+
const tag = options.tag ?? "latest";
|
|
7949
|
+
const registry = options.registry;
|
|
7950
|
+
const fullImageName = validateImageRef(registry ? `${registry}/${app.imageName}:${tag}` : `${app.imageName}:${tag}`);
|
|
7951
|
+
logger.log(`\n🐳 Building Docker image: ${fullImageName}`);
|
|
7952
|
+
try {
|
|
7953
|
+
await run$1("docker", [
|
|
7954
|
+
"build",
|
|
7955
|
+
`--file=${relative(buildRoot, app.dockerfile)}`,
|
|
7956
|
+
`--tag=${fullImageName}`,
|
|
7957
|
+
"."
|
|
7958
|
+
], {
|
|
7959
|
+
cwd: buildRoot,
|
|
7960
|
+
env: {
|
|
7961
|
+
...process.env,
|
|
7962
|
+
DOCKER_BUILDKIT: "1"
|
|
7963
|
+
}
|
|
7964
|
+
});
|
|
7965
|
+
logger.log(`✅ Docker image built: ${fullImageName}`);
|
|
7966
|
+
} catch (error) {
|
|
7967
|
+
throw new DockerBuildFailed(fullImageName, error);
|
|
7968
|
+
}
|
|
9469
7969
|
}
|
|
9470
7970
|
/**
|
|
9471
|
-
*
|
|
9472
|
-
* then a *policy* may make it public (`BlockPublicPolicy`,
|
|
9473
|
-
* `RestrictPublicBuckets` off), and ACLs still may not.
|
|
7971
|
+
* Push Docker image to registry
|
|
9474
7972
|
*/
|
|
9475
|
-
function
|
|
9476
|
-
|
|
9477
|
-
|
|
9478
|
-
|
|
9479
|
-
|
|
9480
|
-
|
|
9481
|
-
|
|
9482
|
-
}
|
|
9483
|
-
|
|
9484
|
-
|
|
9485
|
-
|
|
9486
|
-
|
|
9487
|
-
"HEAD",
|
|
9488
|
-
"POST",
|
|
9489
|
-
"PUT"
|
|
9490
|
-
],
|
|
9491
|
-
AllowedOrigins: [...new Set(origins)].sort(),
|
|
9492
|
-
AllowedHeaders: ["*"],
|
|
9493
|
-
ExposeHeaders: ["ETag"],
|
|
9494
|
-
MaxAgeSeconds: 3e3
|
|
9495
|
-
};
|
|
7973
|
+
async function pushDockerImage(imageName, options, cwd) {
|
|
7974
|
+
const tag = options.tag ?? "latest";
|
|
7975
|
+
const registry = options.registry;
|
|
7976
|
+
if (!registry) throw new PushNeedsRegistry(imageName);
|
|
7977
|
+
const fullImageName = validateImageRef(`${registry}/${imageName}:${tag}`);
|
|
7978
|
+
logger.log(`\n🚀 Pushing Docker image: ${fullImageName}`);
|
|
7979
|
+
try {
|
|
7980
|
+
await run$1("docker", ["push", fullImageName], { cwd });
|
|
7981
|
+
logger.log(`✅ Docker image pushed: ${fullImageName}`);
|
|
7982
|
+
} catch (error) {
|
|
7983
|
+
throw new DockerPushFailed(fullImageName, error);
|
|
7984
|
+
}
|
|
9496
7985
|
}
|
|
9497
7986
|
/**
|
|
9498
|
-
*
|
|
9499
|
-
*
|
|
9500
|
-
* document rather than as drift.
|
|
7987
|
+
* Generate Dockerfiles for all apps in a workspace.
|
|
7988
|
+
* @internal Exported for testing
|
|
9501
7989
|
*/
|
|
9502
|
-
function
|
|
9503
|
-
|
|
9504
|
-
|
|
9505
|
-
|
|
9506
|
-
|
|
9507
|
-
|
|
9508
|
-
|
|
7990
|
+
async function workspaceDockerCommand(workspace, manifest, imageNames = {}, background = {}, stage) {
|
|
7991
|
+
const results = [];
|
|
7992
|
+
const layout = imageLayout(workspace);
|
|
7993
|
+
logger.log(`\n🐳 Generating Dockerfiles for workspace: ${workspace.name}`);
|
|
7994
|
+
logger.log(` Build root: ${layout.buildRoot}`);
|
|
7995
|
+
logger.log(` Package manager: ${layout.tools.packageManager}${layout.tools.packageManagerVersion ? `@${layout.tools.packageManagerVersion}` : ""}, turbo@${layout.tools.turboVersion}`);
|
|
7996
|
+
for (const [appName, app] of Object.entries(workspace.apps)) {
|
|
7997
|
+
if (app.type === "mobile") {
|
|
7998
|
+
logger.log(`\n 📱 Skipping Docker for ${appName} (mobile app — deploy via framework toolchain)`);
|
|
7999
|
+
continue;
|
|
8000
|
+
}
|
|
8001
|
+
const buildType = app.entry ? "entry" : app.type;
|
|
8002
|
+
logger.log(`\n 📄 Generating Dockerfile for ${appName} (${buildType})`);
|
|
8003
|
+
const dockerfile = appDockerfile(appName, app, {
|
|
8004
|
+
layout,
|
|
8005
|
+
workspaceRoot: workspace.root,
|
|
8006
|
+
apps: workspace.apps,
|
|
8007
|
+
...manifest ? { manifest } : {},
|
|
8008
|
+
workspace,
|
|
8009
|
+
...stage ? { stage } : {}
|
|
8010
|
+
});
|
|
8011
|
+
const path = dockerfileOf(appName, app.path);
|
|
8012
|
+
const dockerfilePath = join(workspace.root, path);
|
|
8013
|
+
await mkdir(dirname(dockerfilePath), { recursive: true });
|
|
8014
|
+
await writeFile(dockerfilePath, dockerfile);
|
|
8015
|
+
logger.log(` Generated: ${path}`);
|
|
8016
|
+
results.push({
|
|
8017
|
+
appName,
|
|
8018
|
+
type: app.type,
|
|
8019
|
+
dockerfile: dockerfilePath,
|
|
8020
|
+
imageName: imageNames[appName] ?? appName
|
|
8021
|
+
});
|
|
8022
|
+
}
|
|
8023
|
+
for (const worker of manifest ? workerUnits(workspace, manifest, background) : []) {
|
|
8024
|
+
logger.log(`\n 📄 Generating Dockerfile for ${worker.name} (worker ${worker.id}, built from ${worker.app})`);
|
|
8025
|
+
const dockerfile = workerDockerfile(worker, workspace.apps[worker.app], {
|
|
8026
|
+
layout,
|
|
8027
|
+
workspaceRoot: workspace.root
|
|
8028
|
+
});
|
|
8029
|
+
const path = workerDockerfileOf(worker.name);
|
|
8030
|
+
const dockerfilePath = join(workspace.root, path);
|
|
8031
|
+
await mkdir(dirname(dockerfilePath), { recursive: true });
|
|
8032
|
+
await writeFile(dockerfilePath, dockerfile);
|
|
8033
|
+
logger.log(` Generated: ${path}`);
|
|
8034
|
+
results.push({
|
|
8035
|
+
appName: worker.name,
|
|
8036
|
+
type: "backend",
|
|
8037
|
+
dockerfile: dockerfilePath,
|
|
8038
|
+
imageName: imageNames[worker.name] ?? worker.name
|
|
8039
|
+
});
|
|
9509
8040
|
}
|
|
9510
|
-
|
|
9511
|
-
|
|
9512
|
-
|
|
9513
|
-
}));
|
|
9514
|
-
|
|
9515
|
-
|
|
9516
|
-
|
|
9517
|
-
|
|
9518
|
-
/** The tags gkm finds its own buckets and users by. */
|
|
9519
|
-
const TAG_PROJECT = "gkm:project";
|
|
9520
|
-
const TAG_STAGE = "gkm:stage";
|
|
9521
|
-
const TAG_CONSTRUCT = "gkm:construct";
|
|
9522
|
-
function bucketConstructs(manifest) {
|
|
9523
|
-
return Object.entries(manifest).filter(([, d]) => d.kind === "objects").map(([id, d]) => ({
|
|
9524
|
-
id,
|
|
9525
|
-
versioned: d.versioned === true,
|
|
9526
|
-
servers: Object.entries(manifest).filter(([, s]) => s.kind === "file-server" && s.of === id).map(([sid, s]) => ({
|
|
9527
|
-
id: sid,
|
|
9528
|
-
open: s.open ?? []
|
|
9529
|
-
})).sort((a, b) => a.id.localeCompare(b.id))
|
|
9530
|
-
})).sort((a, b) => a.id.localeCompare(b.id));
|
|
9531
|
-
}
|
|
9532
|
-
/**
|
|
9533
|
-
* The region the stage's buckets are created in: the entry's, the secrets
|
|
9534
|
-
* store's when it is in AWS, the credential's, `AWS_REGION`.
|
|
9535
|
-
*/
|
|
9536
|
-
function s3Region(ctx, env = process.env) {
|
|
9537
|
-
const store = ctx.workspace.secrets?.store;
|
|
9538
|
-
const storeRegion = store && typeof store === "object" && "region" in store ? store.region : void 0;
|
|
9539
|
-
const region = ctx.config.region ?? storeRegion ?? ctx.credential?.region ?? env.AWS_REGION ?? env.AWS_DEFAULT_REGION;
|
|
9540
|
-
if (!region) throw new S3RegionRequired(ctx.stage);
|
|
9541
|
-
return region;
|
|
9542
|
-
}
|
|
9543
|
-
/**
|
|
9544
|
-
* The address a file server over `bucket` serves on: the bucket's regional
|
|
9545
|
-
* virtual-host endpoint, which is what `FileServer.url(key)` appends a key to
|
|
9546
|
-
* — `https://<bucket>.s3.<region>.amazonaws.com/<key>`. Against an emulator,
|
|
9547
|
-
* its path-style address.
|
|
9548
|
-
*/
|
|
9549
|
-
function bucketPublicUrl(bucket, region, endpoint) {
|
|
9550
|
-
if (endpoint) return `${endpoint.replace(/\/+$/, "")}/${bucket}`;
|
|
9551
|
-
return `https://${bucket}.s3.${region}.amazonaws.com`;
|
|
9552
|
-
}
|
|
9553
|
-
/** What an AWS SDK error is called, whichever field carries it. */
|
|
9554
|
-
function errorName(error) {
|
|
9555
|
-
const e = error;
|
|
9556
|
-
return e?.Code ?? e?.name ?? "";
|
|
9557
|
-
}
|
|
9558
|
-
/** A read whose "there is none" answer is one of `missing`. */
|
|
9559
|
-
async function readOr(read, missing) {
|
|
9560
|
-
try {
|
|
9561
|
-
return await read();
|
|
9562
|
-
} catch (error) {
|
|
9563
|
-
if (missing.includes(errorName(error))) return void 0;
|
|
9564
|
-
throw error;
|
|
8041
|
+
const dockerignorePath = await ensureDockerignore(layout.buildRoot);
|
|
8042
|
+
logger.log(`\n Ensured: ${dockerignorePath}`);
|
|
8043
|
+
const composePath = await writeConstructsCompose(workspace);
|
|
8044
|
+
logger.log(`\n✅ Generated ${results.length} Dockerfile(s) + ${COMPOSE_PATH}`);
|
|
8045
|
+
logger.log(`\n📋 Build commands (from ${layout.buildRoot}):`);
|
|
8046
|
+
for (const result of results) {
|
|
8047
|
+
const icon = result.type === "backend" ? "⚙️" : result.type === "mobile" ? "📱" : "🌐";
|
|
8048
|
+
logger.log(` ${icon} docker build -f ${relative(layout.buildRoot, result.dockerfile)} -t ${result.imageName} .`);
|
|
9565
8049
|
}
|
|
9566
|
-
|
|
9567
|
-
const defaultClients = async (credential, region) => {
|
|
9568
|
-
const S3 = await import("@aws-sdk/client-s3");
|
|
9569
|
-
const IAM = await import("@aws-sdk/client-iam");
|
|
9570
|
-
const config = await awsClientConfig(credential, region);
|
|
9571
|
-
const endpoint = awsEndpoint(process.env, "S3");
|
|
8050
|
+
printRunInstructions(workspace);
|
|
9572
8051
|
return {
|
|
9573
|
-
|
|
9574
|
-
|
|
9575
|
-
|
|
9576
|
-
|
|
9577
|
-
...endpoint ? {
|
|
9578
|
-
endpoint,
|
|
9579
|
-
forcePathStyle: true
|
|
9580
|
-
} : {}
|
|
9581
|
-
}),
|
|
9582
|
-
iam: new IAM.IAMClient(config),
|
|
9583
|
-
region,
|
|
9584
|
-
...endpoint ? { endpoint } : {}
|
|
9585
|
-
};
|
|
9586
|
-
};
|
|
9587
|
-
/** For tests: what makes the clients. */
|
|
9588
|
-
let makeClients = defaultClients;
|
|
9589
|
-
async function ensureS3(ctx) {
|
|
9590
|
-
const region = s3Region(ctx);
|
|
9591
|
-
const clients = await makeClients(ctx.credential, region);
|
|
9592
|
-
for (const construct of bucketConstructs(ctx.manifest)) await ensureBucket(ctx, clients, construct);
|
|
9593
|
-
}
|
|
9594
|
-
/** The keys gkm tags a bucket and a user with. */
|
|
9595
|
-
function identityTags(ctx, id) {
|
|
9596
|
-
return [
|
|
9597
|
-
{
|
|
9598
|
-
Key: TAG_PROJECT,
|
|
9599
|
-
Value: ctx.identity.key
|
|
9600
|
-
},
|
|
9601
|
-
{
|
|
9602
|
-
Key: TAG_STAGE,
|
|
9603
|
-
Value: ctx.stage
|
|
9604
|
-
},
|
|
9605
|
-
{
|
|
9606
|
-
Key: TAG_CONSTRUCT,
|
|
9607
|
-
Value: id
|
|
9608
|
-
}
|
|
9609
|
-
];
|
|
9610
|
-
}
|
|
9611
|
-
async function ensureBucket(ctx, clients, construct) {
|
|
9612
|
-
const { id } = construct;
|
|
9613
|
-
const nameInput = {
|
|
9614
|
-
scope: ctx.identity.scope,
|
|
9615
|
-
stage: ctx.stage,
|
|
9616
|
-
id: appKey(id)
|
|
8052
|
+
apps: results,
|
|
8053
|
+
dockerCompose: composePath,
|
|
8054
|
+
dockerignore: dockerignorePath,
|
|
8055
|
+
buildRoot: layout.buildRoot
|
|
9617
8056
|
};
|
|
9618
|
-
const versioning = ctx.config.versioning ?? construct.versioned;
|
|
9619
|
-
const open = [...new Set(construct.servers.flatMap((s) => s.open))].sort();
|
|
9620
|
-
const { bucket, exists } = await ensureBucketName(ctx, clients, id, bucketName(nameInput));
|
|
9621
|
-
await ensureBucketSettings(ctx, clients, {
|
|
9622
|
-
id,
|
|
9623
|
-
bucket,
|
|
9624
|
-
exists,
|
|
9625
|
-
versioning,
|
|
9626
|
-
open
|
|
9627
|
-
});
|
|
9628
|
-
await ensureAccessKey(ctx, clients, {
|
|
9629
|
-
id,
|
|
9630
|
-
bucket,
|
|
9631
|
-
...await ensureUser(ctx, clients, id, iamUserName(nameInput), userPolicy(bucket, { versioning }))
|
|
9632
|
-
});
|
|
9633
|
-
const publicUrl = bucketPublicUrl(bucket, clients.region, clients.endpoint);
|
|
9634
|
-
for (const server of construct.servers) {
|
|
9635
|
-
const key = provideKey(server.id, "url");
|
|
9636
|
-
const entry = {
|
|
9637
|
-
key: `file-server:${server.id}`,
|
|
9638
|
-
type: "url"
|
|
9639
|
-
};
|
|
9640
|
-
const recorded = ctx.state.record(entry.key)?.id;
|
|
9641
|
-
const current = ctx.secrets[key];
|
|
9642
|
-
if (current === publicUrl) {
|
|
9643
|
-
await ctx.state.ready(entry, publicUrl);
|
|
9644
|
-
continue;
|
|
9645
|
-
}
|
|
9646
|
-
if (current !== void 0 && current !== recorded) {
|
|
9647
|
-
ctx.log(` ${key} is set to ${current} by you, and kept — the bucket's own address is ${publicUrl}`);
|
|
9648
|
-
continue;
|
|
9649
|
-
}
|
|
9650
|
-
await ctx.change({
|
|
9651
|
-
construct: server.id,
|
|
9652
|
-
resource: key,
|
|
9653
|
-
change: `set to ${publicUrl}`
|
|
9654
|
-
}, async () => {
|
|
9655
|
-
await ctx.writeSecrets({ [key]: publicUrl });
|
|
9656
|
-
await ctx.state.ready(entry, publicUrl);
|
|
9657
|
-
});
|
|
9658
|
-
}
|
|
9659
8057
|
}
|
|
9660
8058
|
/**
|
|
9661
|
-
*
|
|
9662
|
-
*
|
|
8059
|
+
* One app's Dockerfile, by what it is: a gkm backend bundled by `gkm build`,
|
|
8060
|
+
* a backend with its own entry bundled by esbuild, or a site by its
|
|
8061
|
+
* framework. The one choice every builder of images makes.
|
|
9663
8062
|
*/
|
|
9664
|
-
|
|
9665
|
-
const
|
|
9666
|
-
|
|
9667
|
-
|
|
9668
|
-
|
|
9669
|
-
|
|
9670
|
-
type: "s3-bucket",
|
|
9671
|
-
data: {
|
|
9672
|
-
name,
|
|
9673
|
-
region
|
|
9674
|
-
}
|
|
9675
|
-
});
|
|
9676
|
-
/** Whether this account holds `name`: yes, no, or someone else does. */
|
|
9677
|
-
const probe = async (name) => {
|
|
9678
|
-
try {
|
|
9679
|
-
await s3.send(new S3.HeadBucketCommand({ Bucket: name }));
|
|
9680
|
-
return "ours";
|
|
9681
|
-
} catch (error) {
|
|
9682
|
-
const name = errorName(error);
|
|
9683
|
-
if (name === "NotFound" || name === "NoSuchBucket") return "none";
|
|
9684
|
-
if (name === "Forbidden" || name === "AccessDenied") return "taken";
|
|
9685
|
-
throw error;
|
|
9686
|
-
}
|
|
9687
|
-
};
|
|
9688
|
-
const create = async (name) => {
|
|
9689
|
-
if (ctx.dryRun) {
|
|
9690
|
-
const found = await probe(name);
|
|
9691
|
-
if (found === "none") {
|
|
9692
|
-
await ctx.change({
|
|
9693
|
-
construct: id,
|
|
9694
|
-
resource: `bucket ${name}`,
|
|
9695
|
-
change: `create in ${region}`
|
|
9696
|
-
}, async () => {});
|
|
9697
|
-
return "created";
|
|
9698
|
-
}
|
|
9699
|
-
return found;
|
|
9700
|
-
}
|
|
9701
|
-
await ctx.state.pending(entry(name));
|
|
9702
|
-
try {
|
|
9703
|
-
await s3.send(new S3.CreateBucketCommand({
|
|
9704
|
-
Bucket: name,
|
|
9705
|
-
...region === "us-east-1" ? {} : { CreateBucketConfiguration: { LocationConstraint: region } }
|
|
9706
|
-
}));
|
|
9707
|
-
} catch (error) {
|
|
9708
|
-
const code = errorName(error);
|
|
9709
|
-
if (code === "BucketAlreadyOwnedByYou") return "ours";
|
|
9710
|
-
if (code === "BucketAlreadyExists") return "taken";
|
|
9711
|
-
throw error;
|
|
9712
|
-
}
|
|
9713
|
-
await ctx.change({
|
|
9714
|
-
construct: id,
|
|
9715
|
-
resource: `bucket ${name}`,
|
|
9716
|
-
change: `create in ${region}`
|
|
9717
|
-
}, async () => {});
|
|
9718
|
-
return "created";
|
|
9719
|
-
};
|
|
9720
|
-
const adopt = async (name) => {
|
|
9721
|
-
const owner = (await readOr(() => s3.send(new S3.GetBucketTaggingCommand({ Bucket: name })), ["NoSuchTagSet", "NoSuchTagSetError"]))?.TagSet?.find((t) => t.Key === TAG_PROJECT)?.Value;
|
|
9722
|
-
if (owner && owner !== ctx.identity.key) throw new BucketOwnedByAnotherProject(name, owner, ctx.identity.key);
|
|
9723
|
-
};
|
|
9724
|
-
if (record?.status === "ready" && record.id) {
|
|
9725
|
-
const found = await probe(record.id);
|
|
9726
|
-
if (found === "ours") return {
|
|
9727
|
-
bucket: record.id,
|
|
9728
|
-
exists: true
|
|
9729
|
-
};
|
|
9730
|
-
if (found === "taken") throw new RecordedBucketUnreachable(id, record.id);
|
|
9731
|
-
const made = await create(record.id);
|
|
9732
|
-
if (made === "taken") throw new RecordedBucketUnreachable(id, record.id);
|
|
9733
|
-
if (!ctx.dryRun) await ctx.state.ready(entry(record.id), record.id);
|
|
9734
|
-
return {
|
|
9735
|
-
bucket: record.id,
|
|
9736
|
-
exists: made === "ours"
|
|
9737
|
-
};
|
|
9738
|
-
}
|
|
9739
|
-
const pending = record?.data?.name;
|
|
9740
|
-
const names = [
|
|
9741
|
-
...typeof pending === "string" && pending !== good ? [pending] : [],
|
|
9742
|
-
good,
|
|
9743
|
-
...Array.from({ length: 5 }, () => suffixedBucketName(good, randomSuffix()))
|
|
9744
|
-
];
|
|
9745
|
-
const tried = [];
|
|
9746
|
-
for (const name of names) {
|
|
9747
|
-
tried.push(name);
|
|
9748
|
-
const made = await create(name);
|
|
9749
|
-
if (made === "taken") continue;
|
|
9750
|
-
if (made === "ours") await adopt(name);
|
|
9751
|
-
if (!ctx.dryRun) await ctx.state.ready(entry(name), name);
|
|
9752
|
-
if (name !== good) ctx.log(` '${good}' is taken by another account; '${id}' is '${name}', recorded in the stage's state`);
|
|
9753
|
-
return {
|
|
9754
|
-
bucket: name,
|
|
9755
|
-
exists: made === "ours"
|
|
9756
|
-
};
|
|
9757
|
-
}
|
|
9758
|
-
throw new BucketNameUnavailable(id, tried.filter((n) => n !== pending));
|
|
9759
|
-
}
|
|
9760
|
-
/** Tags, Block Public Access, encryption, policy, versioning and CORS. */
|
|
9761
|
-
async function ensureBucketSettings(ctx, clients, input) {
|
|
9762
|
-
const { S3, s3 } = clients;
|
|
9763
|
-
const { id, bucket } = input;
|
|
9764
|
-
const Bucket = bucket;
|
|
9765
|
-
const fresh = ctx.dryRun && !input.exists;
|
|
9766
|
-
const read = async (fn, missing) => fresh ? void 0 : readOr(fn, missing);
|
|
9767
|
-
const set = (change, apply) => ctx.change({
|
|
9768
|
-
construct: id,
|
|
9769
|
-
resource: `bucket ${bucket}`,
|
|
9770
|
-
change
|
|
9771
|
-
}, () => apply().then(() => void 0));
|
|
9772
|
-
const tagged = await read(() => s3.send(new S3.GetBucketTaggingCommand({ Bucket })), ["NoSuchTagSet", "NoSuchTagSetError"]);
|
|
9773
|
-
const ours = identityTags(ctx, id);
|
|
9774
|
-
const others = (tagged?.TagSet ?? []).filter((t) => !ours.some((o) => o.Key === t.Key));
|
|
9775
|
-
if (ours.some((o) => !tagged?.TagSet?.some((t) => t.Key === o.Key && t.Value === o.Value))) await set("tag", () => s3.send(new S3.PutBucketTaggingCommand({
|
|
9776
|
-
Bucket,
|
|
9777
|
-
Tagging: { TagSet: [...others, ...ours].map((t) => ({
|
|
9778
|
-
Key: t.Key,
|
|
9779
|
-
Value: t.Value
|
|
9780
|
-
})) }
|
|
9781
|
-
})));
|
|
9782
|
-
const isOpen = input.open.length > 0;
|
|
9783
|
-
const block = publicAccessBlock(isOpen);
|
|
9784
|
-
const ensureBlock = async () => {
|
|
9785
|
-
const now = (await read(() => s3.send(new S3.GetPublicAccessBlockCommand({ Bucket })), ["NoSuchPublicAccessBlockConfiguration"]))?.PublicAccessBlockConfiguration;
|
|
9786
|
-
if (!(now && Object.keys(block).every((k) => (now[k] ?? false) === block[k]))) await set(isOpen ? "block public access (ACLs; the policy opens the open paths)" : "block all public access", () => s3.send(new S3.PutPublicAccessBlockCommand({
|
|
9787
|
-
Bucket,
|
|
9788
|
-
PublicAccessBlockConfiguration: block
|
|
9789
|
-
})));
|
|
9790
|
-
};
|
|
9791
|
-
const policy = bucketPolicy(bucket, input.open);
|
|
9792
|
-
const ensurePolicy = async () => {
|
|
9793
|
-
const current = await read(() => s3.send(new S3.GetBucketPolicyCommand({ Bucket })), ["NoSuchBucketPolicy"]);
|
|
9794
|
-
if (!(current?.Policy !== void 0 && canonicalPolicy(JSON.parse(current.Policy)) === canonicalPolicy(policy))) await set(isOpen ? `policy: TLS only, public read on ${input.open.join(", ")}` : "policy: TLS only", () => s3.send(new S3.PutBucketPolicyCommand({
|
|
9795
|
-
Bucket,
|
|
9796
|
-
Policy: JSON.stringify(policy)
|
|
9797
|
-
})));
|
|
9798
|
-
};
|
|
9799
|
-
if (isOpen) {
|
|
9800
|
-
await ensureBlock();
|
|
9801
|
-
await ensurePolicy();
|
|
9802
|
-
} else {
|
|
9803
|
-
await ensurePolicy();
|
|
9804
|
-
await ensureBlock();
|
|
9805
|
-
}
|
|
9806
|
-
if ((await read(() => s3.send(new S3.GetBucketEncryptionCommand({ Bucket })), ["ServerSideEncryptionConfigurationNotFoundError"]))?.ServerSideEncryptionConfiguration?.Rules?.[0]?.ApplyServerSideEncryptionByDefault?.SSEAlgorithm !== "AES256" && !fresh) await set("encrypt by default (SSE-S3)", () => s3.send(new S3.PutBucketEncryptionCommand({
|
|
9807
|
-
Bucket,
|
|
9808
|
-
ServerSideEncryptionConfiguration: { Rules: [{ ApplyServerSideEncryptionByDefault: { SSEAlgorithm: "AES256" } }] }
|
|
9809
|
-
})));
|
|
9810
|
-
const status = (await read(() => s3.send(new S3.GetBucketVersioningCommand({ Bucket })), []))?.Status;
|
|
9811
|
-
if (input.versioning && status !== "Enabled") await set("enable versioning", () => s3.send(new S3.PutBucketVersioningCommand({
|
|
9812
|
-
Bucket,
|
|
9813
|
-
VersioningConfiguration: { Status: "Enabled" }
|
|
9814
|
-
})));
|
|
9815
|
-
else if (!input.versioning && status === "Enabled") await set("suspend versioning", () => s3.send(new S3.PutBucketVersioningCommand({
|
|
9816
|
-
Bucket,
|
|
9817
|
-
VersioningConfiguration: { Status: "Suspended" }
|
|
9818
|
-
})));
|
|
9819
|
-
const { origins, unresolved } = bucketOrigins({
|
|
9820
|
-
workspace: ctx.workspace,
|
|
9821
|
-
manifest: ctx.manifest,
|
|
9822
|
-
runnables: ctx.runnables,
|
|
9823
|
-
stage: ctx.stage,
|
|
9824
|
-
bucket: id
|
|
9825
|
-
});
|
|
9826
|
-
if (unresolved.length > 0) ctx.log(` ${unresolved.join(", ")} ${unresolved.length === 1 ? "has" : "have"} no address on '${ctx.stage}' (domains.${ctx.stage}), so ${unresolved.length === 1 ? "is" : "are"} not in '${bucket}''s CORS`);
|
|
9827
|
-
if (origins.length === 0) return;
|
|
9828
|
-
const rule = corsRule(origins);
|
|
9829
|
-
const cors = await read(() => s3.send(new S3.GetBucketCorsCommand({ Bucket })), ["NoSuchCORSConfiguration"]);
|
|
9830
|
-
if (!(cors?.CORSRules?.length === 1 && canonicalCors(cors.CORSRules[0]) === canonicalCors(rule))) await set(`CORS for ${origins.join(", ")}`, () => s3.send(new S3.PutBucketCorsCommand({
|
|
9831
|
-
Bucket,
|
|
9832
|
-
CORSConfiguration: { CORSRules: [rule] }
|
|
9833
|
-
})));
|
|
9834
|
-
}
|
|
9835
|
-
function canonicalCors(rule) {
|
|
9836
|
-
const sorted = (list) => [...list ?? []].sort();
|
|
9837
|
-
return JSON.stringify({
|
|
9838
|
-
AllowedMethods: sorted(rule.AllowedMethods),
|
|
9839
|
-
AllowedOrigins: sorted(rule.AllowedOrigins),
|
|
9840
|
-
AllowedHeaders: sorted(rule.AllowedHeaders),
|
|
9841
|
-
ExposeHeaders: sorted(rule.ExposeHeaders),
|
|
9842
|
-
MaxAgeSeconds: rule.MaxAgeSeconds ?? null
|
|
8063
|
+
function appDockerfile(appName, app, options) {
|
|
8064
|
+
const image = appImageOptions(options.layout, appName, app, options.workspaceRoot, options.apps, app.type === "web" && options.manifest && options.workspace ? siteClientTelemetry(options.manifest, appName, options.workspace, options.stage) : void 0);
|
|
8065
|
+
if (app.type === "web") return siteDockerfile(appName, app, {
|
|
8066
|
+
image,
|
|
8067
|
+
...options.manifest ? { manifest: options.manifest } : {},
|
|
8068
|
+
...options.publicUrlArgs ? { publicUrlArgs: options.publicUrlArgs } : {}
|
|
9843
8069
|
});
|
|
9844
|
-
|
|
9845
|
-
|
|
9846
|
-
|
|
9847
|
-
|
|
9848
|
-
const entry = {
|
|
9849
|
-
key: `iam-user:${id}`,
|
|
9850
|
-
type: "iam-user"
|
|
9851
|
-
};
|
|
9852
|
-
const found = await readOr(() => iam.send(new IAM.GetUserCommand({ UserName: user })), ["NoSuchEntity", "NoSuchEntityException"]);
|
|
9853
|
-
if (found?.User) {
|
|
9854
|
-
const tags = await iam.send(new IAM.ListUserTagsCommand({ UserName: user }));
|
|
9855
|
-
const tag = (key) => tags.Tags?.find((t) => t.Key === key)?.Value;
|
|
9856
|
-
if (found.User.Path !== "/gkm/" || tag("gkm:project") !== ctx.identity.key || tag("gkm:stage") !== ctx.stage) throw new IamUserNotOwned(user, ctx.identity.key, ctx.stage);
|
|
9857
|
-
await ctx.state.ready(entry, user);
|
|
9858
|
-
} else await ctx.change({
|
|
9859
|
-
construct: id,
|
|
9860
|
-
resource: `IAM user ${user}`,
|
|
9861
|
-
change: `create under ${IAM_USER_PATH}`
|
|
9862
|
-
}, async () => {
|
|
9863
|
-
await ctx.state.pending(entry);
|
|
9864
|
-
await iam.send(new IAM.CreateUserCommand({
|
|
9865
|
-
UserName: user,
|
|
9866
|
-
Path: IAM_USER_PATH,
|
|
9867
|
-
Tags: identityTags(ctx, id)
|
|
9868
|
-
}));
|
|
9869
|
-
await ctx.state.ready(entry, user);
|
|
8070
|
+
if (app.entry) return generateEntryDockerfile({
|
|
8071
|
+
...image,
|
|
8072
|
+
entry: app.entry,
|
|
8073
|
+
healthCheckPath: "/health"
|
|
9870
8074
|
});
|
|
9871
|
-
|
|
9872
|
-
|
|
9873
|
-
|
|
9874
|
-
|
|
9875
|
-
})), ["NoSuchEntity", "NoSuchEntityException"]) : void 0;
|
|
9876
|
-
const document = current?.PolicyDocument ? JSON.parse(decodePolicy(current.PolicyDocument)) : void 0;
|
|
9877
|
-
if (!document || canonicalPolicy(document) !== canonicalPolicy(policy)) await ctx.change({
|
|
9878
|
-
construct: id,
|
|
9879
|
-
resource: `IAM user ${user}`,
|
|
9880
|
-
change: `policy ${IAM_POLICY_NAME}: this bucket only`
|
|
9881
|
-
}, async () => {
|
|
9882
|
-
await iam.send(new IAM.PutUserPolicyCommand({
|
|
9883
|
-
UserName: user,
|
|
9884
|
-
PolicyName: IAM_POLICY_NAME,
|
|
9885
|
-
PolicyDocument: JSON.stringify(policy)
|
|
9886
|
-
}));
|
|
8075
|
+
return generateBackendDockerfile({
|
|
8076
|
+
...image,
|
|
8077
|
+
healthCheckPath: "/health",
|
|
8078
|
+
...options.cache ? { cache: options.cache } : {}
|
|
9887
8079
|
});
|
|
9888
|
-
return {
|
|
9889
|
-
user,
|
|
9890
|
-
exists
|
|
9891
|
-
};
|
|
9892
8080
|
}
|
|
9893
|
-
/**
|
|
9894
|
-
function
|
|
9895
|
-
|
|
9896
|
-
return decodeURIComponent(document);
|
|
9897
|
-
} catch {
|
|
9898
|
-
return document;
|
|
9899
|
-
}
|
|
8081
|
+
/** Where `gkm docker` writes a worker's Dockerfile. */
|
|
8082
|
+
function workerDockerfileOf(worker) {
|
|
8083
|
+
return `.gkm/docker/Dockerfile.${worker}`;
|
|
9900
8084
|
}
|
|
9901
8085
|
/**
|
|
9902
|
-
*
|
|
9903
|
-
*
|
|
9904
|
-
* - First run: one key, written into the stage's secrets.
|
|
9905
|
-
* - `--rotate-keys`: a second key, written in place of the first; the first
|
|
9906
|
-
* stays active until the stage has deployed with the second, and the next
|
|
9907
|
-
* run after that deploy — or `--retire-old-keys` — deletes it.
|
|
9908
|
-
* - A key the secrets no longer hold (a store emptied by hand): replaced the
|
|
9909
|
-
* same way, since its secret cannot be read back.
|
|
8086
|
+
* A Worker's Dockerfile: its host app's build, and the worker's bundle as the
|
|
8087
|
+
* runner — see `generateWorkerDockerfile`.
|
|
9910
8088
|
*/
|
|
9911
|
-
|
|
9912
|
-
|
|
9913
|
-
|
|
9914
|
-
|
|
9915
|
-
|
|
9916
|
-
|
|
9917
|
-
|
|
9918
|
-
|
|
9919
|
-
|
|
9920
|
-
let parsed;
|
|
9921
|
-
try {
|
|
9922
|
-
parsed = s3Url.parse(given);
|
|
9923
|
-
} catch {
|
|
9924
|
-
parsed = void 0;
|
|
9925
|
-
}
|
|
9926
|
-
if (parsed?.bucket !== bucket) throw new BucketKeySetElsewhere(urlKey, bucket, parsed?.bucket ?? given.replace(/\/\/[^@]*@/, "//"), ctx.stage);
|
|
9927
|
-
inUrl = parsed.accessKeyId;
|
|
9928
|
-
}
|
|
9929
|
-
const listed = input.exists ? (await iam.send(new IAM.ListAccessKeysCommand({ UserName: user }))).AccessKeyMetadata?.map((k) => k.AccessKeyId) ?? [] : [];
|
|
9930
|
-
let current = record?.status === "ready" ? record.id : void 0;
|
|
9931
|
-
let previous = typeof record?.data?.previous === "string" ? record.data.previous : void 0;
|
|
9932
|
-
const rotatedAt = typeof record?.data?.rotatedAt === "string" ? record.data.rotatedAt : void 0;
|
|
9933
|
-
const entry = (data = {}) => ({
|
|
9934
|
-
key: entryKey,
|
|
9935
|
-
type: "iam-access-key",
|
|
9936
|
-
data: {
|
|
9937
|
-
user,
|
|
9938
|
-
...data
|
|
9939
|
-
}
|
|
9940
|
-
});
|
|
9941
|
-
if (previous && current) {
|
|
9942
|
-
const deployed = rotatedAt !== void 0 && ctx.state.lastDeployedAt !== void 0 && ctx.state.lastDeployedAt > rotatedAt;
|
|
9943
|
-
if (ctx.retireOldKeys || deployed) {
|
|
9944
|
-
const old = previous;
|
|
9945
|
-
const keep = current;
|
|
9946
|
-
await ctx.change({
|
|
9947
|
-
construct: id,
|
|
9948
|
-
resource: `IAM user ${user}`,
|
|
9949
|
-
change: `delete the rotated-out access key ${old}`
|
|
9950
|
-
}, async () => {
|
|
9951
|
-
if (listed.includes(old)) await iam.send(new IAM.DeleteAccessKeyCommand({
|
|
9952
|
-
UserName: user,
|
|
9953
|
-
AccessKeyId: old
|
|
9954
|
-
}));
|
|
9955
|
-
await ctx.state.ready(entry(), keep);
|
|
9956
|
-
});
|
|
9957
|
-
previous = void 0;
|
|
9958
|
-
} else ctx.log(` ${id}: the old access key ${previous} stays active until '${ctx.stage}' is deployed with the new one; the next gkm setup --stage ${ctx.stage} after that deletes it (or pass --retire-old-keys)`);
|
|
9959
|
-
}
|
|
9960
|
-
if (!current && inUrl && listed.includes(inUrl)) {
|
|
9961
|
-
await ctx.state.ready(entry(), inUrl);
|
|
9962
|
-
current = inUrl;
|
|
9963
|
-
}
|
|
9964
|
-
if (current !== void 0 && listed.includes(current) && inUrl === current && !ctx.rotateKeys) return;
|
|
9965
|
-
if (ctx.rotateKeys && previous) throw new RotationInProgress(id, previous, ctx.stage);
|
|
9966
|
-
const outgoing = current && listed.includes(current) ? current : void 0;
|
|
9967
|
-
if (listed.length >= 2) throw new AccessKeyLimit(user, listed);
|
|
9968
|
-
const why = !current ? "create an access key" : ctx.rotateKeys ? "rotate: issue a new access key" : "issue a new access key (the secrets no longer hold the recorded one)";
|
|
9969
|
-
await ctx.change({
|
|
9970
|
-
construct: id,
|
|
9971
|
-
resource: `IAM user ${user}`,
|
|
9972
|
-
change: why
|
|
9973
|
-
}, async () => {
|
|
9974
|
-
await ctx.state.pending(entry(outgoing ? { previous: outgoing } : {}));
|
|
9975
|
-
const key = (await iam.send(new IAM.CreateAccessKeyCommand({ UserName: user }))).AccessKey;
|
|
9976
|
-
await ctx.writeSecrets({ [urlKey]: s3Url.build({
|
|
9977
|
-
bucket,
|
|
9978
|
-
region: clients.region,
|
|
9979
|
-
...clients.endpoint ? {
|
|
9980
|
-
endpoint: clients.endpoint,
|
|
9981
|
-
forcePathStyle: true
|
|
9982
|
-
} : {},
|
|
9983
|
-
accessKeyId: key.AccessKeyId,
|
|
9984
|
-
secretAccessKey: key.SecretAccessKey
|
|
9985
|
-
}) });
|
|
9986
|
-
await ctx.state.ready(entry(outgoing ? {
|
|
9987
|
-
previous: outgoing,
|
|
9988
|
-
rotatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
9989
|
-
} : {}), key.AccessKeyId);
|
|
8089
|
+
function workerDockerfile(worker, host, options) {
|
|
8090
|
+
return generateWorkerDockerfile({
|
|
8091
|
+
...appImageOptions(options.layout, worker.app, host, options.workspaceRoot),
|
|
8092
|
+
imageName: worker.name,
|
|
8093
|
+
port: WORKER_PORT,
|
|
8094
|
+
worker: worker.id,
|
|
8095
|
+
bundle: workerBundleName(worker.id),
|
|
8096
|
+
healthCheckPath: "/health",
|
|
8097
|
+
...options.cache ? { cache: options.cache } : {}
|
|
9990
8098
|
});
|
|
9991
|
-
if (outgoing) ctx.log(` ${urlKey} holds the new key. Deploy '${ctx.stage}' now; the next gkm setup --stage ${ctx.stage} after that deploy deletes the old key ${outgoing}`);
|
|
9992
|
-
}
|
|
9993
|
-
/**
|
|
9994
|
-
* Each provisioned bucket answers `HeadBucket` to the key in the stage's
|
|
9995
|
-
* secrets — the one the apps will use. A bucket whose URL carries no key is
|
|
9996
|
-
* not one this provider wrote, and the deploy's own check covers a URL that
|
|
9997
|
-
* is missing.
|
|
9998
|
-
*/
|
|
9999
|
-
async function verifyS3(ctx) {
|
|
10000
|
-
const S3 = await import("@aws-sdk/client-s3");
|
|
10001
|
-
for (const { id } of bucketConstructs(ctx.manifest)) {
|
|
10002
|
-
const url = ctx.secrets[provideKey(id, "url")];
|
|
10003
|
-
if (url === void 0) continue;
|
|
10004
|
-
const address = s3Url.parse(url);
|
|
10005
|
-
if (!address.accessKeyId || !address.secretAccessKey) continue;
|
|
10006
|
-
const client = new S3.S3Client({
|
|
10007
|
-
region: address.region ?? ctx.config.region ?? "us-east-1",
|
|
10008
|
-
credentials: {
|
|
10009
|
-
accessKeyId: address.accessKeyId,
|
|
10010
|
-
secretAccessKey: address.secretAccessKey
|
|
10011
|
-
},
|
|
10012
|
-
...address.endpoint ? { endpoint: address.endpoint } : {},
|
|
10013
|
-
...address.forcePathStyle ? { forcePathStyle: true } : {}
|
|
10014
|
-
});
|
|
10015
|
-
try {
|
|
10016
|
-
await client.send(new S3.HeadBucketCommand({ Bucket: address.bucket }));
|
|
10017
|
-
} catch (error) {
|
|
10018
|
-
const name = errorName(error);
|
|
10019
|
-
throw new ProvisionedBucketUnreachable(id, address.bucket, ctx.stage, name === "NotFound" ? "it does not exist" : name === "Forbidden" ? "the key is refused" : name || String(error));
|
|
10020
|
-
} finally {
|
|
10021
|
-
client.destroy();
|
|
10022
|
-
}
|
|
10023
|
-
}
|
|
10024
|
-
}
|
|
10025
|
-
//#endregion
|
|
10026
|
-
//#region src/providers/registry.ts
|
|
10027
|
-
/**
|
|
10028
|
-
* The providers gkm ships, by kind and name — registered the way a deploy
|
|
10029
|
-
* target's built-ins are (`target/builtins.ts`). A provider from a package is
|
|
10030
|
-
* one more entry here once there is a loading story for it.
|
|
10031
|
-
*/
|
|
10032
|
-
const BUILTIN_PROVIDERS = { objects: { s3: {
|
|
10033
|
-
kind: "objects",
|
|
10034
|
-
name: "s3",
|
|
10035
|
-
check: checkS3Config,
|
|
10036
|
-
provisioning: S3_PROVISIONING,
|
|
10037
|
-
credentials: awsProvisioningCredentials,
|
|
10038
|
-
runtimeKeys(manifest) {
|
|
10039
|
-
return bucketConstructs(manifest).flatMap((b) => [provideKey(b.id, "url"), ...b.servers.map((s) => provideKey(s.id, "url"))]);
|
|
10040
|
-
},
|
|
10041
|
-
ensure: ensureS3,
|
|
10042
|
-
verify: verifyS3
|
|
10043
|
-
} } };
|
|
10044
|
-
/** The provider `kind` names `name`, if gkm has one. */
|
|
10045
|
-
function builtinProvider(kind, name) {
|
|
10046
|
-
return Object.hasOwn(BUILTIN_PROVIDERS[kind], name) ? BUILTIN_PROVIDERS[kind][name] : void 0;
|
|
10047
|
-
}
|
|
10048
|
-
//#endregion
|
|
10049
|
-
//#region src/providers/index.ts
|
|
10050
|
-
/** The constructs of a kind: `objects` is every bucket. */
|
|
10051
|
-
function constructsOf(manifest, kind) {
|
|
10052
|
-
return Object.entries(manifest).filter(([, d]) => d.kind === kind).map(([id]) => id).sort();
|
|
10053
8099
|
}
|
|
10054
8100
|
/**
|
|
10055
|
-
*
|
|
10056
|
-
* what it created is still there and the stage's key reaches it. Nothing on
|
|
10057
|
-
* the local stage, and nothing for a kind on `external`.
|
|
8101
|
+
* A site's Dockerfile, with an `ARG` for every key its bundler inlines.
|
|
10058
8102
|
*
|
|
10059
|
-
*
|
|
8103
|
+
* The keys come from the site's declaration where there is one — every
|
|
8104
|
+
* public key its edges give it, `VITE_UPLOADS_SERVER_URL` included — and an
|
|
8105
|
+
* `ARG` the Dockerfile does not declare is a build argument the build drops,
|
|
8106
|
+
* leaving the bundle with an empty URL.
|
|
10060
8107
|
*/
|
|
10061
|
-
|
|
10062
|
-
|
|
10063
|
-
const
|
|
10064
|
-
|
|
10065
|
-
|
|
10066
|
-
|
|
10067
|
-
if (choice.mode !== "provider") continue;
|
|
10068
|
-
const provider = builtinProvider(kind, choice.name);
|
|
10069
|
-
await provider.verify({
|
|
10070
|
-
workspace: input.workspace,
|
|
10071
|
-
manifest: input.manifest,
|
|
10072
|
-
stage: input.stage,
|
|
10073
|
-
config: choice.config,
|
|
10074
|
-
secrets: input.secrets
|
|
10075
|
-
});
|
|
10076
|
-
verified.push(`${kind}: ${provider.name}`);
|
|
10077
|
-
}
|
|
10078
|
-
return verified;
|
|
10079
|
-
}
|
|
10080
|
-
/**
|
|
10081
|
-
* `ensure()` for every provider the stage configures — find or create, fix
|
|
10082
|
-
* drift, record in the stage's state, write the runtime keys into its
|
|
10083
|
-
* secrets. A provider without provisioning credentials leaves the stage as
|
|
10084
|
-
* `external` and says how to supply them.
|
|
10085
|
-
*/
|
|
10086
|
-
async function provisionStage(options) {
|
|
10087
|
-
const { workspace, stage } = options;
|
|
10088
|
-
const dryRun = options.dryRun === true;
|
|
10089
|
-
const env = options.env ?? process.env;
|
|
10090
|
-
const log = options.log ?? ((line) => console.log(line));
|
|
10091
|
-
const named = PROVIDER_KINDS.some((kind) => stageProvider(workspace, kind, stage).mode === "provider");
|
|
10092
|
-
let found = options.manifest;
|
|
10093
|
-
let runnables = options.runnables ?? {};
|
|
10094
|
-
if (!found && named) {
|
|
10095
|
-
runnables = {};
|
|
10096
|
-
found = await discover({
|
|
10097
|
-
patterns: constructGlobs(workspace),
|
|
10098
|
-
cwd: workspace.root,
|
|
10099
|
-
runnables
|
|
10100
|
-
});
|
|
10101
|
-
}
|
|
10102
|
-
const manifest = found ?? {};
|
|
10103
|
-
const reports = [];
|
|
10104
|
-
const configured = [];
|
|
10105
|
-
for (const kind of PROVIDER_KINDS) {
|
|
10106
|
-
const choice = stageProvider(workspace, kind, stage);
|
|
10107
|
-
if (choice.mode !== "provider") {
|
|
10108
|
-
reports.push({
|
|
10109
|
-
kind,
|
|
10110
|
-
mode: choice.mode,
|
|
10111
|
-
actions: []
|
|
10112
|
-
});
|
|
10113
|
-
continue;
|
|
10114
|
-
}
|
|
10115
|
-
const provider = builtinProvider(kind, choice.name);
|
|
10116
|
-
const report = {
|
|
10117
|
-
kind,
|
|
10118
|
-
mode: "provider",
|
|
10119
|
-
provider: provider.name,
|
|
10120
|
-
actions: []
|
|
10121
|
-
};
|
|
10122
|
-
reports.push(report);
|
|
10123
|
-
if (constructsOf(manifest, kind).length === 0) {
|
|
10124
|
-
log(` ${kind}: ${provider.name} — nothing declared to provision`);
|
|
10125
|
-
continue;
|
|
10126
|
-
}
|
|
10127
|
-
const credential = await provider.credentials({
|
|
10128
|
-
stage,
|
|
10129
|
-
env,
|
|
10130
|
-
...options.profile ? { profile: options.profile } : {},
|
|
10131
|
-
...options.home ? { home: options.home } : {}
|
|
10132
|
-
});
|
|
10133
|
-
report.credentials = credential !== void 0;
|
|
10134
|
-
if (credential === void 0) {
|
|
10135
|
-
log(` ${kind}: ${provider.name} — no credentials to provision with, so '${stage}' is external for now: its keys (${provider.runtimeKeys(manifest).join(", ")}) are yours to set with gkm secrets:add --stage ${stage}. To have gkm create them, supply ${provider.provisioning.describe}` + (provider.provisioning.login ? `, or run gkm login --provider ${provider.provisioning.login}` : "") + `, and run ${provisionCommand(stage)}.`);
|
|
10136
|
-
continue;
|
|
10137
|
-
}
|
|
10138
|
-
configured.push({
|
|
10139
|
-
kind,
|
|
10140
|
-
provider,
|
|
10141
|
-
config: choice.config,
|
|
10142
|
-
credential,
|
|
10143
|
-
report
|
|
10144
|
-
});
|
|
10145
|
-
}
|
|
10146
|
-
if (configured.length === 0) return reports;
|
|
10147
|
-
const identity = deployIdentity(workspace, stage);
|
|
10148
|
-
const secretsStore = await secretsStoreFor(workspace, stage, {
|
|
10149
|
-
...options.profile ? { profile: options.profile } : {},
|
|
10150
|
-
...options.home ? { home: options.home } : {}
|
|
10151
|
-
});
|
|
10152
|
-
let secrets = (await secretsStore.read(stage))?.custom ?? {};
|
|
10153
|
-
const stateStore = await createStateStore({
|
|
10154
|
-
config: workspace.state,
|
|
10155
|
-
workspaceRoot: workspace.root,
|
|
10156
|
-
workspaceName: workspace.name
|
|
10157
|
-
});
|
|
10158
|
-
const lock = dryRun ? void 0 : await stateStore.lock(stage, { operation: "provision" });
|
|
10159
|
-
try {
|
|
10160
|
-
const state = dryRun ? await readOnlyState(stateStore, stage) : journalState(await DeployJournal.open(stateStore, stage, () => deploysWithCompose(workspace) ? createComposeState(stage) : createEmptyState(stage, "", "")));
|
|
10161
|
-
const writeSecrets = async (values) => {
|
|
10162
|
-
if (dryRun) return;
|
|
10163
|
-
const stored = await secretsStore.read(stage) ?? initStageSecrets(stage);
|
|
10164
|
-
const next = {
|
|
10165
|
-
...stored,
|
|
10166
|
-
custom: {
|
|
10167
|
-
...stored.custom,
|
|
10168
|
-
...values
|
|
10169
|
-
},
|
|
10170
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10171
|
-
};
|
|
10172
|
-
await secretsStore.write(stage, next);
|
|
10173
|
-
secrets = next.custom;
|
|
10174
|
-
};
|
|
10175
|
-
for (const entry of configured) {
|
|
10176
|
-
log(` ${entry.kind}: ${entry.provider.name}${dryRun ? " (dry run)" : ""}`);
|
|
10177
|
-
await entry.provider.ensure({
|
|
10178
|
-
workspace,
|
|
10179
|
-
manifest,
|
|
10180
|
-
runnables,
|
|
10181
|
-
stage,
|
|
10182
|
-
identity,
|
|
10183
|
-
config: entry.config,
|
|
10184
|
-
credential: entry.credential,
|
|
10185
|
-
get secrets() {
|
|
10186
|
-
return secrets;
|
|
10187
|
-
},
|
|
10188
|
-
state,
|
|
10189
|
-
dryRun,
|
|
10190
|
-
rotateKeys: options.rotateKeys === true,
|
|
10191
|
-
retireOldKeys: options.retireOldKeys === true,
|
|
10192
|
-
async change(action, apply) {
|
|
10193
|
-
if (!dryRun) await apply();
|
|
10194
|
-
entry.report.actions.push(action);
|
|
10195
|
-
log(` ${dryRun ? "would " : ""}${action.change} — ${action.resource}`);
|
|
10196
|
-
},
|
|
10197
|
-
writeSecrets,
|
|
10198
|
-
log
|
|
10199
|
-
});
|
|
10200
|
-
if (entry.report.actions.length === 0) log(" up to date");
|
|
10201
|
-
}
|
|
10202
|
-
} finally {
|
|
10203
|
-
await lock?.release();
|
|
10204
|
-
}
|
|
10205
|
-
return reports;
|
|
10206
|
-
}
|
|
10207
|
-
/** The journal as `ensure()` sees it. */
|
|
10208
|
-
function journalState(journal) {
|
|
10209
|
-
return {
|
|
10210
|
-
record: (key) => journal.record(key),
|
|
10211
|
-
pending: (entry) => journal.pending(entry),
|
|
10212
|
-
ready: (entry, id) => journal.ready(entry, id),
|
|
10213
|
-
lastDeployedAt: journal.existed ? journal.state.lastDeployedAt : void 0
|
|
10214
|
-
};
|
|
10215
|
-
}
|
|
10216
|
-
/** What a dry run reads, and the writes it does not make. */
|
|
10217
|
-
async function readOnlyState(store, stage) {
|
|
10218
|
-
const stored = await store.read(stage);
|
|
10219
|
-
return {
|
|
10220
|
-
record: (key) => stored?.resources[key],
|
|
10221
|
-
pending: async () => {},
|
|
10222
|
-
ready: async () => {},
|
|
10223
|
-
...stored ? { lastDeployedAt: stored.state.lastDeployedAt } : {}
|
|
8108
|
+
function siteDockerfile(appName, app, options) {
|
|
8109
|
+
const declared = options.manifest ? Object.entries(options.manifest).find(([id, d]) => d.kind === "site" && appKey(id) === appName)?.[1] : void 0;
|
|
8110
|
+
const publicUrlArgs = options.publicUrlArgs ?? (declared?.kind === "site" && options.manifest ? Object.keys(publicEnvFor(declared, options.manifest)) : getPublicUrlArgNames(app));
|
|
8111
|
+
const webOpts = {
|
|
8112
|
+
...options.image,
|
|
8113
|
+
publicUrlArgs
|
|
10224
8114
|
};
|
|
10225
|
-
|
|
10226
|
-
|
|
10227
|
-
|
|
10228
|
-
|
|
10229
|
-
|
|
10230
|
-
*
|
|
10231
|
-
* Before constructs, initialising a stage wrote an address for each app into
|
|
10232
|
-
* its custom secrets: `<APP>_DATABASE_URL` as a `postgresql://…@localhost`
|
|
10233
|
-
* URL with a password of its own, and `http://localhost:<port>` for a site or
|
|
10234
|
-
* an auth server. Those keys are now a construct's — `database.schema('AuthDatabase')`
|
|
10235
|
-
* provides `AUTH_DATABASE_URL`, `new BetterAuth('Auth')` provides `AUTH_URL`
|
|
10236
|
-
* — and a deploy derives them. A value the stage holds is a value set by hand,
|
|
10237
|
-
* and set by hand wins: the container is handed `localhost`, where nothing
|
|
10238
|
-
* answers, with a password no role has.
|
|
10239
|
-
*
|
|
10240
|
-
* So a deploy refuses such a stage rather than run it, naming each key and
|
|
10241
|
-
* the command that removes it. Nothing is deleted for the user: the value is
|
|
10242
|
-
* theirs, and only they can say it is stale. A URL that names a real host — a
|
|
10243
|
-
* managed database — is a deliberate choice and still wins.
|
|
10244
|
-
*/
|
|
10245
|
-
/** The kinds whose URL an older gkm wrote into a stage's secrets. */
|
|
10246
|
-
const ADDRESSED = /* @__PURE__ */ new Set([
|
|
10247
|
-
"database",
|
|
10248
|
-
"database-reader",
|
|
10249
|
-
"database-schema",
|
|
10250
|
-
"rest-api",
|
|
10251
|
-
"site"
|
|
10252
|
-
]);
|
|
10253
|
-
/** Hosts that are this machine — never anything, inside a container. */
|
|
10254
|
-
const LOOPBACK = /* @__PURE__ */ new Set([
|
|
10255
|
-
"localhost",
|
|
10256
|
-
"127.0.0.1",
|
|
10257
|
-
"[::1]",
|
|
10258
|
-
"::1"
|
|
10259
|
-
]);
|
|
10260
|
-
/**
|
|
10261
|
-
* A stage holds a construct's address as a custom secret, pointing at
|
|
10262
|
-
* `localhost` — what an older `gkm secrets:init`, `gkm setup` or `gkm init`
|
|
10263
|
-
* generated. Set by hand wins over derived, so the app would be handed it.
|
|
10264
|
-
*/
|
|
10265
|
-
var StaleStageSecrets = class extends GkmError {
|
|
10266
|
-
stage;
|
|
10267
|
-
stale;
|
|
10268
|
-
constructor(stage, stale) {
|
|
10269
|
-
const one = stale.length === 1;
|
|
10270
|
-
super(`The stage '${stage}' holds ${one ? "a value" : "values"} an older gkm generated for ${one ? "an address a construct" : "addresses constructs"} now ${one ? "provides" : "provide"}:\n` + stale.map((s) => ` ${s.key}: ${s.construct}'s, stored pointing at ${s.host}`).join("\n") + `
|
|
10271
|
-
A stored value wins over the derived one, so the app would be handed an address where nothing answers inside its container. Remove ${one ? "it" : "each"}, and the construct's own value is used:\n` + stale.map((s) => ` gkm secrets:unset ${s.key} --stage ${stage}`).join("\n") + "\nA managed service set with its real host is not affected.");
|
|
10272
|
-
this.stage = stage;
|
|
10273
|
-
this.stale = stale;
|
|
10274
|
-
this.name = "StaleStageSecrets";
|
|
10275
|
-
}
|
|
10276
|
-
};
|
|
10277
|
-
/** The host a stored value names, if it is a URL. */
|
|
10278
|
-
function hostOf(value) {
|
|
10279
|
-
try {
|
|
10280
|
-
return new URL(value).hostname;
|
|
10281
|
-
} catch {
|
|
10282
|
-
return;
|
|
8115
|
+
switch (app.framework) {
|
|
8116
|
+
case "vite": return generateViteStaticDockerfile(webOpts);
|
|
8117
|
+
case "tanstack-start":
|
|
8118
|
+
case "remix": return generateNodeWebDockerfile(webOpts);
|
|
8119
|
+
default: return generateNextjsDockerfile(webOpts);
|
|
10283
8120
|
}
|
|
10284
8121
|
}
|
|
10285
8122
|
/**
|
|
10286
|
-
*
|
|
10287
|
-
*
|
|
8123
|
+
* What a build root's `.dockerignore` must leave out of every image's
|
|
8124
|
+
* context: dependencies installed for another platform, anything built on the
|
|
8125
|
+
* host (every image builds from source), git, and gkm's own output —
|
|
8126
|
+
* `.gkm/compose`'s env files hold a stage's secrets.
|
|
10288
8127
|
*/
|
|
10289
|
-
|
|
10290
|
-
|
|
10291
|
-
|
|
10292
|
-
|
|
10293
|
-
|
|
10294
|
-
|
|
10295
|
-
|
|
10296
|
-
|
|
10297
|
-
|
|
10298
|
-
|
|
10299
|
-
|
|
10300
|
-
|
|
10301
|
-
|
|
10302
|
-
|
|
8128
|
+
const REQUIRED_IGNORES = [
|
|
8129
|
+
"**/node_modules",
|
|
8130
|
+
".git",
|
|
8131
|
+
"**/dist",
|
|
8132
|
+
"**/.next",
|
|
8133
|
+
"**/.turbo",
|
|
8134
|
+
"**/.gkm",
|
|
8135
|
+
"**/.gkm/compose"
|
|
8136
|
+
];
|
|
8137
|
+
/**
|
|
8138
|
+
* Make sure the build root's `.dockerignore` leaves out what no image may
|
|
8139
|
+
* hold: written whole where there is none, and each missing line appended to
|
|
8140
|
+
* one the project keeps.
|
|
8141
|
+
*/
|
|
8142
|
+
async function ensureDockerignore(buildRoot) {
|
|
8143
|
+
const path = join(buildRoot, ".dockerignore");
|
|
8144
|
+
if (!existsSync(path)) {
|
|
8145
|
+
await writeFile(path, generateDockerignore());
|
|
8146
|
+
return path;
|
|
10303
8147
|
}
|
|
10304
|
-
|
|
8148
|
+
const current = await readFile(path, "utf-8");
|
|
8149
|
+
const lines = new Set(current.split(/\r?\n/).map((line) => line.trim()));
|
|
8150
|
+
const missing = REQUIRED_IGNORES.filter((line) => !lines.has(line));
|
|
8151
|
+
if (missing.length === 0) return path;
|
|
8152
|
+
await appendFile(path, `${current.endsWith("\n") || current === "" ? "" : "\n"}\n# gkm: images build from source, and a stack's env files hold its secrets\n${missing.join("\n")}\n`);
|
|
8153
|
+
return path;
|
|
10305
8154
|
}
|
|
10306
8155
|
/**
|
|
10307
|
-
*
|
|
10308
|
-
*
|
|
8156
|
+
* Write `docker-compose.constructs.yml` — the same file `gkm dev` writes, with
|
|
8157
|
+
* the apps beside the containers their constructs derive.
|
|
10309
8158
|
*
|
|
10310
|
-
*
|
|
8159
|
+
* Through reconcile rather than a second generator, so what `gkm docker`
|
|
8160
|
+
* describes and what `gkm dev` runs cannot disagree. Nothing is started.
|
|
10311
8161
|
*/
|
|
10312
|
-
function
|
|
10313
|
-
|
|
10314
|
-
|
|
8162
|
+
async function writeConstructsCompose(workspace) {
|
|
8163
|
+
await reconcileWorkspace(workspace, {
|
|
8164
|
+
stage: workspace.stages.local,
|
|
8165
|
+
start: false
|
|
8166
|
+
});
|
|
8167
|
+
logger.log(` Generated: ${COMPOSE_PATH} (project root)`);
|
|
8168
|
+
return join(workspace.root, COMPOSE_PATH);
|
|
8169
|
+
}
|
|
8170
|
+
function printRunInstructions(workspace) {
|
|
8171
|
+
const project = existsSync(join(workspace.root, "docker-compose.yml")) ? " -f docker-compose.yml" : "";
|
|
8172
|
+
logger.log("\n📋 Run everything (the apps sit behind the \"apps\" profile):");
|
|
8173
|
+
logger.log(" gkm setup # starts the containers and creates what they hold");
|
|
8174
|
+
logger.log(` docker compose -f ${COMPOSE_PATH}${project} --profile apps up --build`);
|
|
10315
8175
|
}
|
|
10316
8176
|
//#endregion
|
|
10317
8177
|
//#region src/target/seeds.ts
|
|
@@ -10372,6 +8232,6 @@ function reportPlannedSeeds(planned, log) {
|
|
|
10372
8232
|
for (const { folder, seeds } of planned) log(`🌱 ${folder}: would run ${seeds.length} (${seeds.join(", ")})`);
|
|
10373
8233
|
}
|
|
10374
8234
|
//#endregion
|
|
10375
|
-
export {
|
|
8235
|
+
export { createDnsProvider as $, stackLogs as $n, storageBackendFor as $t, StageProviderDisabled as A, store_exports as An, postgresStatements as At, verifyDnsRecords as B, isOwnedBy as Bn, envFor as Bt, assertDevServicesFlag as C, loadEnvFiles as Cn, LOCAL_CA_PATH as Ct, requiredStageKeys as D, isRemoteStore as Dn, applyPostgres as Dt, reportDevServices as E, prepareEntryCredentials as En, applyPolicies as Et, DeployJournal as F, readKey as Fn, pgClient as Ft, getGeneratedSecret as G, maskPassword as Gn, WORKER_PORT as Gt, createComposeState as H, ownershipMarker as Hn, composeBuildPaths as Ht, extractSubdomain as I, applicationName as In, appEnvKeys as It, setApplicationId as J, withCustomSecret as Jn, imageBuildCredentials as Jt, recordRelease as K, storage_exports as Kn, detectPackageManager as Kt, findRootDomain as L, deployIdentity as Ln, dockerfileOf as Lt, provisionCommand as M, getKeyPath as Mn, LEGACY_LOCAL_LOGIN as Mt, stageProviderNotes as N, keystoreProject as Nn, ensurePostgresLogin as Nt, stageServiceDeclarations as O, secretsStoreFor as On, bucketPolicies as Ot, PROVIDER_KINDS as P, projectKey as Pn, bucketClient as Pt, DnsRecordsUnreadable as Q, logsService as Qn, providerOf as Qt, normalizeDnsConfig as R, imageName as Rn, networkEnv as Rt, STORAGE_KEY_PAIR as S, isPortAvailable as Sn, withRedisPassword as St, externalServices as T, loadSecretsForApp as Tn, applyBuckets as Tt, createEmptyState as U, projectName as Un, imageLayout as Ut, asDokployState as V, markerOf as Vn, localRolePassword as Vt, getBackupState as W, initStageSecrets as Wn, TURBO_VERSION as Wt, setGeneratedSecret as X, LOGS_SERVICE as Xn, cacheBackendFor as Xt, setBackupState as Y, LOCAL_LOGS_EMAIL as Yn, isImageBuild as Yt, setPostgresBackupId as Z, logsAccess as Zn, eventsBackendFor as Zt, isCi as _, portsOf as _n, REDIS_SERVICE as _t, DockerPushFailed as a, resourceName as an, CronGenerator as at, DevServicesNeedServerTarget as b, createEntryWrapper as bn, runsRedis as bt, ensureDockerignore as c, isTelemetryKey as cn, constructGlobs as ct, validateImageRef as d, telemetryOf as dn, workspace_exports as dt, CLUSTER_DATABASE as en, withLogsPassword as er, workerUnits as et, dnsRecordResource as f, usesTelemetry as fn, LOCAL_RABBITMQ_USER as ft, describeActor as g, portKeys as gn, withGeneratedSecrets as gt, currentActor as h, fakeKey as hn, readLocalCredentials as ht, DockerBuildFailed as i, planFor as in, QueueGenerator as it, assertStageProvidersEnabled as j, FileSecretsStore as jn, quoteIdentifier as jt, suppliedOnly as k, storeFromConfig as kn, postgresDatabaseNames as kt, workerDockerfile as l, scopeTelemetryEnv as ln, derivedContainers as lt, recordDnsChanges as m, describeServices as mn, postgresSuperuser as mt, reportDatabaseRuns as n, EMULATOR_ACCESS_KEY_ID as nn, isMainFrontendApp as nr, workerBundleName as nt, appDockerfile as o, hostFor as on, FAKE_ENV as ot, dnsResources as p, DEFAULT_IMAGES as pn, loadLocalCredentials as pt, recordRollback as q, toEmbeddableSecrets as qn, findBuildRoot as qt, reportPlannedSeeds as r, TELEMETRY_CONTAINER as rn, resolveHost as rr, workerEntryDir as rt, dockerCommand as s, appTelemetry as sn, backendsOf as st, DeploySeedsFailed as t, composeFor as tn, NoDomainForStage as tr, WorkerGenerator as tt, workerDockerfileOf as u, surfaceClientTraced as un, reconcileWorkspace as ut, AllowDevServicesTakesNoValue as v, primaryPortKey as vn, STACK_CACHE as vt, devServicesUsed as w, loadPortState as wn, forgetState as wt, ExternalServicesNotConfigured as x, findAvailablePort as xn, stackRedis as xt, DEV_SERVICES as y, createCredentialsPreload as yn, redisService as yt, orchestrateDns as z, imageRef as zn, workerEnvKeys as zt };
|
|
10376
8236
|
|
|
10377
|
-
//# sourceMappingURL=seeds-
|
|
8237
|
+
//# sourceMappingURL=seeds-CoKkrSny.mjs.map
|