@geekmidas/cli 10.0.0-alpha.93 → 10.0.0-alpha.94
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-BRZUWD8z.mjs} +3 -3
- package/dist/{GoDaddyProvider-Mi3vptSR.mjs.map → GoDaddyProvider-BRZUWD8z.mjs.map} +1 -1
- package/dist/{GoDaddyProvider-BZb-V14e.cjs → GoDaddyProvider-DzguxU_a.cjs} +3 -3
- package/dist/{GoDaddyProvider-BZb-V14e.cjs.map → GoDaddyProvider-DzguxU_a.cjs.map} +1 -1
- package/dist/{HostingerProvider-1mqD6aG8.mjs → HostingerProvider-Cjh_2Ml6.mjs} +9 -4
- package/dist/{HostingerProvider-1mqD6aG8.mjs.map → HostingerProvider-Cjh_2Ml6.mjs.map} +1 -1
- package/dist/{HostingerProvider-C6t-PSNe.cjs → HostingerProvider-OdQmKXE3.cjs} +9 -4
- package/dist/HostingerProvider-OdQmKXE3.cjs.map +1 -0
- package/dist/{LegacyStateStore-CtwO7fwy.mjs → LegacyStateStore-BQZb6-j2.mjs} +3 -2
- package/dist/{LegacyStateStore-CtwO7fwy.mjs.map → LegacyStateStore-BQZb6-j2.mjs.map} +1 -1
- package/dist/{LegacyStateStore-BlYXQQSE.cjs → LegacyStateStore-D50dgUPc.cjs} +10 -9
- package/dist/{LegacyStateStore-BlYXQQSE.cjs.map → LegacyStateStore-D50dgUPc.cjs.map} +1 -1
- package/dist/{LocalStateStore-D40_SDmJ.mjs → LocalStateStore-BSaS2535.mjs} +2 -2
- package/dist/{LocalStateStore-D40_SDmJ.mjs.map → LocalStateStore-BSaS2535.mjs.map} +1 -1
- package/dist/{LocalStateStore-oX_zxj9V.cjs → LocalStateStore-EFBUjuMX.cjs} +8 -8
- package/dist/{LocalStateStore-oX_zxj9V.cjs.map → LocalStateStore-EFBUjuMX.cjs.map} +1 -1
- package/dist/{S3StateStore-DeI7jtvi.cjs → S3StateStore-C5mv8To3.cjs} +5 -5
- package/dist/{S3StateStore-DeI7jtvi.cjs.map → S3StateStore-C5mv8To3.cjs.map} +1 -1
- package/dist/{S3StateStore-BzS0IeSt.mjs → S3StateStore-CrFq4UZU.mjs} +2 -2
- package/dist/{S3StateStore-BzS0IeSt.mjs.map → S3StateStore-CrFq4UZU.mjs.map} +1 -1
- package/dist/{SSMStateStore-k_Ew7TWx.mjs → SSMStateStore-D3Z6mZnA.mjs} +2 -2
- package/dist/{SSMStateStore-k_Ew7TWx.mjs.map → SSMStateStore-D3Z6mZnA.mjs.map} +1 -1
- package/dist/{SSMStateStore-CaCtrzFL.cjs → SSMStateStore-DuZCH7_v.cjs} +9 -9
- package/dist/{SSMStateStore-CaCtrzFL.cjs.map → SSMStateStore-DuZCH7_v.cjs.map} +1 -1
- package/dist/{add-CP1podDH.mjs → add-C85isXFl.mjs} +4 -4
- package/dist/add-C85isXFl.mjs.map +1 -0
- package/dist/{add-VcyJnsPX.cjs → add-DsVDdvon.cjs} +4 -4
- package/dist/add-DsVDdvon.cjs.map +1 -0
- package/dist/{bundler-Dfsr6KXU.cjs → bundler-BEgUKc4j.cjs} +5 -5
- package/dist/{bundler-Dfsr6KXU.cjs.map → bundler-BEgUKc4j.cjs.map} +1 -1
- package/dist/{bundler-BWrh-AJH.mjs → bundler-BSHog8CH.mjs} +5 -5
- package/dist/{bundler-BWrh-AJH.mjs.map → bundler-BSHog8CH.mjs.map} +1 -1
- package/dist/{cli-B8otnZfX.mjs → cli-BsFenyaf.mjs} +9 -5
- package/dist/cli-BsFenyaf.mjs.map +1 -0
- package/dist/{cli-DL19w4OE.cjs → cli-BvBdv660.cjs} +9 -5
- package/dist/cli-BvBdv660.cjs.map +1 -0
- package/dist/{compose-BHVKLluX.cjs → compose-Cd09gW-n.cjs} +58 -19
- package/dist/compose-Cd09gW-n.cjs.map +1 -0
- package/dist/{compose-DrEB_4F-.mjs → compose-D2RFYd_f.mjs} +56 -17
- package/dist/compose-D2RFYd_f.mjs.map +1 -0
- package/dist/{config-DCCcrOSo.d.cts → config-DvHNMoOp.d.cts} +4 -4
- package/dist/{config-CHX5GOo3.d.mts.map → config-DvHNMoOp.d.cts.map} +1 -1
- package/dist/{config-CHX5GOo3.d.mts → config-Ngesg4O8.d.mts} +4 -4
- package/dist/{config-DCCcrOSo.d.cts.map → config-Ngesg4O8.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-QBu7VgaQ.cjs → credentials-D7XJypNd.cjs} +9 -3
- package/dist/{credentials-QBu7VgaQ.cjs.map → credentials-D7XJypNd.cjs.map} +1 -1
- package/dist/{credentials-C4KII3PD.mjs → credentials-DpfnUvIR.mjs} +9 -3
- package/dist/{credentials-C4KII3PD.mjs.map → credentials-DpfnUvIR.mjs.map} +1 -1
- package/dist/deploy/public.cjs +13 -13
- 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 +10 -10
- package/dist/{deploy-C3Mhnmus.cjs → deploy-C911Akkn.cjs} +125 -11
- package/dist/deploy-C911Akkn.cjs.map +1 -0
- package/dist/{deploy-DYIyIUYd.mjs → deploy-CeJ5D0xn.mjs} +123 -9
- package/dist/deploy-CeJ5D0xn.mjs.map +1 -0
- package/dist/{discover-vk2r5giK.d.cts → discover-CTzOSGUF.d.cts} +2 -2
- package/dist/{discover-DqwlBVyY.d.mts.map → discover-CTzOSGUF.d.cts.map} +1 -1
- package/dist/{discover-DqwlBVyY.d.mts → discover-CihAtaoj.d.mts} +2 -2
- package/dist/{discover-vk2r5giK.d.cts.map → discover-CihAtaoj.d.mts.map} +1 -1
- package/dist/{dokploy-rivb1RFt.cjs → dokploy-CV7GcobZ.cjs} +2 -2
- package/dist/{dokploy-rivb1RFt.cjs.map → dokploy-CV7GcobZ.cjs.map} +1 -1
- package/dist/{dokploy-o4I6vb9W.mjs → dokploy-D_aat2Xj.mjs} +2 -2
- package/dist/{dokploy-o4I6vb9W.mjs.map → dokploy-D_aat2Xj.mjs.map} +1 -1
- package/dist/{encryption-D7zmwzWz.cjs → encryption-DFcivJaO.cjs} +3 -3
- package/dist/{encryption-D7zmwzWz.cjs.map → encryption-DFcivJaO.cjs.map} +1 -1
- package/dist/{encryption-B6ilrT7p.mjs → encryption-_4qKqtTx.mjs} +3 -3
- package/dist/{encryption-B6ilrT7p.mjs.map → encryption-_4qKqtTx.mjs.map} +1 -1
- package/dist/{engine-Cfx8fEZU.cjs → engine-Bv-LZpmj.cjs} +10 -9
- package/dist/{engine-Cfx8fEZU.cjs.map → engine-Bv-LZpmj.cjs.map} +1 -1
- package/dist/{engine-DjEcovm-.mjs → engine-DPMHThkg.mjs} +7 -6
- package/dist/{engine-DjEcovm-.mjs.map → engine-DPMHThkg.mjs.map} +1 -1
- package/dist/{github-zneTMvnO.cjs → github-Br42NMrW.cjs} +94 -6
- package/dist/github-Br42NMrW.cjs.map +1 -0
- package/dist/{github-CzBbs43W.mjs → github-xsDT19im.mjs} +94 -6
- package/dist/github-xsDT19im.mjs.map +1 -0
- package/dist/{index-BiYU7cUl.d.mts → index-BlpcClwM.d.mts} +2 -2
- package/dist/{index-BiYU7cUl.d.mts.map → index-BlpcClwM.d.mts.map} +1 -1
- package/dist/{index-Dm6V9bwh.d.cts → index-CwLvFqtH.d.cts} +2 -2
- package/dist/{index-Dm6V9bwh.d.cts.map → index-CwLvFqtH.d.cts.map} +1 -1
- package/dist/index.cjs +147 -116
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +143 -112
- package/dist/index.mjs.map +1 -1
- package/dist/{local-Otp74ak0.mjs → local-B7Zv1NoA.mjs} +2 -2
- package/dist/{local-Otp74ak0.mjs.map → local-B7Zv1NoA.mjs.map} +1 -1
- package/dist/{local-B61kUh0w.cjs → local-OPC9BCZt.cjs} +2 -2
- package/dist/{local-B61kUh0w.cjs.map → local-OPC9BCZt.cjs.map} +1 -1
- package/dist/{migrate-BaC4w0sb.mjs → migrate-B7evHv7O.mjs} +3 -3
- package/dist/{migrate-BaC4w0sb.mjs.map → migrate-B7evHv7O.mjs.map} +1 -1
- package/dist/{migrate-C8wTpakT.cjs → migrate-D5jUz20_.cjs} +3 -3
- package/dist/{migrate-C8wTpakT.cjs.map → migrate-D5jUz20_.cjs.map} +1 -1
- package/dist/{migrate-CsBZKALY.mjs → migrate-DhvFPGLi.mjs} +3 -3
- package/dist/{migrate-CsBZKALY.mjs.map → migrate-DhvFPGLi.mjs.map} +1 -1
- package/dist/{migrate-DFjbGnSr.cjs → migrate-SkWJotn5.cjs} +3 -3
- package/dist/{migrate-DFjbGnSr.cjs.map → migrate-SkWJotn5.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/providers-BFeP_mYw.cjs +1098 -0
- package/dist/providers-BFeP_mYw.cjs.map +1 -0
- package/dist/providers-sTj-BG0S.mjs +1079 -0
- package/dist/providers-sTj-BG0S.mjs.map +1 -0
- package/dist/reconcile/public.d.cts +2 -2
- package/dist/reconcile/public.d.mts +2 -2
- package/dist/{resolve-g8htuR80.cjs → resolve-CFEOtRsl.cjs} +4 -4
- package/dist/{resolve-g8htuR80.cjs.map → resolve-CFEOtRsl.cjs.map} +1 -1
- package/dist/{resolve-Ccjlpnd9.mjs → resolve-Dyd1S2wn.mjs} +4 -4
- package/dist/{resolve-Ccjlpnd9.mjs.map → resolve-Dyd1S2wn.mjs.map} +1 -1
- package/dist/{rollback--ympG9ss.cjs → rollback-D720SFhV.cjs} +11 -9
- package/dist/{rollback--ympG9ss.cjs.map → rollback-D720SFhV.cjs.map} +1 -1
- package/dist/{rollback-Drbg0OeR.mjs → rollback-NroNbGC0.mjs} +8 -6
- package/dist/{rollback-Drbg0OeR.mjs.map → rollback-NroNbGC0.mjs.map} +1 -1
- package/dist/{runtime-_wEVtshH.d.mts → runtime-BRCq6dQ7.d.mts} +2 -2
- package/dist/{runtime-D_J3ExJn.d.cts.map → runtime-BRCq6dQ7.d.mts.map} +1 -1
- package/dist/{runtime-D_J3ExJn.d.cts → runtime-VktAo_8B.d.cts} +2 -2
- package/dist/{runtime-_wEVtshH.d.mts.map → runtime-VktAo_8B.d.cts.map} +1 -1
- package/dist/{sandbox-CPJq1Kia.d.mts → sandbox-D1gd0iDz.d.cts} +2 -2
- package/dist/{sandbox-CPJq1Kia.d.mts.map → sandbox-D1gd0iDz.d.cts.map} +1 -1
- package/dist/{sandbox-JwuPAl7I.d.cts → sandbox-zHnRLCj0.d.mts} +2 -2
- package/dist/{sandbox-JwuPAl7I.d.cts.map → sandbox-zHnRLCj0.d.mts.map} +1 -1
- package/dist/{secrets-DbPmAS4k.mjs → secrets-CwLauUB_.mjs} +3 -3
- package/dist/secrets-CwLauUB_.mjs.map +1 -0
- package/dist/{secrets-DJteijZo.cjs → secrets-DAvi_O9g.cjs} +3 -3
- package/dist/secrets-DAvi_O9g.cjs.map +1 -0
- package/dist/{seeds-c-ByX9je.cjs → seeds-BCg8X36S.cjs} +1143 -2645
- package/dist/seeds-BCg8X36S.cjs.map +1 -0
- package/dist/{seeds-BSL55F6q.mjs → seeds-CTxTZa-R.mjs} +1289 -2760
- package/dist/seeds-CTxTZa-R.mjs.map +1 -0
- package/dist/{sniffer-Dq5BS98f.cjs → sniffer-C0z3NB2A.cjs} +572 -3
- package/dist/sniffer-C0z3NB2A.cjs.map +1 -0
- package/dist/{sniffer-BTtGAtm3.mjs → sniffer-DCcuzHz_.mjs} +496 -5
- package/dist/sniffer-DCcuzHz_.mjs.map +1 -0
- package/dist/{sst-CnKY-CSh.cjs → sst-DKNH243z.cjs} +3 -3
- package/dist/{sst-CnKY-CSh.cjs.map → sst-DKNH243z.cjs.map} +1 -1
- package/dist/{sst-BaRXp2Dd.mjs → sst-DRDSDwOk.mjs} +3 -3
- package/dist/{sst-BaRXp2Dd.mjs.map → sst-DRDSDwOk.mjs.map} +1 -1
- package/dist/{stack-1PeLJS2j.cjs → stack-B3fPDTBg.cjs} +3 -3
- package/dist/{stack-1PeLJS2j.cjs.map → stack-B3fPDTBg.cjs.map} +1 -1
- package/dist/{stack-Co4uZkAo.mjs → stack-NrdH5ngX.mjs} +3 -3
- package/dist/{stack-Co4uZkAo.mjs.map → stack-NrdH5ngX.mjs.map} +1 -1
- package/dist/{stages-BI8JvPzI.cjs → stages-CkqP1N4S.cjs} +16 -3
- package/dist/stages-CkqP1N4S.cjs.map +1 -0
- package/dist/{stages-DY-fQKmo.mjs → stages-veC4TcQe.mjs} +16 -3
- package/dist/stages-veC4TcQe.mjs.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-CkUYuAPd.cjs} +2 -2
- package/dist/{terminal-8XngdSTo.cjs.map → terminal-CkUYuAPd.cjs.map} +1 -1
- package/dist/{terminal-C4XRdwnU.mjs → terminal-DAHs2Zko.mjs} +2 -2
- package/dist/{terminal-C4XRdwnU.mjs.map → terminal-DAHs2Zko.mjs.map} +1 -1
- package/dist/{types-BibpiHKB.d.cts → types-DMoaFrjg.d.mts} +32 -9
- package/dist/types-DMoaFrjg.d.mts.map +1 -0
- package/dist/{types-B6lmD3en.d.mts → types-DXyaSmqu.d.cts} +33 -8
- package/dist/types-DXyaSmqu.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/deploy-C3Mhnmus.cjs.map +0 -1
- package/dist/deploy-DYIyIUYd.mjs.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/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
|
const require_rolldown_runtime = require("./rolldown-runtime-C0BPl7ul.cjs");
|
|
2
2
|
const require_errors = require("./errors-DMC4Umsb.cjs");
|
|
3
|
-
const require_credentials = require("./credentials-
|
|
3
|
+
const require_credentials = require("./credentials-D7XJypNd.cjs");
|
|
4
4
|
const require_discover = require("./discover-DPgu-ScS.cjs");
|
|
5
5
|
const require_derive = require("./derive-DAbo49sm.cjs");
|
|
6
|
-
const require_workspace = require("./workspace-
|
|
6
|
+
const require_workspace = require("./workspace-cSyi5nb1.cjs");
|
|
7
7
|
const require_config = require("./config.cjs");
|
|
8
8
|
const require_EndpointGenerator = require("./EndpointGenerator-De03t1hd.cjs");
|
|
9
9
|
const require_SubscriberGenerator = require("./SubscriberGenerator-Dvz7EzqK.cjs");
|
|
10
|
-
const require_resolve = require("./resolve-
|
|
10
|
+
const require_resolve = require("./resolve-CFEOtRsl.cjs");
|
|
11
11
|
let node_path = require("node:path");
|
|
12
12
|
let node_fs = require("node:fs");
|
|
13
13
|
let node_fs_promises = require("node:fs/promises");
|
|
@@ -33,8 +33,6 @@ let _aws_sdk_client_sqs = require("@aws-sdk/client-sqs");
|
|
|
33
33
|
let pg = require("pg");
|
|
34
34
|
let _geekmidas_cache_postgres = require("@geekmidas/cache/postgres");
|
|
35
35
|
let node_dns_promises = require("node:dns/promises");
|
|
36
|
-
let _geekmidas_storage_s3_url = require("@geekmidas/storage/s3-url");
|
|
37
|
-
_geekmidas_storage_s3_url = require_rolldown_runtime.__toESM(_geekmidas_storage_s3_url, 1);
|
|
38
36
|
//#region src/workspace/rootSite.ts
|
|
39
37
|
/**
|
|
40
38
|
* Which site the base domain points at — deployed and locally alike.
|
|
@@ -4927,7 +4925,7 @@ function bucketPolicies(plan) {
|
|
|
4927
4925
|
* client, can be admitted by the policy where the client would have said no.
|
|
4928
4926
|
* Prefer `**` where crossing segments is what you meant.
|
|
4929
4927
|
*/
|
|
4930
|
-
function bucketPolicy
|
|
4928
|
+
function bucketPolicy(bucket, open) {
|
|
4931
4929
|
return JSON.stringify({
|
|
4932
4930
|
Version: "2012-10-17",
|
|
4933
4931
|
Statement: [{
|
|
@@ -5009,7 +5007,7 @@ async function databasesExist(client, names) {
|
|
|
5009
5007
|
async function applyPolicies(client, policies) {
|
|
5010
5008
|
const applied = [];
|
|
5011
5009
|
for (const { bucket, open } of policies) {
|
|
5012
|
-
const wanted = bucketPolicy
|
|
5010
|
+
const wanted = bucketPolicy(bucket, open);
|
|
5013
5011
|
const describe = `open paths on ${bucket}`;
|
|
5014
5012
|
if (equivalent(await client.policy(bucket), wanted)) {
|
|
5015
5013
|
applied.push({
|
|
@@ -6649,916 +6647,6 @@ function isUnder(file, dir) {
|
|
|
6649
6647
|
return rel !== "" && !rel.startsWith("..") && !(0, node_path.isAbsolute)(rel) ? !rel.startsWith(`..${node_path.sep}`) : false;
|
|
6650
6648
|
}
|
|
6651
6649
|
//#endregion
|
|
6652
|
-
//#region src/deploy/devServices.ts
|
|
6653
|
-
/**
|
|
6654
|
-
* Mail and object storage on a deployed stage: real ones, or — only when
|
|
6655
|
-
* someone asked — the dev services the local stage runs.
|
|
6656
|
-
*
|
|
6657
|
-
* Locally a declared `Email` is Mailpit and a declared bucket is MinIO, and
|
|
6658
|
-
* nobody has to configure either. Deployed, the same two containers would be
|
|
6659
|
-
* a stage that silently delivers no mail and keeps its files on one
|
|
6660
|
-
* container's disk. So a deployed stage on a server target takes both from
|
|
6661
|
-
* its secrets — the mail server's URL and sending address, and each bucket's
|
|
6662
|
-
* URL — and a stage missing any of them fails before anything is built,
|
|
6663
|
-
* provisioned or written, naming every key at once.
|
|
6664
|
-
*
|
|
6665
|
-
* A bucket's credentials are not checked: they are optional, and come from
|
|
6666
|
-
* either of two places. The bucket's URL may carry a key of its own
|
|
6667
|
-
* (`s3://KEY:SECRET@uploads`), which wins; otherwise the S3 client's default
|
|
6668
|
-
* chain applies — the stage's shared `AWS_ACCESS_KEY_ID`/
|
|
6669
|
-
* `AWS_SECRET_ACCESS_KEY` when it set them, or a role.
|
|
6670
|
-
*
|
|
6671
|
-
* `--allow-dev-services` is the way out for a stage that is not production —
|
|
6672
|
-
* a preview, a demo: every construct the stage does not account for gets the
|
|
6673
|
-
* dev service the local stage runs for it — a bucket MinIO, mail Mailpit —
|
|
6674
|
-
* and the target derives the keys from it. What the stage does account for
|
|
6675
|
-
* still wins: a key set in its secrets, or a provider (`deploy.objects`)
|
|
6676
|
-
* that backs the kind. Every run that uses one says so, loudly.
|
|
6677
|
-
*
|
|
6678
|
-
* Pure: what each target runs is decided here from declarations and the
|
|
6679
|
-
* stage's values, and the targets do the running.
|
|
6680
|
-
*/
|
|
6681
|
-
/**
|
|
6682
|
-
* The dev services a deployed stage can run — one per kind that has one on
|
|
6683
|
-
* every server target: a bucket's MinIO and mail's Mailpit. A cache or a
|
|
6684
|
-
* database is not here because a server stage runs its own anyway.
|
|
6685
|
-
*/
|
|
6686
|
-
const DEV_SERVICES = ["minio", "mailpit"];
|
|
6687
|
-
/**
|
|
6688
|
-
* The S3 client's shared key pair. Optional: a bucket whose URL carries its own
|
|
6689
|
-
* key (`s3://KEY:SECRET@bucket`) signs with that, and one without signs with
|
|
6690
|
-
* this pair when the stage set it, or with whatever else the SDK's default
|
|
6691
|
-
* chain finds — a role.
|
|
6692
|
-
*/
|
|
6693
|
-
const STORAGE_KEY_PAIR = ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"];
|
|
6694
|
-
/** Dev services asked of a target that runs no containers. */
|
|
6695
|
-
var DevServicesNeedServerTarget = class extends require_errors.GkmError {
|
|
6696
|
-
target;
|
|
6697
|
-
constructor(target) {
|
|
6698
|
-
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.`);
|
|
6699
|
-
this.target = target;
|
|
6700
|
-
this.name = "DevServicesNeedServerTarget";
|
|
6701
|
-
}
|
|
6702
|
-
};
|
|
6703
|
-
/**
|
|
6704
|
-
* `--allow-dev-services` given a value — the list it used to take. It is a
|
|
6705
|
-
* switch now: every construct the stage does not account for gets its dev
|
|
6706
|
-
* service.
|
|
6707
|
-
*/
|
|
6708
|
-
var AllowDevServicesTakesNoValue = class extends require_errors.GkmError {
|
|
6709
|
-
value;
|
|
6710
|
-
constructor(value) {
|
|
6711
|
-
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.`);
|
|
6712
|
-
this.value = value;
|
|
6713
|
-
this.name = "AllowDevServicesTakesNoValue";
|
|
6714
|
-
}
|
|
6715
|
-
};
|
|
6716
|
-
/**
|
|
6717
|
-
* Refuses `--allow-dev-services <value>` and `--allow-dev-services=<value>` on
|
|
6718
|
-
* a command line, with the reason, before the parser calls the value an
|
|
6719
|
-
* excess argument. `gkm deploy` and `gkm compose` take no positional
|
|
6720
|
-
* argument, so whatever follows the flag and is not a flag was meant for it.
|
|
6721
|
-
*
|
|
6722
|
-
* @throws {AllowDevServicesTakesNoValue}
|
|
6723
|
-
*/
|
|
6724
|
-
function assertDevServicesFlag(argv) {
|
|
6725
|
-
for (let i = 0; i < argv.length; i++) {
|
|
6726
|
-
const arg = argv[i];
|
|
6727
|
-
if (arg.startsWith("--allow-dev-services=")) throw new AllowDevServicesTakesNoValue(arg.slice(arg.indexOf("=") + 1));
|
|
6728
|
-
const next = argv[i + 1];
|
|
6729
|
-
if (arg === "--allow-dev-services" && next && !next.startsWith("-")) throw new AllowDevServicesTakesNoValue(next);
|
|
6730
|
-
}
|
|
6731
|
-
}
|
|
6732
|
-
/**
|
|
6733
|
-
* A deployed stage with mail or object storage that its secrets do not
|
|
6734
|
-
* configure — every missing key, across every app, at once.
|
|
6735
|
-
*/
|
|
6736
|
-
var ExternalServicesNotConfigured = class extends require_errors.GkmError {
|
|
6737
|
-
stage;
|
|
6738
|
-
missing;
|
|
6739
|
-
constructor(stage, missing) {
|
|
6740
|
-
const standIns = missing.some((m) => m.service !== void 0);
|
|
6741
|
-
const lines = missing.map((m) => {
|
|
6742
|
-
const by = m.apps?.length ? `, read by ${m.apps.join(", ")}` : "";
|
|
6743
|
-
return ` gkm secrets:set ${m.key} '${m.example}' --stage ${stage}\n ${m.what}${by}` + (m.note ? `\n ${m.note}` : "") + (m.hint ? `\n ${m.hint}` : "");
|
|
6744
|
-
});
|
|
6745
|
-
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}`);
|
|
6746
|
-
this.stage = stage;
|
|
6747
|
-
this.missing = missing;
|
|
6748
|
-
this.name = "ExternalServicesNotConfigured";
|
|
6749
|
-
}
|
|
6750
|
-
};
|
|
6751
|
-
/**
|
|
6752
|
-
* Every mail and storage construct a manifest declares, with the apps that
|
|
6753
|
-
* depend on each — what a target that provisions the whole manifest checks.
|
|
6754
|
-
*/
|
|
6755
|
-
function manifestServiceDeclarations(manifest) {
|
|
6756
|
-
return Object.entries(manifest).flatMap(([id, declaration]) => {
|
|
6757
|
-
const { kind } = declaration;
|
|
6758
|
-
if (kind !== "email" && kind !== "objects" && kind !== "file-server") return [];
|
|
6759
|
-
const apps = (0, _geekmidas_manifest.dependentsOf)(manifest, id).filter((caller) => {
|
|
6760
|
-
const callerKind = manifest[caller]?.kind;
|
|
6761
|
-
return callerKind === "rest-api" || callerKind === "site";
|
|
6762
|
-
}).map(require_derive.appKey);
|
|
6763
|
-
return [{
|
|
6764
|
-
id,
|
|
6765
|
-
kind,
|
|
6766
|
-
...declaration.kind === "file-server" ? { of: declaration.of } : {},
|
|
6767
|
-
...apps.length ? { apps } : {}
|
|
6768
|
-
}];
|
|
6769
|
-
});
|
|
6770
|
-
}
|
|
6771
|
-
/**
|
|
6772
|
-
* Every key a stage must be given rather than derive — the one list both a
|
|
6773
|
-
* deploy's checks and `gkm secrets:add` read, so what a deploy refuses and
|
|
6774
|
-
* what the builder offers cannot drift.
|
|
6775
|
-
*
|
|
6776
|
-
* - Deployed, mail is its URL and its sending address, a bucket its URL, and
|
|
6777
|
-
* a file server its public URL. Locally all of it is Mailpit and MinIO.
|
|
6778
|
-
* - On every stage, a third party's credentials: nobody but the third party
|
|
6779
|
-
* can issue them.
|
|
6780
|
-
*
|
|
6781
|
-
* Never a derived value: a database URL, a generated secret, the seed.
|
|
6782
|
-
*/
|
|
6783
|
-
function requiredStageKeys(input) {
|
|
6784
|
-
const domain = input.domain ?? "example.com";
|
|
6785
|
-
const keys = [];
|
|
6786
|
-
const add = (entry) => {
|
|
6787
|
-
if (!keys.some((k) => k.key === entry.key)) keys.push(entry);
|
|
6788
|
-
};
|
|
6789
|
-
const byId = (list) => [...list ?? []].sort((a, b) => a.id.localeCompare(b.id));
|
|
6790
|
-
const apps = (d) => d.apps?.length ? { apps: [...d.apps].sort() } : {};
|
|
6791
|
-
if (!input.local) {
|
|
6792
|
-
const services = byId(input.services);
|
|
6793
|
-
const objects = input.providers?.objects;
|
|
6794
|
-
const storage = objects?.accounted ? objects.hint ? { hint: objects.hint } : {} : { service: "minio" };
|
|
6795
|
-
for (const d of services.filter((d) => d.kind === "email")) {
|
|
6796
|
-
add({
|
|
6797
|
-
key: (0, _geekmidas_manifest.provideKey)(d.id, "url"),
|
|
6798
|
-
id: d.id,
|
|
6799
|
-
kind: "email",
|
|
6800
|
-
what: `where '${d.id}' sends mail — any SMTP server`,
|
|
6801
|
-
example: `smtp://user:password@smtp.${domain}:587`,
|
|
6802
|
-
service: "mailpit",
|
|
6803
|
-
...apps(d)
|
|
6804
|
-
});
|
|
6805
|
-
add({
|
|
6806
|
-
key: (0, _geekmidas_manifest.provideKey)(d.id, "from"),
|
|
6807
|
-
id: d.id,
|
|
6808
|
-
kind: "email",
|
|
6809
|
-
what: `the address '${d.id}' sends from, on a domain the mail server has verified`,
|
|
6810
|
-
example: `noreply@${domain}`,
|
|
6811
|
-
service: "mailpit",
|
|
6812
|
-
...apps(d)
|
|
6813
|
-
});
|
|
6814
|
-
}
|
|
6815
|
-
for (const d of services.filter((d) => d.kind === "objects")) add({
|
|
6816
|
-
key: (0, _geekmidas_manifest.provideKey)(d.id, "url"),
|
|
6817
|
-
id: d.id,
|
|
6818
|
-
kind: "bucket",
|
|
6819
|
-
what: `the bucket '${d.id}' — S3, R2, or any S3-compatible store (add &endpoint=… for one that is not S3)`,
|
|
6820
|
-
note: `credentials are optional: a key for this bucket alone in the URL (s3://KEY:SECRET@${require_derive.appKey(d.id)}?…) wins; without one, the shared AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY or a role signs`,
|
|
6821
|
-
example: `s3://${require_derive.appKey(d.id)}?region=eu-west-1`,
|
|
6822
|
-
...storage,
|
|
6823
|
-
...apps(d)
|
|
6824
|
-
});
|
|
6825
|
-
for (const d of services.filter((d) => d.kind === "file-server")) add({
|
|
6826
|
-
key: (0, _geekmidas_manifest.provideKey)(d.id, "url"),
|
|
6827
|
-
id: d.id,
|
|
6828
|
-
kind: "file-server",
|
|
6829
|
-
what: `the public address '${d.id}' serves its bucket on — a CDN or the bucket's own domain`,
|
|
6830
|
-
example: `https://${require_derive.appKey(d.id)}.${domain}`,
|
|
6831
|
-
...storage,
|
|
6832
|
-
...d.of ? { of: d.of } : {},
|
|
6833
|
-
...apps(d)
|
|
6834
|
-
});
|
|
6835
|
-
}
|
|
6836
|
-
if (!input.local && input.server) add({
|
|
6837
|
-
key: require_workspace.SERVER_IPV4_KEY,
|
|
6838
|
-
id: "server",
|
|
6839
|
-
kind: "server",
|
|
6840
|
-
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",
|
|
6841
|
-
note: `${require_workspace.SERVER_IPV6_KEY} adds its IPv6 address (AAAA records); neither is ever handed to an app`,
|
|
6842
|
-
example: "203.0.113.10"
|
|
6843
|
-
});
|
|
6844
|
-
for (const d of byId(input.credentials)) add({
|
|
6845
|
-
key: (0, _geekmidas_manifest.provideKey)(d.id, "credentials"),
|
|
6846
|
-
id: d.id,
|
|
6847
|
-
kind: d.kind,
|
|
6848
|
-
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`,
|
|
6849
|
-
example: "{\"apiKey\":\"…\"}",
|
|
6850
|
-
...apps(d)
|
|
6851
|
-
});
|
|
6852
|
-
return keys;
|
|
6853
|
-
}
|
|
6854
|
-
/**
|
|
6855
|
-
* Whether only the stage's secrets can supply `key` to a construct of `kind`
|
|
6856
|
-
* — what nothing on the stage derives.
|
|
6857
|
-
*
|
|
6858
|
-
* A third party's credentials, always. Deployed, also each generated secret
|
|
6859
|
-
* and keyring: the stage generated each one once, and every run reads the
|
|
6860
|
-
* same value back.
|
|
6861
|
-
*/
|
|
6862
|
-
function suppliedOnly(kind, id, key, local) {
|
|
6863
|
-
if (kind === "credential") return true;
|
|
6864
|
-
if (kind === "external-api") return key === (0, _geekmidas_manifest.provideKey)(id, "credentials");
|
|
6865
|
-
if (local) return false;
|
|
6866
|
-
return kind === "secret" || kind === "encryption";
|
|
6867
|
-
}
|
|
6868
|
-
/**
|
|
6869
|
-
* Where a deployed stage's mail and storage come from, and what it lacks.
|
|
6870
|
-
*
|
|
6871
|
-
* - Mail is external once its URL is set, and then needs its sending
|
|
6872
|
-
* address too. Unset, it is Mailpit where allowed — the address derived
|
|
6873
|
-
* unless set — and missing otherwise.
|
|
6874
|
-
* - A bucket is external once its URL is set; its credentials, in the URL or
|
|
6875
|
-
* the shared pair, are optional. Unset, it is MinIO where allowed, and
|
|
6876
|
-
* missing otherwise.
|
|
6877
|
-
* - A file server's URL is derived where its bucket is MinIO, unless set;
|
|
6878
|
-
* over an external bucket the stage must set it.
|
|
6879
|
-
*
|
|
6880
|
-
* The keys themselves are {@link requiredStageKeys}'s.
|
|
6881
|
-
*/
|
|
6882
|
-
function externalServices(input) {
|
|
6883
|
-
const { supplied, allow } = input;
|
|
6884
|
-
const has = (key) => supplied[key] !== void 0;
|
|
6885
|
-
const required = requiredStageKeys({
|
|
6886
|
-
local: false,
|
|
6887
|
-
services: input.declarations,
|
|
6888
|
-
...input.domain ? { domain: input.domain } : {},
|
|
6889
|
-
...input.providers ? { providers: input.providers } : {}
|
|
6890
|
-
});
|
|
6891
|
-
const standIn = (k) => allow && k.service !== void 0 && !has(k.key);
|
|
6892
|
-
const mailpit = required.filter((k) => k.kind === "email" && k.key === (0, _geekmidas_manifest.provideKey)(k.id, "url")).filter(standIn).map((k) => k.id);
|
|
6893
|
-
const minio = required.filter((k) => k.kind === "bucket").filter(standIn).map((k) => k.id);
|
|
6894
|
-
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);
|
|
6895
|
-
return {
|
|
6896
|
-
missing: required.filter((k) => !has(k.key) && !covered(k)).map(({ key, id, what, note, example, service, apps, hint }) => ({
|
|
6897
|
-
key,
|
|
6898
|
-
id,
|
|
6899
|
-
what,
|
|
6900
|
-
...note ? { note } : {},
|
|
6901
|
-
example,
|
|
6902
|
-
...service ? { service } : {},
|
|
6903
|
-
...apps ? { apps } : {},
|
|
6904
|
-
...hint ? { hint } : {}
|
|
6905
|
-
})),
|
|
6906
|
-
minio,
|
|
6907
|
-
mailpit
|
|
6908
|
-
};
|
|
6909
|
-
}
|
|
6910
|
-
/**
|
|
6911
|
-
* `externalServices`, refusing a stage that lacks anything.
|
|
6912
|
-
*
|
|
6913
|
-
* @throws {ExternalServicesNotConfigured} naming every missing key
|
|
6914
|
-
*/
|
|
6915
|
-
function assertExternalServices(input) {
|
|
6916
|
-
const services = externalServices(input);
|
|
6917
|
-
if (services.missing.length > 0) throw new ExternalServicesNotConfigured(input.stage, services.missing);
|
|
6918
|
-
return services;
|
|
6919
|
-
}
|
|
6920
|
-
/** The dev services a run uses, one entry per service it runs. */
|
|
6921
|
-
function devServicesUsed(services) {
|
|
6922
|
-
const used = [];
|
|
6923
|
-
if (services.mailpit.length > 0) used.push({
|
|
6924
|
-
service: "mailpit",
|
|
6925
|
-
ids: services.mailpit
|
|
6926
|
-
});
|
|
6927
|
-
if (services.minio.length > 0) used.push({
|
|
6928
|
-
service: "minio",
|
|
6929
|
-
ids: services.minio
|
|
6930
|
-
});
|
|
6931
|
-
return used;
|
|
6932
|
-
}
|
|
6933
|
-
/** The line a run prints for each dev service a deployed stage uses. */
|
|
6934
|
-
function devServiceWarning(stage, use) {
|
|
6935
|
-
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.`;
|
|
6936
|
-
}
|
|
6937
|
-
/**
|
|
6938
|
-
* Report each dev service a run uses: a loud line, and a `dev-service.used`
|
|
6939
|
-
* event a host can act on.
|
|
6940
|
-
*/
|
|
6941
|
-
function reportDevServices(ctx, uses) {
|
|
6942
|
-
for (const use of uses) {
|
|
6943
|
-
ctx.logger.warn(`\n${devServiceWarning(ctx.stage, use)}`);
|
|
6944
|
-
ctx.emit({
|
|
6945
|
-
type: "dev-service.used",
|
|
6946
|
-
service: use.service,
|
|
6947
|
-
stage: ctx.stage,
|
|
6948
|
-
constructs: use.ids
|
|
6949
|
-
});
|
|
6950
|
-
}
|
|
6951
|
-
}
|
|
6952
|
-
//#endregion
|
|
6953
|
-
//#region src/deploy/actor.ts
|
|
6954
|
-
/**
|
|
6955
|
-
* Who wrote a stage's deploy state: a GitHub Actions run, or a person at a
|
|
6956
|
-
* machine. Recorded on every state write (`updatedBy`), on every resource
|
|
6957
|
-
* record and on every release (`releasedBy`), so "who put this here" has an
|
|
6958
|
-
* answer that does not depend on anyone's memory.
|
|
6959
|
-
*/
|
|
6960
|
-
/** Whether `env` is a CI job — GitHub Actions or any runner that sets `CI`. */
|
|
6961
|
-
function isCi(env = process.env) {
|
|
6962
|
-
return env.GITHUB_ACTIONS === "true" || env.CI === "true";
|
|
6963
|
-
}
|
|
6964
|
-
/** Who this process is writing as. */
|
|
6965
|
-
function currentActor(env = process.env) {
|
|
6966
|
-
if (env.GITHUB_ACTIONS === "true") {
|
|
6967
|
-
const server = env.GITHUB_SERVER_URL ?? "https://github.com";
|
|
6968
|
-
return {
|
|
6969
|
-
kind: "github",
|
|
6970
|
-
actor: env.GITHUB_ACTOR ?? "unknown",
|
|
6971
|
-
run: `${server}/${env.GITHUB_REPOSITORY ?? "unknown"}/actions/runs/${env.GITHUB_RUN_ID ?? "unknown"}`,
|
|
6972
|
-
...env.GITHUB_WORKFLOW ? { workflow: env.GITHUB_WORKFLOW } : {}
|
|
6973
|
-
};
|
|
6974
|
-
}
|
|
6975
|
-
let user = "unknown";
|
|
6976
|
-
try {
|
|
6977
|
-
user = (0, node_os.userInfo)().username;
|
|
6978
|
-
} catch {}
|
|
6979
|
-
return {
|
|
6980
|
-
kind: "local",
|
|
6981
|
-
user,
|
|
6982
|
-
host: (0, node_os.hostname)()
|
|
6983
|
-
};
|
|
6984
|
-
}
|
|
6985
|
-
/** `octocat (deploy, https://github.com/…/runs/1)` or `ada@laptop`. */
|
|
6986
|
-
function describeActor(actor) {
|
|
6987
|
-
if (!actor) return "unknown";
|
|
6988
|
-
if (actor.kind === "local") return `${actor.user}@${actor.host}`;
|
|
6989
|
-
return `${actor.actor} (${actor.workflow ? `${actor.workflow}, ` : ""}${actor.run})`;
|
|
6990
|
-
}
|
|
6991
|
-
//#endregion
|
|
6992
|
-
//#region src/deploy/dnsResources.ts
|
|
6993
|
-
/** The resource type of a DNS record gkm wrote. */
|
|
6994
|
-
const DNS_RECORD_RESOURCE = "dns-record";
|
|
6995
|
-
/** A record's full name: `api.example.com`, or the domain for `@`. */
|
|
6996
|
-
function dnsRecordFqdn(domain, name) {
|
|
6997
|
-
return name === "@" || name === "" ? domain : `${name}.${domain}`;
|
|
6998
|
-
}
|
|
6999
|
-
/** `dns-record:<fqdn>:<type>` */
|
|
7000
|
-
function dnsResourceKey(fqdn, type) {
|
|
7001
|
-
return `${DNS_RECORD_RESOURCE}:${fqdn.toLowerCase()}:${type.toUpperCase()}`;
|
|
7002
|
-
}
|
|
7003
|
-
/** The resource record a written DNS record is kept as. */
|
|
7004
|
-
function dnsRecordResource(record) {
|
|
7005
|
-
const fqdn = dnsRecordFqdn(record.domain, record.name).toLowerCase();
|
|
7006
|
-
const type = record.type.toUpperCase();
|
|
7007
|
-
const data = {
|
|
7008
|
-
domain: record.domain,
|
|
7009
|
-
name: record.name,
|
|
7010
|
-
value: record.value,
|
|
7011
|
-
ttl: record.ttl,
|
|
7012
|
-
...record.provider ? { provider: record.provider } : {}
|
|
7013
|
-
};
|
|
7014
|
-
return {
|
|
7015
|
-
key: dnsResourceKey(fqdn, type),
|
|
7016
|
-
type: DNS_RECORD_RESOURCE,
|
|
7017
|
-
id: `${fqdn} ${type}`,
|
|
7018
|
-
status: "ready",
|
|
7019
|
-
data
|
|
7020
|
-
};
|
|
7021
|
-
}
|
|
7022
|
-
/** Records that gkm wrote `record` — or updates the value it holds. */
|
|
7023
|
-
async function recordDnsResource(journal, record) {
|
|
7024
|
-
const { key, type, id, data } = dnsRecordResource(record);
|
|
7025
|
-
await journal.ready({
|
|
7026
|
-
key,
|
|
7027
|
-
type,
|
|
7028
|
-
...data ? { data } : {}
|
|
7029
|
-
}, id);
|
|
7030
|
-
}
|
|
7031
|
-
/** Forgets a DNS record gkm deleted. */
|
|
7032
|
-
async function forgetDnsResource(journal, record) {
|
|
7033
|
-
await journal.forget(dnsResourceKey(dnsRecordFqdn(record.domain, record.name), record.type));
|
|
7034
|
-
}
|
|
7035
|
-
/**
|
|
7036
|
-
* The changes a DNS step made, kept: what it created, updated or wrote is
|
|
7037
|
-
* recorded, what it deleted is forgotten. A record it found already right is
|
|
7038
|
-
* recorded only if gkm wrote it before; one it left to a person is not.
|
|
7039
|
-
*/
|
|
7040
|
-
async function recordDnsChanges(journal, changes, providerOf) {
|
|
7041
|
-
for (const change of changes) {
|
|
7042
|
-
if (change.action === "delete") {
|
|
7043
|
-
await forgetDnsResource(journal, change);
|
|
7044
|
-
continue;
|
|
7045
|
-
}
|
|
7046
|
-
const written = change.action === "create" || change.action === "update" || change.action === "write";
|
|
7047
|
-
const known = change.action === "unchanged" && journal.record(dnsResourceKey(dnsRecordFqdn(change.domain, change.name), change.type)) !== void 0;
|
|
7048
|
-
if (!written && !known || change.value === void 0) continue;
|
|
7049
|
-
const provider = providerOf(change.domain);
|
|
7050
|
-
await recordDnsResource(journal, {
|
|
7051
|
-
domain: change.domain,
|
|
7052
|
-
name: change.name,
|
|
7053
|
-
type: change.type,
|
|
7054
|
-
value: change.value,
|
|
7055
|
-
ttl: change.ttl ?? 0,
|
|
7056
|
-
...provider ? { provider } : {}
|
|
7057
|
-
});
|
|
7058
|
-
}
|
|
7059
|
-
}
|
|
7060
|
-
/** Every DNS record gkm wrote for the stage, from its resource records. */
|
|
7061
|
-
function dnsResources(resources) {
|
|
7062
|
-
return Object.values(resources).filter((record) => record.type === DNS_RECORD_RESOURCE).map((record) => {
|
|
7063
|
-
const data = record.data ?? {};
|
|
7064
|
-
const [fqdn = "", type = ""] = (record.id ?? "").split(" ");
|
|
7065
|
-
return {
|
|
7066
|
-
...data,
|
|
7067
|
-
type,
|
|
7068
|
-
fqdn
|
|
7069
|
-
};
|
|
7070
|
-
}).sort((a, b) => a.fqdn.localeCompare(b.fqdn) || a.type.localeCompare(b.type));
|
|
7071
|
-
}
|
|
7072
|
-
//#endregion
|
|
7073
|
-
//#region src/deploy/StateProvider.ts
|
|
7074
|
-
/**
|
|
7075
|
-
* Check if value is a StateProvider implementation.
|
|
7076
|
-
*/
|
|
7077
|
-
function isStateProvider(value) {
|
|
7078
|
-
return typeof value === "object" && value !== null && typeof value.read === "function" && typeof value.write === "function";
|
|
7079
|
-
}
|
|
7080
|
-
var StateLocked = class extends require_errors.GkmError {
|
|
7081
|
-
stage;
|
|
7082
|
-
holder;
|
|
7083
|
-
location;
|
|
7084
|
-
constructor(stage, holder, location) {
|
|
7085
|
-
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";
|
|
7086
|
-
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}\`.`);
|
|
7087
|
-
this.stage = stage;
|
|
7088
|
-
this.holder = holder;
|
|
7089
|
-
this.location = location;
|
|
7090
|
-
this.name = "StateLocked";
|
|
7091
|
-
}
|
|
7092
|
-
};
|
|
7093
|
-
var StateVersionConflict = class extends require_errors.GkmError {
|
|
7094
|
-
stage;
|
|
7095
|
-
expectedVersion;
|
|
7096
|
-
actualVersion;
|
|
7097
|
-
constructor(stage, expectedVersion, actualVersion) {
|
|
7098
|
-
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.`);
|
|
7099
|
-
this.stage = stage;
|
|
7100
|
-
this.expectedVersion = expectedVersion;
|
|
7101
|
-
this.actualVersion = actualVersion;
|
|
7102
|
-
this.name = "StateVersionConflict";
|
|
7103
|
-
}
|
|
7104
|
-
};
|
|
7105
|
-
var StageStateMissing = class extends require_errors.GkmError {
|
|
7106
|
-
stage;
|
|
7107
|
-
constructor(stage) {
|
|
7108
|
-
super(`There is no deploy state for stage '${stage}' to record a resource in. Write the stage's state first (write with expectedVersion: null).`);
|
|
7109
|
-
this.stage = stage;
|
|
7110
|
-
this.name = "StageStateMissing";
|
|
7111
|
-
}
|
|
7112
|
-
};
|
|
7113
|
-
var StateUnreadable = class extends require_errors.GkmError {
|
|
7114
|
-
stage;
|
|
7115
|
-
location;
|
|
7116
|
-
constructor(stage, location, cause) {
|
|
7117
|
-
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 });
|
|
7118
|
-
this.stage = stage;
|
|
7119
|
-
this.location = location;
|
|
7120
|
-
this.name = "StateUnreadable";
|
|
7121
|
-
}
|
|
7122
|
-
};
|
|
7123
|
-
var StateSchemaTooNew = class extends require_errors.GkmError {
|
|
7124
|
-
stage;
|
|
7125
|
-
schemaVersion;
|
|
7126
|
-
constructor(stage, schemaVersion) {
|
|
7127
|
-
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.`);
|
|
7128
|
-
this.stage = stage;
|
|
7129
|
-
this.schemaVersion = schemaVersion;
|
|
7130
|
-
this.name = "StateSchemaTooNew";
|
|
7131
|
-
}
|
|
7132
|
-
};
|
|
7133
|
-
/**
|
|
7134
|
-
* Not thrown: emitted (once per store) when a custom `StateProvider` stands
|
|
7135
|
-
* in for a store, so a deploy that cannot be protected from a concurrent one
|
|
7136
|
-
* says so.
|
|
7137
|
-
*/
|
|
7138
|
-
var StateStoreWithoutLocking = class extends require_errors.GkmError {
|
|
7139
|
-
provider;
|
|
7140
|
-
constructor(provider) {
|
|
7141
|
-
super(`The custom state provider '${provider}' has no lock and no versions, so two deploys of the same stage can still overwrite each other. Implement StateStore (lock, versioned read, conditional write) or use the 'local', 'ssm' or 's3' state provider.`);
|
|
7142
|
-
this.provider = provider;
|
|
7143
|
-
this.name = "StateStoreWithoutLocking";
|
|
7144
|
-
}
|
|
7145
|
-
};
|
|
7146
|
-
var StateStoreNeedsWorkspaceName = class extends require_errors.GkmError {
|
|
7147
|
-
provider;
|
|
7148
|
-
constructor(provider) {
|
|
7149
|
-
super(`The '${provider}' state provider keys state by workspace name. Set "name" in gkm.config.ts.`);
|
|
7150
|
-
this.provider = provider;
|
|
7151
|
-
this.name = "StateStoreNeedsWorkspaceName";
|
|
7152
|
-
}
|
|
7153
|
-
};
|
|
7154
|
-
var UnknownStateProvider = class extends require_errors.GkmError {
|
|
7155
|
-
config;
|
|
7156
|
-
constructor(config) {
|
|
7157
|
-
super(`Unknown state provider ${JSON.stringify(config)}. Use 'local', 'ssm', 's3', or an object implementing StateStore or StateProvider.`);
|
|
7158
|
-
this.config = config;
|
|
7159
|
-
this.name = "UnknownStateProvider";
|
|
7160
|
-
}
|
|
7161
|
-
};
|
|
7162
|
-
/**
|
|
7163
|
-
* A deploy, setup or rollback of a stage in CI, with the stage's state kept on
|
|
7164
|
-
* the runner — which is discarded when the job ends.
|
|
7165
|
-
*/
|
|
7166
|
-
var LocalStateInCi = class extends require_errors.GkmError {
|
|
7167
|
-
stage;
|
|
7168
|
-
operation;
|
|
7169
|
-
region;
|
|
7170
|
-
constructor(stage, operation, region) {
|
|
7171
|
-
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`);
|
|
7172
|
-
this.stage = stage;
|
|
7173
|
-
this.operation = operation;
|
|
7174
|
-
this.region = region;
|
|
7175
|
-
this.name = "LocalStateInCi";
|
|
7176
|
-
}
|
|
7177
|
-
};
|
|
7178
|
-
/** A lock or write that could not get at the state backend in time. */
|
|
7179
|
-
var StateStoreBusy = class extends require_errors.GkmError {
|
|
7180
|
-
stage;
|
|
7181
|
-
location;
|
|
7182
|
-
constructor(stage, location) {
|
|
7183
|
-
super(`Timed out waiting to write deploy state for stage '${stage}' (${location}). If no other gkm process is running, remove ${location}.`);
|
|
7184
|
-
this.stage = stage;
|
|
7185
|
-
this.location = location;
|
|
7186
|
-
this.name = "StateStoreBusy";
|
|
7187
|
-
}
|
|
7188
|
-
};
|
|
7189
|
-
function decodeDocument(stage, body, location) {
|
|
7190
|
-
let parsed;
|
|
7191
|
-
try {
|
|
7192
|
-
parsed = JSON.parse(body);
|
|
7193
|
-
} catch (error) {
|
|
7194
|
-
throw new StateUnreadable(stage, location, error);
|
|
7195
|
-
}
|
|
7196
|
-
if (typeof parsed !== "object" || parsed === null) throw new StateUnreadable(stage, location, parsed);
|
|
7197
|
-
const schemaVersion = parsed.schemaVersion;
|
|
7198
|
-
if (schemaVersion === void 0) return {
|
|
7199
|
-
schemaVersion: 1,
|
|
7200
|
-
state: parsed
|
|
7201
|
-
};
|
|
7202
|
-
if (schemaVersion === 2) return {
|
|
7203
|
-
schemaVersion: 2,
|
|
7204
|
-
document: parsed
|
|
7205
|
-
};
|
|
7206
|
-
if (schemaVersion === 3) return {
|
|
7207
|
-
schemaVersion: 3,
|
|
7208
|
-
document: parsed
|
|
7209
|
-
};
|
|
7210
|
-
throw new StateSchemaTooNew(stage, schemaVersion);
|
|
7211
|
-
}
|
|
7212
|
-
function encodeDocument(document) {
|
|
7213
|
-
return JSON.stringify(document, null, 2);
|
|
7214
|
-
}
|
|
7215
|
-
/**
|
|
7216
|
-
* v1 state as a v2 document. The ids v1 kept are seeded as `ready` records,
|
|
7217
|
-
* so the first journaled deploy after an upgrade adopts them rather than
|
|
7218
|
-
* treating them as unknown.
|
|
7219
|
-
*/
|
|
7220
|
-
function migrateV1(stage, state) {
|
|
7221
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7222
|
-
const resources = {};
|
|
7223
|
-
const seed = (type, name, id) => {
|
|
7224
|
-
if (!id) return;
|
|
7225
|
-
const key = name ? `${type}:${name}` : type;
|
|
7226
|
-
resources[key] = {
|
|
7227
|
-
key,
|
|
7228
|
-
type,
|
|
7229
|
-
id,
|
|
7230
|
-
status: "ready",
|
|
7231
|
-
updatedAt: now
|
|
7232
|
-
};
|
|
7233
|
-
};
|
|
7234
|
-
seed("project", null, state.projectId);
|
|
7235
|
-
seed("environment", null, state.environmentId);
|
|
7236
|
-
for (const [app, id] of Object.entries(state.applications ?? {})) seed("application", app, id);
|
|
7237
|
-
seed("postgres", null, state.services?.postgresId);
|
|
7238
|
-
seed("redis", null, state.services?.redisId);
|
|
7239
|
-
seed("backup-destination", null, state.backups?.destinationId);
|
|
7240
|
-
return {
|
|
7241
|
-
schemaVersion: 2,
|
|
7242
|
-
stage,
|
|
7243
|
-
serial: 1,
|
|
7244
|
-
state,
|
|
7245
|
-
resources,
|
|
7246
|
-
updatedAt: now
|
|
7247
|
-
};
|
|
7248
|
-
}
|
|
7249
|
-
const isEmpty = (value) => !value || Object.keys(value).length === 0;
|
|
7250
|
-
/**
|
|
7251
|
-
* Whether a v2 state in the Dokploy shape was written by the compose target.
|
|
7252
|
-
*
|
|
7253
|
-
* Before v3 every target wrote the Dokploy shape, and compose filled none of
|
|
7254
|
-
* its Dokploy fields. So the rule is the shape's content, not the target the
|
|
7255
|
-
* workspace names now: a state whose every Dokploy-only field is empty — no
|
|
7256
|
-
* project or environment id, no application or service ids, no credentials,
|
|
7257
|
-
* generated secrets, backups or registry — holds nothing only Dokploy could
|
|
7258
|
-
* use, and reading it as compose loses nothing. One with any of them was
|
|
7259
|
-
* written by Dokploy and is kept exactly as it is.
|
|
7260
|
-
*
|
|
7261
|
-
* The configured target is not used: a document decodes the same wherever it
|
|
7262
|
-
* is read (`state:show`, `state:push`) without a workspace, a stage can be
|
|
7263
|
-
* moved between targets, and `--target` can override a run.
|
|
7264
|
-
*/
|
|
7265
|
-
function writtenByCompose(state) {
|
|
7266
|
-
return !state.projectId && !state.environmentId && isEmpty(state.applications) && !state.services?.postgresId && !state.services?.redisId && isEmpty(state.appCredentials) && isEmpty(state.generatedSecrets) && !state.backups && !state.registryId;
|
|
7267
|
-
}
|
|
7268
|
-
/**
|
|
7269
|
-
* A v2 state and its records as v3 keeps them. A compose-written state
|
|
7270
|
-
* becomes the compose shape: its `dnsRecords` become `dns-record` resources,
|
|
7271
|
-
* and `dnsVerified` and the empty Dokploy fields go — the DNS check is made
|
|
7272
|
-
* on every deploy. A Dokploy state is returned as it is.
|
|
7273
|
-
*/
|
|
7274
|
-
function upgradeV2State(state, resources) {
|
|
7275
|
-
if (!writtenByCompose(state)) return {
|
|
7276
|
-
state,
|
|
7277
|
-
resources
|
|
7278
|
-
};
|
|
7279
|
-
const compose = {
|
|
7280
|
-
provider: "compose",
|
|
7281
|
-
stage: state.stage,
|
|
7282
|
-
lastDeployedAt: state.lastDeployedAt,
|
|
7283
|
-
...state.identity ? { identity: state.identity } : {},
|
|
7284
|
-
...state.releases ? { releases: state.releases } : {}
|
|
7285
|
-
};
|
|
7286
|
-
const upgraded = { ...resources };
|
|
7287
|
-
for (const record of Object.values(state.dnsRecords ?? {})) {
|
|
7288
|
-
const resource = migratedDnsResource(record);
|
|
7289
|
-
upgraded[resource.key] = resource;
|
|
7290
|
-
}
|
|
7291
|
-
return {
|
|
7292
|
-
state: compose,
|
|
7293
|
-
resources: upgraded
|
|
7294
|
-
};
|
|
7295
|
-
}
|
|
7296
|
-
/** A v2 `dnsRecords` entry as the `dns-record` resource v3 keeps. */
|
|
7297
|
-
function migratedDnsResource(record) {
|
|
7298
|
-
return {
|
|
7299
|
-
...dnsRecordResource({
|
|
7300
|
-
domain: record.domain,
|
|
7301
|
-
name: record.name,
|
|
7302
|
-
type: record.type,
|
|
7303
|
-
value: record.value,
|
|
7304
|
-
ttl: record.ttl
|
|
7305
|
-
}),
|
|
7306
|
-
updatedAt: record.createdAt
|
|
7307
|
-
};
|
|
7308
|
-
}
|
|
7309
|
-
/** A v2 document as v3 — not written until the next write. */
|
|
7310
|
-
function migrateV2(document) {
|
|
7311
|
-
const { state, resources } = upgradeV2State(document.state, document.resources);
|
|
7312
|
-
return {
|
|
7313
|
-
schemaVersion: 3,
|
|
7314
|
-
stage: document.stage,
|
|
7315
|
-
serial: document.serial,
|
|
7316
|
-
state,
|
|
7317
|
-
resources,
|
|
7318
|
-
updatedAt: document.updatedAt,
|
|
7319
|
-
history: []
|
|
7320
|
-
};
|
|
7321
|
-
}
|
|
7322
|
-
function newLockHolder(options) {
|
|
7323
|
-
let owner = "unknown";
|
|
7324
|
-
try {
|
|
7325
|
-
owner = (0, node_os.userInfo)().username;
|
|
7326
|
-
} catch {}
|
|
7327
|
-
return {
|
|
7328
|
-
id: (0, node_crypto.randomUUID)(),
|
|
7329
|
-
owner,
|
|
7330
|
-
host: (0, node_os.hostname)(),
|
|
7331
|
-
pid: process.pid,
|
|
7332
|
-
acquiredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7333
|
-
operation: options?.operation
|
|
7334
|
-
};
|
|
7335
|
-
}
|
|
7336
|
-
function contentVersion(body) {
|
|
7337
|
-
return (0, node_crypto.createHash)("sha256").update(body).digest("hex").slice(0, 16);
|
|
7338
|
-
}
|
|
7339
|
-
/**
|
|
7340
|
-
* `history` with this write at its head. A run makes many writes — one per
|
|
7341
|
-
* resource it journals — so a run is one entry: a write by the run that
|
|
7342
|
-
* wrote the newest entry (the same lock) replaces it, and any other is
|
|
7343
|
-
* added. Without a lock, each write is its own entry.
|
|
7344
|
-
*/
|
|
7345
|
-
function appendHistory(history, entry, sameRun) {
|
|
7346
|
-
return [entry, ...sameRun ? history.slice(1) : history].slice(0, 20);
|
|
7347
|
-
}
|
|
7348
|
-
/**
|
|
7349
|
-
* A store over a backend that can read and conditionally write one blob per
|
|
7350
|
-
* stage, and create a lock object only if it does not exist. Migration,
|
|
7351
|
-
* resource records and history are the same for every such backend, so they
|
|
7352
|
-
* live here.
|
|
7353
|
-
*/
|
|
7354
|
-
var DocumentStateStore = class {
|
|
7355
|
-
/** The lock this store holds for each stage — what its writes are for. */
|
|
7356
|
-
held = /* @__PURE__ */ new Map();
|
|
7357
|
-
/** Per stage, the lock whose run wrote the newest history entry. */
|
|
7358
|
-
logged = /* @__PURE__ */ new Map();
|
|
7359
|
-
/** Who this store writes as. */
|
|
7360
|
-
actor() {
|
|
7361
|
-
return currentActor();
|
|
7362
|
-
}
|
|
7363
|
-
async lock(stage, options) {
|
|
7364
|
-
const holder = newLockHolder(options);
|
|
7365
|
-
await this.createLock(stage, holder);
|
|
7366
|
-
this.held.set(stage, holder);
|
|
7367
|
-
let released = false;
|
|
7368
|
-
return {
|
|
7369
|
-
stage,
|
|
7370
|
-
holder,
|
|
7371
|
-
release: async () => {
|
|
7372
|
-
if (released) return;
|
|
7373
|
-
released = true;
|
|
7374
|
-
if (this.held.get(stage)?.id === holder.id) this.held.delete(stage);
|
|
7375
|
-
await this.removeLock(stage, holder);
|
|
7376
|
-
}
|
|
7377
|
-
};
|
|
7378
|
-
}
|
|
7379
|
-
async forceUnlock(stage) {
|
|
7380
|
-
const holder = await this.readLock(stage);
|
|
7381
|
-
await this.removeLock(stage, null);
|
|
7382
|
-
return holder;
|
|
7383
|
-
}
|
|
7384
|
-
async read(stage) {
|
|
7385
|
-
const current = await this.readDocument(stage);
|
|
7386
|
-
if (!current) return null;
|
|
7387
|
-
return {
|
|
7388
|
-
state: current.document.state,
|
|
7389
|
-
resources: current.document.resources,
|
|
7390
|
-
history: current.document.history,
|
|
7391
|
-
version: current.version
|
|
7392
|
-
};
|
|
7393
|
-
}
|
|
7394
|
-
async write(stage, state, options) {
|
|
7395
|
-
if (options.expectedVersion === null) {
|
|
7396
|
-
const document = this.stamp(stage, {
|
|
7397
|
-
schemaVersion: 3,
|
|
7398
|
-
stage,
|
|
7399
|
-
serial: 1,
|
|
7400
|
-
state,
|
|
7401
|
-
resources: {},
|
|
7402
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7403
|
-
history: []
|
|
7404
|
-
});
|
|
7405
|
-
return this.writeRaw(stage, encodeDocument(document), null);
|
|
7406
|
-
}
|
|
7407
|
-
return this.mutate(stage, options.expectedVersion, (document) => ({
|
|
7408
|
-
...document,
|
|
7409
|
-
state
|
|
7410
|
-
}));
|
|
7411
|
-
}
|
|
7412
|
-
async putResource(stage, record, options) {
|
|
7413
|
-
return this.mutate(stage, options?.expectedVersion, (document) => ({
|
|
7414
|
-
...document,
|
|
7415
|
-
resources: {
|
|
7416
|
-
...document.resources,
|
|
7417
|
-
[record.key]: {
|
|
7418
|
-
...record,
|
|
7419
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7420
|
-
updatedBy: this.actor()
|
|
7421
|
-
}
|
|
7422
|
-
}
|
|
7423
|
-
}));
|
|
7424
|
-
}
|
|
7425
|
-
async deleteResource(stage, key, options) {
|
|
7426
|
-
return this.mutate(stage, options?.expectedVersion, (document) => {
|
|
7427
|
-
const { [key]: _removed, ...resources } = document.resources;
|
|
7428
|
-
return {
|
|
7429
|
-
...document,
|
|
7430
|
-
resources
|
|
7431
|
-
};
|
|
7432
|
-
});
|
|
7433
|
-
}
|
|
7434
|
-
/** `document` as this write leaves it: who wrote it, in its history. */
|
|
7435
|
-
stamp(stage, document) {
|
|
7436
|
-
const by = this.actor();
|
|
7437
|
-
const holder = this.held.get(stage);
|
|
7438
|
-
const operation = holder?.operation ?? "write";
|
|
7439
|
-
const history = document.history ?? [];
|
|
7440
|
-
const sameRun = holder !== void 0 && this.logged.get(stage) === holder.id && history[0]?.operation === operation;
|
|
7441
|
-
if (holder) this.logged.set(stage, holder.id);
|
|
7442
|
-
return {
|
|
7443
|
-
...document,
|
|
7444
|
-
updatedBy: by,
|
|
7445
|
-
history: appendHistory(history, {
|
|
7446
|
-
serial: document.serial,
|
|
7447
|
-
at: document.updatedAt,
|
|
7448
|
-
by,
|
|
7449
|
-
operation
|
|
7450
|
-
}, sameRun)
|
|
7451
|
-
};
|
|
7452
|
-
}
|
|
7453
|
-
async mutate(stage, expectedVersion, change) {
|
|
7454
|
-
const current = await this.readDocument(stage);
|
|
7455
|
-
if (!current) {
|
|
7456
|
-
if (expectedVersion !== void 0) throw new StateVersionConflict(stage, expectedVersion, null);
|
|
7457
|
-
throw new StageStateMissing(stage);
|
|
7458
|
-
}
|
|
7459
|
-
if (expectedVersion !== void 0 && current.version !== expectedVersion) throw new StateVersionConflict(stage, expectedVersion, current.version);
|
|
7460
|
-
const next = change(current.document);
|
|
7461
|
-
next.schemaVersion = 3;
|
|
7462
|
-
next.serial = current.document.serial + 1;
|
|
7463
|
-
next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7464
|
-
return this.writeRaw(stage, encodeDocument(this.stamp(stage, next)), current.version);
|
|
7465
|
-
}
|
|
7466
|
-
/**
|
|
7467
|
-
* Reads the stage's document as v3. A v1 document is migrated in place on
|
|
7468
|
-
* the way — itself a conditional write: if another run migrated (or wrote)
|
|
7469
|
-
* first, its result is read instead. A v2 document is upgraded in memory
|
|
7470
|
-
* and written as v3 by the next write.
|
|
7471
|
-
*/
|
|
7472
|
-
async readDocument(stage, attempt = 0) {
|
|
7473
|
-
const raw = await this.readRaw(stage);
|
|
7474
|
-
if (!raw) return null;
|
|
7475
|
-
const decoded = decodeDocument(stage, raw.body, this.location(stage));
|
|
7476
|
-
if (decoded.schemaVersion === 3) return {
|
|
7477
|
-
document: decoded.document,
|
|
7478
|
-
version: raw.version
|
|
7479
|
-
};
|
|
7480
|
-
if (decoded.schemaVersion === 2) return {
|
|
7481
|
-
document: migrateV2(decoded.document),
|
|
7482
|
-
version: raw.version
|
|
7483
|
-
};
|
|
7484
|
-
await this.writeV1Backup(stage, raw.body);
|
|
7485
|
-
const document = this.stamp(stage, migrateV2(migrateV1(stage, decoded.state)));
|
|
7486
|
-
try {
|
|
7487
|
-
return {
|
|
7488
|
-
document,
|
|
7489
|
-
version: await this.writeRaw(stage, encodeDocument(document), raw.version)
|
|
7490
|
-
};
|
|
7491
|
-
} catch (error) {
|
|
7492
|
-
if (error instanceof StateVersionConflict && attempt < 2) return this.readDocument(stage, attempt + 1);
|
|
7493
|
-
throw error;
|
|
7494
|
-
}
|
|
7495
|
-
}
|
|
7496
|
-
};
|
|
7497
|
-
/** Whether `config` keeps state on this machine — `local`, or none given. */
|
|
7498
|
-
function isLocalState(config) {
|
|
7499
|
-
return !config || config.provider === "local";
|
|
7500
|
-
}
|
|
7501
|
-
/**
|
|
7502
|
-
* Refuses, with `LocalStateInCi`, a run that would write a deployed stage's
|
|
7503
|
-
* state to a CI runner's disk — the local stage, which runs on the runner
|
|
7504
|
-
* itself, is exempt. Local use is untouched. The region the message
|
|
7505
|
-
* suggests is the secrets store's, when the workspace keeps them in AWS.
|
|
7506
|
-
*/
|
|
7507
|
-
function assertStateOutlivesRun(workspace, stage, operation, env = process.env) {
|
|
7508
|
-
if (stage === workspace.stages?.local) return;
|
|
7509
|
-
if (!isCi(env) || !isLocalState(workspace.state)) return;
|
|
7510
|
-
const store = workspace.secrets?.store;
|
|
7511
|
-
throw new LocalStateInCi(stage, operation, typeof store === "object" && store && typeof store.region === "string" ? store.region : "<region>");
|
|
7512
|
-
}
|
|
7513
|
-
function isStateStore(value) {
|
|
7514
|
-
return typeof value === "object" && value !== null && typeof value.lock === "function" && typeof value.read === "function" && typeof value.write === "function" && typeof value.putResource === "function";
|
|
7515
|
-
}
|
|
7516
|
-
/**
|
|
7517
|
-
* The store for a workspace's `state` config:
|
|
7518
|
-
*
|
|
7519
|
-
* - none / `'local'`: `LocalStateStore` under `.gkm/`
|
|
7520
|
-
* - `'ssm'`: `SSMStateStore`, with no local cache — SSM is read directly, so
|
|
7521
|
-
* a stale local copy can never win over it
|
|
7522
|
-
* - `'s3'`: `S3StateStore`
|
|
7523
|
-
* - a `StateStore` object: used as is
|
|
7524
|
-
* - a `StateProvider` object: wrapped by `LegacyStateStore`
|
|
7525
|
-
*/
|
|
7526
|
-
async function createStateStore(options) {
|
|
7527
|
-
const { config, workspaceRoot, workspaceName } = options;
|
|
7528
|
-
if (!config || config.provider === "local") {
|
|
7529
|
-
const { LocalStateStore } = await Promise.resolve().then(() => require("./LocalStateStore-oX_zxj9V.cjs")).then((n) => n.LocalStateStore_exports);
|
|
7530
|
-
return new LocalStateStore(workspaceRoot);
|
|
7531
|
-
}
|
|
7532
|
-
if (isStateStore(config.provider)) return config.provider;
|
|
7533
|
-
if (isStateProvider(config.provider)) {
|
|
7534
|
-
const { LegacyStateStore } = await Promise.resolve().then(() => require("./LegacyStateStore-BlYXQQSE.cjs"));
|
|
7535
|
-
return new LegacyStateStore(config.provider, { warn: options.warn });
|
|
7536
|
-
}
|
|
7537
|
-
if (config.provider === "ssm") {
|
|
7538
|
-
if (!workspaceName) throw new StateStoreNeedsWorkspaceName("ssm");
|
|
7539
|
-
const { SSMStateStore } = await Promise.resolve().then(() => require("./SSMStateStore-CaCtrzFL.cjs"));
|
|
7540
|
-
const ssm = config;
|
|
7541
|
-
return SSMStateStore.create({
|
|
7542
|
-
workspaceName,
|
|
7543
|
-
region: ssm.region,
|
|
7544
|
-
profile: ssm.profile
|
|
7545
|
-
});
|
|
7546
|
-
}
|
|
7547
|
-
if (config.provider === "s3") {
|
|
7548
|
-
if (!workspaceName) throw new StateStoreNeedsWorkspaceName("s3");
|
|
7549
|
-
const { S3StateStore } = await Promise.resolve().then(() => require("./S3StateStore-DeI7jtvi.cjs"));
|
|
7550
|
-
const s3 = config;
|
|
7551
|
-
return S3StateStore.create({
|
|
7552
|
-
workspaceName,
|
|
7553
|
-
bucket: s3.bucket,
|
|
7554
|
-
prefix: s3.prefix,
|
|
7555
|
-
region: s3.region,
|
|
7556
|
-
profile: s3.profile
|
|
7557
|
-
});
|
|
7558
|
-
}
|
|
7559
|
-
throw new UnknownStateProvider(config);
|
|
7560
|
-
}
|
|
7561
|
-
//#endregion
|
|
7562
6650
|
//#region src/target/dokploy/dns/DnsProvider.ts
|
|
7563
6651
|
/**
|
|
7564
6652
|
* Check if value is a DnsProvider implementation.
|
|
@@ -7581,8 +6669,8 @@ async function createDnsProvider(options) {
|
|
|
7581
6669
|
if (isDnsProvider(config.provider)) return config.provider;
|
|
7582
6670
|
const provider = config.provider;
|
|
7583
6671
|
if (provider === "hostinger") {
|
|
7584
|
-
const { HostingerProvider } = await Promise.resolve().then(() => require("./HostingerProvider-
|
|
7585
|
-
return new HostingerProvider();
|
|
6672
|
+
const { HostingerProvider } = await Promise.resolve().then(() => require("./HostingerProvider-OdQmKXE3.cjs"));
|
|
6673
|
+
return new HostingerProvider(options.hostinger);
|
|
7586
6674
|
}
|
|
7587
6675
|
if (provider === "route53") {
|
|
7588
6676
|
const { Route53Provider } = await Promise.resolve().then(() => require("./Route53Provider-Bllus2NR.cjs"));
|
|
@@ -7594,7 +6682,7 @@ async function createDnsProvider(options) {
|
|
|
7594
6682
|
});
|
|
7595
6683
|
}
|
|
7596
6684
|
if (provider === "godaddy") {
|
|
7597
|
-
const { GoDaddyProvider } = await Promise.resolve().then(() => require("./GoDaddyProvider-
|
|
6685
|
+
const { GoDaddyProvider } = await Promise.resolve().then(() => require("./GoDaddyProvider-DzguxU_a.cjs"));
|
|
7598
6686
|
return new GoDaddyProvider(options.godaddy ?? {});
|
|
7599
6687
|
}
|
|
7600
6688
|
if (provider === "cloudflare") throw new DnsProviderNotImplemented("cloudflare");
|
|
@@ -8183,6 +7271,10 @@ async function verifyDnsRecords(appHostnames, serverIp, state) {
|
|
|
8183
7271
|
* resolver. One that is not the server stops the deploy with
|
|
8184
7272
|
* {@link HostNotPointingAtServer}, before Caddy or Traefik ask Let's Encrypt
|
|
8185
7273
|
* for a certificate it cannot get — and use up the rate limit trying.
|
|
7274
|
+
* - **The step** (`stageDns`): all three, as every deploy runs them — the plan applied through each provider,
|
|
7275
|
+
* what was written read back from the provider, and only the hosts no
|
|
7276
|
+
* provider wrote resolved publicly. A brand-new name is never looked up
|
|
7277
|
+
* before its record exists, so no resolver caches it as missing.
|
|
8186
7278
|
*/
|
|
8187
7279
|
/** The line that says how to set the server's address. */
|
|
8188
7280
|
function serverAddressHint(stage) {
|
|
@@ -8271,6 +7363,21 @@ function recordTtl(config) {
|
|
|
8271
7363
|
function providerName(config) {
|
|
8272
7364
|
return typeof config.provider === "string" ? config.provider : "custom";
|
|
8273
7365
|
}
|
|
7366
|
+
/**
|
|
7367
|
+
* A wildcard host: gkm writes one record per public host, so a stage on
|
|
7368
|
+
* another server — or a name someone else points elsewhere — is never caught
|
|
7369
|
+
* by a record it did not ask for.
|
|
7370
|
+
*/
|
|
7371
|
+
var DnsWildcardRefused = class extends require_errors.GkmError {
|
|
7372
|
+
stage;
|
|
7373
|
+
host;
|
|
7374
|
+
constructor(stage, host) {
|
|
7375
|
+
super(`'${stage}' would point ${host} at its server, and gkm never writes a wildcard record: each public host gets its own. Give the app a subdomain of its own instead.`);
|
|
7376
|
+
this.stage = stage;
|
|
7377
|
+
this.host = host;
|
|
7378
|
+
this.name = "DnsWildcardRefused";
|
|
7379
|
+
}
|
|
7380
|
+
};
|
|
8274
7381
|
/** The records a stage's hosts need — pure. */
|
|
8275
7382
|
function planStackDns(input) {
|
|
8276
7383
|
const { stage, server } = input;
|
|
@@ -8294,6 +7401,7 @@ function planStackDns(input) {
|
|
|
8294
7401
|
};
|
|
8295
7402
|
for (const raw of input.hosts) {
|
|
8296
7403
|
const host = raw.toLowerCase();
|
|
7404
|
+
if (host.includes("*")) throw new DnsWildcardRefused(stage, host);
|
|
8297
7405
|
const domain = findRootDomain(host, dns);
|
|
8298
7406
|
if (!domain) {
|
|
8299
7407
|
uncovered.push(host);
|
|
@@ -8323,6 +7431,22 @@ function planStackDns(input) {
|
|
|
8323
7431
|
uncovered: uncovered.sort()
|
|
8324
7432
|
};
|
|
8325
7433
|
}
|
|
7434
|
+
/** Records a provider accepted and then did not read back with their value. */
|
|
7435
|
+
var DnsRecordsNotConfirmed = class extends require_errors.GkmError {
|
|
7436
|
+
stage;
|
|
7437
|
+
domain;
|
|
7438
|
+
provider;
|
|
7439
|
+
records;
|
|
7440
|
+
constructor(stage, domain, provider, records) {
|
|
7441
|
+
const lines = records.map((r) => ` - ${r.host} ${r.type}: ${r.found.length > 0 ? r.found.join(", ") : "nothing"}, expected ${r.expected}`);
|
|
7442
|
+
super(`${provider} accepted '${stage}''s records for ${domain}, and reading them back does not show them:\n${lines.join("\n")}\nDeploy again; if it persists, check the records in the provider's console — another tool may be writing the same names.`);
|
|
7443
|
+
this.stage = stage;
|
|
7444
|
+
this.domain = domain;
|
|
7445
|
+
this.provider = provider;
|
|
7446
|
+
this.records = records;
|
|
7447
|
+
this.name = "DnsRecordsNotConfirmed";
|
|
7448
|
+
}
|
|
7449
|
+
};
|
|
8326
7450
|
/** `api.example.com A 203.0.113.10 (TTL 600)` */
|
|
8327
7451
|
function describe(record) {
|
|
8328
7452
|
return `${record.host.padEnd(32)} ${record.type.padEnd(5)} ${record.value} (TTL ${record.ttl})`;
|
|
@@ -8428,37 +7552,48 @@ function printChange(change, dryRun, log) {
|
|
|
8428
7552
|
}
|
|
8429
7553
|
}
|
|
8430
7554
|
/**
|
|
8431
|
-
* Each domain's records, through its provider
|
|
8432
|
-
*
|
|
7555
|
+
* Each domain's records, through its provider — and, with `confirm`, what was
|
|
7556
|
+
* written read back from it. A record already holding its value is neither
|
|
7557
|
+
* written nor read again.
|
|
7558
|
+
*
|
|
7559
|
+
* @throws {DnsRecordsNotConfirmed} when a written record does not read back
|
|
8433
7560
|
*/
|
|
8434
|
-
async function
|
|
7561
|
+
async function settleStackDns(plan, options = {}) {
|
|
8435
7562
|
const dryRun = options.dryRun === true;
|
|
8436
7563
|
const log = options.log ?? ((line) => console.log(line));
|
|
8437
7564
|
const providerFor = options.providerFor ?? ((config) => createDnsProvider({
|
|
8438
7565
|
config,
|
|
8439
|
-
...options.credentials ? { godaddy: options.credentials } : {}
|
|
7566
|
+
...options.credentials ? { godaddy: options.credentials } : {},
|
|
7567
|
+
...options.hostinger ? { hostinger: options.hostinger } : {}
|
|
8440
7568
|
}));
|
|
8441
7569
|
const changes = [];
|
|
7570
|
+
const confirmed = /* @__PURE__ */ new Set();
|
|
7571
|
+
const unconfirmed = /* @__PURE__ */ new Set();
|
|
7572
|
+
const hostsOf = (domain) => new Set(domain.records.map((r) => r.host));
|
|
8442
7573
|
for (const host of plan.uncovered) log(` ⚠ ${host} is under no domain in dns — point it at ${plan.server.ipv4} yourself`);
|
|
8443
7574
|
for (const domain of plan.domains) {
|
|
8444
7575
|
if (domain.provider === "manual") {
|
|
8445
7576
|
changes.push(...printManual(domain, log, "manual"));
|
|
7577
|
+
for (const host of hostsOf(domain)) unconfirmed.add(host);
|
|
8446
7578
|
continue;
|
|
8447
7579
|
}
|
|
8448
7580
|
const provider = await providerFor(domain.config);
|
|
8449
7581
|
if (!provider) {
|
|
8450
7582
|
changes.push(...printManual(domain, log, "manual"));
|
|
7583
|
+
for (const host of hostsOf(domain)) unconfirmed.add(host);
|
|
8451
7584
|
continue;
|
|
8452
7585
|
}
|
|
8453
7586
|
log(` ${domain.domain} (${provider.name})${dryRun ? " — dry run" : ""}`);
|
|
8454
7587
|
let existing;
|
|
8455
7588
|
let diff;
|
|
7589
|
+
let unreadable = false;
|
|
8456
7590
|
try {
|
|
8457
7591
|
existing = await existingRecords(provider, domain);
|
|
8458
7592
|
diff = diffDomainDns(domain, existing);
|
|
8459
7593
|
} catch (error) {
|
|
8460
7594
|
if (!(error instanceof DnsRecordsUnreadable)) throw error;
|
|
8461
7595
|
log(` ℹ The ${provider.name} key cannot read ${domain.domain}'s DNS records, so changes cannot be diffed: every record is written (idempotent).`);
|
|
7596
|
+
unreadable = true;
|
|
8462
7597
|
existing = [];
|
|
8463
7598
|
diff = domain.records.map((r) => ({
|
|
8464
7599
|
domain: r.domain,
|
|
@@ -8476,6 +7611,10 @@ async function applyStackDns(plan, options = {}) {
|
|
|
8476
7611
|
if (aaaa && planned && aaaa.values.length > 0) log(` ⚠ ${fullName(domain.domain, name)} also has AAAA ${aaaa.values.join(", ")}: set ${require_workspace.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`);
|
|
8477
7612
|
}
|
|
8478
7613
|
changes.push(...diff);
|
|
7614
|
+
const written = new Set(diff.filter((c) => c.action !== "unchanged").map((c) => fullName(c.domain, c.name)));
|
|
7615
|
+
for (const host of hostsOf(domain)) if (unreadable) {
|
|
7616
|
+
if (!dryRun) unconfirmed.add(host);
|
|
7617
|
+
} else if (!written.has(host)) confirmed.add(host);
|
|
8479
7618
|
if (dryRun) continue;
|
|
8480
7619
|
const deletes = diff.filter((c) => c.action === "delete");
|
|
8481
7620
|
if (deletes.length > 0) await provider.deleteRecords(domain.domain, deletes.map((c) => ({
|
|
@@ -8489,403 +7628,264 @@ async function applyStackDns(plan, options = {}) {
|
|
|
8489
7628
|
value: c.value,
|
|
8490
7629
|
ttl: c.ttl
|
|
8491
7630
|
})));
|
|
8492
|
-
|
|
8493
|
-
|
|
8494
|
-
|
|
8495
|
-
|
|
8496
|
-
const systemLookup = async (host) => {
|
|
8497
|
-
return (await (0, node_dns_promises.lookup)(host, { all: true })).map((a) => a.address);
|
|
8498
|
-
};
|
|
8499
|
-
/** Hosts that do not resolve to the stage's server. */
|
|
8500
|
-
var HostNotPointingAtServer = class extends require_errors.GkmError {
|
|
8501
|
-
stage;
|
|
8502
|
-
hosts;
|
|
8503
|
-
expected;
|
|
8504
|
-
fix;
|
|
8505
|
-
constructor(stage, hosts, expected, fix) {
|
|
8506
|
-
const want = expected.ipv6 ? `${expected.ipv4} (and ${expected.ipv6})` : expected.ipv4;
|
|
8507
|
-
const lines = hosts.map((h) => ` - ${h.host} → ${h.resolved.length > 0 ? h.resolved.join(", ") : `nothing${h.error ? ` (${h.error})` : ""}`}, expected ${want}`);
|
|
8508
|
-
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.`);
|
|
8509
|
-
this.stage = stage;
|
|
8510
|
-
this.hosts = hosts;
|
|
8511
|
-
this.expected = expected;
|
|
8512
|
-
this.fix = fix;
|
|
8513
|
-
this.name = "HostNotPointingAtServer";
|
|
8514
|
-
}
|
|
8515
|
-
};
|
|
8516
|
-
/**
|
|
8517
|
-
* Every host resolved: each must resolve to the server's IPv4 address and to
|
|
8518
|
-
* nothing but the server's addresses.
|
|
8519
|
-
*
|
|
8520
|
-
* @throws {HostNotPointingAtServer} naming every host that does not
|
|
8521
|
-
*/
|
|
8522
|
-
async function checkStackDns(input) {
|
|
8523
|
-
const lookup = input.lookup ?? systemLookup;
|
|
8524
|
-
const { server } = input;
|
|
8525
|
-
const ours = new Set([server.ipv4, server.ipv6].filter(Boolean).map((a) => a.toLowerCase()));
|
|
8526
|
-
const bad = [];
|
|
8527
|
-
const ok = [];
|
|
8528
|
-
for (const host of input.hosts) {
|
|
8529
|
-
let resolved;
|
|
8530
|
-
try {
|
|
8531
|
-
resolved = [...new Set(await lookup(host))];
|
|
8532
|
-
} catch (error) {
|
|
8533
|
-
const code = error && typeof error === "object" && "code" in error ? String(error.code) : error instanceof Error ? error.message : String(error);
|
|
8534
|
-
bad.push({
|
|
8535
|
-
host,
|
|
8536
|
-
resolved: [],
|
|
8537
|
-
error: code
|
|
8538
|
-
});
|
|
8539
|
-
continue;
|
|
8540
|
-
}
|
|
8541
|
-
if (resolved.some((a) => a.toLowerCase() === server.ipv4.toLowerCase()) && resolved.every((a) => ours.has(a.toLowerCase()))) ok.push(host);
|
|
8542
|
-
else bad.push({
|
|
8543
|
-
host,
|
|
8544
|
-
resolved
|
|
8545
|
-
});
|
|
8546
|
-
}
|
|
8547
|
-
if (bad.length > 0) throw new HostNotPointingAtServer(input.stage, bad, {
|
|
8548
|
-
ipv4: server.ipv4,
|
|
8549
|
-
...server.ipv6 ? { ipv6: server.ipv6 } : {}
|
|
8550
|
-
}, dnsFix(input, bad));
|
|
8551
|
-
return ok;
|
|
8552
|
-
}
|
|
8553
|
-
/** How to fix it: `gkm setup`, or the records to create by hand. */
|
|
8554
|
-
function dnsFix(input, bad) {
|
|
8555
|
-
const hosts = bad.map((h) => h.host);
|
|
8556
|
-
let plan;
|
|
8557
|
-
try {
|
|
8558
|
-
plan = planStackDns({
|
|
8559
|
-
stage: input.stage,
|
|
8560
|
-
hosts,
|
|
8561
|
-
server: input.server,
|
|
8562
|
-
dns: input.dns
|
|
8563
|
-
});
|
|
8564
|
-
} catch {
|
|
8565
|
-
plan = {
|
|
8566
|
-
stage: input.stage,
|
|
8567
|
-
server: input.server,
|
|
8568
|
-
domains: [],
|
|
8569
|
-
uncovered: hosts
|
|
7631
|
+
if (!options.confirm || unreadable || written.size === 0) continue;
|
|
7632
|
+
const rewritten = {
|
|
7633
|
+
...domain,
|
|
7634
|
+
records: domain.records.filter((r) => written.has(r.host))
|
|
8570
7635
|
};
|
|
7636
|
+
const after = await existingRecords(provider, rewritten);
|
|
7637
|
+
const missing = [];
|
|
7638
|
+
for (const record of rewritten.records) {
|
|
7639
|
+
const values = after.find((r) => r.name === record.name && r.type === record.type)?.values ?? [];
|
|
7640
|
+
if (values.length !== 1 || !same(values[0], record.value)) missing.push({
|
|
7641
|
+
host: record.host,
|
|
7642
|
+
type: record.type,
|
|
7643
|
+
expected: record.value,
|
|
7644
|
+
found: values
|
|
7645
|
+
});
|
|
7646
|
+
}
|
|
7647
|
+
if (missing.length > 0) throw new DnsRecordsNotConfirmed(plan.stage, domain.domain, provider.name, missing);
|
|
7648
|
+
for (const host of written) confirmed.add(host);
|
|
7649
|
+
log(` ✓ ${written.size} host${written.size === 1 ? "" : "s"} read back from ${provider.name}`);
|
|
8571
7650
|
}
|
|
8572
|
-
const
|
|
8573
|
-
|
|
8574
|
-
|
|
8575
|
-
|
|
8576
|
-
|
|
8577
|
-
|
|
8578
|
-
}
|
|
8579
|
-
//#endregion
|
|
8580
|
-
//#region src/docker/imageRef.ts
|
|
8581
|
-
/**
|
|
8582
|
-
* An image reference, checked before it reaches `docker`.
|
|
8583
|
-
*
|
|
8584
|
-
* The parts come from the workspace — a registry in `gkm.config.ts`, an image
|
|
8585
|
-
* name from the project, a tag from `--tag` — and an argument array already
|
|
8586
|
-
* keeps them out of a shell. What it cannot do is stop `docker` reading a value
|
|
8587
|
-
* that starts with `-` as one of its own flags, so a ref is held to Docker's
|
|
8588
|
-
* own grammar (distribution/reference) and anything else is refused by name
|
|
8589
|
-
* before a build starts.
|
|
8590
|
-
*/
|
|
8591
|
-
const DOMAIN_COMPONENT = "(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])";
|
|
8592
|
-
const DOMAIN = `${DOMAIN_COMPONENT}(?:\\.${DOMAIN_COMPONENT})*(?::[0-9]+)?`;
|
|
8593
|
-
const PATH_COMPONENT = "[a-z0-9]+(?:(?:[._]|__|-+)[a-z0-9]+)*";
|
|
8594
|
-
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,})?$`);
|
|
8595
|
-
/** Docker refuses a repository name longer than this. */
|
|
8596
|
-
const MAX_NAME_LENGTH = 255;
|
|
8597
|
-
/** A value that is not an image reference was about to be built or pushed. */
|
|
8598
|
-
var ImageRefInvalid = class extends require_errors.GkmError {
|
|
8599
|
-
ref;
|
|
8600
|
-
constructor(ref) {
|
|
8601
|
-
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.`);
|
|
8602
|
-
this.ref = ref;
|
|
8603
|
-
this.name = "ImageRefInvalid";
|
|
8604
|
-
}
|
|
8605
|
-
};
|
|
8606
|
-
/** Throw {@link ImageRefInvalid} unless `ref` is a Docker image reference. */
|
|
8607
|
-
function validateImageRef(ref) {
|
|
8608
|
-
const name = ref.split("@")[0].replace(/:[\w][\w.-]*$/, "");
|
|
8609
|
-
if (!REFERENCE.test(ref) || name.length > MAX_NAME_LENGTH) throw new ImageRefInvalid(ref);
|
|
8610
|
-
return ref;
|
|
8611
|
-
}
|
|
8612
|
-
//#endregion
|
|
8613
|
-
//#region src/docker/index.ts
|
|
8614
|
-
const logger = require_discover.output;
|
|
8615
|
-
/** `docker build` failed; what docker printed above says why. */
|
|
8616
|
-
var DockerBuildFailed = class extends Error {
|
|
8617
|
-
imageRef;
|
|
8618
|
-
constructor(imageRef, cause) {
|
|
8619
|
-
super(`Failed to build Docker image: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
8620
|
-
this.imageRef = imageRef;
|
|
8621
|
-
this.name = "DockerBuildFailed";
|
|
8622
|
-
}
|
|
8623
|
-
};
|
|
8624
|
-
/** `docker push` failed — usually the registry login or its permissions. */
|
|
8625
|
-
var DockerPushFailed = class extends Error {
|
|
8626
|
-
imageRef;
|
|
8627
|
-
constructor(imageRef, cause) {
|
|
8628
|
-
super(`Failed to push Docker image: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
8629
|
-
this.imageRef = imageRef;
|
|
8630
|
-
this.name = "DockerPushFailed";
|
|
8631
|
-
}
|
|
8632
|
-
};
|
|
8633
|
-
/** `gkm docker --push` was asked for with nowhere to push to. */
|
|
8634
|
-
var PushNeedsRegistry = class extends require_errors.GkmError {
|
|
8635
|
-
imageName;
|
|
8636
|
-
constructor(imageName) {
|
|
8637
|
-
super(`Registry is required to push Docker image '${imageName}'. Use --registry or configure docker.registry in gkm.config.ts`);
|
|
8638
|
-
this.imageName = imageName;
|
|
8639
|
-
this.name = "PushNeedsRegistry";
|
|
8640
|
-
}
|
|
8641
|
-
};
|
|
8642
|
-
/**
|
|
8643
|
-
* Docker command implementation: one Dockerfile per deployable app, each
|
|
8644
|
-
* building its image inside Docker from a turbo-pruned slice of the build
|
|
8645
|
-
* root — nothing is built on the host — plus the build root's
|
|
8646
|
-
* `.dockerignore` and `docker-compose.constructs.yml`.
|
|
8647
|
-
*/
|
|
8648
|
-
async function dockerCommand(options) {
|
|
8649
|
-
const cwd = options.cwd ?? process.cwd();
|
|
8650
|
-
const loaded = await require_config.loadWorkspaceConfig(cwd);
|
|
8651
|
-
const names = loaded.type === "single" ? Object.fromEntries(Object.keys(loaded.workspace.apps).map((name) => [name, resolveDockerConfig(loaded.raw).imageName])) : {};
|
|
8652
|
-
const result = await workspaceDockerCommand(loaded.workspace, loaded.manifest, names, loaded.background, options.stage);
|
|
8653
|
-
if (options.build) for (const app of result.apps) await buildDockerImage(app, result.buildRoot, options);
|
|
8654
|
-
if (options.push) for (const app of result.apps) await pushDockerImage(app.imageName, options, result.buildRoot);
|
|
8655
|
-
return result;
|
|
8656
|
-
}
|
|
8657
|
-
/**
|
|
8658
|
-
* Build one app's image, from the build root.
|
|
8659
|
-
* Uses BuildKit for cache mount support
|
|
8660
|
-
*/
|
|
8661
|
-
async function buildDockerImage(app, buildRoot, options) {
|
|
8662
|
-
const tag = options.tag ?? "latest";
|
|
8663
|
-
const registry = options.registry;
|
|
8664
|
-
const fullImageName = validateImageRef(registry ? `${registry}/${app.imageName}:${tag}` : `${app.imageName}:${tag}`);
|
|
8665
|
-
logger.log(`\n🐳 Building Docker image: ${fullImageName}`);
|
|
8666
|
-
try {
|
|
8667
|
-
await require_credentials.run("docker", [
|
|
8668
|
-
"build",
|
|
8669
|
-
`--file=${(0, node_path.relative)(buildRoot, app.dockerfile)}`,
|
|
8670
|
-
`--tag=${fullImageName}`,
|
|
8671
|
-
"."
|
|
8672
|
-
], {
|
|
8673
|
-
cwd: buildRoot,
|
|
8674
|
-
env: {
|
|
8675
|
-
...process.env,
|
|
8676
|
-
DOCKER_BUILDKIT: "1"
|
|
8677
|
-
}
|
|
8678
|
-
});
|
|
8679
|
-
logger.log(`✅ Docker image built: ${fullImageName}`);
|
|
8680
|
-
} catch (error) {
|
|
8681
|
-
throw new DockerBuildFailed(fullImageName, error);
|
|
8682
|
-
}
|
|
8683
|
-
}
|
|
8684
|
-
/**
|
|
8685
|
-
* Push Docker image to registry
|
|
8686
|
-
*/
|
|
8687
|
-
async function pushDockerImage(imageName, options, cwd) {
|
|
8688
|
-
const tag = options.tag ?? "latest";
|
|
8689
|
-
const registry = options.registry;
|
|
8690
|
-
if (!registry) throw new PushNeedsRegistry(imageName);
|
|
8691
|
-
const fullImageName = validateImageRef(`${registry}/${imageName}:${tag}`);
|
|
8692
|
-
logger.log(`\n🚀 Pushing Docker image: ${fullImageName}`);
|
|
8693
|
-
try {
|
|
8694
|
-
await require_credentials.run("docker", ["push", fullImageName], { cwd });
|
|
8695
|
-
logger.log(`✅ Docker image pushed: ${fullImageName}`);
|
|
8696
|
-
} catch (error) {
|
|
8697
|
-
throw new DockerPushFailed(fullImageName, error);
|
|
8698
|
-
}
|
|
7651
|
+
for (const host of plan.uncovered) unconfirmed.add(host);
|
|
7652
|
+
return {
|
|
7653
|
+
changes,
|
|
7654
|
+
confirmed: [...confirmed].sort(),
|
|
7655
|
+
unconfirmed: [...unconfirmed].filter((h) => !confirmed.has(h)).sort()
|
|
7656
|
+
};
|
|
8699
7657
|
}
|
|
7658
|
+
/** The system resolver: what the server's own clients would be told. */
|
|
7659
|
+
const systemLookup = async (host) => {
|
|
7660
|
+
return (await (0, node_dns_promises.lookup)(host, { all: true })).map((a) => a.address);
|
|
7661
|
+
};
|
|
7662
|
+
/** Hosts that do not resolve to the stage's server. */
|
|
7663
|
+
var HostNotPointingAtServer = class extends require_errors.GkmError {
|
|
7664
|
+
stage;
|
|
7665
|
+
hosts;
|
|
7666
|
+
expected;
|
|
7667
|
+
fix;
|
|
7668
|
+
constructor(stage, hosts, expected, fix) {
|
|
7669
|
+
const want = expected.ipv6 ? `${expected.ipv4} (and ${expected.ipv6})` : expected.ipv4;
|
|
7670
|
+
const lines = hosts.map((h) => ` - ${h.host} → ${h.resolved.length > 0 ? h.resolved.join(", ") : `nothing${h.error ? ` (${h.error})` : ""}`}, expected ${want}`);
|
|
7671
|
+
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.`);
|
|
7672
|
+
this.stage = stage;
|
|
7673
|
+
this.hosts = hosts;
|
|
7674
|
+
this.expected = expected;
|
|
7675
|
+
this.fix = fix;
|
|
7676
|
+
this.name = "HostNotPointingAtServer";
|
|
7677
|
+
}
|
|
7678
|
+
};
|
|
8700
7679
|
/**
|
|
8701
|
-
*
|
|
8702
|
-
*
|
|
7680
|
+
* Every host resolved: each must resolve to the server's IPv4 address and to
|
|
7681
|
+
* nothing but the server's addresses.
|
|
7682
|
+
*
|
|
7683
|
+
* @throws {HostNotPointingAtServer} naming every host that does not
|
|
8703
7684
|
*/
|
|
8704
|
-
async function
|
|
8705
|
-
const
|
|
8706
|
-
const
|
|
8707
|
-
|
|
8708
|
-
|
|
8709
|
-
|
|
8710
|
-
for (const
|
|
8711
|
-
|
|
8712
|
-
|
|
7685
|
+
async function checkStackDns(input) {
|
|
7686
|
+
const lookup = input.lookup ?? systemLookup;
|
|
7687
|
+
const { server } = input;
|
|
7688
|
+
const ours = new Set([server.ipv4, server.ipv6].filter(Boolean).map((a) => a.toLowerCase()));
|
|
7689
|
+
const bad = [];
|
|
7690
|
+
const ok = [];
|
|
7691
|
+
for (const host of input.hosts) {
|
|
7692
|
+
let resolved;
|
|
7693
|
+
try {
|
|
7694
|
+
resolved = [...new Set(await lookup(host))];
|
|
7695
|
+
} catch (error) {
|
|
7696
|
+
const code = error && typeof error === "object" && "code" in error ? String(error.code) : error instanceof Error ? error.message : String(error);
|
|
7697
|
+
bad.push({
|
|
7698
|
+
host,
|
|
7699
|
+
resolved: [],
|
|
7700
|
+
error: code
|
|
7701
|
+
});
|
|
8713
7702
|
continue;
|
|
8714
7703
|
}
|
|
8715
|
-
|
|
8716
|
-
|
|
8717
|
-
|
|
8718
|
-
|
|
8719
|
-
workspaceRoot: workspace.root,
|
|
8720
|
-
apps: workspace.apps,
|
|
8721
|
-
...manifest ? { manifest } : {},
|
|
8722
|
-
workspace,
|
|
8723
|
-
...stage ? { stage } : {}
|
|
8724
|
-
});
|
|
8725
|
-
const path = dockerfileOf(appName, app.path);
|
|
8726
|
-
const dockerfilePath = (0, node_path.join)(workspace.root, path);
|
|
8727
|
-
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(dockerfilePath), { recursive: true });
|
|
8728
|
-
await (0, node_fs_promises.writeFile)(dockerfilePath, dockerfile);
|
|
8729
|
-
logger.log(` Generated: ${path}`);
|
|
8730
|
-
results.push({
|
|
8731
|
-
appName,
|
|
8732
|
-
type: app.type,
|
|
8733
|
-
dockerfile: dockerfilePath,
|
|
8734
|
-
imageName: imageNames[appName] ?? appName
|
|
7704
|
+
if (resolved.some((a) => a.toLowerCase() === server.ipv4.toLowerCase()) && resolved.every((a) => ours.has(a.toLowerCase()))) ok.push(host);
|
|
7705
|
+
else bad.push({
|
|
7706
|
+
host,
|
|
7707
|
+
resolved
|
|
8735
7708
|
});
|
|
8736
7709
|
}
|
|
8737
|
-
|
|
8738
|
-
|
|
8739
|
-
|
|
8740
|
-
|
|
8741
|
-
|
|
8742
|
-
|
|
8743
|
-
|
|
8744
|
-
|
|
8745
|
-
|
|
8746
|
-
|
|
8747
|
-
|
|
8748
|
-
|
|
8749
|
-
|
|
8750
|
-
|
|
8751
|
-
|
|
8752
|
-
|
|
7710
|
+
if (bad.length > 0) throw new HostNotPointingAtServer(input.stage, bad, {
|
|
7711
|
+
ipv4: server.ipv4,
|
|
7712
|
+
...server.ipv6 ? { ipv6: server.ipv6 } : {}
|
|
7713
|
+
}, dnsFix(input, bad));
|
|
7714
|
+
return ok;
|
|
7715
|
+
}
|
|
7716
|
+
/** How to fix it: the deploy, or the records to create by hand. */
|
|
7717
|
+
function dnsFix(input, bad) {
|
|
7718
|
+
const hosts = bad.map((h) => h.host);
|
|
7719
|
+
let plan;
|
|
7720
|
+
try {
|
|
7721
|
+
plan = planStackDns({
|
|
7722
|
+
stage: input.stage,
|
|
7723
|
+
hosts,
|
|
7724
|
+
server: input.server,
|
|
7725
|
+
dns: input.dns
|
|
8753
7726
|
});
|
|
7727
|
+
} catch {
|
|
7728
|
+
plan = {
|
|
7729
|
+
stage: input.stage,
|
|
7730
|
+
server: input.server,
|
|
7731
|
+
domains: [],
|
|
7732
|
+
uncovered: hosts
|
|
7733
|
+
};
|
|
8754
7734
|
}
|
|
8755
|
-
const
|
|
8756
|
-
|
|
8757
|
-
const
|
|
8758
|
-
|
|
8759
|
-
|
|
8760
|
-
|
|
8761
|
-
const icon = result.type === "backend" ? "⚙️" : result.type === "mobile" ? "📱" : "🌐";
|
|
8762
|
-
logger.log(` ${icon} docker build -f ${(0, node_path.relative)(layout.buildRoot, result.dockerfile)} -t ${result.imageName} .`);
|
|
8763
|
-
}
|
|
8764
|
-
printRunInstructions(workspace);
|
|
8765
|
-
return {
|
|
8766
|
-
apps: results,
|
|
8767
|
-
dockerCompose: composePath,
|
|
8768
|
-
dockerignore: dockerignorePath,
|
|
8769
|
-
buildRoot: layout.buildRoot
|
|
8770
|
-
};
|
|
7735
|
+
const automatic = plan.domains.filter((d) => d.provider !== "manual");
|
|
7736
|
+
const byHand = [...plan.domains.filter((d) => d.provider === "manual").flatMap((d) => d.records.map(describe)), ...plan.uncovered.flatMap((host) => [`${host.padEnd(32)} A ${input.server.ipv4}`, ...input.server.ipv6 ? [`${host.padEnd(32)} AAAA ${input.server.ipv6}`] : []])];
|
|
7737
|
+
const lines = [];
|
|
7738
|
+
if (automatic.length > 0) lines.push(`Fix: gkm deploy --stage ${input.stage} writes the records for ${automatic.map((d) => d.domain).join(", ")} through their provider (--dry-run shows them first).`);
|
|
7739
|
+
if (byHand.length > 0) lines.push(`${automatic.length > 0 ? "And create" : "Fix: create"} these records at the DNS host:`, ...byHand.map((line) => ` ${line}`));
|
|
7740
|
+
return lines.join("\n");
|
|
8771
7741
|
}
|
|
7742
|
+
/** The environment variable each token-authenticated provider reads. */
|
|
7743
|
+
const DNS_TOKEN_KEYS = {
|
|
7744
|
+
godaddy: "GODADDY_API_TOKEN",
|
|
7745
|
+
hostinger: "HOSTINGER_API_TOKEN"
|
|
7746
|
+
};
|
|
8772
7747
|
/**
|
|
8773
|
-
*
|
|
8774
|
-
*
|
|
8775
|
-
* framework. The one choice every builder of images makes.
|
|
7748
|
+
* A domain whose records the deploy writes, and no token to write them with.
|
|
7749
|
+
* Raised before anything is built, started or written.
|
|
8776
7750
|
*/
|
|
8777
|
-
|
|
8778
|
-
|
|
8779
|
-
|
|
8780
|
-
|
|
8781
|
-
|
|
8782
|
-
|
|
8783
|
-
|
|
8784
|
-
|
|
8785
|
-
|
|
8786
|
-
|
|
8787
|
-
|
|
8788
|
-
|
|
8789
|
-
|
|
8790
|
-
|
|
8791
|
-
healthCheckPath: "/health",
|
|
8792
|
-
...options.cache ? { cache: options.cache } : {}
|
|
8793
|
-
});
|
|
8794
|
-
}
|
|
8795
|
-
/** Where `gkm docker` writes a worker's Dockerfile. */
|
|
8796
|
-
function workerDockerfileOf(worker) {
|
|
8797
|
-
return `.gkm/docker/Dockerfile.${worker}`;
|
|
8798
|
-
}
|
|
7751
|
+
var DnsCredentialMissing = class extends require_errors.GkmError {
|
|
7752
|
+
stage;
|
|
7753
|
+
domain;
|
|
7754
|
+
provider;
|
|
7755
|
+
key;
|
|
7756
|
+
constructor(stage, domain, provider, key) {
|
|
7757
|
+
super(`'${stage}''s hosts are under ${domain}, whose DNS is ${provider}'s, and the deploy writes their records — but ${key} is not set. In CI, add ${key} as a secret on the '${stage}' GitHub environment and pass it to the step that writes the records: env: { ${key}: \${{ secrets.${key} }} }. On this machine, export ${key} or run gkm login --provider ${provider}. With a CDN or proxy in front of the server, pass --skip-dns.`);
|
|
7758
|
+
this.stage = stage;
|
|
7759
|
+
this.domain = domain;
|
|
7760
|
+
this.provider = provider;
|
|
7761
|
+
this.key = key;
|
|
7762
|
+
this.name = "DnsCredentialMissing";
|
|
7763
|
+
}
|
|
7764
|
+
};
|
|
8799
7765
|
/**
|
|
8800
|
-
*
|
|
8801
|
-
*
|
|
7766
|
+
* The `dns` domain a stage's own domain (`domains.<stage>`) is under — where
|
|
7767
|
+
* its hosts' records go — or none.
|
|
8802
7768
|
*/
|
|
8803
|
-
function
|
|
8804
|
-
|
|
8805
|
-
|
|
8806
|
-
|
|
8807
|
-
|
|
8808
|
-
|
|
8809
|
-
|
|
8810
|
-
|
|
8811
|
-
|
|
8812
|
-
});
|
|
7769
|
+
function stageDnsDomains(stage, domains, dns) {
|
|
7770
|
+
const own = domains?.[stage];
|
|
7771
|
+
if (!own || !dns || isLocalHost(own)) return [];
|
|
7772
|
+
const normalized = normalizeDnsConfig(dns);
|
|
7773
|
+
const root = findRootDomain(own.toLowerCase(), normalized);
|
|
7774
|
+
return root ? [{
|
|
7775
|
+
domain: root,
|
|
7776
|
+
config: normalized[root]
|
|
7777
|
+
}] : [];
|
|
8813
7778
|
}
|
|
8814
7779
|
/**
|
|
8815
|
-
*
|
|
7780
|
+
* Each token the domains' providers need, from `credentials` — `manual`,
|
|
7781
|
+
* Route53 (the AWS SDK's own chain) and a provider object need none.
|
|
8816
7782
|
*
|
|
8817
|
-
*
|
|
8818
|
-
* public key its edges give it, `VITE_UPLOADS_SERVER_URL` included — and an
|
|
8819
|
-
* `ARG` the Dockerfile does not declare is a build argument the build drops,
|
|
8820
|
-
* leaving the bundle with an empty URL.
|
|
7783
|
+
* @throws {DnsCredentialMissing} naming the first domain with no token
|
|
8821
7784
|
*/
|
|
8822
|
-
function
|
|
8823
|
-
const
|
|
8824
|
-
|
|
8825
|
-
|
|
8826
|
-
|
|
8827
|
-
|
|
8828
|
-
|
|
8829
|
-
|
|
8830
|
-
|
|
8831
|
-
case "tanstack-start":
|
|
8832
|
-
case "remix": return generateNodeWebDockerfile(webOpts);
|
|
8833
|
-
default: return generateNextjsDockerfile(webOpts);
|
|
7785
|
+
async function dnsCredentials(stage, domains, credentials) {
|
|
7786
|
+
const found = {};
|
|
7787
|
+
for (const { domain, config } of domains) {
|
|
7788
|
+
const provider = config.provider;
|
|
7789
|
+
if (provider !== "godaddy" && provider !== "hostinger") continue;
|
|
7790
|
+
if (found[provider]) continue;
|
|
7791
|
+
const token = await credentials.get({ kind: provider });
|
|
7792
|
+
if (!token?.token) throw new DnsCredentialMissing(stage, domain, provider, DNS_TOKEN_KEYS[provider]);
|
|
7793
|
+
found[provider] = token;
|
|
8834
7794
|
}
|
|
7795
|
+
return found;
|
|
8835
7796
|
}
|
|
8836
7797
|
/**
|
|
8837
|
-
*
|
|
8838
|
-
*
|
|
8839
|
-
*
|
|
8840
|
-
*
|
|
8841
|
-
|
|
8842
|
-
|
|
8843
|
-
|
|
8844
|
-
|
|
8845
|
-
|
|
8846
|
-
|
|
8847
|
-
|
|
8848
|
-
"**/.gkm",
|
|
8849
|
-
"**/.gkm/compose"
|
|
8850
|
-
];
|
|
8851
|
-
/**
|
|
8852
|
-
* Make sure the build root's `.dockerignore` leaves out what no image may
|
|
8853
|
-
* hold: written whole where there is none, and each missing line appended to
|
|
8854
|
-
* one the project keeps.
|
|
7798
|
+
* A stage's DNS, as every deploy runs it: the
|
|
7799
|
+
* stage's own hosts planned, written through each domain's provider where
|
|
7800
|
+
* they differ, and read back from it. Only A, AAAA and CNAME records of
|
|
7801
|
+
* those names are touched; nothing is a wildcard, and no other name is read,
|
|
7802
|
+
* written or deleted. What no provider confirms — a `manual` domain, a host
|
|
7803
|
+
* under no domain in `dns`, a key that cannot read — is resolved publicly.
|
|
7804
|
+
* A dry run prints the plan and writes nothing.
|
|
7805
|
+
*
|
|
7806
|
+
* @throws {DnsCredentialMissing} before anything is read or written
|
|
7807
|
+
* @throws {DnsRecordsNotConfirmed} when a written record does not read back
|
|
7808
|
+
* @throws {HostNotPointingAtServer} when a resolved host is not the server
|
|
8855
7809
|
*/
|
|
8856
|
-
async function
|
|
8857
|
-
const
|
|
8858
|
-
|
|
8859
|
-
|
|
8860
|
-
|
|
8861
|
-
|
|
8862
|
-
|
|
8863
|
-
|
|
8864
|
-
|
|
8865
|
-
|
|
8866
|
-
|
|
8867
|
-
|
|
7810
|
+
async function stageDns(input) {
|
|
7811
|
+
const { stage, server } = input;
|
|
7812
|
+
const log = input.log ?? ((line) => console.log(line));
|
|
7813
|
+
const plan = planStackDns({
|
|
7814
|
+
stage,
|
|
7815
|
+
hosts: input.hosts,
|
|
7816
|
+
server,
|
|
7817
|
+
dns: input.dns
|
|
7818
|
+
});
|
|
7819
|
+
const tokens = await dnsCredentials(stage, plan.domains.map(({ domain, config }) => ({
|
|
7820
|
+
domain,
|
|
7821
|
+
config
|
|
7822
|
+
})), input.credentials);
|
|
7823
|
+
const applied = !input.dns ? {
|
|
7824
|
+
changes: [],
|
|
7825
|
+
confirmed: [],
|
|
7826
|
+
unconfirmed: [...plan.uncovered]
|
|
7827
|
+
} : await settleStackDns(plan, {
|
|
7828
|
+
...input.dryRun ? { dryRun: true } : { confirm: true },
|
|
7829
|
+
log,
|
|
7830
|
+
credentials: {
|
|
7831
|
+
...input.godaddy ?? {},
|
|
7832
|
+
...tokens.godaddy ? { credential: tokens.godaddy } : {},
|
|
7833
|
+
log
|
|
7834
|
+
},
|
|
7835
|
+
...tokens.hostinger ? { hostinger: tokens.hostinger } : {},
|
|
7836
|
+
...input.providerFor ? { providerFor: input.providerFor } : {}
|
|
7837
|
+
});
|
|
7838
|
+
let resolved = [];
|
|
7839
|
+
if (applied.unconfirmed.length > 0) resolved = await checkStackDns({
|
|
7840
|
+
stage,
|
|
7841
|
+
hosts: applied.unconfirmed,
|
|
7842
|
+
server,
|
|
7843
|
+
dns: input.dns,
|
|
7844
|
+
...input.lookup ? { lookup: input.lookup } : {}
|
|
7845
|
+
});
|
|
7846
|
+
return {
|
|
7847
|
+
plan,
|
|
7848
|
+
changes: applied.changes,
|
|
7849
|
+
confirmed: applied.confirmed,
|
|
7850
|
+
resolved
|
|
7851
|
+
};
|
|
7852
|
+
}
|
|
7853
|
+
//#endregion
|
|
7854
|
+
//#region src/providers/dns.ts
|
|
7855
|
+
var dns_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
|
|
7856
|
+
composeStageHosts: () => composeStageHosts,
|
|
7857
|
+
deploysWithCompose: () => deploysWithCompose
|
|
7858
|
+
});
|
|
7859
|
+
/** Whether the workspace deploys through the compose target. */
|
|
7860
|
+
function deploysWithCompose(workspace) {
|
|
7861
|
+
return workspace.deploy?.default === "compose" || Object.values(workspace.apps).some((app) => app.resolvedDeployTarget === "compose");
|
|
8868
7862
|
}
|
|
8869
7863
|
/**
|
|
8870
|
-
*
|
|
8871
|
-
* the
|
|
8872
|
-
*
|
|
8873
|
-
* Through reconcile rather than a second generator, so what `gkm docker`
|
|
8874
|
-
* describes and what `gkm dev` runs cannot disagree. Nothing is started.
|
|
7864
|
+
* The hosts a compose stage serves: its stack composed as a deploy would —
|
|
7865
|
+
* with the secrets it would generate made up in memory, never written — and
|
|
7866
|
+
* every route's host read off it.
|
|
8875
7867
|
*/
|
|
8876
|
-
async function
|
|
8877
|
-
|
|
8878
|
-
|
|
8879
|
-
|
|
8880
|
-
});
|
|
8881
|
-
|
|
8882
|
-
|
|
8883
|
-
|
|
8884
|
-
|
|
8885
|
-
|
|
8886
|
-
|
|
8887
|
-
|
|
8888
|
-
|
|
7868
|
+
async function composeStageHosts(options) {
|
|
7869
|
+
const { workspace, stage, manifest } = options;
|
|
7870
|
+
const { deployedStackSecrets } = await Promise.resolve().then(() => require("./secrets-DAvi_O9g.cjs")).then((n) => n.secrets_exports);
|
|
7871
|
+
const { composeStack } = await Promise.resolve().then(() => require("./stack-B3fPDTBg.cjs")).then((n) => n.stack_exports);
|
|
7872
|
+
const { secrets } = deployedStackSecrets(workspace, stage, options.stored, manifest);
|
|
7873
|
+
return stackHosts(composeStack({
|
|
7874
|
+
workspace,
|
|
7875
|
+
manifest,
|
|
7876
|
+
runnables: options.runnables,
|
|
7877
|
+
background: options.background,
|
|
7878
|
+
stage,
|
|
7879
|
+
identity: deployIdentity(workspace, stage),
|
|
7880
|
+
images: {
|
|
7881
|
+
mode: "pull",
|
|
7882
|
+
tag: "dns",
|
|
7883
|
+
...workspace.deploy?.registry ? { registry: workspace.deploy.registry } : {}
|
|
7884
|
+
},
|
|
7885
|
+
secrets,
|
|
7886
|
+
buildOnly: true,
|
|
7887
|
+
allowDevServices: true
|
|
7888
|
+
}));
|
|
8889
7889
|
}
|
|
8890
7890
|
//#endregion
|
|
8891
7891
|
//#region src/providers/types.ts
|
|
@@ -8894,7 +7894,7 @@ const PROVIDER_KINDS = ["objects"];
|
|
|
8894
7894
|
//#endregion
|
|
8895
7895
|
//#region src/providers/notes.ts
|
|
8896
7896
|
/** The constructs of a kind: `objects` is every bucket. */
|
|
8897
|
-
function constructsOf
|
|
7897
|
+
function constructsOf(manifest, kind) {
|
|
8898
7898
|
return Object.entries(manifest).filter(([, d]) => d.kind === kind).map(([id]) => id).sort();
|
|
8899
7899
|
}
|
|
8900
7900
|
/** A stage that names `false` for a kind, and declares constructs of it. */
|
|
@@ -8910,1333 +7910,861 @@ var StageProviderDisabled = class extends require_errors.GkmError {
|
|
|
8910
7910
|
this.name = "StageProviderDisabled";
|
|
8911
7911
|
}
|
|
8912
7912
|
};
|
|
8913
|
-
/** The command a stage's providers are run with. */
|
|
8914
|
-
function provisionCommand(stage) {
|
|
8915
|
-
return `gkm
|
|
8916
|
-
}
|
|
8917
|
-
/**
|
|
8918
|
-
* The line a stage missing a key a provider would write is given: what
|
|
8919
|
-
* creates it, and with which credentials. Undefined when no provider backs
|
|
8920
|
-
* the kind on the stage.
|
|
8921
|
-
*/
|
|
8922
|
-
function provisionHint(workspace, kind, stage) {
|
|
8923
|
-
const choice = require_workspace.stageProvider(workspace, kind, stage);
|
|
8924
|
-
if (choice.mode !== "provider") return void 0;
|
|
8925
|
-
const provisioning = require_workspace.provisioningOf(kind, choice.name);
|
|
8926
|
-
if (!provisioning) return void 0;
|
|
8927
|
-
return `deploy.${kind}.${stage} is ${choice.name}: ${provisionCommand(stage)} creates it and writes this key, with ${provisioning.describe}` + (provisioning.login ? `, or gkm login --provider ${provisioning.login}` : "");
|
|
8928
|
-
}
|
|
7913
|
+
/** The command a stage's providers are run with: every deploy runs them. */
|
|
7914
|
+
function provisionCommand(stage) {
|
|
7915
|
+
return `gkm deploy --stage ${stage}`;
|
|
7916
|
+
}
|
|
7917
|
+
/**
|
|
7918
|
+
* The line a stage missing a key a provider would write is given: what
|
|
7919
|
+
* creates it, and with which credentials. Undefined when no provider backs
|
|
7920
|
+
* the kind on the stage.
|
|
7921
|
+
*/
|
|
7922
|
+
function provisionHint(workspace, kind, stage) {
|
|
7923
|
+
const choice = require_workspace.stageProvider(workspace, kind, stage);
|
|
7924
|
+
if (choice.mode !== "provider") return void 0;
|
|
7925
|
+
const provisioning = require_workspace.provisioningOf(kind, choice.name);
|
|
7926
|
+
if (!provisioning) return void 0;
|
|
7927
|
+
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}` : "");
|
|
7928
|
+
}
|
|
7929
|
+
/**
|
|
7930
|
+
* What a deploy's checks read off the stage's providers: whether each kind is
|
|
7931
|
+
* accounted for — a provider, or `false` — so no dev service stands in for
|
|
7932
|
+
* it, and what creates a missing key of it.
|
|
7933
|
+
*/
|
|
7934
|
+
function stageProviderNotes(workspace, stage) {
|
|
7935
|
+
if (require_workspace.stageProvider(workspace, "objects", stage).mode === "external") return {};
|
|
7936
|
+
const hint = provisionHint(workspace, "objects", stage);
|
|
7937
|
+
return { objects: {
|
|
7938
|
+
accounted: true,
|
|
7939
|
+
...hint ? { hint } : {}
|
|
7940
|
+
} };
|
|
7941
|
+
}
|
|
7942
|
+
/**
|
|
7943
|
+
* A kind the stage set to `false`, declared anyway — refused before anything
|
|
7944
|
+
* else is checked. Pure: what a stack is composed from.
|
|
7945
|
+
*
|
|
7946
|
+
* @throws {StageProviderDisabled}
|
|
7947
|
+
*/
|
|
7948
|
+
function assertStageProvidersEnabled(workspace, manifest, stage) {
|
|
7949
|
+
for (const kind of PROVIDER_KINDS) {
|
|
7950
|
+
const ids = constructsOf(manifest, kind);
|
|
7951
|
+
if (ids.length === 0) continue;
|
|
7952
|
+
if (require_workspace.stageProvider(workspace, kind, stage).mode === "none") throw new StageProviderDisabled(kind, stage, ids);
|
|
7953
|
+
}
|
|
7954
|
+
}
|
|
7955
|
+
const SUFFIX_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
7956
|
+
/** A name that S3 would refuse — what the name builder never returns. */
|
|
7957
|
+
var InvalidBucketName = class extends require_errors.GkmError {
|
|
7958
|
+
bucket;
|
|
7959
|
+
reason;
|
|
7960
|
+
constructor(bucket, reason) {
|
|
7961
|
+
super(`'${bucket}' is not a valid S3 bucket name: ${reason}. Rename the construct or set deploy.namespace in gkm.config.ts to something that leaves room for it.`);
|
|
7962
|
+
this.bucket = bucket;
|
|
7963
|
+
this.reason = reason;
|
|
7964
|
+
this.name = "InvalidBucketName";
|
|
7965
|
+
}
|
|
7966
|
+
};
|
|
7967
|
+
/** Lowercase `[a-z0-9-]`, runs of anything else one `-`, no edge dashes. */
|
|
7968
|
+
function part(value) {
|
|
7969
|
+
return (0, _geekmidas_manifest.kebabCase)(value).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
7970
|
+
}
|
|
7971
|
+
/**
|
|
7972
|
+
* `parts` joined by `-`, shortened to `max` characters by trimming the longest
|
|
7973
|
+
* part one character at a time — so a long construct id gives way before a
|
|
7974
|
+
* short stage name does, and every part keeps its start.
|
|
7975
|
+
*/
|
|
7976
|
+
function fitParts(parts, max) {
|
|
7977
|
+
const kept = parts.map(part).filter(Boolean);
|
|
7978
|
+
const length = () => kept.reduce((sum, p) => sum + p.length, 0) + kept.length - 1;
|
|
7979
|
+
while (length() > max) {
|
|
7980
|
+
let longest = 0;
|
|
7981
|
+
for (let i = 1; i < kept.length; i++) if (kept[i].length > kept[longest].length) longest = i;
|
|
7982
|
+
if (kept[longest].length <= 1) break;
|
|
7983
|
+
kept[longest] = kept[longest].slice(0, -1).replace(/-+$/, "");
|
|
7984
|
+
}
|
|
7985
|
+
return kept.join("-");
|
|
7986
|
+
}
|
|
7987
|
+
/** Why `name` is not a valid bucket name, or undefined when it is. */
|
|
7988
|
+
function bucketNameProblem(name) {
|
|
7989
|
+
if (name.length < 3 || name.length > 63) return `it is ${name.length} characters, and S3 takes 3 to 63`;
|
|
7990
|
+
if (!/^[a-z0-9-]+$/.test(name)) return "it may hold only lowercase letters, digits and -";
|
|
7991
|
+
if (!/^[a-z0-9].*[a-z0-9]$/.test(name)) return "it must start and end with a letter or a digit";
|
|
7992
|
+
if (name.startsWith("xn--") || name.startsWith("sthree-")) return "it starts with a prefix S3 reserves ('xn--', 'sthree-')";
|
|
7993
|
+
if (name.endsWith("-s3alias") || name.endsWith("--ol-s3")) return "it ends with a suffix S3 reserves ('-s3alias', '--ol-s3')";
|
|
7994
|
+
}
|
|
7995
|
+
/**
|
|
7996
|
+
* The good name: `<namespace>-<project>-<stage>-<bucket id>`, lowercased, no
|
|
7997
|
+
* dots, shortened to leave room for a clash's suffix.
|
|
7998
|
+
*
|
|
7999
|
+
* @throws {InvalidBucketName} when nothing usable is left
|
|
8000
|
+
*/
|
|
8001
|
+
function bucketName(input) {
|
|
8002
|
+
const name = fitParts([
|
|
8003
|
+
input.scope,
|
|
8004
|
+
input.stage,
|
|
8005
|
+
input.id
|
|
8006
|
+
], 56);
|
|
8007
|
+
const problem = bucketNameProblem(name);
|
|
8008
|
+
if (problem) throw new InvalidBucketName(name, problem);
|
|
8009
|
+
return name;
|
|
8010
|
+
}
|
|
8011
|
+
/** Six random `[a-z0-9]` characters. */
|
|
8012
|
+
function randomSuffix(random = node_crypto.randomInt) {
|
|
8013
|
+
let suffix = "";
|
|
8014
|
+
for (let i = 0; i < 6; i++) suffix += SUFFIX_ALPHABET[random(36)];
|
|
8015
|
+
return suffix;
|
|
8016
|
+
}
|
|
8017
|
+
/** The good name with a clash's suffix. */
|
|
8018
|
+
function suffixedBucketName(good, suffix) {
|
|
8019
|
+
return `${good}-${suffix}`;
|
|
8020
|
+
}
|
|
8021
|
+
/**
|
|
8022
|
+
* The IAM user a bucket is reached with on a stage:
|
|
8023
|
+
* `gkm-<project>-<stage>-<bucket id>`, at most 64 characters.
|
|
8024
|
+
*/
|
|
8025
|
+
function iamUserName(input) {
|
|
8026
|
+
return fitParts([
|
|
8027
|
+
"gkm",
|
|
8028
|
+
input.scope,
|
|
8029
|
+
input.stage,
|
|
8030
|
+
input.id
|
|
8031
|
+
], 64);
|
|
8032
|
+
}
|
|
8033
|
+
/** The path every user gkm creates is under, so it finds its own. */
|
|
8034
|
+
const IAM_USER_PATH = "/gkm/";
|
|
8035
|
+
/** The inline policy each user is given. */
|
|
8036
|
+
const IAM_POLICY_NAME = "gkm-bucket";
|
|
8037
|
+
//#endregion
|
|
8038
|
+
//#region src/deploy/devServices.ts
|
|
8039
|
+
/**
|
|
8040
|
+
* Mail and object storage on a deployed stage: real ones, or — only when
|
|
8041
|
+
* someone asked — the dev services the local stage runs.
|
|
8042
|
+
*
|
|
8043
|
+
* Locally a declared `Email` is Mailpit and a declared bucket is MinIO, and
|
|
8044
|
+
* nobody has to configure either. Deployed, the same two containers would be
|
|
8045
|
+
* a stage that silently delivers no mail and keeps its files on one
|
|
8046
|
+
* container's disk. So a deployed stage on a server target takes both from
|
|
8047
|
+
* its secrets — the mail server's URL and sending address, and each bucket's
|
|
8048
|
+
* URL — and a stage missing any of them fails before anything is built,
|
|
8049
|
+
* provisioned or written, naming every key at once.
|
|
8050
|
+
*
|
|
8051
|
+
* A bucket's credentials are not checked: they are optional, and come from
|
|
8052
|
+
* either of two places. The bucket's URL may carry a key of its own
|
|
8053
|
+
* (`s3://KEY:SECRET@uploads`), which wins; otherwise the S3 client's default
|
|
8054
|
+
* chain applies — the stage's shared `AWS_ACCESS_KEY_ID`/
|
|
8055
|
+
* `AWS_SECRET_ACCESS_KEY` when it set them, or a role.
|
|
8056
|
+
*
|
|
8057
|
+
* `--allow-dev-services` is the way out for a stage that is not production —
|
|
8058
|
+
* a preview, a demo: every construct the stage does not account for gets the
|
|
8059
|
+
* dev service the local stage runs for it — a bucket MinIO, mail Mailpit —
|
|
8060
|
+
* and the target derives the keys from it. What the stage does account for
|
|
8061
|
+
* still wins: a key set in its secrets, or a provider (`deploy.objects`)
|
|
8062
|
+
* that backs the kind. Every run that uses one says so, loudly.
|
|
8063
|
+
*
|
|
8064
|
+
* Pure: what each target runs is decided here from declarations and the
|
|
8065
|
+
* stage's values, and the targets do the running.
|
|
8066
|
+
*/
|
|
8067
|
+
/**
|
|
8068
|
+
* The dev services a deployed stage can run — one per kind that has one on
|
|
8069
|
+
* every server target: a bucket's MinIO and mail's Mailpit. A cache or a
|
|
8070
|
+
* database is not here because a server stage runs its own anyway.
|
|
8071
|
+
*/
|
|
8072
|
+
const DEV_SERVICES = ["minio", "mailpit"];
|
|
8073
|
+
/**
|
|
8074
|
+
* The S3 client's shared key pair. Optional: a bucket whose URL carries its own
|
|
8075
|
+
* key (`s3://KEY:SECRET@bucket`) signs with that, and one without signs with
|
|
8076
|
+
* this pair when the stage set it, or with whatever else the SDK's default
|
|
8077
|
+
* chain finds — a role.
|
|
8078
|
+
*/
|
|
8079
|
+
const STORAGE_KEY_PAIR = ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"];
|
|
8080
|
+
/** Dev services asked of a target that runs no containers. */
|
|
8081
|
+
var DevServicesNeedServerTarget = class extends require_errors.GkmError {
|
|
8082
|
+
target;
|
|
8083
|
+
constructor(target) {
|
|
8084
|
+
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.`);
|
|
8085
|
+
this.target = target;
|
|
8086
|
+
this.name = "DevServicesNeedServerTarget";
|
|
8087
|
+
}
|
|
8088
|
+
};
|
|
8089
|
+
/**
|
|
8090
|
+
* `--allow-dev-services` given a value — the list it used to take. It is a
|
|
8091
|
+
* switch now: every construct the stage does not account for gets its dev
|
|
8092
|
+
* service.
|
|
8093
|
+
*/
|
|
8094
|
+
var AllowDevServicesTakesNoValue = class extends require_errors.GkmError {
|
|
8095
|
+
value;
|
|
8096
|
+
constructor(value) {
|
|
8097
|
+
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.`);
|
|
8098
|
+
this.value = value;
|
|
8099
|
+
this.name = "AllowDevServicesTakesNoValue";
|
|
8100
|
+
}
|
|
8101
|
+
};
|
|
8102
|
+
/**
|
|
8103
|
+
* Refuses `--allow-dev-services <value>` and `--allow-dev-services=<value>` on
|
|
8104
|
+
* a command line, with the reason, before the parser calls the value an
|
|
8105
|
+
* excess argument. `gkm deploy` and `gkm compose` take no positional
|
|
8106
|
+
* argument, so whatever follows the flag and is not a flag was meant for it.
|
|
8107
|
+
*
|
|
8108
|
+
* @throws {AllowDevServicesTakesNoValue}
|
|
8109
|
+
*/
|
|
8110
|
+
function assertDevServicesFlag(argv) {
|
|
8111
|
+
for (let i = 0; i < argv.length; i++) {
|
|
8112
|
+
const arg = argv[i];
|
|
8113
|
+
if (arg.startsWith("--allow-dev-services=")) throw new AllowDevServicesTakesNoValue(arg.slice(arg.indexOf("=") + 1));
|
|
8114
|
+
const next = argv[i + 1];
|
|
8115
|
+
if (arg === "--allow-dev-services" && next && !next.startsWith("-")) throw new AllowDevServicesTakesNoValue(next);
|
|
8116
|
+
}
|
|
8117
|
+
}
|
|
8118
|
+
/**
|
|
8119
|
+
* A deployed stage with mail or object storage that its secrets do not
|
|
8120
|
+
* configure — every missing key, across every app, at once.
|
|
8121
|
+
*/
|
|
8122
|
+
var ExternalServicesNotConfigured = class extends require_errors.GkmError {
|
|
8123
|
+
stage;
|
|
8124
|
+
missing;
|
|
8125
|
+
constructor(stage, missing) {
|
|
8126
|
+
const standIns = missing.some((m) => m.service !== void 0);
|
|
8127
|
+
const lines = missing.map((m) => {
|
|
8128
|
+
const by = m.apps?.length ? `, read by ${m.apps.join(", ")}` : "";
|
|
8129
|
+
return ` gkm secrets:set ${m.key} '${m.example}' --stage ${stage}\n ${m.what}${by}` + (m.note ? `\n ${m.note}` : "") + (m.hint ? `\n ${m.hint}` : "");
|
|
8130
|
+
});
|
|
8131
|
+
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}`);
|
|
8132
|
+
this.stage = stage;
|
|
8133
|
+
this.missing = missing;
|
|
8134
|
+
this.name = "ExternalServicesNotConfigured";
|
|
8135
|
+
}
|
|
8136
|
+
};
|
|
8929
8137
|
/**
|
|
8930
|
-
*
|
|
8931
|
-
*
|
|
8932
|
-
* it, and what creates a missing key of it.
|
|
8138
|
+
* Every mail and storage construct a manifest declares, with the apps that
|
|
8139
|
+
* depend on each — what a target that provisions the whole manifest checks.
|
|
8933
8140
|
*/
|
|
8934
|
-
function
|
|
8935
|
-
|
|
8936
|
-
|
|
8937
|
-
|
|
8938
|
-
|
|
8939
|
-
|
|
8940
|
-
|
|
8141
|
+
function manifestServiceDeclarations(manifest) {
|
|
8142
|
+
return Object.entries(manifest).flatMap(([id, declaration]) => {
|
|
8143
|
+
const { kind } = declaration;
|
|
8144
|
+
if (kind !== "email" && kind !== "objects" && kind !== "file-server") return [];
|
|
8145
|
+
const apps = (0, _geekmidas_manifest.dependentsOf)(manifest, id).filter((caller) => {
|
|
8146
|
+
const callerKind = manifest[caller]?.kind;
|
|
8147
|
+
return callerKind === "rest-api" || callerKind === "site";
|
|
8148
|
+
}).map(require_derive.appKey);
|
|
8149
|
+
return [{
|
|
8150
|
+
id,
|
|
8151
|
+
kind,
|
|
8152
|
+
...declaration.kind === "file-server" ? { of: declaration.of } : {},
|
|
8153
|
+
...apps.length ? { apps } : {}
|
|
8154
|
+
}];
|
|
8155
|
+
});
|
|
8941
8156
|
}
|
|
8942
8157
|
/**
|
|
8943
|
-
*
|
|
8944
|
-
*
|
|
8158
|
+
* Every key a stage must be given rather than derive — the one list both a
|
|
8159
|
+
* deploy's checks and `gkm secrets:add` read, so what a deploy refuses and
|
|
8160
|
+
* what the builder offers cannot drift.
|
|
8945
8161
|
*
|
|
8946
|
-
*
|
|
8162
|
+
* - Deployed, mail is its URL and its sending address, a bucket its URL, and
|
|
8163
|
+
* a file server its public URL. Locally all of it is Mailpit and MinIO.
|
|
8164
|
+
* - On every stage, a third party's credentials: nobody but the third party
|
|
8165
|
+
* can issue them.
|
|
8166
|
+
*
|
|
8167
|
+
* Never a derived value: a database URL, a generated secret, the seed.
|
|
8947
8168
|
*/
|
|
8948
|
-
function
|
|
8949
|
-
|
|
8950
|
-
|
|
8951
|
-
|
|
8952
|
-
if (
|
|
8953
|
-
}
|
|
8954
|
-
|
|
8955
|
-
|
|
8956
|
-
|
|
8957
|
-
|
|
8958
|
-
|
|
8959
|
-
|
|
8960
|
-
|
|
8961
|
-
|
|
8962
|
-
|
|
8963
|
-
|
|
8964
|
-
|
|
8965
|
-
|
|
8966
|
-
|
|
8967
|
-
|
|
8968
|
-
|
|
8969
|
-
|
|
8970
|
-
|
|
8971
|
-
|
|
8972
|
-
|
|
8973
|
-
|
|
8974
|
-
|
|
8975
|
-
|
|
8976
|
-
|
|
8977
|
-
|
|
8978
|
-
|
|
8979
|
-
|
|
8980
|
-
|
|
8981
|
-
|
|
8982
|
-
|
|
8983
|
-
|
|
8984
|
-
|
|
8985
|
-
|
|
8986
|
-
|
|
8987
|
-
|
|
8988
|
-
|
|
8989
|
-
...entry,
|
|
8990
|
-
status: "pending"
|
|
8169
|
+
function requiredStageKeys(input) {
|
|
8170
|
+
const domain = input.domain ?? "example.com";
|
|
8171
|
+
const keys = [];
|
|
8172
|
+
const add = (entry) => {
|
|
8173
|
+
if (!keys.some((k) => k.key === entry.key)) keys.push(entry);
|
|
8174
|
+
};
|
|
8175
|
+
const byId = (list) => [...list ?? []].sort((a, b) => a.id.localeCompare(b.id));
|
|
8176
|
+
const apps = (d) => d.apps?.length ? { apps: [...d.apps].sort() } : {};
|
|
8177
|
+
if (!input.local) {
|
|
8178
|
+
const services = byId(input.services);
|
|
8179
|
+
const objects = input.providers?.objects;
|
|
8180
|
+
const storage = objects?.accounted ? objects.hint ? { hint: objects.hint } : {} : { service: "minio" };
|
|
8181
|
+
for (const d of services.filter((d) => d.kind === "email")) {
|
|
8182
|
+
add({
|
|
8183
|
+
key: (0, _geekmidas_manifest.provideKey)(d.id, "url"),
|
|
8184
|
+
id: d.id,
|
|
8185
|
+
kind: "email",
|
|
8186
|
+
what: `where '${d.id}' sends mail — any SMTP server`,
|
|
8187
|
+
example: `smtp://user:password@smtp.${domain}:587`,
|
|
8188
|
+
service: "mailpit",
|
|
8189
|
+
...apps(d)
|
|
8190
|
+
});
|
|
8191
|
+
add({
|
|
8192
|
+
key: (0, _geekmidas_manifest.provideKey)(d.id, "from"),
|
|
8193
|
+
id: d.id,
|
|
8194
|
+
kind: "email",
|
|
8195
|
+
what: `the address '${d.id}' sends from, on a domain the mail server has verified`,
|
|
8196
|
+
example: `noreply@${domain}`,
|
|
8197
|
+
service: "mailpit",
|
|
8198
|
+
...apps(d)
|
|
8199
|
+
});
|
|
8200
|
+
}
|
|
8201
|
+
for (const d of services.filter((d) => d.kind === "objects")) add({
|
|
8202
|
+
key: (0, _geekmidas_manifest.provideKey)(d.id, "url"),
|
|
8203
|
+
id: d.id,
|
|
8204
|
+
kind: "bucket",
|
|
8205
|
+
what: `the bucket '${d.id}' — S3, R2, or any S3-compatible store (add &endpoint=… for one that is not S3)`,
|
|
8206
|
+
note: `credentials are optional: a key for this bucket alone in the URL (s3://KEY:SECRET@${require_derive.appKey(d.id)}?…) wins; without one, the shared AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY or a role signs`,
|
|
8207
|
+
example: `s3://${require_derive.appKey(d.id)}?region=eu-west-1`,
|
|
8208
|
+
...storage,
|
|
8209
|
+
...apps(d)
|
|
8991
8210
|
});
|
|
8992
|
-
|
|
8993
|
-
|
|
8994
|
-
|
|
8995
|
-
|
|
8996
|
-
|
|
8997
|
-
|
|
8998
|
-
|
|
8999
|
-
|
|
9000
|
-
|
|
9001
|
-
...entry,
|
|
9002
|
-
status: "ready",
|
|
9003
|
-
id
|
|
8211
|
+
for (const d of services.filter((d) => d.kind === "file-server")) add({
|
|
8212
|
+
key: (0, _geekmidas_manifest.provideKey)(d.id, "url"),
|
|
8213
|
+
id: d.id,
|
|
8214
|
+
kind: "file-server",
|
|
8215
|
+
what: `the public address '${d.id}' serves its bucket on — a CDN or the bucket's own domain`,
|
|
8216
|
+
example: `https://${require_derive.appKey(d.id)}.${domain}`,
|
|
8217
|
+
...storage,
|
|
8218
|
+
...d.of ? { of: d.of } : {},
|
|
8219
|
+
...apps(d)
|
|
9004
8220
|
});
|
|
9005
8221
|
}
|
|
9006
|
-
|
|
9007
|
-
|
|
9008
|
-
|
|
9009
|
-
|
|
9010
|
-
|
|
9011
|
-
|
|
9012
|
-
|
|
9013
|
-
|
|
9014
|
-
|
|
9015
|
-
|
|
9016
|
-
|
|
9017
|
-
|
|
9018
|
-
|
|
9019
|
-
|
|
9020
|
-
|
|
9021
|
-
|
|
9022
|
-
|
|
9023
|
-
}
|
|
9024
|
-
staleId = record.id;
|
|
9025
|
-
}
|
|
9026
|
-
const existing = await steps.find();
|
|
9027
|
-
if (existing) {
|
|
9028
|
-
await this.ready(entry, steps.id(existing));
|
|
9029
|
-
return {
|
|
9030
|
-
resource: existing,
|
|
9031
|
-
via: record?.status === "pending" ? "resumed" : "found",
|
|
9032
|
-
...staleId ? { staleId } : {}
|
|
9033
|
-
};
|
|
9034
|
-
}
|
|
9035
|
-
await this.pending(entry);
|
|
9036
|
-
const created = await steps.create();
|
|
9037
|
-
await this.ready(entry, steps.id(created));
|
|
9038
|
-
return {
|
|
9039
|
-
resource: created,
|
|
9040
|
-
via: "created",
|
|
9041
|
-
...staleId ? { staleId } : {}
|
|
9042
|
-
};
|
|
9043
|
-
}
|
|
9044
|
-
/** Removes `key`'s record — a resource that no longer exists. */
|
|
9045
|
-
async forget(key) {
|
|
9046
|
-
if (!(key in this.resources)) return;
|
|
9047
|
-
this.version = await this.store.deleteResource(this.stage, key, { expectedVersion: this.version });
|
|
9048
|
-
const { [key]: _removed, ...resources } = this.resources;
|
|
9049
|
-
this.resources = resources;
|
|
9050
|
-
}
|
|
9051
|
-
/** Every record, as the journal last wrote or read it. */
|
|
9052
|
-
records() {
|
|
9053
|
-
return this.resources;
|
|
9054
|
-
}
|
|
9055
|
-
/** Writes the stage's state, keeping its resource records. */
|
|
9056
|
-
async save() {
|
|
9057
|
-
this.state.lastDeployedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
9058
|
-
this.version = await this.store.write(this.stage, this.state, { expectedVersion: this.version });
|
|
9059
|
-
}
|
|
9060
|
-
async put(record) {
|
|
9061
|
-
this.version = await this.store.putResource(this.stage, record, { expectedVersion: this.version });
|
|
9062
|
-
this.resources = {
|
|
9063
|
-
...this.resources,
|
|
9064
|
-
[record.key]: {
|
|
9065
|
-
...record,
|
|
9066
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
9067
|
-
}
|
|
9068
|
-
};
|
|
9069
|
-
}
|
|
9070
|
-
};
|
|
9071
|
-
//#endregion
|
|
9072
|
-
//#region src/providers/dns.ts
|
|
9073
|
-
/** Whether the workspace deploys through the compose target. */
|
|
9074
|
-
function deploysWithCompose(workspace) {
|
|
9075
|
-
return workspace.deploy?.default === "compose" || Object.values(workspace.apps).some((app) => app.resolvedDeployTarget === "compose");
|
|
8222
|
+
if (!input.local && input.server) add({
|
|
8223
|
+
key: require_workspace.SERVER_IPV4_KEY,
|
|
8224
|
+
id: "server",
|
|
8225
|
+
kind: "server",
|
|
8226
|
+
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",
|
|
8227
|
+
note: `${require_workspace.SERVER_IPV6_KEY} adds its IPv6 address (AAAA records); neither is ever handed to an app`,
|
|
8228
|
+
example: "203.0.113.10"
|
|
8229
|
+
});
|
|
8230
|
+
for (const d of byId(input.credentials)) add({
|
|
8231
|
+
key: (0, _geekmidas_manifest.provideKey)(d.id, "credentials"),
|
|
8232
|
+
id: d.id,
|
|
8233
|
+
kind: d.kind,
|
|
8234
|
+
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`,
|
|
8235
|
+
example: "{\"apiKey\":\"…\"}",
|
|
8236
|
+
...apps(d)
|
|
8237
|
+
});
|
|
8238
|
+
return keys;
|
|
9076
8239
|
}
|
|
9077
8240
|
/**
|
|
9078
|
-
*
|
|
9079
|
-
*
|
|
8241
|
+
* Whether only the stage's secrets can supply `key` to a construct of `kind`
|
|
8242
|
+
* — what nothing on the stage derives.
|
|
9080
8243
|
*
|
|
9081
|
-
*
|
|
9082
|
-
*
|
|
8244
|
+
* A third party's credentials, always. Deployed, also each generated secret
|
|
8245
|
+
* and keyring: the stage generated each one once, and every run reads the
|
|
8246
|
+
* same value back.
|
|
9083
8247
|
*/
|
|
9084
|
-
function
|
|
9085
|
-
if (
|
|
9086
|
-
if (
|
|
9087
|
-
|
|
8248
|
+
function suppliedOnly(kind, id, key, local) {
|
|
8249
|
+
if (kind === "credential") return true;
|
|
8250
|
+
if (kind === "external-api") return key === (0, _geekmidas_manifest.provideKey)(id, "credentials");
|
|
8251
|
+
if (local) return false;
|
|
8252
|
+
return kind === "secret" || kind === "encryption";
|
|
9088
8253
|
}
|
|
9089
8254
|
/**
|
|
9090
|
-
*
|
|
9091
|
-
*
|
|
9092
|
-
*
|
|
8255
|
+
* Where a deployed stage's mail and storage come from, and what it lacks.
|
|
8256
|
+
*
|
|
8257
|
+
* - Mail is external once its URL is set, and then needs its sending
|
|
8258
|
+
* address too. Unset, it is Mailpit where allowed — the address derived
|
|
8259
|
+
* unless set — and missing otherwise.
|
|
8260
|
+
* - A bucket is external once its URL is set; its credentials, in the URL or
|
|
8261
|
+
* the shared pair, are optional. Unset, it is MinIO where allowed, and
|
|
8262
|
+
* missing otherwise.
|
|
8263
|
+
* - A file server's URL is derived where its bucket is MinIO, unless set;
|
|
8264
|
+
* over an external bucket the stage must set it.
|
|
8265
|
+
*
|
|
8266
|
+
* The keys themselves are {@link requiredStageKeys}'s.
|
|
9093
8267
|
*/
|
|
9094
|
-
|
|
9095
|
-
const {
|
|
9096
|
-
const
|
|
9097
|
-
const
|
|
9098
|
-
|
|
9099
|
-
|
|
9100
|
-
|
|
9101
|
-
|
|
9102
|
-
background
|
|
8268
|
+
function externalServices(input) {
|
|
8269
|
+
const { supplied, allow } = input;
|
|
8270
|
+
const has = (key) => supplied[key] !== void 0;
|
|
8271
|
+
const required = requiredStageKeys({
|
|
8272
|
+
local: false,
|
|
8273
|
+
services: input.declarations,
|
|
8274
|
+
...input.domain ? { domain: input.domain } : {},
|
|
8275
|
+
...input.providers ? { providers: input.providers } : {}
|
|
9103
8276
|
});
|
|
9104
|
-
const
|
|
9105
|
-
|
|
9106
|
-
|
|
9107
|
-
|
|
9108
|
-
|
|
9109
|
-
|
|
9110
|
-
|
|
9111
|
-
|
|
9112
|
-
|
|
9113
|
-
|
|
9114
|
-
|
|
9115
|
-
|
|
9116
|
-
|
|
9117
|
-
|
|
9118
|
-
|
|
9119
|
-
|
|
9120
|
-
|
|
9121
|
-
|
|
9122
|
-
},
|
|
9123
|
-
secrets,
|
|
9124
|
-
buildOnly: true,
|
|
9125
|
-
allowDevServices: true
|
|
9126
|
-
}));
|
|
8277
|
+
const standIn = (k) => allow && k.service !== void 0 && !has(k.key);
|
|
8278
|
+
const mailpit = required.filter((k) => k.kind === "email" && k.key === (0, _geekmidas_manifest.provideKey)(k.id, "url")).filter(standIn).map((k) => k.id);
|
|
8279
|
+
const minio = required.filter((k) => k.kind === "bucket").filter(standIn).map((k) => k.id);
|
|
8280
|
+
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);
|
|
8281
|
+
return {
|
|
8282
|
+
missing: required.filter((k) => !has(k.key) && !covered(k)).map(({ key, id, what, note, example, service, apps, hint }) => ({
|
|
8283
|
+
key,
|
|
8284
|
+
id,
|
|
8285
|
+
what,
|
|
8286
|
+
...note ? { note } : {},
|
|
8287
|
+
example,
|
|
8288
|
+
...service ? { service } : {},
|
|
8289
|
+
...apps ? { apps } : {},
|
|
8290
|
+
...hint ? { hint } : {}
|
|
8291
|
+
})),
|
|
8292
|
+
minio,
|
|
8293
|
+
mailpit
|
|
8294
|
+
};
|
|
9127
8295
|
}
|
|
9128
8296
|
/**
|
|
9129
|
-
* `
|
|
9130
|
-
*
|
|
9131
|
-
*
|
|
8297
|
+
* `externalServices`, refusing a stage that lacks anything.
|
|
8298
|
+
*
|
|
8299
|
+
* @throws {ExternalServicesNotConfigured} naming every missing key
|
|
9132
8300
|
*/
|
|
9133
|
-
|
|
9134
|
-
const
|
|
9135
|
-
|
|
9136
|
-
|
|
9137
|
-
|
|
9138
|
-
|
|
9139
|
-
|
|
9140
|
-
|
|
9141
|
-
|
|
9142
|
-
|
|
9143
|
-
|
|
9144
|
-
})).read(stage))?.custom);
|
|
9145
|
-
if (!server) {
|
|
9146
|
-
log(`\n🌐 DNS for '${stage}': no GKM_SERVER_IPV4 in its secrets, so no records — ${serverAddressHint(stage)}`);
|
|
9147
|
-
return {
|
|
9148
|
-
mode: "none",
|
|
9149
|
-
hosts: [],
|
|
9150
|
-
changes: []
|
|
9151
|
-
};
|
|
9152
|
-
}
|
|
9153
|
-
const hosts = [...options.hosts ?? await composeStageHosts({
|
|
9154
|
-
workspace,
|
|
9155
|
-
stage,
|
|
9156
|
-
...options.profile ? { profile: options.profile } : {},
|
|
9157
|
-
...options.home ? { home: options.home } : {},
|
|
9158
|
-
...options.manifest ? { manifest: options.manifest } : {},
|
|
9159
|
-
...options.runnables ? { runnables: options.runnables } : {},
|
|
9160
|
-
...options.background ? { background: options.background } : {}
|
|
9161
|
-
})];
|
|
9162
|
-
log(`\n🌐 DNS for '${stage}' → ${server.ipv4}${server.ipv6 ? ` / ${server.ipv6}` : ""}${options.dryRun ? " (dry run — nothing is written)" : ""}`);
|
|
9163
|
-
const plan = planStackDns({
|
|
9164
|
-
stage,
|
|
9165
|
-
hosts,
|
|
9166
|
-
server,
|
|
9167
|
-
dns: workspace.dns
|
|
8301
|
+
function assertExternalServices(input) {
|
|
8302
|
+
const services = externalServices(input);
|
|
8303
|
+
if (services.missing.length > 0) throw new ExternalServicesNotConfigured(input.stage, services.missing);
|
|
8304
|
+
return services;
|
|
8305
|
+
}
|
|
8306
|
+
/** The dev services a run uses, one entry per service it runs. */
|
|
8307
|
+
function devServicesUsed(services) {
|
|
8308
|
+
const used = [];
|
|
8309
|
+
if (services.mailpit.length > 0) used.push({
|
|
8310
|
+
service: "mailpit",
|
|
8311
|
+
ids: services.mailpit
|
|
9168
8312
|
});
|
|
9169
|
-
|
|
9170
|
-
|
|
9171
|
-
|
|
9172
|
-
|
|
9173
|
-
|
|
9174
|
-
|
|
9175
|
-
|
|
9176
|
-
|
|
9177
|
-
|
|
9178
|
-
|
|
8313
|
+
if (services.minio.length > 0) used.push({
|
|
8314
|
+
service: "minio",
|
|
8315
|
+
ids: services.minio
|
|
8316
|
+
});
|
|
8317
|
+
return used;
|
|
8318
|
+
}
|
|
8319
|
+
/** The line a run prints for each dev service a deployed stage uses. */
|
|
8320
|
+
function devServiceWarning(stage, use) {
|
|
8321
|
+
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.`;
|
|
8322
|
+
}
|
|
8323
|
+
/**
|
|
8324
|
+
* Report each dev service a run uses: a loud line, and a `dev-service.used`
|
|
8325
|
+
* event a host can act on.
|
|
8326
|
+
*/
|
|
8327
|
+
function reportDevServices(ctx, uses) {
|
|
8328
|
+
for (const use of uses) {
|
|
8329
|
+
ctx.logger.warn(`\n${devServiceWarning(ctx.stage, use)}`);
|
|
8330
|
+
ctx.emit({
|
|
8331
|
+
type: "dev-service.used",
|
|
8332
|
+
service: use.service,
|
|
8333
|
+
stage: ctx.stage,
|
|
8334
|
+
constructs: use.ids
|
|
9179
8335
|
});
|
|
9180
|
-
if (options.journal && !options.dryRun) {
|
|
9181
|
-
const providers = new Map(plan.domains.map((domain) => [domain.domain, domain.provider]));
|
|
9182
|
-
await recordDnsChanges(options.journal, changes, (domain) => providers.get(domain));
|
|
9183
|
-
}
|
|
9184
|
-
return {
|
|
9185
|
-
mode: "records",
|
|
9186
|
-
hosts,
|
|
9187
|
-
changes
|
|
9188
|
-
};
|
|
9189
|
-
} catch (error) {
|
|
9190
|
-
if (error instanceof require_credentials.MissingCredential) {
|
|
9191
|
-
log(` ⚠ ${error.message}`);
|
|
9192
|
-
log(" Until then, create these records yourself:");
|
|
9193
|
-
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})`);
|
|
9194
|
-
return {
|
|
9195
|
-
mode: "records",
|
|
9196
|
-
hosts,
|
|
9197
|
-
changes: []
|
|
9198
|
-
};
|
|
9199
|
-
}
|
|
9200
|
-
throw error;
|
|
9201
8336
|
}
|
|
9202
8337
|
}
|
|
9203
8338
|
//#endregion
|
|
9204
|
-
//#region src/
|
|
9205
|
-
/**
|
|
9206
|
-
* The AWS account a provider provisions in — the stage's, found the way the
|
|
9207
|
-
* SSM and Secrets Manager stores find it.
|
|
9208
|
-
*
|
|
9209
|
-
* 1. `--profile`, resolved on its own: exported keys left over from another
|
|
9210
|
-
* account never win over a profile someone named.
|
|
9211
|
-
* 2. `AWS_PROFILE`, else `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY` — the
|
|
9212
|
-
* deploy's own `aws` credential kind (`storedCredentials`).
|
|
9213
|
-
* 3. The SDK's default chain: `~/.aws` default profile, SSO, a role. In CI,
|
|
9214
|
-
* whatever `aws-actions/configure-aws-credentials` exported.
|
|
9215
|
-
*
|
|
9216
|
-
* None of them: the provider has no provisioning credentials, and the stage
|
|
9217
|
-
* is treated as `external`.
|
|
9218
|
-
*/
|
|
9219
|
-
async function awsProvisioningCredentials(lookup) {
|
|
9220
|
-
const region = lookup.env.AWS_REGION ?? lookup.env.AWS_DEFAULT_REGION;
|
|
9221
|
-
const where = region ? { region } : {};
|
|
9222
|
-
if (lookup.profile) return {
|
|
9223
|
-
profile: lookup.profile,
|
|
9224
|
-
...where
|
|
9225
|
-
};
|
|
9226
|
-
const stored = await require_credentials.storedCredentials({
|
|
9227
|
-
env: lookup.env,
|
|
9228
|
-
...lookup.home ? { home: lookup.home } : {}
|
|
9229
|
-
}).get({
|
|
9230
|
-
kind: "aws",
|
|
9231
|
-
stage: lookup.stage
|
|
9232
|
-
});
|
|
9233
|
-
if (stored) return stored;
|
|
9234
|
-
try {
|
|
9235
|
-
const { fromNodeProviderChain } = await import("@aws-sdk/credential-providers");
|
|
9236
|
-
await fromNodeProviderChain({
|
|
9237
|
-
timeout: 1e3,
|
|
9238
|
-
maxRetries: 0
|
|
9239
|
-
})();
|
|
9240
|
-
return {
|
|
9241
|
-
chain: true,
|
|
9242
|
-
...where
|
|
9243
|
-
};
|
|
9244
|
-
} catch {
|
|
9245
|
-
return;
|
|
9246
|
-
}
|
|
9247
|
-
}
|
|
8339
|
+
//#region src/deploy/actor.ts
|
|
9248
8340
|
/**
|
|
9249
|
-
*
|
|
9250
|
-
*
|
|
9251
|
-
*
|
|
9252
|
-
*
|
|
8341
|
+
* Who wrote a stage's deploy state: a GitHub Actions run, or a person at a
|
|
8342
|
+
* machine. Recorded on every state write (`updatedBy`), on every resource
|
|
8343
|
+
* record and on every release (`releasedBy`), so "who put this here" has an
|
|
8344
|
+
* answer that does not depend on anyone's memory.
|
|
9253
8345
|
*/
|
|
9254
|
-
|
|
9255
|
-
|
|
8346
|
+
/** Whether `env` is a CI job — GitHub Actions or any runner that sets `CI`. */
|
|
8347
|
+
function isCi(env = process.env) {
|
|
8348
|
+
return env.GITHUB_ACTIONS === "true" || env.CI === "true";
|
|
9256
8349
|
}
|
|
9257
|
-
/**
|
|
9258
|
-
|
|
9259
|
-
if ("
|
|
9260
|
-
const
|
|
8350
|
+
/** Who this process is writing as. */
|
|
8351
|
+
function currentActor(env = process.env) {
|
|
8352
|
+
if (env.GITHUB_ACTIONS === "true") {
|
|
8353
|
+
const server = env.GITHUB_SERVER_URL ?? "https://github.com";
|
|
9261
8354
|
return {
|
|
9262
|
-
|
|
9263
|
-
|
|
8355
|
+
kind: "github",
|
|
8356
|
+
actor: env.GITHUB_ACTOR ?? "unknown",
|
|
8357
|
+
run: `${server}/${env.GITHUB_REPOSITORY ?? "unknown"}/actions/runs/${env.GITHUB_RUN_ID ?? "unknown"}`,
|
|
8358
|
+
...env.GITHUB_WORKFLOW ? { workflow: env.GITHUB_WORKFLOW } : {}
|
|
9264
8359
|
};
|
|
9265
8360
|
}
|
|
9266
|
-
|
|
9267
|
-
|
|
9268
|
-
|
|
9269
|
-
|
|
9270
|
-
|
|
9271
|
-
|
|
9272
|
-
|
|
8361
|
+
let user = "unknown";
|
|
8362
|
+
try {
|
|
8363
|
+
user = (0, node_os.userInfo)().username;
|
|
8364
|
+
} catch {}
|
|
8365
|
+
return {
|
|
8366
|
+
kind: "local",
|
|
8367
|
+
user,
|
|
8368
|
+
host: (0, node_os.hostname)()
|
|
9273
8369
|
};
|
|
9274
|
-
return { region };
|
|
9275
8370
|
}
|
|
9276
|
-
|
|
9277
|
-
|
|
9278
|
-
|
|
9279
|
-
|
|
9280
|
-
|
|
9281
|
-
constructor(bucket, reason) {
|
|
9282
|
-
super(`'${bucket}' is not a valid S3 bucket name: ${reason}. Rename the construct or set deploy.namespace in gkm.config.ts to something that leaves room for it.`);
|
|
9283
|
-
this.bucket = bucket;
|
|
9284
|
-
this.reason = reason;
|
|
9285
|
-
this.name = "InvalidBucketName";
|
|
9286
|
-
}
|
|
9287
|
-
};
|
|
9288
|
-
/** Lowercase `[a-z0-9-]`, runs of anything else one `-`, no edge dashes. */
|
|
9289
|
-
function part(value) {
|
|
9290
|
-
return (0, _geekmidas_manifest.kebabCase)(value).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
8371
|
+
/** `octocat (deploy, https://github.com/…/runs/1)` or `ada@laptop`. */
|
|
8372
|
+
function describeActor(actor) {
|
|
8373
|
+
if (!actor) return "unknown";
|
|
8374
|
+
if (actor.kind === "local") return `${actor.user}@${actor.host}`;
|
|
8375
|
+
return `${actor.actor} (${actor.workflow ? `${actor.workflow}, ` : ""}${actor.run})`;
|
|
9291
8376
|
}
|
|
9292
|
-
|
|
9293
|
-
|
|
9294
|
-
|
|
9295
|
-
|
|
9296
|
-
*/
|
|
9297
|
-
function
|
|
9298
|
-
|
|
9299
|
-
const length = () => kept.reduce((sum, p) => sum + p.length, 0) + kept.length - 1;
|
|
9300
|
-
while (length() > max) {
|
|
9301
|
-
let longest = 0;
|
|
9302
|
-
for (let i = 1; i < kept.length; i++) if (kept[i].length > kept[longest].length) longest = i;
|
|
9303
|
-
if (kept[longest].length <= 1) break;
|
|
9304
|
-
kept[longest] = kept[longest].slice(0, -1).replace(/-+$/, "");
|
|
9305
|
-
}
|
|
9306
|
-
return kept.join("-");
|
|
8377
|
+
//#endregion
|
|
8378
|
+
//#region src/deploy/dnsResources.ts
|
|
8379
|
+
/** The resource type of a DNS record gkm wrote. */
|
|
8380
|
+
const DNS_RECORD_RESOURCE = "dns-record";
|
|
8381
|
+
/** A record's full name: `api.example.com`, or the domain for `@`. */
|
|
8382
|
+
function dnsRecordFqdn(domain, name) {
|
|
8383
|
+
return name === "@" || name === "" ? domain : `${name}.${domain}`;
|
|
9307
8384
|
}
|
|
9308
|
-
/**
|
|
9309
|
-
function
|
|
9310
|
-
|
|
9311
|
-
if (!/^[a-z0-9-]+$/.test(name)) return "it may hold only lowercase letters, digits and -";
|
|
9312
|
-
if (!/^[a-z0-9].*[a-z0-9]$/.test(name)) return "it must start and end with a letter or a digit";
|
|
9313
|
-
if (name.startsWith("xn--") || name.startsWith("sthree-")) return "it starts with a prefix S3 reserves ('xn--', 'sthree-')";
|
|
9314
|
-
if (name.endsWith("-s3alias") || name.endsWith("--ol-s3")) return "it ends with a suffix S3 reserves ('-s3alias', '--ol-s3')";
|
|
8385
|
+
/** `dns-record:<fqdn>:<type>` */
|
|
8386
|
+
function dnsResourceKey(fqdn, type) {
|
|
8387
|
+
return `${DNS_RECORD_RESOURCE}:${fqdn.toLowerCase()}:${type.toUpperCase()}`;
|
|
9315
8388
|
}
|
|
9316
|
-
/**
|
|
9317
|
-
|
|
9318
|
-
|
|
9319
|
-
|
|
9320
|
-
|
|
9321
|
-
|
|
9322
|
-
|
|
9323
|
-
|
|
9324
|
-
|
|
9325
|
-
|
|
9326
|
-
|
|
9327
|
-
|
|
9328
|
-
|
|
9329
|
-
|
|
9330
|
-
|
|
8389
|
+
/** The resource record a written DNS record is kept as. */
|
|
8390
|
+
function dnsRecordResource(record) {
|
|
8391
|
+
const fqdn = dnsRecordFqdn(record.domain, record.name).toLowerCase();
|
|
8392
|
+
const type = record.type.toUpperCase();
|
|
8393
|
+
const data = {
|
|
8394
|
+
domain: record.domain,
|
|
8395
|
+
name: record.name,
|
|
8396
|
+
value: record.value,
|
|
8397
|
+
ttl: record.ttl,
|
|
8398
|
+
...record.provider ? { provider: record.provider } : {}
|
|
8399
|
+
};
|
|
8400
|
+
return {
|
|
8401
|
+
key: dnsResourceKey(fqdn, type),
|
|
8402
|
+
type: DNS_RECORD_RESOURCE,
|
|
8403
|
+
id: `${fqdn} ${type}`,
|
|
8404
|
+
status: "ready",
|
|
8405
|
+
data
|
|
8406
|
+
};
|
|
9331
8407
|
}
|
|
9332
|
-
/**
|
|
9333
|
-
function
|
|
9334
|
-
|
|
9335
|
-
|
|
9336
|
-
|
|
8408
|
+
/** Records that gkm wrote `record` — or updates the value it holds. */
|
|
8409
|
+
async function recordDnsResource(journal, record) {
|
|
8410
|
+
const { key, type, id, data } = dnsRecordResource(record);
|
|
8411
|
+
await journal.ready({
|
|
8412
|
+
key,
|
|
8413
|
+
type,
|
|
8414
|
+
...data ? { data } : {}
|
|
8415
|
+
}, id);
|
|
9337
8416
|
}
|
|
9338
|
-
/**
|
|
9339
|
-
function
|
|
9340
|
-
|
|
8417
|
+
/** Forgets a DNS record gkm deleted. */
|
|
8418
|
+
async function forgetDnsResource(journal, record) {
|
|
8419
|
+
await journal.forget(dnsResourceKey(dnsRecordFqdn(record.domain, record.name), record.type));
|
|
9341
8420
|
}
|
|
9342
8421
|
/**
|
|
9343
|
-
* The
|
|
9344
|
-
*
|
|
8422
|
+
* The changes a DNS step made, kept: what it created, updated or wrote is
|
|
8423
|
+
* recorded, what it deleted is forgotten. A record it found already right is
|
|
8424
|
+
* recorded only if gkm wrote it before; one it left to a person is not.
|
|
9345
8425
|
*/
|
|
9346
|
-
function
|
|
9347
|
-
|
|
9348
|
-
"
|
|
9349
|
-
|
|
9350
|
-
input.stage,
|
|
9351
|
-
input.id
|
|
9352
|
-
], 64);
|
|
9353
|
-
}
|
|
9354
|
-
/** The path every user gkm creates is under, so it finds its own. */
|
|
9355
|
-
const IAM_USER_PATH = "/gkm/";
|
|
9356
|
-
/** The inline policy each user is given. */
|
|
9357
|
-
const IAM_POLICY_NAME = "gkm-bucket";
|
|
9358
|
-
//#endregion
|
|
9359
|
-
//#region src/providers/s3/origins.ts
|
|
9360
|
-
/**
|
|
9361
|
-
* Which browser origins may call a bucket: the stage's sites that depend on
|
|
9362
|
-
* an API using it.
|
|
9363
|
-
*
|
|
9364
|
-
* The same reading of the graph an API's own CORS origins are — its callers —
|
|
9365
|
-
* taken one step further: a site that calls an API which hands it a presigned
|
|
9366
|
-
* upload URL is a site whose browser PUTs to the bucket. A site that reads the
|
|
9367
|
-
* bucket (or a file server over it) itself is one too. Nothing is written
|
|
9368
|
-
* down: add a site that calls the API and the next provision run allows it.
|
|
9369
|
-
*/
|
|
9370
|
-
/** The keys a reader of this bucket holds: its URL, and each server's over it. */
|
|
9371
|
-
function bucketKeys(manifest, bucket) {
|
|
9372
|
-
return [(0, _geekmidas_manifest.provideKey)(bucket, "url"), ...Object.entries(manifest).filter(([, d]) => d.kind === "file-server" && d.of === bucket).map(([id]) => (0, _geekmidas_manifest.provideKey)(id, "url"))];
|
|
9373
|
-
}
|
|
9374
|
-
function bucketOrigins(input) {
|
|
9375
|
-
const { manifest, workspace, stage } = input;
|
|
9376
|
-
const keys = bucketKeys(manifest, input.bucket);
|
|
9377
|
-
const readers = Object.entries(manifest).filter(([, d]) => d.kind === "rest-api" || d.kind === "site").filter(([id]) => {
|
|
9378
|
-
const env = appEnvKeys(manifest, require_derive.appKey(id), input.runnables);
|
|
9379
|
-
return env !== void 0 && keys.some((key) => env.has(key));
|
|
9380
|
-
});
|
|
9381
|
-
const sites = /* @__PURE__ */ new Set();
|
|
9382
|
-
for (const [id, declaration] of readers) {
|
|
9383
|
-
if (declaration.kind === "site") {
|
|
9384
|
-
sites.add(id);
|
|
9385
|
-
continue;
|
|
9386
|
-
}
|
|
9387
|
-
for (const caller of (0, _geekmidas_manifest.dependentsOf)(manifest, id)) if (manifest[caller]?.kind === "site") sites.add(caller);
|
|
9388
|
-
}
|
|
9389
|
-
const origins = /* @__PURE__ */ new Set();
|
|
9390
|
-
const unresolved = [];
|
|
9391
|
-
for (const id of [...sites].sort()) {
|
|
9392
|
-
const name = require_derive.appKey(id);
|
|
9393
|
-
const app = workspace.apps[name];
|
|
9394
|
-
if (!app) {
|
|
9395
|
-
unresolved.push(name);
|
|
8426
|
+
async function recordDnsChanges(journal, changes, providerOf) {
|
|
8427
|
+
for (const change of changes) {
|
|
8428
|
+
if (change.action === "delete") {
|
|
8429
|
+
await forgetDnsResource(journal, change);
|
|
9396
8430
|
continue;
|
|
9397
8431
|
}
|
|
9398
|
-
|
|
9399
|
-
|
|
9400
|
-
|
|
9401
|
-
|
|
9402
|
-
|
|
9403
|
-
|
|
9404
|
-
|
|
8432
|
+
const written = change.action === "create" || change.action === "update" || change.action === "write";
|
|
8433
|
+
const known = change.action === "unchanged" && journal.record(dnsResourceKey(dnsRecordFqdn(change.domain, change.name), change.type)) !== void 0;
|
|
8434
|
+
if (!written && !known || change.value === void 0) continue;
|
|
8435
|
+
const provider = providerOf(change.domain);
|
|
8436
|
+
await recordDnsResource(journal, {
|
|
8437
|
+
domain: change.domain,
|
|
8438
|
+
name: change.name,
|
|
8439
|
+
type: change.type,
|
|
8440
|
+
value: change.value,
|
|
8441
|
+
ttl: change.ttl ?? 0,
|
|
8442
|
+
...provider ? { provider } : {}
|
|
8443
|
+
});
|
|
9405
8444
|
}
|
|
9406
|
-
|
|
9407
|
-
|
|
9408
|
-
|
|
9409
|
-
|
|
8445
|
+
}
|
|
8446
|
+
/** Every DNS record gkm wrote for the stage, from its resource records. */
|
|
8447
|
+
function dnsResources(resources) {
|
|
8448
|
+
return Object.values(resources).filter((record) => record.type === DNS_RECORD_RESOURCE).map((record) => {
|
|
8449
|
+
const data = record.data ?? {};
|
|
8450
|
+
const [fqdn = "", type = ""] = (record.id ?? "").split(" ");
|
|
8451
|
+
return {
|
|
8452
|
+
...data,
|
|
8453
|
+
type,
|
|
8454
|
+
fqdn
|
|
8455
|
+
};
|
|
8456
|
+
}).sort((a, b) => a.fqdn.localeCompare(b.fqdn) || a.type.localeCompare(b.type));
|
|
9410
8457
|
}
|
|
9411
8458
|
//#endregion
|
|
9412
|
-
//#region src/
|
|
9413
|
-
const bucketArn = (bucket) => `arn:aws:s3:::${bucket}`;
|
|
8459
|
+
//#region src/docker/imageRef.ts
|
|
9414
8460
|
/**
|
|
9415
|
-
*
|
|
9416
|
-
* nothing else in the account.
|
|
8461
|
+
* An image reference, checked before it reaches `docker`.
|
|
9417
8462
|
*
|
|
9418
|
-
*
|
|
9419
|
-
*
|
|
9420
|
-
*
|
|
9421
|
-
|
|
9422
|
-
|
|
9423
|
-
|
|
9424
|
-
Version: "2012-10-17",
|
|
9425
|
-
Statement: [{
|
|
9426
|
-
Sid: "GkmObjects",
|
|
9427
|
-
Effect: "Allow",
|
|
9428
|
-
Action: [
|
|
9429
|
-
"s3:GetObject",
|
|
9430
|
-
"s3:PutObject",
|
|
9431
|
-
"s3:DeleteObject",
|
|
9432
|
-
"s3:AbortMultipartUpload",
|
|
9433
|
-
"s3:ListMultipartUploadParts",
|
|
9434
|
-
...options.versioning ? ["s3:GetObjectVersion"] : []
|
|
9435
|
-
],
|
|
9436
|
-
Resource: `${bucketArn(bucket)}/*`
|
|
9437
|
-
}, {
|
|
9438
|
-
Sid: "GkmBucket",
|
|
9439
|
-
Effect: "Allow",
|
|
9440
|
-
Action: [
|
|
9441
|
-
"s3:ListBucket",
|
|
9442
|
-
"s3:GetBucketLocation",
|
|
9443
|
-
"s3:ListBucketMultipartUploads"
|
|
9444
|
-
],
|
|
9445
|
-
Resource: bucketArn(bucket)
|
|
9446
|
-
}]
|
|
9447
|
-
};
|
|
9448
|
-
}
|
|
9449
|
-
/**
|
|
9450
|
-
* One open pattern as the resource it is public on. A `**` is S3's `*`; a
|
|
9451
|
-
* single `*` is too, which crosses `/` where the construct's own check does
|
|
9452
|
-
* not — the construct is the stricter of the two (see `bucketPolicy` in
|
|
9453
|
-
* reconcile, which the local MinIO is given the same way).
|
|
8463
|
+
* The parts come from the workspace — a registry in `gkm.config.ts`, an image
|
|
8464
|
+
* name from the project, a tag from `--tag` — and an argument array already
|
|
8465
|
+
* keeps them out of a shell. What it cannot do is stop `docker` reading a value
|
|
8466
|
+
* that starts with `-` as one of its own flags, so a ref is held to Docker's
|
|
8467
|
+
* own grammar (distribution/reference) and anything else is refused by name
|
|
8468
|
+
* before a build starts.
|
|
9454
8469
|
*/
|
|
9455
|
-
|
|
9456
|
-
|
|
9457
|
-
|
|
8470
|
+
const DOMAIN_COMPONENT = "(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])";
|
|
8471
|
+
const DOMAIN = `${DOMAIN_COMPONENT}(?:\\.${DOMAIN_COMPONENT})*(?::[0-9]+)?`;
|
|
8472
|
+
const PATH_COMPONENT = "[a-z0-9]+(?:(?:[._]|__|-+)[a-z0-9]+)*";
|
|
8473
|
+
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,})?$`);
|
|
8474
|
+
/** Docker refuses a repository name longer than this. */
|
|
8475
|
+
const MAX_NAME_LENGTH = 255;
|
|
8476
|
+
/** A value that is not an image reference was about to be built or pushed. */
|
|
8477
|
+
var ImageRefInvalid = class extends require_errors.GkmError {
|
|
8478
|
+
ref;
|
|
8479
|
+
constructor(ref) {
|
|
8480
|
+
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.`);
|
|
8481
|
+
this.ref = ref;
|
|
8482
|
+
this.name = "ImageRefInvalid";
|
|
8483
|
+
}
|
|
8484
|
+
};
|
|
8485
|
+
/** Throw {@link ImageRefInvalid} unless `ref` is a Docker image reference. */
|
|
8486
|
+
function validateImageRef(ref) {
|
|
8487
|
+
const name = ref.split("@")[0].replace(/:[\w][\w.-]*$/, "");
|
|
8488
|
+
if (!REFERENCE.test(ref) || name.length > MAX_NAME_LENGTH) throw new ImageRefInvalid(ref);
|
|
8489
|
+
return ref;
|
|
9458
8490
|
}
|
|
8491
|
+
//#endregion
|
|
8492
|
+
//#region src/docker/index.ts
|
|
8493
|
+
const logger = require_discover.output;
|
|
8494
|
+
/** `docker build` failed; what docker printed above says why. */
|
|
8495
|
+
var DockerBuildFailed = class extends Error {
|
|
8496
|
+
imageRef;
|
|
8497
|
+
constructor(imageRef, cause) {
|
|
8498
|
+
super(`Failed to build Docker image: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
8499
|
+
this.imageRef = imageRef;
|
|
8500
|
+
this.name = "DockerBuildFailed";
|
|
8501
|
+
}
|
|
8502
|
+
};
|
|
8503
|
+
/** `docker push` failed — usually the registry login or its permissions. */
|
|
8504
|
+
var DockerPushFailed = class extends Error {
|
|
8505
|
+
imageRef;
|
|
8506
|
+
constructor(imageRef, cause) {
|
|
8507
|
+
super(`Failed to push Docker image: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
8508
|
+
this.imageRef = imageRef;
|
|
8509
|
+
this.name = "DockerPushFailed";
|
|
8510
|
+
}
|
|
8511
|
+
};
|
|
8512
|
+
/** `gkm docker --push` was asked for with nowhere to push to. */
|
|
8513
|
+
var PushNeedsRegistry = class extends require_errors.GkmError {
|
|
8514
|
+
imageName;
|
|
8515
|
+
constructor(imageName) {
|
|
8516
|
+
super(`Registry is required to push Docker image '${imageName}'. Use --registry or configure docker.registry in gkm.config.ts`);
|
|
8517
|
+
this.imageName = imageName;
|
|
8518
|
+
this.name = "PushNeedsRegistry";
|
|
8519
|
+
}
|
|
8520
|
+
};
|
|
9459
8521
|
/**
|
|
9460
|
-
*
|
|
9461
|
-
*
|
|
9462
|
-
* on
|
|
8522
|
+
* Docker command implementation: one Dockerfile per deployable app, each
|
|
8523
|
+
* building its image inside Docker from a turbo-pruned slice of the build
|
|
8524
|
+
* root — nothing is built on the host — plus the build root's
|
|
8525
|
+
* `.dockerignore` and `docker-compose.constructs.yml`.
|
|
9463
8526
|
*/
|
|
9464
|
-
function
|
|
9465
|
-
const
|
|
9466
|
-
|
|
9467
|
-
|
|
9468
|
-
|
|
9469
|
-
|
|
9470
|
-
|
|
9471
|
-
|
|
9472
|
-
}];
|
|
9473
|
-
if (open.length > 0) statements.push({
|
|
9474
|
-
Sid: "GkmOpenPaths",
|
|
9475
|
-
Effect: "Allow",
|
|
9476
|
-
Principal: "*",
|
|
9477
|
-
Action: "s3:GetObject",
|
|
9478
|
-
Resource: [...new Set(open.map((p) => openResource(bucket, p)))].sort()
|
|
9479
|
-
});
|
|
9480
|
-
return {
|
|
9481
|
-
Version: "2012-10-17",
|
|
9482
|
-
Statement: statements
|
|
9483
|
-
};
|
|
8527
|
+
async function dockerCommand(options) {
|
|
8528
|
+
const cwd = options.cwd ?? process.cwd();
|
|
8529
|
+
const loaded = await require_config.loadWorkspaceConfig(cwd);
|
|
8530
|
+
const names = loaded.type === "single" ? Object.fromEntries(Object.keys(loaded.workspace.apps).map((name) => [name, resolveDockerConfig(loaded.raw).imageName])) : {};
|
|
8531
|
+
const result = await workspaceDockerCommand(loaded.workspace, loaded.manifest, names, loaded.background, options.stage);
|
|
8532
|
+
if (options.build) for (const app of result.apps) await buildDockerImage(app, result.buildRoot, options);
|
|
8533
|
+
if (options.push) for (const app of result.apps) await pushDockerImage(app.imageName, options, result.buildRoot);
|
|
8534
|
+
return result;
|
|
9484
8535
|
}
|
|
9485
8536
|
/**
|
|
9486
|
-
*
|
|
9487
|
-
*
|
|
9488
|
-
* `RestrictPublicBuckets` off), and ACLs still may not.
|
|
8537
|
+
* Build one app's image, from the build root.
|
|
8538
|
+
* Uses BuildKit for cache mount support
|
|
9489
8539
|
*/
|
|
9490
|
-
function
|
|
9491
|
-
|
|
9492
|
-
|
|
9493
|
-
|
|
9494
|
-
|
|
9495
|
-
|
|
9496
|
-
|
|
9497
|
-
|
|
9498
|
-
|
|
9499
|
-
|
|
9500
|
-
|
|
9501
|
-
|
|
9502
|
-
|
|
9503
|
-
|
|
9504
|
-
|
|
9505
|
-
|
|
9506
|
-
|
|
9507
|
-
|
|
9508
|
-
|
|
9509
|
-
|
|
9510
|
-
|
|
8540
|
+
async function buildDockerImage(app, buildRoot, options) {
|
|
8541
|
+
const tag = options.tag ?? "latest";
|
|
8542
|
+
const registry = options.registry;
|
|
8543
|
+
const fullImageName = validateImageRef(registry ? `${registry}/${app.imageName}:${tag}` : `${app.imageName}:${tag}`);
|
|
8544
|
+
logger.log(`\n🐳 Building Docker image: ${fullImageName}`);
|
|
8545
|
+
try {
|
|
8546
|
+
await require_credentials.run("docker", [
|
|
8547
|
+
"build",
|
|
8548
|
+
`--file=${(0, node_path.relative)(buildRoot, app.dockerfile)}`,
|
|
8549
|
+
`--tag=${fullImageName}`,
|
|
8550
|
+
"."
|
|
8551
|
+
], {
|
|
8552
|
+
cwd: buildRoot,
|
|
8553
|
+
env: {
|
|
8554
|
+
...process.env,
|
|
8555
|
+
DOCKER_BUILDKIT: "1"
|
|
8556
|
+
}
|
|
8557
|
+
});
|
|
8558
|
+
logger.log(`✅ Docker image built: ${fullImageName}`);
|
|
8559
|
+
} catch (error) {
|
|
8560
|
+
throw new DockerBuildFailed(fullImageName, error);
|
|
8561
|
+
}
|
|
9511
8562
|
}
|
|
9512
8563
|
/**
|
|
9513
|
-
*
|
|
9514
|
-
* one-element list, lists sorted — so one AWS reformatted reads as the same
|
|
9515
|
-
* document rather than as drift.
|
|
8564
|
+
* Push Docker image to registry
|
|
9516
8565
|
*/
|
|
9517
|
-
function
|
|
9518
|
-
|
|
9519
|
-
|
|
9520
|
-
|
|
9521
|
-
|
|
9522
|
-
|
|
9523
|
-
return key === "Statement" ? items : items.map((v) => JSON.stringify(v)).sort();
|
|
9524
|
-
}
|
|
9525
|
-
if (value && typeof value === "object") return Object.fromEntries(Object.keys(value).sort().map((k) => {
|
|
9526
|
-
const v = value[k];
|
|
9527
|
-
return [k, canonical(k === "Action" || k === "Resource" || k === "AWS" ? Array.isArray(v) ? v : [v] : v, k)];
|
|
9528
|
-
}));
|
|
9529
|
-
return value;
|
|
9530
|
-
}
|
|
9531
|
-
//#endregion
|
|
9532
|
-
//#region src/providers/s3/index.ts
|
|
9533
|
-
/** The tags gkm finds its own buckets and users by. */
|
|
9534
|
-
const TAG_PROJECT = "gkm:project";
|
|
9535
|
-
const TAG_STAGE = "gkm:stage";
|
|
9536
|
-
const TAG_CONSTRUCT = "gkm:construct";
|
|
9537
|
-
function bucketConstructs(manifest) {
|
|
9538
|
-
return Object.entries(manifest).filter(([, d]) => d.kind === "objects").map(([id, d]) => ({
|
|
9539
|
-
id,
|
|
9540
|
-
versioned: d.versioned === true,
|
|
9541
|
-
servers: Object.entries(manifest).filter(([, s]) => s.kind === "file-server" && s.of === id).map(([sid, s]) => ({
|
|
9542
|
-
id: sid,
|
|
9543
|
-
open: s.open ?? []
|
|
9544
|
-
})).sort((a, b) => a.id.localeCompare(b.id))
|
|
9545
|
-
})).sort((a, b) => a.id.localeCompare(b.id));
|
|
9546
|
-
}
|
|
9547
|
-
/**
|
|
9548
|
-
* The region the stage's buckets are created in: the entry's, the secrets
|
|
9549
|
-
* store's when it is in AWS, the credential's, `AWS_REGION`.
|
|
9550
|
-
*/
|
|
9551
|
-
function s3Region(ctx, env = process.env) {
|
|
9552
|
-
const store = ctx.workspace.secrets?.store;
|
|
9553
|
-
const storeRegion = store && typeof store === "object" && "region" in store ? store.region : void 0;
|
|
9554
|
-
const region = ctx.config.region ?? storeRegion ?? ctx.credential?.region ?? env.AWS_REGION ?? env.AWS_DEFAULT_REGION;
|
|
9555
|
-
if (!region) throw new require_workspace.S3RegionRequired(ctx.stage);
|
|
9556
|
-
return region;
|
|
9557
|
-
}
|
|
9558
|
-
/**
|
|
9559
|
-
* The address a file server over `bucket` serves on: the bucket's regional
|
|
9560
|
-
* virtual-host endpoint, which is what `FileServer.url(key)` appends a key to
|
|
9561
|
-
* — `https://<bucket>.s3.<region>.amazonaws.com/<key>`. Against an emulator,
|
|
9562
|
-
* its path-style address.
|
|
9563
|
-
*/
|
|
9564
|
-
function bucketPublicUrl(bucket, region, endpoint) {
|
|
9565
|
-
if (endpoint) return `${endpoint.replace(/\/+$/, "")}/${bucket}`;
|
|
9566
|
-
return `https://${bucket}.s3.${region}.amazonaws.com`;
|
|
9567
|
-
}
|
|
9568
|
-
/** What an AWS SDK error is called, whichever field carries it. */
|
|
9569
|
-
function errorName(error) {
|
|
9570
|
-
const e = error;
|
|
9571
|
-
return e?.Code ?? e?.name ?? "";
|
|
9572
|
-
}
|
|
9573
|
-
/** A read whose "there is none" answer is one of `missing`. */
|
|
9574
|
-
async function readOr(read, missing) {
|
|
8566
|
+
async function pushDockerImage(imageName, options, cwd) {
|
|
8567
|
+
const tag = options.tag ?? "latest";
|
|
8568
|
+
const registry = options.registry;
|
|
8569
|
+
if (!registry) throw new PushNeedsRegistry(imageName);
|
|
8570
|
+
const fullImageName = validateImageRef(`${registry}/${imageName}:${tag}`);
|
|
8571
|
+
logger.log(`\n🚀 Pushing Docker image: ${fullImageName}`);
|
|
9575
8572
|
try {
|
|
9576
|
-
|
|
8573
|
+
await require_credentials.run("docker", ["push", fullImageName], { cwd });
|
|
8574
|
+
logger.log(`✅ Docker image pushed: ${fullImageName}`);
|
|
9577
8575
|
} catch (error) {
|
|
9578
|
-
|
|
9579
|
-
throw error;
|
|
8576
|
+
throw new DockerPushFailed(fullImageName, error);
|
|
9580
8577
|
}
|
|
9581
8578
|
}
|
|
9582
|
-
|
|
9583
|
-
|
|
9584
|
-
|
|
9585
|
-
|
|
9586
|
-
|
|
9587
|
-
|
|
9588
|
-
|
|
9589
|
-
|
|
9590
|
-
|
|
9591
|
-
|
|
9592
|
-
|
|
9593
|
-
|
|
9594
|
-
|
|
9595
|
-
} : {}
|
|
9596
|
-
}),
|
|
9597
|
-
iam: new IAM.IAMClient(config),
|
|
9598
|
-
region,
|
|
9599
|
-
...endpoint ? { endpoint } : {}
|
|
9600
|
-
};
|
|
9601
|
-
};
|
|
9602
|
-
/** For tests: what makes the clients. */
|
|
9603
|
-
let makeClients = defaultClients;
|
|
9604
|
-
async function ensureS3(ctx) {
|
|
9605
|
-
const region = s3Region(ctx);
|
|
9606
|
-
const clients = await makeClients(ctx.credential, region);
|
|
9607
|
-
for (const construct of bucketConstructs(ctx.manifest)) await ensureBucket(ctx, clients, construct);
|
|
9608
|
-
}
|
|
9609
|
-
/** The keys gkm tags a bucket and a user with. */
|
|
9610
|
-
function identityTags(ctx, id) {
|
|
9611
|
-
return [
|
|
9612
|
-
{
|
|
9613
|
-
Key: TAG_PROJECT,
|
|
9614
|
-
Value: ctx.identity.key
|
|
9615
|
-
},
|
|
9616
|
-
{
|
|
9617
|
-
Key: TAG_STAGE,
|
|
9618
|
-
Value: ctx.stage
|
|
9619
|
-
},
|
|
9620
|
-
{
|
|
9621
|
-
Key: TAG_CONSTRUCT,
|
|
9622
|
-
Value: id
|
|
9623
|
-
}
|
|
9624
|
-
];
|
|
9625
|
-
}
|
|
9626
|
-
async function ensureBucket(ctx, clients, construct) {
|
|
9627
|
-
const { id } = construct;
|
|
9628
|
-
const nameInput = {
|
|
9629
|
-
scope: ctx.identity.scope,
|
|
9630
|
-
stage: ctx.stage,
|
|
9631
|
-
id: require_derive.appKey(id)
|
|
9632
|
-
};
|
|
9633
|
-
const versioning = ctx.config.versioning ?? construct.versioned;
|
|
9634
|
-
const open = [...new Set(construct.servers.flatMap((s) => s.open))].sort();
|
|
9635
|
-
const { bucket, exists } = await ensureBucketName(ctx, clients, id, bucketName(nameInput));
|
|
9636
|
-
await ensureBucketSettings(ctx, clients, {
|
|
9637
|
-
id,
|
|
9638
|
-
bucket,
|
|
9639
|
-
exists,
|
|
9640
|
-
versioning,
|
|
9641
|
-
open
|
|
9642
|
-
});
|
|
9643
|
-
await ensureAccessKey(ctx, clients, {
|
|
9644
|
-
id,
|
|
9645
|
-
bucket,
|
|
9646
|
-
...await ensureUser(ctx, clients, id, iamUserName(nameInput), userPolicy(bucket, { versioning }))
|
|
9647
|
-
});
|
|
9648
|
-
const publicUrl = bucketPublicUrl(bucket, clients.region, clients.endpoint);
|
|
9649
|
-
for (const server of construct.servers) {
|
|
9650
|
-
const key = (0, _geekmidas_manifest.provideKey)(server.id, "url");
|
|
9651
|
-
const entry = {
|
|
9652
|
-
key: `file-server:${server.id}`,
|
|
9653
|
-
type: "url"
|
|
9654
|
-
};
|
|
9655
|
-
const recorded = ctx.state.record(entry.key)?.id;
|
|
9656
|
-
const current = ctx.secrets[key];
|
|
9657
|
-
if (current === publicUrl) {
|
|
9658
|
-
await ctx.state.ready(entry, publicUrl);
|
|
9659
|
-
continue;
|
|
9660
|
-
}
|
|
9661
|
-
if (current !== void 0 && current !== recorded) {
|
|
9662
|
-
ctx.log(` ${key} is set to ${current} by you, and kept — the bucket's own address is ${publicUrl}`);
|
|
8579
|
+
/**
|
|
8580
|
+
* Generate Dockerfiles for all apps in a workspace.
|
|
8581
|
+
* @internal Exported for testing
|
|
8582
|
+
*/
|
|
8583
|
+
async function workspaceDockerCommand(workspace, manifest, imageNames = {}, background = {}, stage) {
|
|
8584
|
+
const results = [];
|
|
8585
|
+
const layout = imageLayout(workspace);
|
|
8586
|
+
logger.log(`\n🐳 Generating Dockerfiles for workspace: ${workspace.name}`);
|
|
8587
|
+
logger.log(` Build root: ${layout.buildRoot}`);
|
|
8588
|
+
logger.log(` Package manager: ${layout.tools.packageManager}${layout.tools.packageManagerVersion ? `@${layout.tools.packageManagerVersion}` : ""}, turbo@${layout.tools.turboVersion}`);
|
|
8589
|
+
for (const [appName, app] of Object.entries(workspace.apps)) {
|
|
8590
|
+
if (app.type === "mobile") {
|
|
8591
|
+
logger.log(`\n 📱 Skipping Docker for ${appName} (mobile app — deploy via framework toolchain)`);
|
|
9663
8592
|
continue;
|
|
9664
8593
|
}
|
|
9665
|
-
|
|
9666
|
-
|
|
9667
|
-
|
|
9668
|
-
|
|
9669
|
-
|
|
9670
|
-
|
|
9671
|
-
|
|
8594
|
+
const buildType = app.entry ? "entry" : app.type;
|
|
8595
|
+
logger.log(`\n 📄 Generating Dockerfile for ${appName} (${buildType})`);
|
|
8596
|
+
const dockerfile = appDockerfile(appName, app, {
|
|
8597
|
+
layout,
|
|
8598
|
+
workspaceRoot: workspace.root,
|
|
8599
|
+
apps: workspace.apps,
|
|
8600
|
+
...manifest ? { manifest } : {},
|
|
8601
|
+
workspace,
|
|
8602
|
+
...stage ? { stage } : {}
|
|
8603
|
+
});
|
|
8604
|
+
const path = dockerfileOf(appName, app.path);
|
|
8605
|
+
const dockerfilePath = (0, node_path.join)(workspace.root, path);
|
|
8606
|
+
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(dockerfilePath), { recursive: true });
|
|
8607
|
+
await (0, node_fs_promises.writeFile)(dockerfilePath, dockerfile);
|
|
8608
|
+
logger.log(` Generated: ${path}`);
|
|
8609
|
+
results.push({
|
|
8610
|
+
appName,
|
|
8611
|
+
type: app.type,
|
|
8612
|
+
dockerfile: dockerfilePath,
|
|
8613
|
+
imageName: imageNames[appName] ?? appName
|
|
8614
|
+
});
|
|
8615
|
+
}
|
|
8616
|
+
for (const worker of manifest ? workerUnits(workspace, manifest, background) : []) {
|
|
8617
|
+
logger.log(`\n 📄 Generating Dockerfile for ${worker.name} (worker ${worker.id}, built from ${worker.app})`);
|
|
8618
|
+
const dockerfile = workerDockerfile(worker, workspace.apps[worker.app], {
|
|
8619
|
+
layout,
|
|
8620
|
+
workspaceRoot: workspace.root
|
|
8621
|
+
});
|
|
8622
|
+
const path = workerDockerfileOf(worker.name);
|
|
8623
|
+
const dockerfilePath = (0, node_path.join)(workspace.root, path);
|
|
8624
|
+
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(dockerfilePath), { recursive: true });
|
|
8625
|
+
await (0, node_fs_promises.writeFile)(dockerfilePath, dockerfile);
|
|
8626
|
+
logger.log(` Generated: ${path}`);
|
|
8627
|
+
results.push({
|
|
8628
|
+
appName: worker.name,
|
|
8629
|
+
type: "backend",
|
|
8630
|
+
dockerfile: dockerfilePath,
|
|
8631
|
+
imageName: imageNames[worker.name] ?? worker.name
|
|
9672
8632
|
});
|
|
9673
8633
|
}
|
|
8634
|
+
const dockerignorePath = await ensureDockerignore(layout.buildRoot);
|
|
8635
|
+
logger.log(`\n Ensured: ${dockerignorePath}`);
|
|
8636
|
+
const composePath = await writeConstructsCompose(workspace);
|
|
8637
|
+
logger.log(`\n✅ Generated ${results.length} Dockerfile(s) + ${COMPOSE_PATH}`);
|
|
8638
|
+
logger.log(`\n📋 Build commands (from ${layout.buildRoot}):`);
|
|
8639
|
+
for (const result of results) {
|
|
8640
|
+
const icon = result.type === "backend" ? "⚙️" : result.type === "mobile" ? "📱" : "🌐";
|
|
8641
|
+
logger.log(` ${icon} docker build -f ${(0, node_path.relative)(layout.buildRoot, result.dockerfile)} -t ${result.imageName} .`);
|
|
8642
|
+
}
|
|
8643
|
+
printRunInstructions(workspace);
|
|
8644
|
+
return {
|
|
8645
|
+
apps: results,
|
|
8646
|
+
dockerCompose: composePath,
|
|
8647
|
+
dockerignore: dockerignorePath,
|
|
8648
|
+
buildRoot: layout.buildRoot
|
|
8649
|
+
};
|
|
9674
8650
|
}
|
|
9675
8651
|
/**
|
|
9676
|
-
*
|
|
9677
|
-
*
|
|
8652
|
+
* One app's Dockerfile, by what it is: a gkm backend bundled by `gkm build`,
|
|
8653
|
+
* a backend with its own entry bundled by esbuild, or a site by its
|
|
8654
|
+
* framework. The one choice every builder of images makes.
|
|
9678
8655
|
*/
|
|
9679
|
-
|
|
9680
|
-
const
|
|
9681
|
-
|
|
9682
|
-
|
|
9683
|
-
|
|
9684
|
-
|
|
9685
|
-
type: "s3-bucket",
|
|
9686
|
-
data: {
|
|
9687
|
-
name,
|
|
9688
|
-
region
|
|
9689
|
-
}
|
|
8656
|
+
function appDockerfile(appName, app, options) {
|
|
8657
|
+
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);
|
|
8658
|
+
if (app.type === "web") return siteDockerfile(appName, app, {
|
|
8659
|
+
image,
|
|
8660
|
+
...options.manifest ? { manifest: options.manifest } : {},
|
|
8661
|
+
...options.publicUrlArgs ? { publicUrlArgs: options.publicUrlArgs } : {}
|
|
9690
8662
|
});
|
|
9691
|
-
|
|
9692
|
-
|
|
9693
|
-
|
|
9694
|
-
|
|
9695
|
-
return "ours";
|
|
9696
|
-
} catch (error) {
|
|
9697
|
-
const name = errorName(error);
|
|
9698
|
-
if (name === "NotFound" || name === "NoSuchBucket") return "none";
|
|
9699
|
-
if (name === "Forbidden" || name === "AccessDenied") return "taken";
|
|
9700
|
-
throw error;
|
|
9701
|
-
}
|
|
9702
|
-
};
|
|
9703
|
-
const create = async (name) => {
|
|
9704
|
-
if (ctx.dryRun) {
|
|
9705
|
-
const found = await probe(name);
|
|
9706
|
-
if (found === "none") {
|
|
9707
|
-
await ctx.change({
|
|
9708
|
-
construct: id,
|
|
9709
|
-
resource: `bucket ${name}`,
|
|
9710
|
-
change: `create in ${region}`
|
|
9711
|
-
}, async () => {});
|
|
9712
|
-
return "created";
|
|
9713
|
-
}
|
|
9714
|
-
return found;
|
|
9715
|
-
}
|
|
9716
|
-
await ctx.state.pending(entry(name));
|
|
9717
|
-
try {
|
|
9718
|
-
await s3.send(new S3.CreateBucketCommand({
|
|
9719
|
-
Bucket: name,
|
|
9720
|
-
...region === "us-east-1" ? {} : { CreateBucketConfiguration: { LocationConstraint: region } }
|
|
9721
|
-
}));
|
|
9722
|
-
} catch (error) {
|
|
9723
|
-
const code = errorName(error);
|
|
9724
|
-
if (code === "BucketAlreadyOwnedByYou") return "ours";
|
|
9725
|
-
if (code === "BucketAlreadyExists") return "taken";
|
|
9726
|
-
throw error;
|
|
9727
|
-
}
|
|
9728
|
-
await ctx.change({
|
|
9729
|
-
construct: id,
|
|
9730
|
-
resource: `bucket ${name}`,
|
|
9731
|
-
change: `create in ${region}`
|
|
9732
|
-
}, async () => {});
|
|
9733
|
-
return "created";
|
|
9734
|
-
};
|
|
9735
|
-
const adopt = async (name) => {
|
|
9736
|
-
const owner = (await readOr(() => s3.send(new S3.GetBucketTaggingCommand({ Bucket: name })), ["NoSuchTagSet", "NoSuchTagSetError"]))?.TagSet?.find((t) => t.Key === TAG_PROJECT)?.Value;
|
|
9737
|
-
if (owner && owner !== ctx.identity.key) throw new require_workspace.BucketOwnedByAnotherProject(name, owner, ctx.identity.key);
|
|
9738
|
-
};
|
|
9739
|
-
if (record?.status === "ready" && record.id) {
|
|
9740
|
-
const found = await probe(record.id);
|
|
9741
|
-
if (found === "ours") return {
|
|
9742
|
-
bucket: record.id,
|
|
9743
|
-
exists: true
|
|
9744
|
-
};
|
|
9745
|
-
if (found === "taken") throw new require_workspace.RecordedBucketUnreachable(id, record.id);
|
|
9746
|
-
const made = await create(record.id);
|
|
9747
|
-
if (made === "taken") throw new require_workspace.RecordedBucketUnreachable(id, record.id);
|
|
9748
|
-
if (!ctx.dryRun) await ctx.state.ready(entry(record.id), record.id);
|
|
9749
|
-
return {
|
|
9750
|
-
bucket: record.id,
|
|
9751
|
-
exists: made === "ours"
|
|
9752
|
-
};
|
|
9753
|
-
}
|
|
9754
|
-
const pending = record?.data?.name;
|
|
9755
|
-
const names = [
|
|
9756
|
-
...typeof pending === "string" && pending !== good ? [pending] : [],
|
|
9757
|
-
good,
|
|
9758
|
-
...Array.from({ length: 5 }, () => suffixedBucketName(good, randomSuffix()))
|
|
9759
|
-
];
|
|
9760
|
-
const tried = [];
|
|
9761
|
-
for (const name of names) {
|
|
9762
|
-
tried.push(name);
|
|
9763
|
-
const made = await create(name);
|
|
9764
|
-
if (made === "taken") continue;
|
|
9765
|
-
if (made === "ours") await adopt(name);
|
|
9766
|
-
if (!ctx.dryRun) await ctx.state.ready(entry(name), name);
|
|
9767
|
-
if (name !== good) ctx.log(` '${good}' is taken by another account; '${id}' is '${name}', recorded in the stage's state`);
|
|
9768
|
-
return {
|
|
9769
|
-
bucket: name,
|
|
9770
|
-
exists: made === "ours"
|
|
9771
|
-
};
|
|
9772
|
-
}
|
|
9773
|
-
throw new require_workspace.BucketNameUnavailable(id, tried.filter((n) => n !== pending));
|
|
9774
|
-
}
|
|
9775
|
-
/** Tags, Block Public Access, encryption, policy, versioning and CORS. */
|
|
9776
|
-
async function ensureBucketSettings(ctx, clients, input) {
|
|
9777
|
-
const { S3, s3 } = clients;
|
|
9778
|
-
const { id, bucket } = input;
|
|
9779
|
-
const Bucket = bucket;
|
|
9780
|
-
const fresh = ctx.dryRun && !input.exists;
|
|
9781
|
-
const read = async (fn, missing) => fresh ? void 0 : readOr(fn, missing);
|
|
9782
|
-
const set = (change, apply) => ctx.change({
|
|
9783
|
-
construct: id,
|
|
9784
|
-
resource: `bucket ${bucket}`,
|
|
9785
|
-
change
|
|
9786
|
-
}, () => apply().then(() => void 0));
|
|
9787
|
-
const tagged = await read(() => s3.send(new S3.GetBucketTaggingCommand({ Bucket })), ["NoSuchTagSet", "NoSuchTagSetError"]);
|
|
9788
|
-
const ours = identityTags(ctx, id);
|
|
9789
|
-
const others = (tagged?.TagSet ?? []).filter((t) => !ours.some((o) => o.Key === t.Key));
|
|
9790
|
-
if (ours.some((o) => !tagged?.TagSet?.some((t) => t.Key === o.Key && t.Value === o.Value))) await set("tag", () => s3.send(new S3.PutBucketTaggingCommand({
|
|
9791
|
-
Bucket,
|
|
9792
|
-
Tagging: { TagSet: [...others, ...ours].map((t) => ({
|
|
9793
|
-
Key: t.Key,
|
|
9794
|
-
Value: t.Value
|
|
9795
|
-
})) }
|
|
9796
|
-
})));
|
|
9797
|
-
const isOpen = input.open.length > 0;
|
|
9798
|
-
const block = publicAccessBlock(isOpen);
|
|
9799
|
-
const ensureBlock = async () => {
|
|
9800
|
-
const now = (await read(() => s3.send(new S3.GetPublicAccessBlockCommand({ Bucket })), ["NoSuchPublicAccessBlockConfiguration"]))?.PublicAccessBlockConfiguration;
|
|
9801
|
-
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({
|
|
9802
|
-
Bucket,
|
|
9803
|
-
PublicAccessBlockConfiguration: block
|
|
9804
|
-
})));
|
|
9805
|
-
};
|
|
9806
|
-
const policy = bucketPolicy(bucket, input.open);
|
|
9807
|
-
const ensurePolicy = async () => {
|
|
9808
|
-
const current = await read(() => s3.send(new S3.GetBucketPolicyCommand({ Bucket })), ["NoSuchBucketPolicy"]);
|
|
9809
|
-
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({
|
|
9810
|
-
Bucket,
|
|
9811
|
-
Policy: JSON.stringify(policy)
|
|
9812
|
-
})));
|
|
9813
|
-
};
|
|
9814
|
-
if (isOpen) {
|
|
9815
|
-
await ensureBlock();
|
|
9816
|
-
await ensurePolicy();
|
|
9817
|
-
} else {
|
|
9818
|
-
await ensurePolicy();
|
|
9819
|
-
await ensureBlock();
|
|
9820
|
-
}
|
|
9821
|
-
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({
|
|
9822
|
-
Bucket,
|
|
9823
|
-
ServerSideEncryptionConfiguration: { Rules: [{ ApplyServerSideEncryptionByDefault: { SSEAlgorithm: "AES256" } }] }
|
|
9824
|
-
})));
|
|
9825
|
-
const status = (await read(() => s3.send(new S3.GetBucketVersioningCommand({ Bucket })), []))?.Status;
|
|
9826
|
-
if (input.versioning && status !== "Enabled") await set("enable versioning", () => s3.send(new S3.PutBucketVersioningCommand({
|
|
9827
|
-
Bucket,
|
|
9828
|
-
VersioningConfiguration: { Status: "Enabled" }
|
|
9829
|
-
})));
|
|
9830
|
-
else if (!input.versioning && status === "Enabled") await set("suspend versioning", () => s3.send(new S3.PutBucketVersioningCommand({
|
|
9831
|
-
Bucket,
|
|
9832
|
-
VersioningConfiguration: { Status: "Suspended" }
|
|
9833
|
-
})));
|
|
9834
|
-
const { origins, unresolved } = bucketOrigins({
|
|
9835
|
-
workspace: ctx.workspace,
|
|
9836
|
-
manifest: ctx.manifest,
|
|
9837
|
-
runnables: ctx.runnables,
|
|
9838
|
-
stage: ctx.stage,
|
|
9839
|
-
bucket: id
|
|
8663
|
+
if (app.entry) return generateEntryDockerfile({
|
|
8664
|
+
...image,
|
|
8665
|
+
entry: app.entry,
|
|
8666
|
+
healthCheckPath: "/health"
|
|
9840
8667
|
});
|
|
9841
|
-
|
|
9842
|
-
|
|
9843
|
-
|
|
9844
|
-
|
|
9845
|
-
if (!(cors?.CORSRules?.length === 1 && canonicalCors(cors.CORSRules[0]) === canonicalCors(rule))) await set(`CORS for ${origins.join(", ")}`, () => s3.send(new S3.PutBucketCorsCommand({
|
|
9846
|
-
Bucket,
|
|
9847
|
-
CORSConfiguration: { CORSRules: [rule] }
|
|
9848
|
-
})));
|
|
9849
|
-
}
|
|
9850
|
-
function canonicalCors(rule) {
|
|
9851
|
-
const sorted = (list) => [...list ?? []].sort();
|
|
9852
|
-
return JSON.stringify({
|
|
9853
|
-
AllowedMethods: sorted(rule.AllowedMethods),
|
|
9854
|
-
AllowedOrigins: sorted(rule.AllowedOrigins),
|
|
9855
|
-
AllowedHeaders: sorted(rule.AllowedHeaders),
|
|
9856
|
-
ExposeHeaders: sorted(rule.ExposeHeaders),
|
|
9857
|
-
MaxAgeSeconds: rule.MaxAgeSeconds ?? null
|
|
8668
|
+
return generateBackendDockerfile({
|
|
8669
|
+
...image,
|
|
8670
|
+
healthCheckPath: "/health",
|
|
8671
|
+
...options.cache ? { cache: options.cache } : {}
|
|
9858
8672
|
});
|
|
9859
8673
|
}
|
|
9860
|
-
/**
|
|
9861
|
-
|
|
9862
|
-
|
|
9863
|
-
|
|
9864
|
-
|
|
9865
|
-
|
|
9866
|
-
|
|
9867
|
-
|
|
9868
|
-
|
|
9869
|
-
|
|
9870
|
-
|
|
9871
|
-
|
|
9872
|
-
|
|
9873
|
-
|
|
9874
|
-
|
|
9875
|
-
|
|
9876
|
-
|
|
9877
|
-
}, async () => {
|
|
9878
|
-
await ctx.state.pending(entry);
|
|
9879
|
-
await iam.send(new IAM.CreateUserCommand({
|
|
9880
|
-
UserName: user,
|
|
9881
|
-
Path: IAM_USER_PATH,
|
|
9882
|
-
Tags: identityTags(ctx, id)
|
|
9883
|
-
}));
|
|
9884
|
-
await ctx.state.ready(entry, user);
|
|
9885
|
-
});
|
|
9886
|
-
const exists = Boolean(found?.User) || !ctx.dryRun;
|
|
9887
|
-
const current = exists ? await readOr(() => iam.send(new IAM.GetUserPolicyCommand({
|
|
9888
|
-
UserName: user,
|
|
9889
|
-
PolicyName: IAM_POLICY_NAME
|
|
9890
|
-
})), ["NoSuchEntity", "NoSuchEntityException"]) : void 0;
|
|
9891
|
-
const document = current?.PolicyDocument ? JSON.parse(decodePolicy(current.PolicyDocument)) : void 0;
|
|
9892
|
-
if (!document || canonicalPolicy(document) !== canonicalPolicy(policy)) await ctx.change({
|
|
9893
|
-
construct: id,
|
|
9894
|
-
resource: `IAM user ${user}`,
|
|
9895
|
-
change: `policy ${IAM_POLICY_NAME}: this bucket only`
|
|
9896
|
-
}, async () => {
|
|
9897
|
-
await iam.send(new IAM.PutUserPolicyCommand({
|
|
9898
|
-
UserName: user,
|
|
9899
|
-
PolicyName: IAM_POLICY_NAME,
|
|
9900
|
-
PolicyDocument: JSON.stringify(policy)
|
|
9901
|
-
}));
|
|
8674
|
+
/** Where `gkm docker` writes a worker's Dockerfile. */
|
|
8675
|
+
function workerDockerfileOf(worker) {
|
|
8676
|
+
return `.gkm/docker/Dockerfile.${worker}`;
|
|
8677
|
+
}
|
|
8678
|
+
/**
|
|
8679
|
+
* A Worker's Dockerfile: its host app's build, and the worker's bundle as the
|
|
8680
|
+
* runner — see `generateWorkerDockerfile`.
|
|
8681
|
+
*/
|
|
8682
|
+
function workerDockerfile(worker, host, options) {
|
|
8683
|
+
return generateWorkerDockerfile({
|
|
8684
|
+
...appImageOptions(options.layout, worker.app, host, options.workspaceRoot),
|
|
8685
|
+
imageName: worker.name,
|
|
8686
|
+
port: WORKER_PORT,
|
|
8687
|
+
worker: worker.id,
|
|
8688
|
+
bundle: workerBundleName(worker.id),
|
|
8689
|
+
healthCheckPath: "/health",
|
|
8690
|
+
...options.cache ? { cache: options.cache } : {}
|
|
9902
8691
|
});
|
|
9903
|
-
return {
|
|
9904
|
-
user,
|
|
9905
|
-
exists
|
|
9906
|
-
};
|
|
9907
|
-
}
|
|
9908
|
-
/** IAM returns a policy document URL-encoded; an emulator may not. */
|
|
9909
|
-
function decodePolicy(document) {
|
|
9910
|
-
try {
|
|
9911
|
-
return decodeURIComponent(document);
|
|
9912
|
-
} catch {
|
|
9913
|
-
return document;
|
|
9914
|
-
}
|
|
9915
8692
|
}
|
|
9916
8693
|
/**
|
|
9917
|
-
*
|
|
8694
|
+
* A site's Dockerfile, with an `ARG` for every key its bundler inlines.
|
|
9918
8695
|
*
|
|
9919
|
-
*
|
|
9920
|
-
*
|
|
9921
|
-
*
|
|
9922
|
-
*
|
|
9923
|
-
* - A key the secrets no longer hold (a store emptied by hand): replaced the
|
|
9924
|
-
* same way, since its secret cannot be read back.
|
|
8696
|
+
* The keys come from the site's declaration where there is one — every
|
|
8697
|
+
* public key its edges give it, `VITE_UPLOADS_SERVER_URL` included — and an
|
|
8698
|
+
* `ARG` the Dockerfile does not declare is a build argument the build drops,
|
|
8699
|
+
* leaving the bundle with an empty URL.
|
|
9925
8700
|
*/
|
|
9926
|
-
|
|
9927
|
-
const
|
|
9928
|
-
const
|
|
9929
|
-
const
|
|
9930
|
-
|
|
9931
|
-
|
|
9932
|
-
|
|
9933
|
-
|
|
9934
|
-
|
|
9935
|
-
|
|
9936
|
-
|
|
9937
|
-
|
|
9938
|
-
} catch {
|
|
9939
|
-
parsed = void 0;
|
|
9940
|
-
}
|
|
9941
|
-
if (parsed?.bucket !== bucket) throw new require_workspace.BucketKeySetElsewhere(urlKey, bucket, parsed?.bucket ?? given.replace(/\/\/[^@]*@/, "//"), ctx.stage);
|
|
9942
|
-
inUrl = parsed.accessKeyId;
|
|
9943
|
-
}
|
|
9944
|
-
const listed = input.exists ? (await iam.send(new IAM.ListAccessKeysCommand({ UserName: user }))).AccessKeyMetadata?.map((k) => k.AccessKeyId) ?? [] : [];
|
|
9945
|
-
let current = record?.status === "ready" ? record.id : void 0;
|
|
9946
|
-
let previous = typeof record?.data?.previous === "string" ? record.data.previous : void 0;
|
|
9947
|
-
const rotatedAt = typeof record?.data?.rotatedAt === "string" ? record.data.rotatedAt : void 0;
|
|
9948
|
-
const entry = (data = {}) => ({
|
|
9949
|
-
key: entryKey,
|
|
9950
|
-
type: "iam-access-key",
|
|
9951
|
-
data: {
|
|
9952
|
-
user,
|
|
9953
|
-
...data
|
|
9954
|
-
}
|
|
9955
|
-
});
|
|
9956
|
-
if (previous && current) {
|
|
9957
|
-
const deployed = rotatedAt !== void 0 && ctx.state.lastDeployedAt !== void 0 && ctx.state.lastDeployedAt > rotatedAt;
|
|
9958
|
-
if (ctx.retireOldKeys || deployed) {
|
|
9959
|
-
const old = previous;
|
|
9960
|
-
const keep = current;
|
|
9961
|
-
await ctx.change({
|
|
9962
|
-
construct: id,
|
|
9963
|
-
resource: `IAM user ${user}`,
|
|
9964
|
-
change: `delete the rotated-out access key ${old}`
|
|
9965
|
-
}, async () => {
|
|
9966
|
-
if (listed.includes(old)) await iam.send(new IAM.DeleteAccessKeyCommand({
|
|
9967
|
-
UserName: user,
|
|
9968
|
-
AccessKeyId: old
|
|
9969
|
-
}));
|
|
9970
|
-
await ctx.state.ready(entry(), keep);
|
|
9971
|
-
});
|
|
9972
|
-
previous = void 0;
|
|
9973
|
-
} 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)`);
|
|
9974
|
-
}
|
|
9975
|
-
if (!current && inUrl && listed.includes(inUrl)) {
|
|
9976
|
-
await ctx.state.ready(entry(), inUrl);
|
|
9977
|
-
current = inUrl;
|
|
9978
|
-
}
|
|
9979
|
-
if (current !== void 0 && listed.includes(current) && inUrl === current && !ctx.rotateKeys) return;
|
|
9980
|
-
if (ctx.rotateKeys && previous) throw new require_workspace.RotationInProgress(id, previous, ctx.stage);
|
|
9981
|
-
const outgoing = current && listed.includes(current) ? current : void 0;
|
|
9982
|
-
if (listed.length >= 2) throw new require_workspace.AccessKeyLimit(user, listed);
|
|
9983
|
-
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)";
|
|
9984
|
-
await ctx.change({
|
|
9985
|
-
construct: id,
|
|
9986
|
-
resource: `IAM user ${user}`,
|
|
9987
|
-
change: why
|
|
9988
|
-
}, async () => {
|
|
9989
|
-
await ctx.state.pending(entry(outgoing ? { previous: outgoing } : {}));
|
|
9990
|
-
const key = (await iam.send(new IAM.CreateAccessKeyCommand({ UserName: user }))).AccessKey;
|
|
9991
|
-
await ctx.writeSecrets({ [urlKey]: _geekmidas_storage_s3_url.build({
|
|
9992
|
-
bucket,
|
|
9993
|
-
region: clients.region,
|
|
9994
|
-
...clients.endpoint ? {
|
|
9995
|
-
endpoint: clients.endpoint,
|
|
9996
|
-
forcePathStyle: true
|
|
9997
|
-
} : {},
|
|
9998
|
-
accessKeyId: key.AccessKeyId,
|
|
9999
|
-
secretAccessKey: key.SecretAccessKey
|
|
10000
|
-
}) });
|
|
10001
|
-
await ctx.state.ready(entry(outgoing ? {
|
|
10002
|
-
previous: outgoing,
|
|
10003
|
-
rotatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10004
|
-
} : {}), key.AccessKeyId);
|
|
10005
|
-
});
|
|
10006
|
-
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}`);
|
|
10007
|
-
}
|
|
10008
|
-
/**
|
|
10009
|
-
* Each provisioned bucket answers `HeadBucket` to the key in the stage's
|
|
10010
|
-
* secrets — the one the apps will use. A bucket whose URL carries no key is
|
|
10011
|
-
* not one this provider wrote, and the deploy's own check covers a URL that
|
|
10012
|
-
* is missing.
|
|
10013
|
-
*/
|
|
10014
|
-
async function verifyS3(ctx) {
|
|
10015
|
-
const S3 = await import("@aws-sdk/client-s3");
|
|
10016
|
-
for (const { id } of bucketConstructs(ctx.manifest)) {
|
|
10017
|
-
const url = ctx.secrets[(0, _geekmidas_manifest.provideKey)(id, "url")];
|
|
10018
|
-
if (url === void 0) continue;
|
|
10019
|
-
const address = _geekmidas_storage_s3_url.parse(url);
|
|
10020
|
-
if (!address.accessKeyId || !address.secretAccessKey) continue;
|
|
10021
|
-
const client = new S3.S3Client({
|
|
10022
|
-
region: address.region ?? ctx.config.region ?? "us-east-1",
|
|
10023
|
-
credentials: {
|
|
10024
|
-
accessKeyId: address.accessKeyId,
|
|
10025
|
-
secretAccessKey: address.secretAccessKey
|
|
10026
|
-
},
|
|
10027
|
-
...address.endpoint ? { endpoint: address.endpoint } : {},
|
|
10028
|
-
...address.forcePathStyle ? { forcePathStyle: true } : {}
|
|
10029
|
-
});
|
|
10030
|
-
try {
|
|
10031
|
-
await client.send(new S3.HeadBucketCommand({ Bucket: address.bucket }));
|
|
10032
|
-
} catch (error) {
|
|
10033
|
-
const name = errorName(error);
|
|
10034
|
-
throw new require_workspace.ProvisionedBucketUnreachable(id, address.bucket, ctx.stage, name === "NotFound" ? "it does not exist" : name === "Forbidden" ? "the key is refused" : name || String(error));
|
|
10035
|
-
} finally {
|
|
10036
|
-
client.destroy();
|
|
10037
|
-
}
|
|
8701
|
+
function siteDockerfile(appName, app, options) {
|
|
8702
|
+
const declared = options.manifest ? Object.entries(options.manifest).find(([id, d]) => d.kind === "site" && require_derive.appKey(id) === appName)?.[1] : void 0;
|
|
8703
|
+
const publicUrlArgs = options.publicUrlArgs ?? (declared?.kind === "site" && options.manifest ? Object.keys((0, _geekmidas_manifest.publicEnvFor)(declared, options.manifest)) : getPublicUrlArgNames(app));
|
|
8704
|
+
const webOpts = {
|
|
8705
|
+
...options.image,
|
|
8706
|
+
publicUrlArgs
|
|
8707
|
+
};
|
|
8708
|
+
switch (app.framework) {
|
|
8709
|
+
case "vite": return generateViteStaticDockerfile(webOpts);
|
|
8710
|
+
case "tanstack-start":
|
|
8711
|
+
case "remix": return generateNodeWebDockerfile(webOpts);
|
|
8712
|
+
default: return generateNextjsDockerfile(webOpts);
|
|
10038
8713
|
}
|
|
10039
8714
|
}
|
|
10040
|
-
//#endregion
|
|
10041
|
-
//#region src/providers/registry.ts
|
|
10042
|
-
/**
|
|
10043
|
-
* The providers gkm ships, by kind and name — registered the way a deploy
|
|
10044
|
-
* target's built-ins are (`target/builtins.ts`). A provider from a package is
|
|
10045
|
-
* one more entry here once there is a loading story for it.
|
|
10046
|
-
*/
|
|
10047
|
-
const BUILTIN_PROVIDERS = { objects: { s3: {
|
|
10048
|
-
kind: "objects",
|
|
10049
|
-
name: "s3",
|
|
10050
|
-
check: require_workspace.checkS3Config,
|
|
10051
|
-
provisioning: require_workspace.S3_PROVISIONING,
|
|
10052
|
-
credentials: awsProvisioningCredentials,
|
|
10053
|
-
runtimeKeys(manifest) {
|
|
10054
|
-
return bucketConstructs(manifest).flatMap((b) => [(0, _geekmidas_manifest.provideKey)(b.id, "url"), ...b.servers.map((s) => (0, _geekmidas_manifest.provideKey)(s.id, "url"))]);
|
|
10055
|
-
},
|
|
10056
|
-
ensure: ensureS3,
|
|
10057
|
-
verify: verifyS3
|
|
10058
|
-
} } };
|
|
10059
|
-
/** The provider `kind` names `name`, if gkm has one. */
|
|
10060
|
-
function builtinProvider(kind, name) {
|
|
10061
|
-
return Object.hasOwn(BUILTIN_PROVIDERS[kind], name) ? BUILTIN_PROVIDERS[kind][name] : void 0;
|
|
10062
|
-
}
|
|
10063
|
-
//#endregion
|
|
10064
|
-
//#region src/providers/index.ts
|
|
10065
|
-
/** The constructs of a kind: `objects` is every bucket. */
|
|
10066
|
-
function constructsOf(manifest, kind) {
|
|
10067
|
-
return Object.entries(manifest).filter(([, d]) => d.kind === kind).map(([id]) => id).sort();
|
|
10068
|
-
}
|
|
10069
8715
|
/**
|
|
10070
|
-
* What
|
|
10071
|
-
*
|
|
10072
|
-
*
|
|
10073
|
-
*
|
|
10074
|
-
* @throws {StageProviderDisabled} for a declared kind the stage has none of
|
|
8716
|
+
* What a build root's `.dockerignore` must leave out of every image's
|
|
8717
|
+
* context: dependencies installed for another platform, anything built on the
|
|
8718
|
+
* host (every image builds from source), git, and gkm's own output —
|
|
8719
|
+
* `.gkm/compose`'s env files hold a stage's secrets.
|
|
10075
8720
|
*/
|
|
10076
|
-
|
|
10077
|
-
|
|
10078
|
-
|
|
10079
|
-
|
|
10080
|
-
|
|
10081
|
-
|
|
10082
|
-
|
|
10083
|
-
|
|
10084
|
-
|
|
10085
|
-
|
|
10086
|
-
|
|
10087
|
-
|
|
10088
|
-
|
|
10089
|
-
|
|
10090
|
-
|
|
10091
|
-
|
|
8721
|
+
const REQUIRED_IGNORES = [
|
|
8722
|
+
"**/node_modules",
|
|
8723
|
+
".git",
|
|
8724
|
+
"**/dist",
|
|
8725
|
+
"**/.next",
|
|
8726
|
+
"**/.turbo",
|
|
8727
|
+
"**/.gkm",
|
|
8728
|
+
"**/.gkm/compose"
|
|
8729
|
+
];
|
|
8730
|
+
/**
|
|
8731
|
+
* Make sure the build root's `.dockerignore` leaves out what no image may
|
|
8732
|
+
* hold: written whole where there is none, and each missing line appended to
|
|
8733
|
+
* one the project keeps.
|
|
8734
|
+
*/
|
|
8735
|
+
async function ensureDockerignore(buildRoot) {
|
|
8736
|
+
const path = (0, node_path.join)(buildRoot, ".dockerignore");
|
|
8737
|
+
if (!(0, node_fs.existsSync)(path)) {
|
|
8738
|
+
await (0, node_fs_promises.writeFile)(path, generateDockerignore());
|
|
8739
|
+
return path;
|
|
10092
8740
|
}
|
|
10093
|
-
|
|
8741
|
+
const current = await (0, node_fs_promises.readFile)(path, "utf-8");
|
|
8742
|
+
const lines = new Set(current.split(/\r?\n/).map((line) => line.trim()));
|
|
8743
|
+
const missing = REQUIRED_IGNORES.filter((line) => !lines.has(line));
|
|
8744
|
+
if (missing.length === 0) return path;
|
|
8745
|
+
await (0, node_fs_promises.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`);
|
|
8746
|
+
return path;
|
|
10094
8747
|
}
|
|
10095
8748
|
/**
|
|
10096
|
-
* `
|
|
10097
|
-
*
|
|
10098
|
-
*
|
|
10099
|
-
*
|
|
8749
|
+
* Write `docker-compose.constructs.yml` — the same file `gkm dev` writes, with
|
|
8750
|
+
* the apps beside the containers their constructs derive.
|
|
8751
|
+
*
|
|
8752
|
+
* Through reconcile rather than a second generator, so what `gkm docker`
|
|
8753
|
+
* describes and what `gkm dev` runs cannot disagree. Nothing is started.
|
|
10100
8754
|
*/
|
|
10101
|
-
async function
|
|
10102
|
-
|
|
10103
|
-
|
|
10104
|
-
|
|
10105
|
-
const log = options.log ?? ((line) => console.log(line));
|
|
10106
|
-
const named = PROVIDER_KINDS.some((kind) => require_workspace.stageProvider(workspace, kind, stage).mode === "provider");
|
|
10107
|
-
let found = options.manifest;
|
|
10108
|
-
let runnables = options.runnables ?? {};
|
|
10109
|
-
if (!found && named) {
|
|
10110
|
-
runnables = {};
|
|
10111
|
-
found = await require_discover.discover({
|
|
10112
|
-
patterns: constructGlobs(workspace),
|
|
10113
|
-
cwd: workspace.root,
|
|
10114
|
-
runnables
|
|
10115
|
-
});
|
|
10116
|
-
}
|
|
10117
|
-
const manifest = found ?? {};
|
|
10118
|
-
const reports = [];
|
|
10119
|
-
const configured = [];
|
|
10120
|
-
for (const kind of PROVIDER_KINDS) {
|
|
10121
|
-
const choice = require_workspace.stageProvider(workspace, kind, stage);
|
|
10122
|
-
if (choice.mode !== "provider") {
|
|
10123
|
-
reports.push({
|
|
10124
|
-
kind,
|
|
10125
|
-
mode: choice.mode,
|
|
10126
|
-
actions: []
|
|
10127
|
-
});
|
|
10128
|
-
continue;
|
|
10129
|
-
}
|
|
10130
|
-
const provider = builtinProvider(kind, choice.name);
|
|
10131
|
-
const report = {
|
|
10132
|
-
kind,
|
|
10133
|
-
mode: "provider",
|
|
10134
|
-
provider: provider.name,
|
|
10135
|
-
actions: []
|
|
10136
|
-
};
|
|
10137
|
-
reports.push(report);
|
|
10138
|
-
if (constructsOf(manifest, kind).length === 0) {
|
|
10139
|
-
log(` ${kind}: ${provider.name} — nothing declared to provision`);
|
|
10140
|
-
continue;
|
|
10141
|
-
}
|
|
10142
|
-
const credential = await provider.credentials({
|
|
10143
|
-
stage,
|
|
10144
|
-
env,
|
|
10145
|
-
...options.profile ? { profile: options.profile } : {},
|
|
10146
|
-
...options.home ? { home: options.home } : {}
|
|
10147
|
-
});
|
|
10148
|
-
report.credentials = credential !== void 0;
|
|
10149
|
-
if (credential === void 0) {
|
|
10150
|
-
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)}.`);
|
|
10151
|
-
continue;
|
|
10152
|
-
}
|
|
10153
|
-
configured.push({
|
|
10154
|
-
kind,
|
|
10155
|
-
provider,
|
|
10156
|
-
config: choice.config,
|
|
10157
|
-
credential,
|
|
10158
|
-
report
|
|
10159
|
-
});
|
|
10160
|
-
}
|
|
10161
|
-
if (configured.length === 0) return reports;
|
|
10162
|
-
const identity = deployIdentity(workspace, stage);
|
|
10163
|
-
const secretsStore = await secretsStoreFor(workspace, stage, {
|
|
10164
|
-
...options.profile ? { profile: options.profile } : {},
|
|
10165
|
-
...options.home ? { home: options.home } : {}
|
|
10166
|
-
});
|
|
10167
|
-
let secrets = (await secretsStore.read(stage))?.custom ?? {};
|
|
10168
|
-
const stateStore = await createStateStore({
|
|
10169
|
-
config: workspace.state,
|
|
10170
|
-
workspaceRoot: workspace.root,
|
|
10171
|
-
workspaceName: workspace.name
|
|
8755
|
+
async function writeConstructsCompose(workspace) {
|
|
8756
|
+
await reconcileWorkspace(workspace, {
|
|
8757
|
+
stage: workspace.stages.local,
|
|
8758
|
+
start: false
|
|
10172
8759
|
});
|
|
10173
|
-
|
|
10174
|
-
|
|
10175
|
-
const state = dryRun ? await readOnlyState(stateStore, stage) : journalState(await DeployJournal.open(stateStore, stage, () => deploysWithCompose(workspace) ? createComposeState(stage) : createEmptyState(stage, "", "")));
|
|
10176
|
-
const writeSecrets = async (values) => {
|
|
10177
|
-
if (dryRun) return;
|
|
10178
|
-
const stored = await secretsStore.read(stage) ?? initStageSecrets(stage);
|
|
10179
|
-
const next = {
|
|
10180
|
-
...stored,
|
|
10181
|
-
custom: {
|
|
10182
|
-
...stored.custom,
|
|
10183
|
-
...values
|
|
10184
|
-
},
|
|
10185
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10186
|
-
};
|
|
10187
|
-
await secretsStore.write(stage, next);
|
|
10188
|
-
secrets = next.custom;
|
|
10189
|
-
};
|
|
10190
|
-
for (const entry of configured) {
|
|
10191
|
-
log(` ${entry.kind}: ${entry.provider.name}${dryRun ? " (dry run)" : ""}`);
|
|
10192
|
-
await entry.provider.ensure({
|
|
10193
|
-
workspace,
|
|
10194
|
-
manifest,
|
|
10195
|
-
runnables,
|
|
10196
|
-
stage,
|
|
10197
|
-
identity,
|
|
10198
|
-
config: entry.config,
|
|
10199
|
-
credential: entry.credential,
|
|
10200
|
-
get secrets() {
|
|
10201
|
-
return secrets;
|
|
10202
|
-
},
|
|
10203
|
-
state,
|
|
10204
|
-
dryRun,
|
|
10205
|
-
rotateKeys: options.rotateKeys === true,
|
|
10206
|
-
retireOldKeys: options.retireOldKeys === true,
|
|
10207
|
-
async change(action, apply) {
|
|
10208
|
-
if (!dryRun) await apply();
|
|
10209
|
-
entry.report.actions.push(action);
|
|
10210
|
-
log(` ${dryRun ? "would " : ""}${action.change} — ${action.resource}`);
|
|
10211
|
-
},
|
|
10212
|
-
writeSecrets,
|
|
10213
|
-
log
|
|
10214
|
-
});
|
|
10215
|
-
if (entry.report.actions.length === 0) log(" up to date");
|
|
10216
|
-
}
|
|
10217
|
-
} finally {
|
|
10218
|
-
await lock?.release();
|
|
10219
|
-
}
|
|
10220
|
-
return reports;
|
|
10221
|
-
}
|
|
10222
|
-
/** The journal as `ensure()` sees it. */
|
|
10223
|
-
function journalState(journal) {
|
|
10224
|
-
return {
|
|
10225
|
-
record: (key) => journal.record(key),
|
|
10226
|
-
pending: (entry) => journal.pending(entry),
|
|
10227
|
-
ready: (entry, id) => journal.ready(entry, id),
|
|
10228
|
-
lastDeployedAt: journal.existed ? journal.state.lastDeployedAt : void 0
|
|
10229
|
-
};
|
|
8760
|
+
logger.log(` Generated: ${COMPOSE_PATH} (project root)`);
|
|
8761
|
+
return (0, node_path.join)(workspace.root, COMPOSE_PATH);
|
|
10230
8762
|
}
|
|
10231
|
-
|
|
10232
|
-
|
|
10233
|
-
|
|
10234
|
-
|
|
10235
|
-
|
|
10236
|
-
pending: async () => {},
|
|
10237
|
-
ready: async () => {},
|
|
10238
|
-
...stored ? { lastDeployedAt: stored.state.lastDeployedAt } : {}
|
|
10239
|
-
};
|
|
8763
|
+
function printRunInstructions(workspace) {
|
|
8764
|
+
const project = (0, node_fs.existsSync)((0, node_path.join)(workspace.root, "docker-compose.yml")) ? " -f docker-compose.yml" : "";
|
|
8765
|
+
logger.log("\n📋 Run everything (the apps sit behind the \"apps\" profile):");
|
|
8766
|
+
logger.log(" gkm setup # starts the containers and creates what they hold");
|
|
8767
|
+
logger.log(` docker compose -f ${COMPOSE_PATH}${project} --profile apps up --build`);
|
|
10240
8768
|
}
|
|
10241
8769
|
//#endregion
|
|
10242
8770
|
//#region src/secrets/stale.ts
|
|
@@ -10417,12 +8945,6 @@ Object.defineProperty(exports, "DEV_SERVICES", {
|
|
|
10417
8945
|
return DEV_SERVICES;
|
|
10418
8946
|
}
|
|
10419
8947
|
});
|
|
10420
|
-
Object.defineProperty(exports, "DeployJournal", {
|
|
10421
|
-
enumerable: true,
|
|
10422
|
-
get: function() {
|
|
10423
|
-
return DeployJournal;
|
|
10424
|
-
}
|
|
10425
|
-
});
|
|
10426
8948
|
Object.defineProperty(exports, "DeploySeedsFailed", {
|
|
10427
8949
|
enumerable: true,
|
|
10428
8950
|
get: function() {
|
|
@@ -10453,12 +8975,6 @@ Object.defineProperty(exports, "DockerPushFailed", {
|
|
|
10453
8975
|
return DockerPushFailed;
|
|
10454
8976
|
}
|
|
10455
8977
|
});
|
|
10456
|
-
Object.defineProperty(exports, "DocumentStateStore", {
|
|
10457
|
-
enumerable: true,
|
|
10458
|
-
get: function() {
|
|
10459
|
-
return DocumentStateStore;
|
|
10460
|
-
}
|
|
10461
|
-
});
|
|
10462
8978
|
Object.defineProperty(exports, "EMULATOR_ACCESS_KEY_ID", {
|
|
10463
8979
|
enumerable: true,
|
|
10464
8980
|
get: function() {
|
|
@@ -10489,6 +9005,18 @@ Object.defineProperty(exports, "HostNotPointingAtServer", {
|
|
|
10489
9005
|
return HostNotPointingAtServer;
|
|
10490
9006
|
}
|
|
10491
9007
|
});
|
|
9008
|
+
Object.defineProperty(exports, "IAM_POLICY_NAME", {
|
|
9009
|
+
enumerable: true,
|
|
9010
|
+
get: function() {
|
|
9011
|
+
return IAM_POLICY_NAME;
|
|
9012
|
+
}
|
|
9013
|
+
});
|
|
9014
|
+
Object.defineProperty(exports, "IAM_USER_PATH", {
|
|
9015
|
+
enumerable: true,
|
|
9016
|
+
get: function() {
|
|
9017
|
+
return IAM_USER_PATH;
|
|
9018
|
+
}
|
|
9019
|
+
});
|
|
10492
9020
|
Object.defineProperty(exports, "LEGACY_LOCAL_LOGIN", {
|
|
10493
9021
|
enumerable: true,
|
|
10494
9022
|
get: function() {
|
|
@@ -10519,16 +9047,16 @@ Object.defineProperty(exports, "LOGS_SERVICE", {
|
|
|
10519
9047
|
return LOGS_SERVICE;
|
|
10520
9048
|
}
|
|
10521
9049
|
});
|
|
10522
|
-
Object.defineProperty(exports, "
|
|
9050
|
+
Object.defineProperty(exports, "NoDomainForStage", {
|
|
10523
9051
|
enumerable: true,
|
|
10524
9052
|
get: function() {
|
|
10525
|
-
return
|
|
9053
|
+
return NoDomainForStage;
|
|
10526
9054
|
}
|
|
10527
9055
|
});
|
|
10528
|
-
Object.defineProperty(exports, "
|
|
9056
|
+
Object.defineProperty(exports, "PROVIDER_KINDS", {
|
|
10529
9057
|
enumerable: true,
|
|
10530
9058
|
get: function() {
|
|
10531
|
-
return
|
|
9059
|
+
return PROVIDER_KINDS;
|
|
10532
9060
|
}
|
|
10533
9061
|
});
|
|
10534
9062
|
Object.defineProperty(exports, "QueueGenerator", {
|
|
@@ -10561,36 +9089,6 @@ Object.defineProperty(exports, "StageProviderDisabled", {
|
|
|
10561
9089
|
return StageProviderDisabled;
|
|
10562
9090
|
}
|
|
10563
9091
|
});
|
|
10564
|
-
Object.defineProperty(exports, "StageStateMissing", {
|
|
10565
|
-
enumerable: true,
|
|
10566
|
-
get: function() {
|
|
10567
|
-
return StageStateMissing;
|
|
10568
|
-
}
|
|
10569
|
-
});
|
|
10570
|
-
Object.defineProperty(exports, "StateLocked", {
|
|
10571
|
-
enumerable: true,
|
|
10572
|
-
get: function() {
|
|
10573
|
-
return StateLocked;
|
|
10574
|
-
}
|
|
10575
|
-
});
|
|
10576
|
-
Object.defineProperty(exports, "StateStoreBusy", {
|
|
10577
|
-
enumerable: true,
|
|
10578
|
-
get: function() {
|
|
10579
|
-
return StateStoreBusy;
|
|
10580
|
-
}
|
|
10581
|
-
});
|
|
10582
|
-
Object.defineProperty(exports, "StateStoreWithoutLocking", {
|
|
10583
|
-
enumerable: true,
|
|
10584
|
-
get: function() {
|
|
10585
|
-
return StateStoreWithoutLocking;
|
|
10586
|
-
}
|
|
10587
|
-
});
|
|
10588
|
-
Object.defineProperty(exports, "StateVersionConflict", {
|
|
10589
|
-
enumerable: true,
|
|
10590
|
-
get: function() {
|
|
10591
|
-
return StateVersionConflict;
|
|
10592
|
-
}
|
|
10593
|
-
});
|
|
10594
9092
|
Object.defineProperty(exports, "TELEMETRY_CONTAINER", {
|
|
10595
9093
|
enumerable: true,
|
|
10596
9094
|
get: function() {
|
|
@@ -10633,12 +9131,6 @@ Object.defineProperty(exports, "appTelemetry", {
|
|
|
10633
9131
|
return appTelemetry;
|
|
10634
9132
|
}
|
|
10635
9133
|
});
|
|
10636
|
-
Object.defineProperty(exports, "appendHistory", {
|
|
10637
|
-
enumerable: true,
|
|
10638
|
-
get: function() {
|
|
10639
|
-
return appendHistory;
|
|
10640
|
-
}
|
|
10641
|
-
});
|
|
10642
9134
|
Object.defineProperty(exports, "applicationName", {
|
|
10643
9135
|
enumerable: true,
|
|
10644
9136
|
get: function() {
|
|
@@ -10693,18 +9185,6 @@ Object.defineProperty(exports, "assertStageProvidersEnabled", {
|
|
|
10693
9185
|
return assertStageProvidersEnabled;
|
|
10694
9186
|
}
|
|
10695
9187
|
});
|
|
10696
|
-
Object.defineProperty(exports, "assertStageServer", {
|
|
10697
|
-
enumerable: true,
|
|
10698
|
-
get: function() {
|
|
10699
|
-
return assertStageServer;
|
|
10700
|
-
}
|
|
10701
|
-
});
|
|
10702
|
-
Object.defineProperty(exports, "assertStateOutlivesRun", {
|
|
10703
|
-
enumerable: true,
|
|
10704
|
-
get: function() {
|
|
10705
|
-
return assertStateOutlivesRun;
|
|
10706
|
-
}
|
|
10707
|
-
});
|
|
10708
9188
|
Object.defineProperty(exports, "backendsOf", {
|
|
10709
9189
|
enumerable: true,
|
|
10710
9190
|
get: function() {
|
|
@@ -10717,22 +9197,22 @@ Object.defineProperty(exports, "bucketClient", {
|
|
|
10717
9197
|
return bucketClient;
|
|
10718
9198
|
}
|
|
10719
9199
|
});
|
|
10720
|
-
Object.defineProperty(exports, "
|
|
9200
|
+
Object.defineProperty(exports, "bucketName", {
|
|
10721
9201
|
enumerable: true,
|
|
10722
9202
|
get: function() {
|
|
10723
|
-
return
|
|
9203
|
+
return bucketName;
|
|
10724
9204
|
}
|
|
10725
9205
|
});
|
|
10726
|
-
Object.defineProperty(exports, "
|
|
9206
|
+
Object.defineProperty(exports, "bucketPolicies", {
|
|
10727
9207
|
enumerable: true,
|
|
10728
9208
|
get: function() {
|
|
10729
|
-
return
|
|
9209
|
+
return bucketPolicies;
|
|
10730
9210
|
}
|
|
10731
9211
|
});
|
|
10732
|
-
Object.defineProperty(exports, "
|
|
9212
|
+
Object.defineProperty(exports, "cacheBackendFor", {
|
|
10733
9213
|
enumerable: true,
|
|
10734
9214
|
get: function() {
|
|
10735
|
-
return
|
|
9215
|
+
return cacheBackendFor;
|
|
10736
9216
|
}
|
|
10737
9217
|
});
|
|
10738
9218
|
Object.defineProperty(exports, "composeBuildPaths", {
|
|
@@ -10753,12 +9233,6 @@ Object.defineProperty(exports, "constructGlobs", {
|
|
|
10753
9233
|
return constructGlobs;
|
|
10754
9234
|
}
|
|
10755
9235
|
});
|
|
10756
|
-
Object.defineProperty(exports, "contentVersion", {
|
|
10757
|
-
enumerable: true,
|
|
10758
|
-
get: function() {
|
|
10759
|
-
return contentVersion;
|
|
10760
|
-
}
|
|
10761
|
-
});
|
|
10762
9236
|
Object.defineProperty(exports, "createComposeState", {
|
|
10763
9237
|
enumerable: true,
|
|
10764
9238
|
get: function() {
|
|
@@ -10783,12 +9257,6 @@ Object.defineProperty(exports, "createEntryWrapper", {
|
|
|
10783
9257
|
return createEntryWrapper;
|
|
10784
9258
|
}
|
|
10785
9259
|
});
|
|
10786
|
-
Object.defineProperty(exports, "createStateStore", {
|
|
10787
|
-
enumerable: true,
|
|
10788
|
-
get: function() {
|
|
10789
|
-
return createStateStore;
|
|
10790
|
-
}
|
|
10791
|
-
});
|
|
10792
9260
|
Object.defineProperty(exports, "currentActor", {
|
|
10793
9261
|
enumerable: true,
|
|
10794
9262
|
get: function() {
|
|
@@ -10837,12 +9305,30 @@ Object.defineProperty(exports, "devServicesUsed", {
|
|
|
10837
9305
|
return devServicesUsed;
|
|
10838
9306
|
}
|
|
10839
9307
|
});
|
|
9308
|
+
Object.defineProperty(exports, "dnsCredentials", {
|
|
9309
|
+
enumerable: true,
|
|
9310
|
+
get: function() {
|
|
9311
|
+
return dnsCredentials;
|
|
9312
|
+
}
|
|
9313
|
+
});
|
|
9314
|
+
Object.defineProperty(exports, "dnsRecordResource", {
|
|
9315
|
+
enumerable: true,
|
|
9316
|
+
get: function() {
|
|
9317
|
+
return dnsRecordResource;
|
|
9318
|
+
}
|
|
9319
|
+
});
|
|
10840
9320
|
Object.defineProperty(exports, "dnsResources", {
|
|
10841
9321
|
enumerable: true,
|
|
10842
9322
|
get: function() {
|
|
10843
9323
|
return dnsResources;
|
|
10844
9324
|
}
|
|
10845
9325
|
});
|
|
9326
|
+
Object.defineProperty(exports, "dns_exports", {
|
|
9327
|
+
enumerable: true,
|
|
9328
|
+
get: function() {
|
|
9329
|
+
return dns_exports;
|
|
9330
|
+
}
|
|
9331
|
+
});
|
|
10846
9332
|
Object.defineProperty(exports, "dockerCommand", {
|
|
10847
9333
|
enumerable: true,
|
|
10848
9334
|
get: function() {
|
|
@@ -10933,6 +9419,12 @@ Object.defineProperty(exports, "hostFor", {
|
|
|
10933
9419
|
return hostFor;
|
|
10934
9420
|
}
|
|
10935
9421
|
});
|
|
9422
|
+
Object.defineProperty(exports, "iamUserName", {
|
|
9423
|
+
enumerable: true,
|
|
9424
|
+
get: function() {
|
|
9425
|
+
return iamUserName;
|
|
9426
|
+
}
|
|
9427
|
+
});
|
|
10936
9428
|
Object.defineProperty(exports, "imageBuildCredentials", {
|
|
10937
9429
|
enumerable: true,
|
|
10938
9430
|
get: function() {
|
|
@@ -10963,6 +9455,12 @@ Object.defineProperty(exports, "initStageSecrets", {
|
|
|
10963
9455
|
return initStageSecrets;
|
|
10964
9456
|
}
|
|
10965
9457
|
});
|
|
9458
|
+
Object.defineProperty(exports, "isCi", {
|
|
9459
|
+
enumerable: true,
|
|
9460
|
+
get: function() {
|
|
9461
|
+
return isCi;
|
|
9462
|
+
}
|
|
9463
|
+
});
|
|
10966
9464
|
Object.defineProperty(exports, "isImageBuild", {
|
|
10967
9465
|
enumerable: true,
|
|
10968
9466
|
get: function() {
|
|
@@ -11077,12 +9575,6 @@ Object.defineProperty(exports, "networkEnv", {
|
|
|
11077
9575
|
return networkEnv;
|
|
11078
9576
|
}
|
|
11079
9577
|
});
|
|
11080
|
-
Object.defineProperty(exports, "newLockHolder", {
|
|
11081
|
-
enumerable: true,
|
|
11082
|
-
get: function() {
|
|
11083
|
-
return newLockHolder;
|
|
11084
|
-
}
|
|
11085
|
-
});
|
|
11086
9578
|
Object.defineProperty(exports, "orchestrateDns", {
|
|
11087
9579
|
enumerable: true,
|
|
11088
9580
|
get: function() {
|
|
@@ -11173,22 +9665,16 @@ Object.defineProperty(exports, "provisionCommand", {
|
|
|
11173
9665
|
return provisionCommand;
|
|
11174
9666
|
}
|
|
11175
9667
|
});
|
|
11176
|
-
Object.defineProperty(exports, "
|
|
11177
|
-
enumerable: true,
|
|
11178
|
-
get: function() {
|
|
11179
|
-
return provisionStage;
|
|
11180
|
-
}
|
|
11181
|
-
});
|
|
11182
|
-
Object.defineProperty(exports, "provisionStageDns", {
|
|
9668
|
+
Object.defineProperty(exports, "quoteIdentifier", {
|
|
11183
9669
|
enumerable: true,
|
|
11184
9670
|
get: function() {
|
|
11185
|
-
return
|
|
9671
|
+
return quoteIdentifier;
|
|
11186
9672
|
}
|
|
11187
9673
|
});
|
|
11188
|
-
Object.defineProperty(exports, "
|
|
9674
|
+
Object.defineProperty(exports, "randomSuffix", {
|
|
11189
9675
|
enumerable: true,
|
|
11190
9676
|
get: function() {
|
|
11191
|
-
return
|
|
9677
|
+
return randomSuffix;
|
|
11192
9678
|
}
|
|
11193
9679
|
});
|
|
11194
9680
|
Object.defineProperty(exports, "readKey", {
|
|
@@ -11209,6 +9695,12 @@ Object.defineProperty(exports, "reconcileWorkspace", {
|
|
|
11209
9695
|
return reconcileWorkspace;
|
|
11210
9696
|
}
|
|
11211
9697
|
});
|
|
9698
|
+
Object.defineProperty(exports, "recordDnsChanges", {
|
|
9699
|
+
enumerable: true,
|
|
9700
|
+
get: function() {
|
|
9701
|
+
return recordDnsChanges;
|
|
9702
|
+
}
|
|
9703
|
+
});
|
|
11212
9704
|
Object.defineProperty(exports, "recordRelease", {
|
|
11213
9705
|
enumerable: true,
|
|
11214
9706
|
get: function() {
|
|
@@ -11335,6 +9827,18 @@ Object.defineProperty(exports, "stackRedis", {
|
|
|
11335
9827
|
return stackRedis;
|
|
11336
9828
|
}
|
|
11337
9829
|
});
|
|
9830
|
+
Object.defineProperty(exports, "stageDns", {
|
|
9831
|
+
enumerable: true,
|
|
9832
|
+
get: function() {
|
|
9833
|
+
return stageDns;
|
|
9834
|
+
}
|
|
9835
|
+
});
|
|
9836
|
+
Object.defineProperty(exports, "stageDnsDomains", {
|
|
9837
|
+
enumerable: true,
|
|
9838
|
+
get: function() {
|
|
9839
|
+
return stageDnsDomains;
|
|
9840
|
+
}
|
|
9841
|
+
});
|
|
11338
9842
|
Object.defineProperty(exports, "stageProviderNotes", {
|
|
11339
9843
|
enumerable: true,
|
|
11340
9844
|
get: function() {
|
|
@@ -11365,6 +9869,12 @@ Object.defineProperty(exports, "store_exports", {
|
|
|
11365
9869
|
return store_exports;
|
|
11366
9870
|
}
|
|
11367
9871
|
});
|
|
9872
|
+
Object.defineProperty(exports, "suffixedBucketName", {
|
|
9873
|
+
enumerable: true,
|
|
9874
|
+
get: function() {
|
|
9875
|
+
return suffixedBucketName;
|
|
9876
|
+
}
|
|
9877
|
+
});
|
|
11368
9878
|
Object.defineProperty(exports, "suppliedOnly", {
|
|
11369
9879
|
enumerable: true,
|
|
11370
9880
|
get: function() {
|
|
@@ -11395,12 +9905,6 @@ Object.defineProperty(exports, "toEmbeddableSecrets", {
|
|
|
11395
9905
|
return toEmbeddableSecrets;
|
|
11396
9906
|
}
|
|
11397
9907
|
});
|
|
11398
|
-
Object.defineProperty(exports, "upgradeV2State", {
|
|
11399
|
-
enumerable: true,
|
|
11400
|
-
get: function() {
|
|
11401
|
-
return upgradeV2State;
|
|
11402
|
-
}
|
|
11403
|
-
});
|
|
11404
9908
|
Object.defineProperty(exports, "usesTelemetry", {
|
|
11405
9909
|
enumerable: true,
|
|
11406
9910
|
get: function() {
|
|
@@ -11419,12 +9923,6 @@ Object.defineProperty(exports, "verifyDnsRecords", {
|
|
|
11419
9923
|
return verifyDnsRecords;
|
|
11420
9924
|
}
|
|
11421
9925
|
});
|
|
11422
|
-
Object.defineProperty(exports, "verifyStageProviders", {
|
|
11423
|
-
enumerable: true,
|
|
11424
|
-
get: function() {
|
|
11425
|
-
return verifyStageProviders;
|
|
11426
|
-
}
|
|
11427
|
-
});
|
|
11428
9926
|
Object.defineProperty(exports, "withCustomSecret", {
|
|
11429
9927
|
enumerable: true,
|
|
11430
9928
|
get: function() {
|
|
@@ -11492,4 +9990,4 @@ Object.defineProperty(exports, "workspace_exports", {
|
|
|
11492
9990
|
}
|
|
11493
9991
|
});
|
|
11494
9992
|
|
|
11495
|
-
//# sourceMappingURL=seeds-
|
|
9993
|
+
//# sourceMappingURL=seeds-BCg8X36S.cjs.map
|