@geekmidas/cli 10.0.0-alpha.92 → 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-BqYGk9XT.mjs → GoDaddyProvider-BRZUWD8z.mjs} +3 -3
- package/dist/{GoDaddyProvider-BqYGk9XT.mjs.map → GoDaddyProvider-BRZUWD8z.mjs.map} +1 -1
- package/dist/{GoDaddyProvider-CJwg2r_9.cjs → GoDaddyProvider-DzguxU_a.cjs} +3 -3
- package/dist/{GoDaddyProvider-CJwg2r_9.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-BQZb6-j2.mjs +124 -0
- package/dist/LegacyStateStore-BQZb6-j2.mjs.map +1 -0
- package/dist/LegacyStateStore-D50dgUPc.cjs +124 -0
- package/dist/LegacyStateStore-D50dgUPc.cjs.map +1 -0
- package/dist/{LocalStateStore-Y8XHdoCR.mjs → LocalStateStore-BSaS2535.mjs} +2 -2
- package/dist/{LocalStateStore-Y8XHdoCR.mjs.map → LocalStateStore-BSaS2535.mjs.map} +1 -1
- package/dist/{LocalStateStore-BMfseje2.cjs → LocalStateStore-EFBUjuMX.cjs} +8 -8
- package/dist/{LocalStateStore-BMfseje2.cjs.map → LocalStateStore-EFBUjuMX.cjs.map} +1 -1
- package/dist/{S3StateStore-BmMt2HhW.cjs → S3StateStore-C5mv8To3.cjs} +5 -5
- package/dist/{S3StateStore-BmMt2HhW.cjs.map → S3StateStore-C5mv8To3.cjs.map} +1 -1
- package/dist/{S3StateStore-BfoY4MvD.mjs → S3StateStore-CrFq4UZU.mjs} +2 -2
- package/dist/{S3StateStore-BfoY4MvD.mjs.map → S3StateStore-CrFq4UZU.mjs.map} +1 -1
- package/dist/{SSMStateStore-Bdyw_0Jj.mjs → SSMStateStore-D3Z6mZnA.mjs} +2 -2
- package/dist/{SSMStateStore-Bdyw_0Jj.mjs.map → SSMStateStore-D3Z6mZnA.mjs.map} +1 -1
- package/dist/{SSMStateStore-D9K62wM4.cjs → SSMStateStore-DuZCH7_v.cjs} +9 -9
- package/dist/{SSMStateStore-D9K62wM4.cjs.map → SSMStateStore-DuZCH7_v.cjs.map} +1 -1
- package/dist/{add-2Opn2VVE.mjs → add-C85isXFl.mjs} +4 -5
- package/dist/add-C85isXFl.mjs.map +1 -0
- package/dist/{add-oN0b18HN.cjs → add-DsVDdvon.cjs} +5 -6
- package/dist/add-DsVDdvon.cjs.map +1 -0
- package/dist/{bundler-CXQDrhzN.cjs → bundler-BEgUKc4j.cjs} +5 -5
- package/dist/{bundler-CXQDrhzN.cjs.map → bundler-BEgUKc4j.cjs.map} +1 -1
- package/dist/{bundler-C-AdesaG.mjs → bundler-BSHog8CH.mjs} +5 -5
- package/dist/{bundler-C-AdesaG.mjs.map → bundler-BSHog8CH.mjs.map} +1 -1
- package/dist/{cli-BjhaVObS.mjs → cli-BsFenyaf.mjs} +10 -6
- package/dist/cli-BsFenyaf.mjs.map +1 -0
- package/dist/{cli-BRZFYvdb.cjs → cli-BvBdv660.cjs} +9 -5
- package/dist/cli-BvBdv660.cjs.map +1 -0
- package/dist/compose-Cd09gW-n.cjs +1562 -0
- package/dist/compose-Cd09gW-n.cjs.map +1 -0
- package/dist/compose-D2RFYd_f.mjs +1515 -0
- package/dist/compose-D2RFYd_f.mjs.map +1 -0
- package/dist/{config-C9zx7KsX.d.cts → config-DvHNMoOp.d.cts} +4 -4
- package/dist/{config-BJo8Wggp.d.mts.map → config-DvHNMoOp.d.cts.map} +1 -1
- package/dist/{config-BJo8Wggp.d.mts → config-Ngesg4O8.d.mts} +4 -4
- package/dist/{config-C9zx7KsX.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 -12
- package/dist/deploy/public.d.cts +25 -10
- package/dist/deploy/public.d.cts.map +1 -1
- package/dist/deploy/public.d.mts +25 -10
- package/dist/deploy/public.d.mts.map +1 -1
- package/dist/deploy/public.mjs +11 -11
- package/dist/{deploy-6ToUHJmM.cjs → deploy-C911Akkn.cjs} +125 -10
- package/dist/deploy-C911Akkn.cjs.map +1 -0
- package/dist/{deploy-yuJHlkY0.mjs → deploy-CeJ5D0xn.mjs} +124 -9
- package/dist/deploy-CeJ5D0xn.mjs.map +1 -0
- package/dist/{discover-G76EQ4vY.d.mts → discover-CTzOSGUF.d.cts} +2 -2
- package/dist/{discover-CRMhvHG9.d.cts.map → discover-CTzOSGUF.d.cts.map} +1 -1
- package/dist/{discover-CRMhvHG9.d.cts → discover-CihAtaoj.d.mts} +2 -2
- package/dist/{discover-G76EQ4vY.d.mts.map → discover-CihAtaoj.d.mts.map} +1 -1
- package/dist/{dokploy-Bvvfj2qe.cjs → dokploy-CV7GcobZ.cjs} +2 -2
- package/dist/{dokploy-Bvvfj2qe.cjs.map → dokploy-CV7GcobZ.cjs.map} +1 -1
- package/dist/{dokploy-BvbJ_A4Q.mjs → dokploy-D_aat2Xj.mjs} +2 -2
- package/dist/{dokploy-BvbJ_A4Q.mjs.map → dokploy-D_aat2Xj.mjs.map} +1 -1
- package/dist/{encryption-CgW167LO.cjs → encryption-DFcivJaO.cjs} +3 -3
- package/dist/{encryption-CgW167LO.cjs.map → encryption-DFcivJaO.cjs.map} +1 -1
- package/dist/{encryption-C9Jm9dfn.mjs → encryption-_4qKqtTx.mjs} +3 -3
- package/dist/{encryption-C9Jm9dfn.mjs.map → encryption-_4qKqtTx.mjs.map} +1 -1
- package/dist/{engine-Dj7ZKO8H.cjs → engine-Bv-LZpmj.cjs} +13 -11
- package/dist/engine-Bv-LZpmj.cjs.map +1 -0
- package/dist/{engine-C8Ye9MUm.mjs → engine-DPMHThkg.mjs} +11 -9
- package/dist/engine-DPMHThkg.mjs.map +1 -0
- package/dist/{github-BD051KZZ.cjs → github-Br42NMrW.cjs} +94 -6
- package/dist/github-Br42NMrW.cjs.map +1 -0
- package/dist/{github-B1k42hIt.mjs → github-xsDT19im.mjs} +94 -6
- package/dist/github-xsDT19im.mjs.map +1 -0
- package/dist/{index-D1pHwqkK.d.mts → index-BlpcClwM.d.mts} +2 -2
- package/dist/{index-B5uuwx-Y.d.cts.map → index-BlpcClwM.d.mts.map} +1 -1
- package/dist/{index-B5uuwx-Y.d.cts → index-CwLvFqtH.d.cts} +2 -2
- package/dist/{index-D1pHwqkK.d.mts.map → index-CwLvFqtH.d.cts.map} +1 -1
- package/dist/index.cjs +240 -250
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +234 -243
- 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-IZsYyZ5F.mjs → migrate-B7evHv7O.mjs} +3 -3
- package/dist/{migrate-IZsYyZ5F.mjs.map → migrate-B7evHv7O.mjs.map} +1 -1
- package/dist/{migrate-D3VVsiHp.cjs → migrate-D5jUz20_.cjs} +3 -3
- package/dist/{migrate-D3VVsiHp.cjs.map → migrate-D5jUz20_.cjs.map} +1 -1
- package/dist/{migrate-BSbZ7-9n.mjs → migrate-DhvFPGLi.mjs} +4 -4
- package/dist/{migrate-BSbZ7-9n.mjs.map → migrate-DhvFPGLi.mjs.map} +1 -1
- package/dist/{migrate-f6JDjyCp.cjs → migrate-SkWJotn5.cjs} +3 -3
- package/dist/{migrate-f6JDjyCp.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/{reconcile-Dh1Jk7WE.mjs → reconcile--I7V4Klj.mjs} +2 -2
- package/dist/{reconcile-Dh1Jk7WE.mjs.map → reconcile--I7V4Klj.mjs.map} +1 -1
- package/dist/{resolve-DUp65SnJ.cjs → resolve-CFEOtRsl.cjs} +4 -4
- package/dist/{resolve-DUp65SnJ.cjs.map → resolve-CFEOtRsl.cjs.map} +1 -1
- package/dist/{resolve-Be4bF1w4.mjs → resolve-Dyd1S2wn.mjs} +4 -4
- package/dist/{resolve-Be4bF1w4.mjs.map → resolve-Dyd1S2wn.mjs.map} +1 -1
- package/dist/{rollback-DDHtd0GJ.cjs → rollback-D720SFhV.cjs} +11 -8
- package/dist/rollback-D720SFhV.cjs.map +1 -0
- package/dist/{rollback-D1d-fW0r.mjs → rollback-NroNbGC0.mjs} +10 -7
- package/dist/rollback-NroNbGC0.mjs.map +1 -0
- package/dist/{runtime-Es1V4e9M.d.mts → runtime-BRCq6dQ7.d.mts} +2 -2
- package/dist/{runtime-DDrHlYe5.d.cts.map → runtime-BRCq6dQ7.d.mts.map} +1 -1
- package/dist/{runtime-DDrHlYe5.d.cts → runtime-VktAo_8B.d.cts} +2 -2
- package/dist/{runtime-Es1V4e9M.d.mts.map → runtime-VktAo_8B.d.cts.map} +1 -1
- package/dist/{sandbox-MaxSeU92.d.cts → sandbox-D1gd0iDz.d.cts} +2 -2
- package/dist/{sandbox-C3WsTa5m.d.mts.map → sandbox-D1gd0iDz.d.cts.map} +1 -1
- package/dist/{sandbox-C3WsTa5m.d.mts → sandbox-zHnRLCj0.d.mts} +2 -2
- package/dist/{sandbox-MaxSeU92.d.cts.map → sandbox-zHnRLCj0.d.mts.map} +1 -1
- package/dist/secrets-CwLauUB_.mjs +36 -0
- package/dist/secrets-CwLauUB_.mjs.map +1 -0
- package/dist/secrets-DAvi_O9g.cjs +47 -0
- package/dist/secrets-DAvi_O9g.cjs.map +1 -0
- package/dist/{seeds-BKoJYsE7.cjs → seeds-BCg8X36S.cjs} +1732 -2300
- package/dist/seeds-BCg8X36S.cjs.map +1 -0
- package/dist/{seeds-CvGQJnPO.mjs → seeds-CTxTZa-R.mjs} +1637 -2270
- 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-B3fPDTBg.cjs +1346 -0
- package/dist/stack-B3fPDTBg.cjs.map +1 -0
- package/dist/stack-NrdH5ngX.mjs +1239 -0
- package/dist/stack-NrdH5ngX.mjs.map +1 -0
- 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-Dtd6BZX9.mjs → terminal-DAHs2Zko.mjs} +3 -3
- package/dist/{terminal-Dtd6BZX9.mjs.map → terminal-DAHs2Zko.mjs.map} +1 -1
- package/dist/{types-Bk9st2cd.d.cts → types-DMoaFrjg.d.mts} +108 -33
- package/dist/types-DMoaFrjg.d.mts.map +1 -0
- package/dist/{types-B3Gqs175.d.mts → types-DXyaSmqu.d.cts} +109 -32
- 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/LegacyStateStore-CeEwayMB.cjs +0 -89
- package/dist/LegacyStateStore-CeEwayMB.cjs.map +0 -1
- package/dist/LegacyStateStore-DdJ1bCRV.mjs +0 -89
- package/dist/LegacyStateStore-DdJ1bCRV.mjs.map +0 -1
- package/dist/add-2Opn2VVE.mjs.map +0 -1
- package/dist/add-oN0b18HN.cjs.map +0 -1
- package/dist/cli-BRZFYvdb.cjs.map +0 -1
- package/dist/cli-BjhaVObS.mjs.map +0 -1
- package/dist/compose-CTdVeeVk.mjs +0 -3151
- package/dist/compose-CTdVeeVk.mjs.map +0 -1
- package/dist/compose-fnYmPk_c.cjs +0 -3276
- package/dist/compose-fnYmPk_c.cjs.map +0 -1
- package/dist/deploy-6ToUHJmM.cjs.map +0 -1
- package/dist/deploy-yuJHlkY0.mjs.map +0 -1
- package/dist/engine-C8Ye9MUm.mjs.map +0 -1
- package/dist/engine-Dj7ZKO8H.cjs.map +0 -1
- package/dist/github-B1k42hIt.mjs.map +0 -1
- package/dist/github-BD051KZZ.cjs.map +0 -1
- package/dist/rollback-D1d-fW0r.mjs.map +0 -1
- package/dist/rollback-DDHtd0GJ.cjs.map +0 -1
- package/dist/seeds-BKoJYsE7.cjs.map +0 -1
- package/dist/seeds-CvGQJnPO.mjs.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-B3Gqs175.d.mts.map +0 -1
- package/dist/types-Bk9st2cd.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,882 +6647,259 @@ 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/
|
|
6650
|
+
//#region src/target/dokploy/dns/DnsProvider.ts
|
|
6653
6651
|
/**
|
|
6654
|
-
*
|
|
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.
|
|
6652
|
+
* Check if value is a DnsProvider implementation.
|
|
6680
6653
|
*/
|
|
6654
|
+
function isDnsProvider(value) {
|
|
6655
|
+
return typeof value === "object" && value !== null && typeof value.name === "string" && typeof value.getRecords === "function" && typeof value.upsertRecords === "function" && typeof value.deleteRecords === "function";
|
|
6656
|
+
}
|
|
6681
6657
|
/**
|
|
6682
|
-
*
|
|
6683
|
-
*
|
|
6684
|
-
*
|
|
6658
|
+
* Create a DNS provider based on configuration.
|
|
6659
|
+
*
|
|
6660
|
+
* - 'hostinger': HostingerProvider
|
|
6661
|
+
* - 'route53': Route53Provider
|
|
6662
|
+
* - 'godaddy': GoDaddyProvider
|
|
6663
|
+
* - 'manual': Returns null (user handles DNS)
|
|
6664
|
+
* - Custom: Use provided DnsProvider implementation
|
|
6685
6665
|
*/
|
|
6686
|
-
|
|
6666
|
+
async function createDnsProvider(options) {
|
|
6667
|
+
const { config } = options;
|
|
6668
|
+
if (config.provider === "manual") return null;
|
|
6669
|
+
if (isDnsProvider(config.provider)) return config.provider;
|
|
6670
|
+
const provider = config.provider;
|
|
6671
|
+
if (provider === "hostinger") {
|
|
6672
|
+
const { HostingerProvider } = await Promise.resolve().then(() => require("./HostingerProvider-OdQmKXE3.cjs"));
|
|
6673
|
+
return new HostingerProvider(options.hostinger);
|
|
6674
|
+
}
|
|
6675
|
+
if (provider === "route53") {
|
|
6676
|
+
const { Route53Provider } = await Promise.resolve().then(() => require("./Route53Provider-Bllus2NR.cjs"));
|
|
6677
|
+
const route53Config = config;
|
|
6678
|
+
return new Route53Provider({
|
|
6679
|
+
region: route53Config.region,
|
|
6680
|
+
profile: route53Config.profile,
|
|
6681
|
+
hostedZoneId: route53Config.hostedZoneId
|
|
6682
|
+
});
|
|
6683
|
+
}
|
|
6684
|
+
if (provider === "godaddy") {
|
|
6685
|
+
const { GoDaddyProvider } = await Promise.resolve().then(() => require("./GoDaddyProvider-DzguxU_a.cjs"));
|
|
6686
|
+
return new GoDaddyProvider(options.godaddy ?? {});
|
|
6687
|
+
}
|
|
6688
|
+
if (provider === "cloudflare") throw new DnsProviderNotImplemented("cloudflare");
|
|
6689
|
+
throw new DnsProviderUnknown(config);
|
|
6690
|
+
}
|
|
6687
6691
|
/**
|
|
6688
|
-
* The
|
|
6689
|
-
*
|
|
6690
|
-
*
|
|
6691
|
-
* chain finds — a role.
|
|
6692
|
+
* The provider's credentials can write records and not read them — a key
|
|
6693
|
+
* scoped to updates only. What reads them falls back to writing every record,
|
|
6694
|
+
* which is idempotent, without a diff.
|
|
6692
6695
|
*/
|
|
6693
|
-
|
|
6694
|
-
|
|
6695
|
-
|
|
6696
|
-
|
|
6697
|
-
constructor(
|
|
6698
|
-
super(
|
|
6699
|
-
this.
|
|
6700
|
-
this.
|
|
6696
|
+
var DnsRecordsUnreadable = class extends require_errors.GkmError {
|
|
6697
|
+
provider;
|
|
6698
|
+
domain;
|
|
6699
|
+
reason;
|
|
6700
|
+
constructor(provider, domain, reason) {
|
|
6701
|
+
super(`The ${provider} key cannot read ${domain}'s DNS records (${reason}), so changes cannot be compared with what is there: every record is written, which leaves one that already has its value as it was.`);
|
|
6702
|
+
this.provider = provider;
|
|
6703
|
+
this.domain = domain;
|
|
6704
|
+
this.reason = reason;
|
|
6705
|
+
this.name = "DnsRecordsUnreadable";
|
|
6701
6706
|
}
|
|
6702
6707
|
};
|
|
6703
|
-
/**
|
|
6704
|
-
|
|
6705
|
-
|
|
6706
|
-
|
|
6707
|
-
|
|
6708
|
-
|
|
6709
|
-
|
|
6710
|
-
|
|
6711
|
-
|
|
6712
|
-
|
|
6713
|
-
|
|
6708
|
+
/** A DNS provider gkm names and does not have yet. */
|
|
6709
|
+
var DnsProviderNotImplemented = class extends Error {
|
|
6710
|
+
provider;
|
|
6711
|
+
constructor(provider) {
|
|
6712
|
+
super(`The ${provider} DNS provider is not implemented yet. Use 'route53', 'godaddy', 'hostinger' or 'manual' for this domain in dns, or pass your own DnsProvider object.`);
|
|
6713
|
+
this.provider = provider;
|
|
6714
|
+
this.name = "DnsProviderNotImplemented";
|
|
6715
|
+
}
|
|
6716
|
+
};
|
|
6717
|
+
/** A `provider:` that is none of the built-ins, nor a DnsProvider object. */
|
|
6718
|
+
var DnsProviderUnknown = class extends require_errors.GkmError {
|
|
6719
|
+
config;
|
|
6720
|
+
constructor(config) {
|
|
6721
|
+
super(`Unknown DNS provider: ${JSON.stringify(config)}. dns takes 'route53', 'godaddy', 'hostinger', 'manual', or an object implementing DnsProvider.`);
|
|
6722
|
+
this.config = config;
|
|
6723
|
+
this.name = "DnsProviderUnknown";
|
|
6714
6724
|
}
|
|
6715
6725
|
};
|
|
6726
|
+
//#endregion
|
|
6727
|
+
//#region src/deploy/state.ts
|
|
6716
6728
|
/**
|
|
6717
|
-
*
|
|
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.
|
|
6729
|
+
* A stage's deploy state.
|
|
6721
6730
|
*
|
|
6722
|
-
*
|
|
6731
|
+
* Every target records the same core — when the stage was last deployed, by
|
|
6732
|
+
* which identity, and each app's releases — and a target that keeps ids of
|
|
6733
|
+
* its own adds them in a shape of its own, told apart by `provider`:
|
|
6734
|
+
*
|
|
6735
|
+
* - `dokploy`: the project, applications, services and credentials Dokploy
|
|
6736
|
+
* gave the stage, so a later deploy updates them rather than making more.
|
|
6737
|
+
* - `compose`: nothing beyond the core. A compose stage is one stack on one
|
|
6738
|
+
* server; what it created elsewhere (DNS records) is a resource record.
|
|
6723
6739
|
*/
|
|
6724
|
-
|
|
6725
|
-
|
|
6726
|
-
|
|
6727
|
-
|
|
6728
|
-
|
|
6729
|
-
|
|
6730
|
-
|
|
6740
|
+
/**
|
|
6741
|
+
* Create a new empty state for a stage
|
|
6742
|
+
*/
|
|
6743
|
+
function createEmptyState(stage, projectId, environmentId) {
|
|
6744
|
+
return {
|
|
6745
|
+
provider: "dokploy",
|
|
6746
|
+
stage,
|
|
6747
|
+
projectId,
|
|
6748
|
+
environmentId,
|
|
6749
|
+
applications: {},
|
|
6750
|
+
services: {},
|
|
6751
|
+
lastDeployedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6752
|
+
};
|
|
6753
|
+
}
|
|
6754
|
+
/** A new compose stage's state. */
|
|
6755
|
+
function createComposeState(stage) {
|
|
6756
|
+
return {
|
|
6757
|
+
provider: "compose",
|
|
6758
|
+
stage,
|
|
6759
|
+
lastDeployedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6760
|
+
};
|
|
6731
6761
|
}
|
|
6732
6762
|
/**
|
|
6733
|
-
*
|
|
6734
|
-
*
|
|
6763
|
+
* `state` as the Dokploy target keeps it. A stage whose state another target
|
|
6764
|
+
* wrote — one moved from compose to Dokploy — keeps its core (releases,
|
|
6765
|
+
* identity) and starts with no Dokploy ids, as a first deploy would.
|
|
6735
6766
|
*/
|
|
6736
|
-
|
|
6737
|
-
|
|
6738
|
-
|
|
6739
|
-
|
|
6740
|
-
|
|
6741
|
-
|
|
6742
|
-
|
|
6743
|
-
|
|
6744
|
-
}
|
|
6745
|
-
|
|
6746
|
-
|
|
6747
|
-
|
|
6748
|
-
this.name = "ExternalServicesNotConfigured";
|
|
6749
|
-
}
|
|
6750
|
-
};
|
|
6767
|
+
function asDokployState(state) {
|
|
6768
|
+
if (state.provider === "dokploy") return state;
|
|
6769
|
+
const { provider: _provider, ...core } = state;
|
|
6770
|
+
return {
|
|
6771
|
+
...core,
|
|
6772
|
+
provider: "dokploy",
|
|
6773
|
+
projectId: "",
|
|
6774
|
+
environmentId: "",
|
|
6775
|
+
applications: {},
|
|
6776
|
+
services: {}
|
|
6777
|
+
};
|
|
6778
|
+
}
|
|
6751
6779
|
/**
|
|
6752
|
-
*
|
|
6753
|
-
* depend on each — what a target that provisions the whole manifest checks.
|
|
6780
|
+
* Set application ID in state (mutates state)
|
|
6754
6781
|
*/
|
|
6755
|
-
function
|
|
6756
|
-
|
|
6757
|
-
|
|
6758
|
-
|
|
6759
|
-
|
|
6760
|
-
|
|
6761
|
-
|
|
6762
|
-
|
|
6763
|
-
|
|
6764
|
-
|
|
6765
|
-
kind,
|
|
6766
|
-
...declaration.kind === "file-server" ? { of: declaration.of } : {},
|
|
6767
|
-
...apps.length ? { apps } : {}
|
|
6768
|
-
}];
|
|
6769
|
-
});
|
|
6782
|
+
function setApplicationId(state, appName, applicationId) {
|
|
6783
|
+
state.applications[appName] = applicationId;
|
|
6784
|
+
}
|
|
6785
|
+
/** Two records naming the same image: by digest when both have one. */
|
|
6786
|
+
function sameImage(a, b) {
|
|
6787
|
+
return a.digest && b.digest ? a.digest === b.digest : a.ref === b.ref;
|
|
6788
|
+
}
|
|
6789
|
+
/** The newest release in `history` after `index` that a rollback may restore. */
|
|
6790
|
+
function restorable(history, index, current) {
|
|
6791
|
+
return history.slice(index + 1).find((entry) => !entry.rolledBack && !sameImage(entry, current));
|
|
6770
6792
|
}
|
|
6771
6793
|
/**
|
|
6772
|
-
*
|
|
6773
|
-
*
|
|
6774
|
-
*
|
|
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.
|
|
6794
|
+
* Record that `image` went live for `appName` (mutates state): it becomes
|
|
6795
|
+
* `current`, and what was current becomes `previous`. Releasing what already
|
|
6796
|
+
* runs — a redeploy of the same digest — changes nothing.
|
|
6782
6797
|
*/
|
|
6783
|
-
function
|
|
6784
|
-
const
|
|
6785
|
-
|
|
6786
|
-
const
|
|
6787
|
-
|
|
6798
|
+
function recordRelease(state, appName, image, by, releasedAt = /* @__PURE__ */ new Date()) {
|
|
6799
|
+
const releases = state.releases?.[appName];
|
|
6800
|
+
if (releases && sameImage(releases.current, image)) return;
|
|
6801
|
+
const entry = {
|
|
6802
|
+
...image,
|
|
6803
|
+
releasedAt: releasedAt.toISOString(),
|
|
6804
|
+
releasedBy: by
|
|
6788
6805
|
};
|
|
6789
|
-
const
|
|
6790
|
-
const
|
|
6791
|
-
|
|
6792
|
-
|
|
6793
|
-
|
|
6794
|
-
|
|
6795
|
-
|
|
6796
|
-
|
|
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
|
-
});
|
|
6806
|
+
const history = [entry, ...releases?.history ?? []].slice(0, 10);
|
|
6807
|
+
const previous = releases?.current;
|
|
6808
|
+
state.releases = {
|
|
6809
|
+
...state.releases,
|
|
6810
|
+
[appName]: {
|
|
6811
|
+
current: entry,
|
|
6812
|
+
...previous ? { previous } : {},
|
|
6813
|
+
history
|
|
6814
6814
|
}
|
|
6815
|
-
|
|
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;
|
|
6815
|
+
};
|
|
6853
6816
|
}
|
|
6854
6817
|
/**
|
|
6855
|
-
*
|
|
6856
|
-
*
|
|
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.
|
|
6818
|
+
* Record that `appName` was rolled back to `to` (mutates state): every
|
|
6819
|
+
* release newer than it is marked rolled back, so neither this rollback nor
|
|
6820
|
+
* a later one restores it, and `previous` moves to the release before `to`.
|
|
6861
6821
|
*/
|
|
6862
|
-
function
|
|
6863
|
-
|
|
6864
|
-
if (
|
|
6865
|
-
|
|
6866
|
-
|
|
6822
|
+
function recordRollback(state, appName, to) {
|
|
6823
|
+
const releases = state.releases?.[appName];
|
|
6824
|
+
if (!releases) return;
|
|
6825
|
+
const index = releases.history.findIndex((entry) => !entry.rolledBack && sameImage(entry, to));
|
|
6826
|
+
if (index === -1) return;
|
|
6827
|
+
const history = releases.history.map((entry, i) => i < index ? {
|
|
6828
|
+
...entry,
|
|
6829
|
+
rolledBack: true
|
|
6830
|
+
} : entry);
|
|
6831
|
+
const current = history[index];
|
|
6832
|
+
const previous = restorable(history, index, current);
|
|
6833
|
+
state.releases = {
|
|
6834
|
+
...state.releases,
|
|
6835
|
+
[appName]: {
|
|
6836
|
+
current,
|
|
6837
|
+
...previous ? { previous } : {},
|
|
6838
|
+
history
|
|
6839
|
+
}
|
|
6840
|
+
};
|
|
6867
6841
|
}
|
|
6868
6842
|
/**
|
|
6869
|
-
*
|
|
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.
|
|
6843
|
+
* Get a generated secret for an app
|
|
6881
6844
|
*/
|
|
6882
|
-
function
|
|
6883
|
-
|
|
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
|
-
};
|
|
6845
|
+
function getGeneratedSecret(state, appName, secretName) {
|
|
6846
|
+
return state?.generatedSecrets?.[appName]?.[secretName];
|
|
6909
6847
|
}
|
|
6910
6848
|
/**
|
|
6911
|
-
*
|
|
6912
|
-
*
|
|
6913
|
-
* @throws {ExternalServicesNotConfigured} naming every missing key
|
|
6849
|
+
* Set a generated secret for an app (mutates state)
|
|
6914
6850
|
*/
|
|
6915
|
-
function
|
|
6916
|
-
|
|
6917
|
-
if (
|
|
6918
|
-
|
|
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.`;
|
|
6851
|
+
function setGeneratedSecret(state, appName, secretName, value) {
|
|
6852
|
+
if (!state.generatedSecrets) state.generatedSecrets = {};
|
|
6853
|
+
if (!state.generatedSecrets[appName]) state.generatedSecrets[appName] = {};
|
|
6854
|
+
state.generatedSecrets[appName][secretName] = value;
|
|
6936
6855
|
}
|
|
6937
6856
|
/**
|
|
6938
|
-
*
|
|
6939
|
-
* event a host can act on.
|
|
6857
|
+
* Set DNS verification record for a hostname (mutates state)
|
|
6940
6858
|
*/
|
|
6941
|
-
function
|
|
6942
|
-
|
|
6943
|
-
|
|
6944
|
-
|
|
6945
|
-
|
|
6946
|
-
|
|
6947
|
-
stage: ctx.stage,
|
|
6948
|
-
constructs: use.ids
|
|
6949
|
-
});
|
|
6950
|
-
}
|
|
6859
|
+
function setDnsVerification(state, hostname, serverIp) {
|
|
6860
|
+
if (!state.dnsVerified) state.dnsVerified = {};
|
|
6861
|
+
state.dnsVerified[hostname] = {
|
|
6862
|
+
serverIp,
|
|
6863
|
+
verifiedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6864
|
+
};
|
|
6951
6865
|
}
|
|
6952
|
-
//#endregion
|
|
6953
|
-
//#region src/deploy/StateProvider.ts
|
|
6954
6866
|
/**
|
|
6955
|
-
* Check if
|
|
6867
|
+
* Check if a hostname is already verified with the given IP
|
|
6956
6868
|
*/
|
|
6957
|
-
function
|
|
6958
|
-
return
|
|
6869
|
+
function isDnsVerified(state, hostname, serverIp) {
|
|
6870
|
+
return (state?.dnsVerified?.[hostname])?.serverIp === serverIp;
|
|
6959
6871
|
}
|
|
6960
|
-
//#endregion
|
|
6961
|
-
//#region src/deploy/StateStore.ts
|
|
6962
6872
|
/**
|
|
6963
|
-
*
|
|
6964
|
-
*
|
|
6965
|
-
* `StateProvider` reads and writes a whole stage at once, with nothing to stop
|
|
6966
|
-
* two runs of the same stage from overwriting each other and nowhere to record
|
|
6967
|
-
* a resource the moment it exists. A `StateStore` adds the three things a
|
|
6968
|
-
* deploy needs to be safe against crashes and concurrent runs:
|
|
6969
|
-
*
|
|
6970
|
-
* - `lock(stage)`: one run per stage. A second gets `StateLocked`, naming the
|
|
6971
|
-
* holder, instead of racing the first.
|
|
6972
|
-
* - Versioned `read` and conditional `write`: a write names the version it was
|
|
6973
|
-
* based on, and raises `StateVersionConflict` when the stored state moved on
|
|
6974
|
-
* underneath it, so nothing is lost silently even without the lock.
|
|
6975
|
-
* - Per-resource records: `putResource` journals one resource at a time, so a
|
|
6976
|
-
* run that dies half way leaves behind the ids of everything it created.
|
|
6977
|
-
*
|
|
6978
|
-
* Built-in stores: `LocalStateStore`, `SSMStateStore`, `S3StateStore`. A
|
|
6979
|
-
* custom `StateProvider` is wrapped by `LegacyStateStore`, which keeps it
|
|
6980
|
-
* working but cannot lock.
|
|
6873
|
+
* Get the key for a DNS record in state
|
|
6981
6874
|
*/
|
|
6982
|
-
|
|
6983
|
-
|
|
6984
|
-
|
|
6985
|
-
location;
|
|
6986
|
-
constructor(stage, holder, location) {
|
|
6987
|
-
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";
|
|
6988
|
-
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}\`.`);
|
|
6989
|
-
this.stage = stage;
|
|
6990
|
-
this.holder = holder;
|
|
6991
|
-
this.location = location;
|
|
6992
|
-
this.name = "StateLocked";
|
|
6993
|
-
}
|
|
6994
|
-
};
|
|
6995
|
-
var StateVersionConflict = class extends require_errors.GkmError {
|
|
6996
|
-
stage;
|
|
6997
|
-
expectedVersion;
|
|
6998
|
-
actualVersion;
|
|
6999
|
-
constructor(stage, expectedVersion, actualVersion) {
|
|
7000
|
-
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.`);
|
|
7001
|
-
this.stage = stage;
|
|
7002
|
-
this.expectedVersion = expectedVersion;
|
|
7003
|
-
this.actualVersion = actualVersion;
|
|
7004
|
-
this.name = "StateVersionConflict";
|
|
7005
|
-
}
|
|
7006
|
-
};
|
|
7007
|
-
var StageStateMissing = class extends require_errors.GkmError {
|
|
7008
|
-
stage;
|
|
7009
|
-
constructor(stage) {
|
|
7010
|
-
super(`There is no deploy state for stage '${stage}' to record a resource in. Write the stage's state first (write with expectedVersion: null).`);
|
|
7011
|
-
this.stage = stage;
|
|
7012
|
-
this.name = "StageStateMissing";
|
|
7013
|
-
}
|
|
7014
|
-
};
|
|
7015
|
-
var StateUnreadable = class extends require_errors.GkmError {
|
|
7016
|
-
stage;
|
|
7017
|
-
location;
|
|
7018
|
-
constructor(stage, location, cause) {
|
|
7019
|
-
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 });
|
|
7020
|
-
this.stage = stage;
|
|
7021
|
-
this.location = location;
|
|
7022
|
-
this.name = "StateUnreadable";
|
|
7023
|
-
}
|
|
7024
|
-
};
|
|
7025
|
-
var StateSchemaTooNew = class extends require_errors.GkmError {
|
|
7026
|
-
stage;
|
|
7027
|
-
schemaVersion;
|
|
7028
|
-
constructor(stage, schemaVersion) {
|
|
7029
|
-
super(`Deploy state for stage '${stage}' has schema version ${String(schemaVersion)}, newer than this @geekmidas/cli understands (2). Upgrade @geekmidas/cli to the version that wrote it.`);
|
|
7030
|
-
this.stage = stage;
|
|
7031
|
-
this.schemaVersion = schemaVersion;
|
|
7032
|
-
this.name = "StateSchemaTooNew";
|
|
7033
|
-
}
|
|
7034
|
-
};
|
|
6875
|
+
function getDnsRecordKey(name, type) {
|
|
6876
|
+
return `${name}:${type}`;
|
|
6877
|
+
}
|
|
7035
6878
|
/**
|
|
7036
|
-
*
|
|
7037
|
-
* in for a store, so a deploy that cannot be protected from a concurrent one
|
|
7038
|
-
* says so.
|
|
6879
|
+
* Set a created DNS record in state (mutates state)
|
|
7039
6880
|
*/
|
|
7040
|
-
|
|
7041
|
-
|
|
7042
|
-
|
|
7043
|
-
|
|
7044
|
-
|
|
7045
|
-
|
|
7046
|
-
}
|
|
7047
|
-
};
|
|
7048
|
-
var StateStoreNeedsWorkspaceName = class extends require_errors.GkmError {
|
|
7049
|
-
provider;
|
|
7050
|
-
constructor(provider) {
|
|
7051
|
-
super(`The '${provider}' state provider keys state by workspace name. Set "name" in gkm.config.ts.`);
|
|
7052
|
-
this.provider = provider;
|
|
7053
|
-
this.name = "StateStoreNeedsWorkspaceName";
|
|
7054
|
-
}
|
|
7055
|
-
};
|
|
7056
|
-
var UnknownStateProvider = class extends require_errors.GkmError {
|
|
7057
|
-
config;
|
|
7058
|
-
constructor(config) {
|
|
7059
|
-
super(`Unknown state provider ${JSON.stringify(config)}. Use 'local', 'ssm', 's3', or an object implementing StateStore or StateProvider.`);
|
|
7060
|
-
this.config = config;
|
|
7061
|
-
this.name = "UnknownStateProvider";
|
|
7062
|
-
}
|
|
7063
|
-
};
|
|
7064
|
-
/** A lock or write that could not get at the state backend in time. */
|
|
7065
|
-
var StateStoreBusy = class extends require_errors.GkmError {
|
|
7066
|
-
stage;
|
|
7067
|
-
location;
|
|
7068
|
-
constructor(stage, location) {
|
|
7069
|
-
super(`Timed out waiting to write deploy state for stage '${stage}' (${location}). If no other gkm process is running, remove ${location}.`);
|
|
7070
|
-
this.stage = stage;
|
|
7071
|
-
this.location = location;
|
|
7072
|
-
this.name = "StateStoreBusy";
|
|
7073
|
-
}
|
|
7074
|
-
};
|
|
7075
|
-
function decodeDocument(stage, body, location) {
|
|
7076
|
-
let parsed;
|
|
7077
|
-
try {
|
|
7078
|
-
parsed = JSON.parse(body);
|
|
7079
|
-
} catch (error) {
|
|
7080
|
-
throw new StateUnreadable(stage, location, error);
|
|
7081
|
-
}
|
|
7082
|
-
if (typeof parsed !== "object" || parsed === null) throw new StateUnreadable(stage, location, parsed);
|
|
7083
|
-
const schemaVersion = parsed.schemaVersion;
|
|
7084
|
-
if (schemaVersion === void 0) return {
|
|
7085
|
-
schemaVersion: 1,
|
|
7086
|
-
state: parsed
|
|
7087
|
-
};
|
|
7088
|
-
if (schemaVersion === 2) return {
|
|
7089
|
-
schemaVersion: 2,
|
|
7090
|
-
document: parsed
|
|
6881
|
+
function setDnsRecord(state, record) {
|
|
6882
|
+
if (!state.dnsRecords) state.dnsRecords = {};
|
|
6883
|
+
const key = getDnsRecordKey(record.name, record.type);
|
|
6884
|
+
state.dnsRecords[key] = {
|
|
6885
|
+
...record,
|
|
6886
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7091
6887
|
};
|
|
7092
|
-
throw new StateSchemaTooNew(stage, schemaVersion);
|
|
7093
|
-
}
|
|
7094
|
-
function encodeDocument(document) {
|
|
7095
|
-
return JSON.stringify(document, null, 2);
|
|
7096
6888
|
}
|
|
7097
6889
|
/**
|
|
7098
|
-
*
|
|
7099
|
-
* so the first journaled deploy after an upgrade adopts them rather than
|
|
7100
|
-
* treating them as unknown.
|
|
6890
|
+
* Get backup state from state
|
|
7101
6891
|
*/
|
|
7102
|
-
function
|
|
7103
|
-
|
|
7104
|
-
const resources = {};
|
|
7105
|
-
const seed = (type, name, id) => {
|
|
7106
|
-
if (!id) return;
|
|
7107
|
-
const key = name ? `${type}:${name}` : type;
|
|
7108
|
-
resources[key] = {
|
|
7109
|
-
key,
|
|
7110
|
-
type,
|
|
7111
|
-
id,
|
|
7112
|
-
status: "ready",
|
|
7113
|
-
updatedAt: now
|
|
7114
|
-
};
|
|
7115
|
-
};
|
|
7116
|
-
seed("project", null, state.projectId);
|
|
7117
|
-
seed("environment", null, state.environmentId);
|
|
7118
|
-
for (const [app, id] of Object.entries(state.applications ?? {})) seed("application", app, id);
|
|
7119
|
-
seed("postgres", null, state.services?.postgresId);
|
|
7120
|
-
seed("redis", null, state.services?.redisId);
|
|
7121
|
-
seed("backup-destination", null, state.backups?.destinationId);
|
|
7122
|
-
return {
|
|
7123
|
-
schemaVersion: 2,
|
|
7124
|
-
stage,
|
|
7125
|
-
serial: 1,
|
|
7126
|
-
state,
|
|
7127
|
-
resources,
|
|
7128
|
-
updatedAt: now
|
|
7129
|
-
};
|
|
7130
|
-
}
|
|
7131
|
-
function newLockHolder(options) {
|
|
7132
|
-
let owner = "unknown";
|
|
7133
|
-
try {
|
|
7134
|
-
owner = (0, node_os.userInfo)().username;
|
|
7135
|
-
} catch {}
|
|
7136
|
-
return {
|
|
7137
|
-
id: (0, node_crypto.randomUUID)(),
|
|
7138
|
-
owner,
|
|
7139
|
-
host: (0, node_os.hostname)(),
|
|
7140
|
-
pid: process.pid,
|
|
7141
|
-
acquiredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7142
|
-
operation: options?.operation
|
|
7143
|
-
};
|
|
6892
|
+
function getBackupState(state) {
|
|
6893
|
+
return state?.backups;
|
|
7144
6894
|
}
|
|
7145
|
-
|
|
7146
|
-
|
|
6895
|
+
/**
|
|
6896
|
+
* Set backup state (mutates state)
|
|
6897
|
+
*/
|
|
6898
|
+
function setBackupState(state, backupState) {
|
|
6899
|
+
state.backups = backupState;
|
|
7147
6900
|
}
|
|
7148
6901
|
/**
|
|
7149
|
-
*
|
|
7150
|
-
* stage, and create a lock object only if it does not exist. Migration and
|
|
7151
|
-
* resource records are the same for every such backend, so they live here.
|
|
7152
|
-
*/
|
|
7153
|
-
var DocumentStateStore = class {
|
|
7154
|
-
async lock(stage, options) {
|
|
7155
|
-
const holder = newLockHolder(options);
|
|
7156
|
-
await this.createLock(stage, holder);
|
|
7157
|
-
let released = false;
|
|
7158
|
-
return {
|
|
7159
|
-
stage,
|
|
7160
|
-
holder,
|
|
7161
|
-
release: async () => {
|
|
7162
|
-
if (released) return;
|
|
7163
|
-
released = true;
|
|
7164
|
-
await this.removeLock(stage, holder);
|
|
7165
|
-
}
|
|
7166
|
-
};
|
|
7167
|
-
}
|
|
7168
|
-
async forceUnlock(stage) {
|
|
7169
|
-
const holder = await this.readLock(stage);
|
|
7170
|
-
await this.removeLock(stage, null);
|
|
7171
|
-
return holder;
|
|
7172
|
-
}
|
|
7173
|
-
async read(stage) {
|
|
7174
|
-
const current = await this.readDocument(stage);
|
|
7175
|
-
if (!current) return null;
|
|
7176
|
-
return {
|
|
7177
|
-
state: current.document.state,
|
|
7178
|
-
resources: current.document.resources,
|
|
7179
|
-
version: current.version
|
|
7180
|
-
};
|
|
7181
|
-
}
|
|
7182
|
-
async write(stage, state, options) {
|
|
7183
|
-
if (options.expectedVersion === null) {
|
|
7184
|
-
const document = {
|
|
7185
|
-
schemaVersion: 2,
|
|
7186
|
-
stage,
|
|
7187
|
-
serial: 1,
|
|
7188
|
-
state,
|
|
7189
|
-
resources: {},
|
|
7190
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7191
|
-
};
|
|
7192
|
-
return this.writeRaw(stage, encodeDocument(document), null);
|
|
7193
|
-
}
|
|
7194
|
-
return this.mutate(stage, options.expectedVersion, (document) => ({
|
|
7195
|
-
...document,
|
|
7196
|
-
state
|
|
7197
|
-
}));
|
|
7198
|
-
}
|
|
7199
|
-
async putResource(stage, record, options) {
|
|
7200
|
-
return this.mutate(stage, options?.expectedVersion, (document) => ({
|
|
7201
|
-
...document,
|
|
7202
|
-
resources: {
|
|
7203
|
-
...document.resources,
|
|
7204
|
-
[record.key]: {
|
|
7205
|
-
...record,
|
|
7206
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7207
|
-
}
|
|
7208
|
-
}
|
|
7209
|
-
}));
|
|
7210
|
-
}
|
|
7211
|
-
async deleteResource(stage, key, options) {
|
|
7212
|
-
return this.mutate(stage, options?.expectedVersion, (document) => {
|
|
7213
|
-
const { [key]: _removed, ...resources } = document.resources;
|
|
7214
|
-
return {
|
|
7215
|
-
...document,
|
|
7216
|
-
resources
|
|
7217
|
-
};
|
|
7218
|
-
});
|
|
7219
|
-
}
|
|
7220
|
-
async mutate(stage, expectedVersion, change) {
|
|
7221
|
-
const current = await this.readDocument(stage);
|
|
7222
|
-
if (!current) {
|
|
7223
|
-
if (expectedVersion !== void 0) throw new StateVersionConflict(stage, expectedVersion, null);
|
|
7224
|
-
throw new StageStateMissing(stage);
|
|
7225
|
-
}
|
|
7226
|
-
if (expectedVersion !== void 0 && current.version !== expectedVersion) throw new StateVersionConflict(stage, expectedVersion, current.version);
|
|
7227
|
-
const next = change(current.document);
|
|
7228
|
-
next.serial = current.document.serial + 1;
|
|
7229
|
-
next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7230
|
-
return this.writeRaw(stage, encodeDocument(next), current.version);
|
|
7231
|
-
}
|
|
7232
|
-
/**
|
|
7233
|
-
* Reads the stage's document, migrating v1 in place on the way. The
|
|
7234
|
-
* migration is itself a conditional write: if another run migrated (or
|
|
7235
|
-
* wrote) first, its result is read instead.
|
|
7236
|
-
*/
|
|
7237
|
-
async readDocument(stage, attempt = 0) {
|
|
7238
|
-
const raw = await this.readRaw(stage);
|
|
7239
|
-
if (!raw) return null;
|
|
7240
|
-
const decoded = decodeDocument(stage, raw.body, this.location(stage));
|
|
7241
|
-
if (decoded.schemaVersion === 2) return {
|
|
7242
|
-
document: decoded.document,
|
|
7243
|
-
version: raw.version
|
|
7244
|
-
};
|
|
7245
|
-
await this.writeV1Backup(stage, raw.body);
|
|
7246
|
-
const document = migrateV1(stage, decoded.state);
|
|
7247
|
-
try {
|
|
7248
|
-
return {
|
|
7249
|
-
document,
|
|
7250
|
-
version: await this.writeRaw(stage, encodeDocument(document), raw.version)
|
|
7251
|
-
};
|
|
7252
|
-
} catch (error) {
|
|
7253
|
-
if (error instanceof StateVersionConflict && attempt < 2) return this.readDocument(stage, attempt + 1);
|
|
7254
|
-
throw error;
|
|
7255
|
-
}
|
|
7256
|
-
}
|
|
7257
|
-
};
|
|
7258
|
-
function isStateStore(value) {
|
|
7259
|
-
return typeof value === "object" && value !== null && typeof value.lock === "function" && typeof value.read === "function" && typeof value.write === "function" && typeof value.putResource === "function";
|
|
7260
|
-
}
|
|
7261
|
-
/**
|
|
7262
|
-
* The store for a workspace's `state` config:
|
|
7263
|
-
*
|
|
7264
|
-
* - none / `'local'`: `LocalStateStore` under `.gkm/`
|
|
7265
|
-
* - `'ssm'`: `SSMStateStore`, with no local cache — SSM is read directly, so
|
|
7266
|
-
* a stale local copy can never win over it
|
|
7267
|
-
* - `'s3'`: `S3StateStore`
|
|
7268
|
-
* - a `StateStore` object: used as is
|
|
7269
|
-
* - a `StateProvider` object: wrapped by `LegacyStateStore`
|
|
7270
|
-
*/
|
|
7271
|
-
async function createStateStore(options) {
|
|
7272
|
-
const { config, workspaceRoot, workspaceName } = options;
|
|
7273
|
-
if (!config || config.provider === "local") {
|
|
7274
|
-
const { LocalStateStore } = await Promise.resolve().then(() => require("./LocalStateStore-BMfseje2.cjs")).then((n) => n.LocalStateStore_exports);
|
|
7275
|
-
return new LocalStateStore(workspaceRoot);
|
|
7276
|
-
}
|
|
7277
|
-
if (isStateStore(config.provider)) return config.provider;
|
|
7278
|
-
if (isStateProvider(config.provider)) {
|
|
7279
|
-
const { LegacyStateStore } = await Promise.resolve().then(() => require("./LegacyStateStore-CeEwayMB.cjs"));
|
|
7280
|
-
return new LegacyStateStore(config.provider, { warn: options.warn });
|
|
7281
|
-
}
|
|
7282
|
-
if (config.provider === "ssm") {
|
|
7283
|
-
if (!workspaceName) throw new StateStoreNeedsWorkspaceName("ssm");
|
|
7284
|
-
const { SSMStateStore } = await Promise.resolve().then(() => require("./SSMStateStore-D9K62wM4.cjs"));
|
|
7285
|
-
const ssm = config;
|
|
7286
|
-
return SSMStateStore.create({
|
|
7287
|
-
workspaceName,
|
|
7288
|
-
region: ssm.region,
|
|
7289
|
-
profile: ssm.profile
|
|
7290
|
-
});
|
|
7291
|
-
}
|
|
7292
|
-
if (config.provider === "s3") {
|
|
7293
|
-
if (!workspaceName) throw new StateStoreNeedsWorkspaceName("s3");
|
|
7294
|
-
const { S3StateStore } = await Promise.resolve().then(() => require("./S3StateStore-BmMt2HhW.cjs"));
|
|
7295
|
-
const s3 = config;
|
|
7296
|
-
return S3StateStore.create({
|
|
7297
|
-
workspaceName,
|
|
7298
|
-
bucket: s3.bucket,
|
|
7299
|
-
prefix: s3.prefix,
|
|
7300
|
-
region: s3.region,
|
|
7301
|
-
profile: s3.profile
|
|
7302
|
-
});
|
|
7303
|
-
}
|
|
7304
|
-
throw new UnknownStateProvider(config);
|
|
7305
|
-
}
|
|
7306
|
-
//#endregion
|
|
7307
|
-
//#region src/target/dokploy/dns/DnsProvider.ts
|
|
7308
|
-
/**
|
|
7309
|
-
* Check if value is a DnsProvider implementation.
|
|
7310
|
-
*/
|
|
7311
|
-
function isDnsProvider(value) {
|
|
7312
|
-
return typeof value === "object" && value !== null && typeof value.name === "string" && typeof value.getRecords === "function" && typeof value.upsertRecords === "function" && typeof value.deleteRecords === "function";
|
|
7313
|
-
}
|
|
7314
|
-
/**
|
|
7315
|
-
* Create a DNS provider based on configuration.
|
|
7316
|
-
*
|
|
7317
|
-
* - 'hostinger': HostingerProvider
|
|
7318
|
-
* - 'route53': Route53Provider
|
|
7319
|
-
* - 'godaddy': GoDaddyProvider
|
|
7320
|
-
* - 'manual': Returns null (user handles DNS)
|
|
7321
|
-
* - Custom: Use provided DnsProvider implementation
|
|
7322
|
-
*/
|
|
7323
|
-
async function createDnsProvider(options) {
|
|
7324
|
-
const { config } = options;
|
|
7325
|
-
if (config.provider === "manual") return null;
|
|
7326
|
-
if (isDnsProvider(config.provider)) return config.provider;
|
|
7327
|
-
const provider = config.provider;
|
|
7328
|
-
if (provider === "hostinger") {
|
|
7329
|
-
const { HostingerProvider } = await Promise.resolve().then(() => require("./HostingerProvider-C6t-PSNe.cjs"));
|
|
7330
|
-
return new HostingerProvider();
|
|
7331
|
-
}
|
|
7332
|
-
if (provider === "route53") {
|
|
7333
|
-
const { Route53Provider } = await Promise.resolve().then(() => require("./Route53Provider-Bllus2NR.cjs"));
|
|
7334
|
-
const route53Config = config;
|
|
7335
|
-
return new Route53Provider({
|
|
7336
|
-
region: route53Config.region,
|
|
7337
|
-
profile: route53Config.profile,
|
|
7338
|
-
hostedZoneId: route53Config.hostedZoneId
|
|
7339
|
-
});
|
|
7340
|
-
}
|
|
7341
|
-
if (provider === "godaddy") {
|
|
7342
|
-
const { GoDaddyProvider } = await Promise.resolve().then(() => require("./GoDaddyProvider-CJwg2r_9.cjs"));
|
|
7343
|
-
return new GoDaddyProvider(options.godaddy ?? {});
|
|
7344
|
-
}
|
|
7345
|
-
if (provider === "cloudflare") throw new DnsProviderNotImplemented("cloudflare");
|
|
7346
|
-
throw new DnsProviderUnknown(config);
|
|
7347
|
-
}
|
|
7348
|
-
/**
|
|
7349
|
-
* The provider's credentials can write records and not read them — a key
|
|
7350
|
-
* scoped to updates only. What reads them falls back to writing every record,
|
|
7351
|
-
* which is idempotent, without a diff.
|
|
7352
|
-
*/
|
|
7353
|
-
var DnsRecordsUnreadable = class extends require_errors.GkmError {
|
|
7354
|
-
provider;
|
|
7355
|
-
domain;
|
|
7356
|
-
reason;
|
|
7357
|
-
constructor(provider, domain, reason) {
|
|
7358
|
-
super(`The ${provider} key cannot read ${domain}'s DNS records (${reason}), so changes cannot be compared with what is there: every record is written, which leaves one that already has its value as it was.`);
|
|
7359
|
-
this.provider = provider;
|
|
7360
|
-
this.domain = domain;
|
|
7361
|
-
this.reason = reason;
|
|
7362
|
-
this.name = "DnsRecordsUnreadable";
|
|
7363
|
-
}
|
|
7364
|
-
};
|
|
7365
|
-
/** A DNS provider gkm names and does not have yet. */
|
|
7366
|
-
var DnsProviderNotImplemented = class extends Error {
|
|
7367
|
-
provider;
|
|
7368
|
-
constructor(provider) {
|
|
7369
|
-
super(`The ${provider} DNS provider is not implemented yet. Use 'route53', 'godaddy', 'hostinger' or 'manual' for this domain in dns, or pass your own DnsProvider object.`);
|
|
7370
|
-
this.provider = provider;
|
|
7371
|
-
this.name = "DnsProviderNotImplemented";
|
|
7372
|
-
}
|
|
7373
|
-
};
|
|
7374
|
-
/** A `provider:` that is none of the built-ins, nor a DnsProvider object. */
|
|
7375
|
-
var DnsProviderUnknown = class extends require_errors.GkmError {
|
|
7376
|
-
config;
|
|
7377
|
-
constructor(config) {
|
|
7378
|
-
super(`Unknown DNS provider: ${JSON.stringify(config)}. dns takes 'route53', 'godaddy', 'hostinger', 'manual', or an object implementing DnsProvider.`);
|
|
7379
|
-
this.config = config;
|
|
7380
|
-
this.name = "DnsProviderUnknown";
|
|
7381
|
-
}
|
|
7382
|
-
};
|
|
7383
|
-
//#endregion
|
|
7384
|
-
//#region src/deploy/state.ts
|
|
7385
|
-
/**
|
|
7386
|
-
* Deploy state management for Dokploy deployments
|
|
7387
|
-
*
|
|
7388
|
-
* Stores resource IDs (applications, services) per stage to avoid
|
|
7389
|
-
* re-creating resources on subsequent deploys.
|
|
7390
|
-
*/
|
|
7391
|
-
/**
|
|
7392
|
-
* Create a new empty state for a stage
|
|
7393
|
-
*/
|
|
7394
|
-
function createEmptyState(stage, projectId, environmentId) {
|
|
7395
|
-
return {
|
|
7396
|
-
provider: "dokploy",
|
|
7397
|
-
stage,
|
|
7398
|
-
projectId,
|
|
7399
|
-
environmentId,
|
|
7400
|
-
applications: {},
|
|
7401
|
-
services: {},
|
|
7402
|
-
lastDeployedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7403
|
-
};
|
|
7404
|
-
}
|
|
7405
|
-
/**
|
|
7406
|
-
* Set application ID in state (mutates state)
|
|
7407
|
-
*/
|
|
7408
|
-
function setApplicationId(state, appName, applicationId) {
|
|
7409
|
-
state.applications[appName] = applicationId;
|
|
7410
|
-
}
|
|
7411
|
-
/** Two records naming the same image: by digest when both have one. */
|
|
7412
|
-
function sameImage(a, b) {
|
|
7413
|
-
return a.digest && b.digest ? a.digest === b.digest : a.ref === b.ref;
|
|
7414
|
-
}
|
|
7415
|
-
/** The newest release in `history` after `index` that a rollback may restore. */
|
|
7416
|
-
function restorable(history, index, current) {
|
|
7417
|
-
return history.slice(index + 1).find((entry) => !entry.rolledBack && !sameImage(entry, current));
|
|
7418
|
-
}
|
|
7419
|
-
/**
|
|
7420
|
-
* Record that `image` went live for `appName` (mutates state): it becomes
|
|
7421
|
-
* `current`, and what was current becomes `previous`. Releasing what already
|
|
7422
|
-
* runs — a redeploy of the same digest — changes nothing.
|
|
7423
|
-
*/
|
|
7424
|
-
function recordRelease(state, appName, image, releasedAt = /* @__PURE__ */ new Date()) {
|
|
7425
|
-
const releases = state.releases?.[appName];
|
|
7426
|
-
if (releases && sameImage(releases.current, image)) return;
|
|
7427
|
-
const entry = {
|
|
7428
|
-
...image,
|
|
7429
|
-
releasedAt: releasedAt.toISOString()
|
|
7430
|
-
};
|
|
7431
|
-
const history = [entry, ...releases?.history ?? []].slice(0, 10);
|
|
7432
|
-
const previous = releases?.current;
|
|
7433
|
-
state.releases = {
|
|
7434
|
-
...state.releases,
|
|
7435
|
-
[appName]: {
|
|
7436
|
-
current: entry,
|
|
7437
|
-
...previous ? { previous } : {},
|
|
7438
|
-
history
|
|
7439
|
-
}
|
|
7440
|
-
};
|
|
7441
|
-
}
|
|
7442
|
-
/**
|
|
7443
|
-
* Record that `appName` was rolled back to `to` (mutates state): every
|
|
7444
|
-
* release newer than it is marked rolled back, so neither this rollback nor
|
|
7445
|
-
* a later one restores it, and `previous` moves to the release before `to`.
|
|
7446
|
-
*/
|
|
7447
|
-
function recordRollback(state, appName, to) {
|
|
7448
|
-
const releases = state.releases?.[appName];
|
|
7449
|
-
if (!releases) return;
|
|
7450
|
-
const index = releases.history.findIndex((entry) => !entry.rolledBack && sameImage(entry, to));
|
|
7451
|
-
if (index === -1) return;
|
|
7452
|
-
const history = releases.history.map((entry, i) => i < index ? {
|
|
7453
|
-
...entry,
|
|
7454
|
-
rolledBack: true
|
|
7455
|
-
} : entry);
|
|
7456
|
-
const current = history[index];
|
|
7457
|
-
const previous = restorable(history, index, current);
|
|
7458
|
-
state.releases = {
|
|
7459
|
-
...state.releases,
|
|
7460
|
-
[appName]: {
|
|
7461
|
-
current,
|
|
7462
|
-
...previous ? { previous } : {},
|
|
7463
|
-
history
|
|
7464
|
-
}
|
|
7465
|
-
};
|
|
7466
|
-
}
|
|
7467
|
-
/**
|
|
7468
|
-
* Get a generated secret for an app
|
|
7469
|
-
*/
|
|
7470
|
-
function getGeneratedSecret(state, appName, secretName) {
|
|
7471
|
-
return state?.generatedSecrets?.[appName]?.[secretName];
|
|
7472
|
-
}
|
|
7473
|
-
/**
|
|
7474
|
-
* Set a generated secret for an app (mutates state)
|
|
7475
|
-
*/
|
|
7476
|
-
function setGeneratedSecret(state, appName, secretName, value) {
|
|
7477
|
-
if (!state.generatedSecrets) state.generatedSecrets = {};
|
|
7478
|
-
if (!state.generatedSecrets[appName]) state.generatedSecrets[appName] = {};
|
|
7479
|
-
state.generatedSecrets[appName][secretName] = value;
|
|
7480
|
-
}
|
|
7481
|
-
/**
|
|
7482
|
-
* Set DNS verification record for a hostname (mutates state)
|
|
7483
|
-
*/
|
|
7484
|
-
function setDnsVerification(state, hostname, serverIp) {
|
|
7485
|
-
if (!state.dnsVerified) state.dnsVerified = {};
|
|
7486
|
-
state.dnsVerified[hostname] = {
|
|
7487
|
-
serverIp,
|
|
7488
|
-
verifiedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7489
|
-
};
|
|
7490
|
-
}
|
|
7491
|
-
/**
|
|
7492
|
-
* Check if a hostname is already verified with the given IP
|
|
7493
|
-
*/
|
|
7494
|
-
function isDnsVerified(state, hostname, serverIp) {
|
|
7495
|
-
return (state?.dnsVerified?.[hostname])?.serverIp === serverIp;
|
|
7496
|
-
}
|
|
7497
|
-
/**
|
|
7498
|
-
* Get the key for a DNS record in state
|
|
7499
|
-
*/
|
|
7500
|
-
function getDnsRecordKey(name, type) {
|
|
7501
|
-
return `${name}:${type}`;
|
|
7502
|
-
}
|
|
7503
|
-
/**
|
|
7504
|
-
* Set a created DNS record in state (mutates state)
|
|
7505
|
-
*/
|
|
7506
|
-
function setDnsRecord(state, record) {
|
|
7507
|
-
if (!state.dnsRecords) state.dnsRecords = {};
|
|
7508
|
-
const key = getDnsRecordKey(record.name, record.type);
|
|
7509
|
-
state.dnsRecords[key] = {
|
|
7510
|
-
...record,
|
|
7511
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7512
|
-
};
|
|
7513
|
-
}
|
|
7514
|
-
/**
|
|
7515
|
-
* Get backup state from state
|
|
7516
|
-
*/
|
|
7517
|
-
function getBackupState(state) {
|
|
7518
|
-
return state?.backups;
|
|
7519
|
-
}
|
|
7520
|
-
/**
|
|
7521
|
-
* Set backup state (mutates state)
|
|
7522
|
-
*/
|
|
7523
|
-
function setBackupState(state, backupState) {
|
|
7524
|
-
state.backups = backupState;
|
|
7525
|
-
}
|
|
7526
|
-
/**
|
|
7527
|
-
* Set postgres backup ID in state (mutates state)
|
|
6902
|
+
* Set postgres backup ID in state (mutates state)
|
|
7528
6903
|
*/
|
|
7529
6904
|
function setPostgresBackupId(state, backupId) {
|
|
7530
6905
|
if (state.backups) state.backups.postgresBackupId = backupId;
|
|
@@ -7874,315 +7249,643 @@ async function verifyDnsRecords(appHostnames, serverIp, state) {
|
|
|
7874
7249
|
return results;
|
|
7875
7250
|
}
|
|
7876
7251
|
//#endregion
|
|
7877
|
-
//#region src/
|
|
7878
|
-
/**
|
|
7879
|
-
*
|
|
7880
|
-
*
|
|
7881
|
-
*
|
|
7882
|
-
*
|
|
7883
|
-
*
|
|
7884
|
-
*
|
|
7885
|
-
*
|
|
7886
|
-
*
|
|
7887
|
-
|
|
7888
|
-
|
|
7889
|
-
|
|
7890
|
-
|
|
7891
|
-
|
|
7892
|
-
|
|
7893
|
-
|
|
7894
|
-
|
|
7895
|
-
|
|
7896
|
-
|
|
7897
|
-
|
|
7898
|
-
|
|
7899
|
-
|
|
7900
|
-
|
|
7901
|
-
|
|
7902
|
-
|
|
7903
|
-
|
|
7904
|
-
|
|
7905
|
-
|
|
7906
|
-
|
|
7907
|
-
|
|
7908
|
-
|
|
7909
|
-
|
|
7910
|
-
|
|
7911
|
-
|
|
7912
|
-
|
|
7913
|
-
|
|
7914
|
-
|
|
7915
|
-
|
|
7916
|
-
|
|
7917
|
-
this.
|
|
7918
|
-
this.
|
|
7919
|
-
|
|
7920
|
-
};
|
|
7921
|
-
/** `docker push` failed — usually the registry login or its permissions. */
|
|
7922
|
-
var DockerPushFailed = class extends Error {
|
|
7923
|
-
imageRef;
|
|
7924
|
-
constructor(imageRef, cause) {
|
|
7925
|
-
super(`Failed to push Docker image: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
7926
|
-
this.imageRef = imageRef;
|
|
7927
|
-
this.name = "DockerPushFailed";
|
|
7252
|
+
//#region src/compose/dns.ts
|
|
7253
|
+
/**
|
|
7254
|
+
* A compose stage's DNS: the records its public hosts need, and the check that
|
|
7255
|
+
* they resolve to its server before a certificate is asked for.
|
|
7256
|
+
*
|
|
7257
|
+
* - **The server** (`serverAddress`): the stage's own secrets hold its public
|
|
7258
|
+
* addresses — `GKM_SERVER_IPV4`, and `GKM_SERVER_IPV6` where it has one.
|
|
7259
|
+
* Reserved for gkm: never handed to an app.
|
|
7260
|
+
* - **The plan** (`planStackDns`, pure): every public host the stack serves —
|
|
7261
|
+
* its apex, each app, each file server, the public log UI — pointed at the
|
|
7262
|
+
* server. An A record per host (and an AAAA with an IPv6 address), or, where
|
|
7263
|
+
* the domain's `dns` entry says `records: { mode: 'cname', target }`, one A
|
|
7264
|
+
* record for the target and a CNAME to it for every other host but the
|
|
7265
|
+
* apex, which DNS forbids a CNAME at.
|
|
7266
|
+
* - **Applying it** (`applyStackDns`): through the domain's provider in `dns`,
|
|
7267
|
+
* a record set at a time. A record with the right value is left alone; one
|
|
7268
|
+
* with another value is replaced and the change printed. A `manual` domain
|
|
7269
|
+
* is printed and nothing else. A dry run reads, prints and writes nothing.
|
|
7270
|
+
* - **The check** (`checkStackDns`): each host resolved with the system
|
|
7271
|
+
* resolver. One that is not the server stops the deploy with
|
|
7272
|
+
* {@link HostNotPointingAtServer}, before Caddy or Traefik ask Let's Encrypt
|
|
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.
|
|
7278
|
+
*/
|
|
7279
|
+
/** The line that says how to set the server's address. */
|
|
7280
|
+
function serverAddressHint(stage) {
|
|
7281
|
+
return `gkm secrets:set ${require_workspace.SERVER_IPV4_KEY} '<ip>' --stage ${stage}`;
|
|
7282
|
+
}
|
|
7283
|
+
/** A server address in the stage's secrets that is not an address. */
|
|
7284
|
+
var ServerAddressInvalid = class extends require_errors.GkmError {
|
|
7285
|
+
stage;
|
|
7286
|
+
key;
|
|
7287
|
+
value;
|
|
7288
|
+
constructor(stage, key, value) {
|
|
7289
|
+
const ipv4 = key === require_workspace.SERVER_IPV4_KEY;
|
|
7290
|
+
super(`${key} in the '${stage}' stage's secrets is ${JSON.stringify(value)}, which is not an ${ipv4 ? "IPv4 address (like 203.0.113.10)" : "IPv6 address (like 2001:db8::10)"}. Set the server's public address: gkm secrets:set ${key} '<ip>' --stage ${stage}.`);
|
|
7291
|
+
this.stage = stage;
|
|
7292
|
+
this.key = key;
|
|
7293
|
+
this.value = value;
|
|
7294
|
+
this.name = "ServerAddressInvalid";
|
|
7928
7295
|
}
|
|
7929
7296
|
};
|
|
7930
|
-
/**
|
|
7931
|
-
|
|
7932
|
-
|
|
7933
|
-
|
|
7934
|
-
|
|
7935
|
-
|
|
7936
|
-
|
|
7297
|
+
/**
|
|
7298
|
+
* A compose stage that serves real domains with no server address: its hosts
|
|
7299
|
+
* cannot be pointed or checked, and a certificate would be asked for blind.
|
|
7300
|
+
*/
|
|
7301
|
+
var ServerAddressMissing = class extends require_errors.GkmError {
|
|
7302
|
+
stage;
|
|
7303
|
+
domain;
|
|
7304
|
+
constructor(stage, domain) {
|
|
7305
|
+
super(`The '${stage}' stage serves ${domain} and its secrets hold no ${require_workspace.SERVER_IPV4_KEY} — the public address of the server its stack runs on, which its hosts' DNS records point at and every deploy checks before asking for certificates. Set it: ${serverAddressHint(stage)} (and ${require_workspace.SERVER_IPV6_KEY} for an IPv6 address).`);
|
|
7306
|
+
this.stage = stage;
|
|
7307
|
+
this.domain = domain;
|
|
7308
|
+
this.name = "ServerAddressMissing";
|
|
7937
7309
|
}
|
|
7938
7310
|
};
|
|
7939
7311
|
/**
|
|
7940
|
-
*
|
|
7941
|
-
*
|
|
7942
|
-
*
|
|
7943
|
-
*
|
|
7312
|
+
* The server's address from the stage's secrets, checked — `undefined` when
|
|
7313
|
+
* `GKM_SERVER_IPV4` is not set.
|
|
7314
|
+
*
|
|
7315
|
+
* @throws {ServerAddressInvalid} for a value that is not an address
|
|
7944
7316
|
*/
|
|
7945
|
-
|
|
7946
|
-
const
|
|
7947
|
-
const
|
|
7948
|
-
|
|
7949
|
-
|
|
7950
|
-
if (
|
|
7951
|
-
|
|
7952
|
-
|
|
7317
|
+
function serverAddress(stage, custom) {
|
|
7318
|
+
const ipv4 = custom?.[require_workspace.SERVER_IPV4_KEY]?.trim();
|
|
7319
|
+
const ipv6 = custom?.[require_workspace.SERVER_IPV6_KEY]?.trim();
|
|
7320
|
+
if (ipv6 && !(0, node_net.isIPv6)(ipv6)) throw new ServerAddressInvalid(stage, require_workspace.SERVER_IPV6_KEY, ipv6);
|
|
7321
|
+
if (!ipv4) return void 0;
|
|
7322
|
+
if (!(0, node_net.isIPv4)(ipv4)) throw new ServerAddressInvalid(stage, require_workspace.SERVER_IPV4_KEY, ipv4);
|
|
7323
|
+
return {
|
|
7324
|
+
ipv4,
|
|
7325
|
+
...ipv6 ? { ipv6 } : {}
|
|
7326
|
+
};
|
|
7953
7327
|
}
|
|
7954
7328
|
/**
|
|
7955
|
-
*
|
|
7956
|
-
*
|
|
7329
|
+
* A name that resolves to this machine by definition — `localhost` and every
|
|
7330
|
+
* `*.localhost` — and is in nobody's DNS.
|
|
7957
7331
|
*/
|
|
7958
|
-
|
|
7959
|
-
const
|
|
7960
|
-
|
|
7961
|
-
const fullImageName = validateImageRef(registry ? `${registry}/${app.imageName}:${tag}` : `${app.imageName}:${tag}`);
|
|
7962
|
-
logger.log(`\n🐳 Building Docker image: ${fullImageName}`);
|
|
7963
|
-
try {
|
|
7964
|
-
await require_credentials.run("docker", [
|
|
7965
|
-
"build",
|
|
7966
|
-
`--file=${(0, node_path.relative)(buildRoot, app.dockerfile)}`,
|
|
7967
|
-
`--tag=${fullImageName}`,
|
|
7968
|
-
"."
|
|
7969
|
-
], {
|
|
7970
|
-
cwd: buildRoot,
|
|
7971
|
-
env: {
|
|
7972
|
-
...process.env,
|
|
7973
|
-
DOCKER_BUILDKIT: "1"
|
|
7974
|
-
}
|
|
7975
|
-
});
|
|
7976
|
-
logger.log(`✅ Docker image built: ${fullImageName}`);
|
|
7977
|
-
} catch (error) {
|
|
7978
|
-
throw new DockerBuildFailed(fullImageName, error);
|
|
7979
|
-
}
|
|
7332
|
+
function isLocalHost(host) {
|
|
7333
|
+
const name = host.toLowerCase().replace(/\.$/, "");
|
|
7334
|
+
return name === "localhost" || name.endsWith(".localhost");
|
|
7980
7335
|
}
|
|
7981
7336
|
/**
|
|
7982
|
-
*
|
|
7337
|
+
* The server's address, required where the stage serves real domains — not
|
|
7338
|
+
* `*.localhost` ones, which point at this machine already.
|
|
7339
|
+
*
|
|
7340
|
+
* @throws {ServerAddressMissing} when the stage has a domain and no address
|
|
7341
|
+
* @throws {ServerAddressInvalid} for a value that is not an address
|
|
7983
7342
|
*/
|
|
7984
|
-
|
|
7985
|
-
const
|
|
7986
|
-
|
|
7987
|
-
|
|
7988
|
-
|
|
7989
|
-
|
|
7990
|
-
|
|
7991
|
-
|
|
7992
|
-
|
|
7993
|
-
|
|
7994
|
-
|
|
7995
|
-
|
|
7343
|
+
function requiredServerAddress(stage, domain, custom) {
|
|
7344
|
+
const server = serverAddress(stage, custom);
|
|
7345
|
+
if (!server && domain && !isLocalHost(domain)) throw new ServerAddressMissing(stage, domain);
|
|
7346
|
+
return server;
|
|
7347
|
+
}
|
|
7348
|
+
const MANAGED = [
|
|
7349
|
+
"A",
|
|
7350
|
+
"AAAA",
|
|
7351
|
+
"CNAME"
|
|
7352
|
+
];
|
|
7353
|
+
/** Every public host the stack serves, once each, sorted. */
|
|
7354
|
+
function stackHosts(stack) {
|
|
7355
|
+
return [...new Set(stack.routes.map((route) => route.host))].sort();
|
|
7356
|
+
}
|
|
7357
|
+
/** The TTL a domain's records are written with. */
|
|
7358
|
+
function recordTtl(config) {
|
|
7359
|
+
const ttl = "ttl" in config ? config.ttl : void 0;
|
|
7360
|
+
if (ttl) return ttl;
|
|
7361
|
+
return config.provider === "godaddy" ? 600 : 300;
|
|
7362
|
+
}
|
|
7363
|
+
function providerName(config) {
|
|
7364
|
+
return typeof config.provider === "string" ? config.provider : "custom";
|
|
7996
7365
|
}
|
|
7997
7366
|
/**
|
|
7998
|
-
*
|
|
7999
|
-
*
|
|
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.
|
|
8000
7370
|
*/
|
|
8001
|
-
|
|
8002
|
-
|
|
8003
|
-
|
|
8004
|
-
|
|
8005
|
-
|
|
8006
|
-
|
|
8007
|
-
|
|
8008
|
-
|
|
8009
|
-
|
|
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
|
+
};
|
|
7381
|
+
/** The records a stage's hosts need — pure. */
|
|
7382
|
+
function planStackDns(input) {
|
|
7383
|
+
const { stage, server } = input;
|
|
7384
|
+
const dns = input.dns ? normalizeDnsConfig(input.dns) : {};
|
|
7385
|
+
const records = /* @__PURE__ */ new Map();
|
|
7386
|
+
const uncovered = [];
|
|
7387
|
+
const add = (host, domain, type, value) => {
|
|
7388
|
+
const name = extractSubdomain(host, domain);
|
|
7389
|
+
records.set(`${domain} ${name} ${type}`, {
|
|
7390
|
+
host,
|
|
7391
|
+
domain,
|
|
7392
|
+
name,
|
|
7393
|
+
type,
|
|
7394
|
+
value,
|
|
7395
|
+
ttl: recordTtl(dns[domain])
|
|
7396
|
+
});
|
|
7397
|
+
};
|
|
7398
|
+
const pointAt = (host, domain) => {
|
|
7399
|
+
add(host, domain, "A", server.ipv4);
|
|
7400
|
+
if (server.ipv6) add(host, domain, "AAAA", server.ipv6);
|
|
7401
|
+
};
|
|
7402
|
+
for (const raw of input.hosts) {
|
|
7403
|
+
const host = raw.toLowerCase();
|
|
7404
|
+
if (host.includes("*")) throw new DnsWildcardRefused(stage, host);
|
|
7405
|
+
const domain = findRootDomain(host, dns);
|
|
7406
|
+
if (!domain) {
|
|
7407
|
+
uncovered.push(host);
|
|
8010
7408
|
continue;
|
|
8011
7409
|
}
|
|
8012
|
-
const
|
|
8013
|
-
|
|
8014
|
-
|
|
8015
|
-
|
|
8016
|
-
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
|
|
8021
|
-
|
|
8022
|
-
|
|
8023
|
-
|
|
8024
|
-
|
|
8025
|
-
|
|
8026
|
-
|
|
8027
|
-
|
|
8028
|
-
appName,
|
|
8029
|
-
type: app.type,
|
|
8030
|
-
dockerfile: dockerfilePath,
|
|
8031
|
-
imageName: imageNames[appName] ?? appName
|
|
8032
|
-
});
|
|
7410
|
+
const target = require_workspace.cnameTarget(dns[domain].records, stage);
|
|
7411
|
+
if (target) pointAt(target, domain);
|
|
7412
|
+
if (target && host !== domain && host !== target) add(host, domain, "CNAME", target);
|
|
7413
|
+
else pointAt(host, domain);
|
|
7414
|
+
}
|
|
7415
|
+
const domains = /* @__PURE__ */ new Map();
|
|
7416
|
+
for (const record of [...records.values()].sort((a, b) => a.domain.localeCompare(b.domain) || a.name.localeCompare(b.name) || a.type.localeCompare(b.type))) {
|
|
7417
|
+
const config = dns[record.domain];
|
|
7418
|
+
const plan = domains.get(record.domain) ?? {
|
|
7419
|
+
domain: record.domain,
|
|
7420
|
+
provider: providerName(config),
|
|
7421
|
+
config,
|
|
7422
|
+
records: []
|
|
7423
|
+
};
|
|
7424
|
+
plan.records.push(record);
|
|
7425
|
+
domains.set(record.domain, plan);
|
|
8033
7426
|
}
|
|
8034
|
-
|
|
8035
|
-
|
|
8036
|
-
|
|
8037
|
-
|
|
8038
|
-
|
|
7427
|
+
return {
|
|
7428
|
+
stage,
|
|
7429
|
+
server,
|
|
7430
|
+
domains: [...domains.values()],
|
|
7431
|
+
uncovered: uncovered.sort()
|
|
7432
|
+
};
|
|
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
|
+
};
|
|
7450
|
+
/** `api.example.com A 203.0.113.10 (TTL 600)` */
|
|
7451
|
+
function describe(record) {
|
|
7452
|
+
return `${record.host.padEnd(32)} ${record.type.padEnd(5)} ${record.value} (TTL ${record.ttl})`;
|
|
7453
|
+
}
|
|
7454
|
+
function printManual(plan, log, why) {
|
|
7455
|
+
log(` ${plan.domain}: ${why} — create these records at its DNS host:`);
|
|
7456
|
+
for (const record of plan.records) log(` ${describe(record)}`);
|
|
7457
|
+
return plan.records.map((r) => ({
|
|
7458
|
+
domain: r.domain,
|
|
7459
|
+
name: r.name,
|
|
7460
|
+
type: r.type,
|
|
7461
|
+
action: "manual",
|
|
7462
|
+
value: r.value,
|
|
7463
|
+
ttl: r.ttl
|
|
7464
|
+
}));
|
|
7465
|
+
}
|
|
7466
|
+
/** The existing records of every planned name, for the types gkm manages. */
|
|
7467
|
+
async function existingRecords(provider, plan) {
|
|
7468
|
+
const names = [...new Set(plan.records.map((r) => r.name))];
|
|
7469
|
+
const wanted = names.flatMap((name) => MANAGED.map((type) => ({
|
|
7470
|
+
name,
|
|
7471
|
+
type
|
|
7472
|
+
})));
|
|
7473
|
+
if (provider.readRecords) return provider.readRecords(plan.domain, wanted);
|
|
7474
|
+
return (await provider.getRecords(plan.domain)).filter((r) => names.includes(r.name) && MANAGED.includes(r.type));
|
|
7475
|
+
}
|
|
7476
|
+
const same = (a, b) => a.trim().replace(/\.$/, "").toLowerCase() === b.trim().replace(/\.$/, "").toLowerCase();
|
|
7477
|
+
/**
|
|
7478
|
+
* What each domain's records need, compared with what is there: creates,
|
|
7479
|
+
* updates, and the deletes a change of mode needs — a name moving from an A
|
|
7480
|
+
* record to a CNAME, or back, cannot hold both. Nothing else is deleted.
|
|
7481
|
+
*/
|
|
7482
|
+
function diffDomainDns(plan, existing) {
|
|
7483
|
+
const changes = [];
|
|
7484
|
+
const find = (name, type) => existing.find((r) => r.name === name && r.type === type);
|
|
7485
|
+
const planned = /* @__PURE__ */ new Map();
|
|
7486
|
+
for (const record of plan.records) {
|
|
7487
|
+
const types = planned.get(record.name) ?? /* @__PURE__ */ new Set();
|
|
7488
|
+
types.add(record.type);
|
|
7489
|
+
planned.set(record.name, types);
|
|
7490
|
+
}
|
|
7491
|
+
for (const [name, types] of planned) {
|
|
7492
|
+
const conflicting = types.has("CNAME") ? ["A", "AAAA"] : ["CNAME"];
|
|
7493
|
+
for (const type of conflicting) {
|
|
7494
|
+
const found = find(name, type);
|
|
7495
|
+
if (!found || found.values.length === 0) continue;
|
|
7496
|
+
changes.push({
|
|
7497
|
+
domain: plan.domain,
|
|
7498
|
+
name,
|
|
7499
|
+
type,
|
|
7500
|
+
action: "delete",
|
|
7501
|
+
previous: found.values.join(", ")
|
|
7502
|
+
});
|
|
7503
|
+
}
|
|
7504
|
+
}
|
|
7505
|
+
for (const record of plan.records) {
|
|
7506
|
+
const found = find(record.name, record.type);
|
|
7507
|
+
const base = {
|
|
7508
|
+
domain: plan.domain,
|
|
7509
|
+
name: record.name,
|
|
7510
|
+
type: record.type,
|
|
7511
|
+
value: record.value,
|
|
7512
|
+
ttl: record.ttl
|
|
7513
|
+
};
|
|
7514
|
+
if (!found || found.values.length === 0) changes.push({
|
|
7515
|
+
...base,
|
|
7516
|
+
action: "create"
|
|
8039
7517
|
});
|
|
8040
|
-
|
|
8041
|
-
|
|
8042
|
-
|
|
8043
|
-
|
|
8044
|
-
|
|
8045
|
-
|
|
8046
|
-
|
|
8047
|
-
|
|
8048
|
-
dockerfile: dockerfilePath,
|
|
8049
|
-
imageName: imageNames[worker.name] ?? worker.name
|
|
7518
|
+
else if (found.values.length === 1 && same(found.values[0], record.value)) changes.push({
|
|
7519
|
+
...base,
|
|
7520
|
+
action: "unchanged"
|
|
7521
|
+
});
|
|
7522
|
+
else changes.push({
|
|
7523
|
+
...base,
|
|
7524
|
+
action: "update",
|
|
7525
|
+
previous: found.values.join(", ")
|
|
8050
7526
|
});
|
|
8051
7527
|
}
|
|
8052
|
-
|
|
8053
|
-
|
|
8054
|
-
|
|
8055
|
-
|
|
8056
|
-
|
|
8057
|
-
|
|
8058
|
-
|
|
8059
|
-
|
|
7528
|
+
return changes;
|
|
7529
|
+
}
|
|
7530
|
+
/** A host's full name from a domain and a relative name. */
|
|
7531
|
+
function fullName(domain, name) {
|
|
7532
|
+
return name === "@" ? domain : `${name}.${domain}`;
|
|
7533
|
+
}
|
|
7534
|
+
function printChange(change, dryRun, log) {
|
|
7535
|
+
const host = fullName(change.domain, change.name).padEnd(32);
|
|
7536
|
+
const type = change.type.padEnd(5);
|
|
7537
|
+
const would = dryRun ? "would " : "";
|
|
7538
|
+
switch (change.action) {
|
|
7539
|
+
case "unchanged":
|
|
7540
|
+
log(` ✓ ${host} ${type} ${change.value} (TTL ${change.ttl}) — up to date`);
|
|
7541
|
+
break;
|
|
7542
|
+
case "create":
|
|
7543
|
+
log(` + ${host} ${type} ${change.value} (TTL ${change.ttl}) — ${would}create`);
|
|
7544
|
+
break;
|
|
7545
|
+
case "update":
|
|
7546
|
+
log(` ~ ${host} ${type} ${change.previous} → ${change.value} (TTL ${change.ttl}) — ${would}update`);
|
|
7547
|
+
break;
|
|
7548
|
+
case "write":
|
|
7549
|
+
log(dryRun ? ` ? ${host} ${type} ${change.value} (TTL ${change.ttl}) — would write; current value unknown (key cannot read DNS)` : ` ! ${host} ${type} ${change.value} (TTL ${change.ttl}) — written (not diffed)`);
|
|
7550
|
+
break;
|
|
7551
|
+
case "delete": log(` - ${host} ${type} ${change.previous} — ${would}delete (a name cannot hold a CNAME and an A record)`);
|
|
8060
7552
|
}
|
|
8061
|
-
|
|
7553
|
+
}
|
|
7554
|
+
/**
|
|
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
|
|
7560
|
+
*/
|
|
7561
|
+
async function settleStackDns(plan, options = {}) {
|
|
7562
|
+
const dryRun = options.dryRun === true;
|
|
7563
|
+
const log = options.log ?? ((line) => console.log(line));
|
|
7564
|
+
const providerFor = options.providerFor ?? ((config) => createDnsProvider({
|
|
7565
|
+
config,
|
|
7566
|
+
...options.credentials ? { godaddy: options.credentials } : {},
|
|
7567
|
+
...options.hostinger ? { hostinger: options.hostinger } : {}
|
|
7568
|
+
}));
|
|
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));
|
|
7573
|
+
for (const host of plan.uncovered) log(` ⚠ ${host} is under no domain in dns — point it at ${plan.server.ipv4} yourself`);
|
|
7574
|
+
for (const domain of plan.domains) {
|
|
7575
|
+
if (domain.provider === "manual") {
|
|
7576
|
+
changes.push(...printManual(domain, log, "manual"));
|
|
7577
|
+
for (const host of hostsOf(domain)) unconfirmed.add(host);
|
|
7578
|
+
continue;
|
|
7579
|
+
}
|
|
7580
|
+
const provider = await providerFor(domain.config);
|
|
7581
|
+
if (!provider) {
|
|
7582
|
+
changes.push(...printManual(domain, log, "manual"));
|
|
7583
|
+
for (const host of hostsOf(domain)) unconfirmed.add(host);
|
|
7584
|
+
continue;
|
|
7585
|
+
}
|
|
7586
|
+
log(` ${domain.domain} (${provider.name})${dryRun ? " — dry run" : ""}`);
|
|
7587
|
+
let existing;
|
|
7588
|
+
let diff;
|
|
7589
|
+
let unreadable = false;
|
|
7590
|
+
try {
|
|
7591
|
+
existing = await existingRecords(provider, domain);
|
|
7592
|
+
diff = diffDomainDns(domain, existing);
|
|
7593
|
+
} catch (error) {
|
|
7594
|
+
if (!(error instanceof DnsRecordsUnreadable)) throw error;
|
|
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;
|
|
7597
|
+
existing = [];
|
|
7598
|
+
diff = domain.records.map((r) => ({
|
|
7599
|
+
domain: r.domain,
|
|
7600
|
+
name: r.name,
|
|
7601
|
+
type: r.type,
|
|
7602
|
+
action: "write",
|
|
7603
|
+
value: r.value,
|
|
7604
|
+
ttl: r.ttl
|
|
7605
|
+
}));
|
|
7606
|
+
}
|
|
7607
|
+
for (const change of diff) printChange(change, dryRun, log);
|
|
7608
|
+
if (!plan.server.ipv6) for (const name of new Set(domain.records.map((r) => r.name))) {
|
|
7609
|
+
const aaaa = existing.find((r) => r.name === name && r.type === "AAAA");
|
|
7610
|
+
const planned = domain.records.find((r) => r.name === name && r.type === "A");
|
|
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`);
|
|
7612
|
+
}
|
|
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);
|
|
7618
|
+
if (dryRun) continue;
|
|
7619
|
+
const deletes = diff.filter((c) => c.action === "delete");
|
|
7620
|
+
if (deletes.length > 0) await provider.deleteRecords(domain.domain, deletes.map((c) => ({
|
|
7621
|
+
name: c.name,
|
|
7622
|
+
type: c.type
|
|
7623
|
+
})));
|
|
7624
|
+
const writes = diff.filter((c) => c.action === "create" || c.action === "update" || c.action === "write");
|
|
7625
|
+
if (writes.length > 0) await provider.upsertRecords(domain.domain, writes.map((c) => ({
|
|
7626
|
+
name: c.name,
|
|
7627
|
+
type: c.type,
|
|
7628
|
+
value: c.value,
|
|
7629
|
+
ttl: c.ttl
|
|
7630
|
+
})));
|
|
7631
|
+
if (!options.confirm || unreadable || written.size === 0) continue;
|
|
7632
|
+
const rewritten = {
|
|
7633
|
+
...domain,
|
|
7634
|
+
records: domain.records.filter((r) => written.has(r.host))
|
|
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}`);
|
|
7650
|
+
}
|
|
7651
|
+
for (const host of plan.uncovered) unconfirmed.add(host);
|
|
8062
7652
|
return {
|
|
8063
|
-
|
|
8064
|
-
|
|
8065
|
-
|
|
8066
|
-
buildRoot: layout.buildRoot
|
|
7653
|
+
changes,
|
|
7654
|
+
confirmed: [...confirmed].sort(),
|
|
7655
|
+
unconfirmed: [...unconfirmed].filter((h) => !confirmed.has(h)).sort()
|
|
8067
7656
|
};
|
|
8068
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
|
+
};
|
|
8069
7679
|
/**
|
|
8070
|
-
*
|
|
8071
|
-
*
|
|
8072
|
-
*
|
|
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
|
|
8073
7684
|
*/
|
|
8074
|
-
function
|
|
8075
|
-
const
|
|
8076
|
-
|
|
8077
|
-
|
|
8078
|
-
|
|
8079
|
-
|
|
8080
|
-
|
|
8081
|
-
|
|
8082
|
-
|
|
8083
|
-
|
|
8084
|
-
|
|
8085
|
-
|
|
8086
|
-
|
|
8087
|
-
|
|
8088
|
-
|
|
8089
|
-
|
|
8090
|
-
|
|
8091
|
-
|
|
8092
|
-
|
|
8093
|
-
|
|
8094
|
-
|
|
8095
|
-
|
|
8096
|
-
|
|
8097
|
-
|
|
8098
|
-
* runner — see `generateWorkerDockerfile`.
|
|
8099
|
-
*/
|
|
8100
|
-
function workerDockerfile(worker, host, options) {
|
|
8101
|
-
return generateWorkerDockerfile({
|
|
8102
|
-
...appImageOptions(options.layout, worker.app, host, options.workspaceRoot),
|
|
8103
|
-
imageName: worker.name,
|
|
8104
|
-
port: WORKER_PORT,
|
|
8105
|
-
worker: worker.id,
|
|
8106
|
-
bundle: workerBundleName(worker.id),
|
|
8107
|
-
healthCheckPath: "/health",
|
|
8108
|
-
...options.cache ? { cache: options.cache } : {}
|
|
8109
|
-
});
|
|
8110
|
-
}
|
|
8111
|
-
/**
|
|
8112
|
-
* A site's Dockerfile, with an `ARG` for every key its bundler inlines.
|
|
8113
|
-
*
|
|
8114
|
-
* The keys come from the site's declaration where there is one — every
|
|
8115
|
-
* public key its edges give it, `VITE_UPLOADS_SERVER_URL` included — and an
|
|
8116
|
-
* `ARG` the Dockerfile does not declare is a build argument the build drops,
|
|
8117
|
-
* leaving the bundle with an empty URL.
|
|
8118
|
-
*/
|
|
8119
|
-
function siteDockerfile(appName, app, options) {
|
|
8120
|
-
const declared = options.manifest ? Object.entries(options.manifest).find(([id, d]) => d.kind === "site" && require_derive.appKey(id) === appName)?.[1] : void 0;
|
|
8121
|
-
const publicUrlArgs = options.publicUrlArgs ?? (declared?.kind === "site" && options.manifest ? Object.keys((0, _geekmidas_manifest.publicEnvFor)(declared, options.manifest)) : getPublicUrlArgNames(app));
|
|
8122
|
-
const webOpts = {
|
|
8123
|
-
...options.image,
|
|
8124
|
-
publicUrlArgs
|
|
8125
|
-
};
|
|
8126
|
-
switch (app.framework) {
|
|
8127
|
-
case "vite": return generateViteStaticDockerfile(webOpts);
|
|
8128
|
-
case "tanstack-start":
|
|
8129
|
-
case "remix": return generateNodeWebDockerfile(webOpts);
|
|
8130
|
-
default: return generateNextjsDockerfile(webOpts);
|
|
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
|
+
});
|
|
7702
|
+
continue;
|
|
7703
|
+
}
|
|
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
|
|
7708
|
+
});
|
|
8131
7709
|
}
|
|
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;
|
|
8132
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
|
|
7726
|
+
});
|
|
7727
|
+
} catch {
|
|
7728
|
+
plan = {
|
|
7729
|
+
stage: input.stage,
|
|
7730
|
+
server: input.server,
|
|
7731
|
+
domains: [],
|
|
7732
|
+
uncovered: hosts
|
|
7733
|
+
};
|
|
7734
|
+
}
|
|
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");
|
|
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
|
+
};
|
|
8133
7747
|
/**
|
|
8134
|
-
*
|
|
8135
|
-
*
|
|
8136
|
-
* host (every image builds from source), git, and gkm's own output —
|
|
8137
|
-
* `.gkm/compose`'s env files hold a stage's secrets.
|
|
8138
|
-
*/
|
|
8139
|
-
const REQUIRED_IGNORES = [
|
|
8140
|
-
"**/node_modules",
|
|
8141
|
-
".git",
|
|
8142
|
-
"**/dist",
|
|
8143
|
-
"**/.next",
|
|
8144
|
-
"**/.turbo",
|
|
8145
|
-
"**/.gkm",
|
|
8146
|
-
"**/.gkm/compose"
|
|
8147
|
-
];
|
|
8148
|
-
/**
|
|
8149
|
-
* Make sure the build root's `.dockerignore` leaves out what no image may
|
|
8150
|
-
* hold: written whole where there is none, and each missing line appended to
|
|
8151
|
-
* one the project keeps.
|
|
7748
|
+
* A domain whose records the deploy writes, and no token to write them with.
|
|
7749
|
+
* Raised before anything is built, started or written.
|
|
8152
7750
|
*/
|
|
8153
|
-
|
|
8154
|
-
|
|
8155
|
-
|
|
8156
|
-
|
|
8157
|
-
|
|
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";
|
|
8158
7763
|
}
|
|
8159
|
-
|
|
8160
|
-
const lines = new Set(current.split(/\r?\n/).map((line) => line.trim()));
|
|
8161
|
-
const missing = REQUIRED_IGNORES.filter((line) => !lines.has(line));
|
|
8162
|
-
if (missing.length === 0) return path;
|
|
8163
|
-
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`);
|
|
8164
|
-
return path;
|
|
8165
|
-
}
|
|
7764
|
+
};
|
|
8166
7765
|
/**
|
|
8167
|
-
*
|
|
8168
|
-
*
|
|
8169
|
-
|
|
8170
|
-
|
|
8171
|
-
|
|
8172
|
-
|
|
8173
|
-
|
|
8174
|
-
|
|
8175
|
-
|
|
8176
|
-
|
|
7766
|
+
* The `dns` domain a stage's own domain (`domains.<stage>`) is under — where
|
|
7767
|
+
* its hosts' records go — or none.
|
|
7768
|
+
*/
|
|
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
|
+
}] : [];
|
|
7778
|
+
}
|
|
7779
|
+
/**
|
|
7780
|
+
* Each token the domains' providers need, from `credentials` — `manual`,
|
|
7781
|
+
* Route53 (the AWS SDK's own chain) and a provider object need none.
|
|
7782
|
+
*
|
|
7783
|
+
* @throws {DnsCredentialMissing} naming the first domain with no token
|
|
7784
|
+
*/
|
|
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;
|
|
7794
|
+
}
|
|
7795
|
+
return found;
|
|
7796
|
+
}
|
|
7797
|
+
/**
|
|
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
|
|
7809
|
+
*/
|
|
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
|
|
8177
7818
|
});
|
|
8178
|
-
|
|
8179
|
-
|
|
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
|
+
};
|
|
8180
7852
|
}
|
|
8181
|
-
|
|
8182
|
-
|
|
8183
|
-
|
|
8184
|
-
|
|
8185
|
-
|
|
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");
|
|
7862
|
+
}
|
|
7863
|
+
/**
|
|
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.
|
|
7867
|
+
*/
|
|
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
|
+
}));
|
|
8186
7889
|
}
|
|
8187
7890
|
//#endregion
|
|
8188
7891
|
//#region src/providers/types.ts
|
|
@@ -8191,7 +7894,7 @@ const PROVIDER_KINDS = ["objects"];
|
|
|
8191
7894
|
//#endregion
|
|
8192
7895
|
//#region src/providers/notes.ts
|
|
8193
7896
|
/** The constructs of a kind: `objects` is every bucket. */
|
|
8194
|
-
function constructsOf
|
|
7897
|
+
function constructsOf(manifest, kind) {
|
|
8195
7898
|
return Object.entries(manifest).filter(([, d]) => d.kind === kind).map(([id]) => id).sort();
|
|
8196
7899
|
}
|
|
8197
7900
|
/** A stage that names `false` for a kind, and declares constructs of it. */
|
|
@@ -8207,9 +7910,9 @@ var StageProviderDisabled = class extends require_errors.GkmError {
|
|
|
8207
7910
|
this.name = "StageProviderDisabled";
|
|
8208
7911
|
}
|
|
8209
7912
|
};
|
|
8210
|
-
/** The command a stage's providers are run with. */
|
|
7913
|
+
/** The command a stage's providers are run with: every deploy runs them. */
|
|
8211
7914
|
function provisionCommand(stage) {
|
|
8212
|
-
return `gkm
|
|
7915
|
+
return `gkm deploy --stage ${stage}`;
|
|
8213
7916
|
}
|
|
8214
7917
|
/**
|
|
8215
7918
|
* The line a stage missing a key a provider would write is given: what
|
|
@@ -8221,1178 +7924,847 @@ function provisionHint(workspace, kind, stage) {
|
|
|
8221
7924
|
if (choice.mode !== "provider") return void 0;
|
|
8222
7925
|
const provisioning = require_workspace.provisioningOf(kind, choice.name);
|
|
8223
7926
|
if (!provisioning) return void 0;
|
|
8224
|
-
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}` : "");
|
|
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
|
+
};
|
|
8137
|
+
/**
|
|
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.
|
|
8140
|
+
*/
|
|
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
|
+
});
|
|
8156
|
+
}
|
|
8157
|
+
/**
|
|
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.
|
|
8161
|
+
*
|
|
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.
|
|
8168
|
+
*/
|
|
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)
|
|
8210
|
+
});
|
|
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)
|
|
8220
|
+
});
|
|
8221
|
+
}
|
|
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;
|
|
8239
|
+
}
|
|
8240
|
+
/**
|
|
8241
|
+
* Whether only the stage's secrets can supply `key` to a construct of `kind`
|
|
8242
|
+
* — what nothing on the stage derives.
|
|
8243
|
+
*
|
|
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.
|
|
8247
|
+
*/
|
|
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";
|
|
8253
|
+
}
|
|
8254
|
+
/**
|
|
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.
|
|
8267
|
+
*/
|
|
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 } : {}
|
|
8276
|
+
});
|
|
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
|
+
};
|
|
8225
8295
|
}
|
|
8226
8296
|
/**
|
|
8227
|
-
*
|
|
8228
|
-
*
|
|
8229
|
-
*
|
|
8297
|
+
* `externalServices`, refusing a stage that lacks anything.
|
|
8298
|
+
*
|
|
8299
|
+
* @throws {ExternalServicesNotConfigured} naming every missing key
|
|
8230
8300
|
*/
|
|
8231
|
-
function
|
|
8232
|
-
|
|
8233
|
-
|
|
8234
|
-
return
|
|
8235
|
-
|
|
8236
|
-
|
|
8237
|
-
|
|
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
|
|
8312
|
+
});
|
|
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.`;
|
|
8238
8322
|
}
|
|
8239
8323
|
/**
|
|
8240
|
-
*
|
|
8241
|
-
*
|
|
8242
|
-
*
|
|
8243
|
-
* @throws {StageProviderDisabled}
|
|
8324
|
+
* Report each dev service a run uses: a loud line, and a `dev-service.used`
|
|
8325
|
+
* event a host can act on.
|
|
8244
8326
|
*/
|
|
8245
|
-
function
|
|
8246
|
-
for (const
|
|
8247
|
-
|
|
8248
|
-
|
|
8249
|
-
|
|
8250
|
-
|
|
8251
|
-
|
|
8252
|
-
|
|
8253
|
-
//#region src/deploy/journal.ts
|
|
8254
|
-
var DeployJournal = class DeployJournal {
|
|
8255
|
-
store;
|
|
8256
|
-
stage;
|
|
8257
|
-
state;
|
|
8258
|
-
resources;
|
|
8259
|
-
version;
|
|
8260
|
-
existed;
|
|
8261
|
-
constructor(store, stage, state, resources, version, existed) {
|
|
8262
|
-
this.store = store;
|
|
8263
|
-
this.stage = stage;
|
|
8264
|
-
this.state = state;
|
|
8265
|
-
this.resources = resources;
|
|
8266
|
-
this.version = version;
|
|
8267
|
-
this.existed = existed;
|
|
8268
|
-
}
|
|
8269
|
-
/**
|
|
8270
|
-
* The stage's journal — its stored state, or `initial` written as the
|
|
8271
|
-
* stage's first state, so there is somewhere to record the very first
|
|
8272
|
-
* resource before it is created.
|
|
8273
|
-
*/
|
|
8274
|
-
static async open(store, stage, initial) {
|
|
8275
|
-
const stored = await store.read(stage);
|
|
8276
|
-
if (stored) return new DeployJournal(store, stage, stored.state, stored.resources, stored.version, true);
|
|
8277
|
-
const state = initial();
|
|
8278
|
-
const version = await store.write(stage, state, { expectedVersion: null });
|
|
8279
|
-
return new DeployJournal(store, stage, state, {}, version, false);
|
|
8280
|
-
}
|
|
8281
|
-
record(key) {
|
|
8282
|
-
return this.resources[key];
|
|
8283
|
-
}
|
|
8284
|
-
/** The keys a previous run marked `pending` and never saw created. */
|
|
8285
|
-
unfinished() {
|
|
8286
|
-
return Object.values(this.resources).filter((record) => record.status === "pending").map((record) => record.key);
|
|
8287
|
-
}
|
|
8288
|
-
/** Records that `entry` is about to be created. */
|
|
8289
|
-
async pending(entry) {
|
|
8290
|
-
await this.put({
|
|
8291
|
-
...entry,
|
|
8292
|
-
status: "pending"
|
|
8293
|
-
});
|
|
8294
|
-
}
|
|
8295
|
-
/** Records `entry` as existing with `id`; a no-op if it already is. */
|
|
8296
|
-
async ready(entry, id) {
|
|
8297
|
-
const current = this.resources[entry.key];
|
|
8298
|
-
if (current?.status === "ready" && current.id === id) return;
|
|
8299
|
-
await this.put({
|
|
8300
|
-
...entry,
|
|
8301
|
-
status: "ready",
|
|
8302
|
-
id
|
|
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
|
|
8303
8335
|
});
|
|
8304
8336
|
}
|
|
8305
|
-
/**
|
|
8306
|
-
* The resource `entry` names: by its recorded id, else by looking it up,
|
|
8307
|
-
* else created — with `pending` written before the create and `ready`
|
|
8308
|
-
* after, so a run that dies in between leaves a record the next run
|
|
8309
|
-
* resolves by looking the resource up rather than creating another.
|
|
8310
|
-
*/
|
|
8311
|
-
async ensure(entry, steps) {
|
|
8312
|
-
const record = this.resources[entry.key];
|
|
8313
|
-
let staleId;
|
|
8314
|
-
if (record?.status === "ready" && record.id && steps.get) {
|
|
8315
|
-
const known = await steps.get(record.id);
|
|
8316
|
-
if (known) {
|
|
8317
|
-
await this.ready(entry, steps.id(known));
|
|
8318
|
-
return {
|
|
8319
|
-
resource: known,
|
|
8320
|
-
via: "recorded"
|
|
8321
|
-
};
|
|
8322
|
-
}
|
|
8323
|
-
staleId = record.id;
|
|
8324
|
-
}
|
|
8325
|
-
const existing = await steps.find();
|
|
8326
|
-
if (existing) {
|
|
8327
|
-
await this.ready(entry, steps.id(existing));
|
|
8328
|
-
return {
|
|
8329
|
-
resource: existing,
|
|
8330
|
-
via: record?.status === "pending" ? "resumed" : "found",
|
|
8331
|
-
...staleId ? { staleId } : {}
|
|
8332
|
-
};
|
|
8333
|
-
}
|
|
8334
|
-
await this.pending(entry);
|
|
8335
|
-
const created = await steps.create();
|
|
8336
|
-
await this.ready(entry, steps.id(created));
|
|
8337
|
-
return {
|
|
8338
|
-
resource: created,
|
|
8339
|
-
via: "created",
|
|
8340
|
-
...staleId ? { staleId } : {}
|
|
8341
|
-
};
|
|
8342
|
-
}
|
|
8343
|
-
/** Writes the stage's state, keeping its resource records. */
|
|
8344
|
-
async save() {
|
|
8345
|
-
this.state.lastDeployedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
8346
|
-
this.version = await this.store.write(this.stage, this.state, { expectedVersion: this.version });
|
|
8347
|
-
}
|
|
8348
|
-
async put(record) {
|
|
8349
|
-
this.version = await this.store.putResource(this.stage, record, { expectedVersion: this.version });
|
|
8350
|
-
this.resources = {
|
|
8351
|
-
...this.resources,
|
|
8352
|
-
[record.key]: {
|
|
8353
|
-
...record,
|
|
8354
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8355
|
-
}
|
|
8356
|
-
};
|
|
8357
|
-
}
|
|
8358
|
-
};
|
|
8359
|
-
//#endregion
|
|
8360
|
-
//#region src/providers/aws.ts
|
|
8361
|
-
/**
|
|
8362
|
-
* The AWS account a provider provisions in — the stage's, found the way the
|
|
8363
|
-
* SSM and Secrets Manager stores find it.
|
|
8364
|
-
*
|
|
8365
|
-
* 1. `--profile`, resolved on its own: exported keys left over from another
|
|
8366
|
-
* account never win over a profile someone named.
|
|
8367
|
-
* 2. `AWS_PROFILE`, else `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY` — the
|
|
8368
|
-
* deploy's own `aws` credential kind (`storedCredentials`).
|
|
8369
|
-
* 3. The SDK's default chain: `~/.aws` default profile, SSO, a role. In CI,
|
|
8370
|
-
* whatever `aws-actions/configure-aws-credentials` exported.
|
|
8371
|
-
*
|
|
8372
|
-
* None of them: the provider has no provisioning credentials, and the stage
|
|
8373
|
-
* is treated as `external`.
|
|
8374
|
-
*/
|
|
8375
|
-
async function awsProvisioningCredentials(lookup) {
|
|
8376
|
-
const region = lookup.env.AWS_REGION ?? lookup.env.AWS_DEFAULT_REGION;
|
|
8377
|
-
const where = region ? { region } : {};
|
|
8378
|
-
if (lookup.profile) return {
|
|
8379
|
-
profile: lookup.profile,
|
|
8380
|
-
...where
|
|
8381
|
-
};
|
|
8382
|
-
const stored = await require_credentials.storedCredentials({
|
|
8383
|
-
env: lookup.env,
|
|
8384
|
-
...lookup.home ? { home: lookup.home } : {}
|
|
8385
|
-
}).get({
|
|
8386
|
-
kind: "aws",
|
|
8387
|
-
stage: lookup.stage
|
|
8388
|
-
});
|
|
8389
|
-
if (stored) return stored;
|
|
8390
|
-
try {
|
|
8391
|
-
const { fromNodeProviderChain } = await import("@aws-sdk/credential-providers");
|
|
8392
|
-
await fromNodeProviderChain({
|
|
8393
|
-
timeout: 1e3,
|
|
8394
|
-
maxRetries: 0
|
|
8395
|
-
})();
|
|
8396
|
-
return {
|
|
8397
|
-
chain: true,
|
|
8398
|
-
...where
|
|
8399
|
-
};
|
|
8400
|
-
} catch {
|
|
8401
|
-
return;
|
|
8402
|
-
}
|
|
8403
8337
|
}
|
|
8338
|
+
//#endregion
|
|
8339
|
+
//#region src/deploy/actor.ts
|
|
8404
8340
|
/**
|
|
8405
|
-
*
|
|
8406
|
-
*
|
|
8407
|
-
*
|
|
8408
|
-
*
|
|
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.
|
|
8409
8345
|
*/
|
|
8410
|
-
|
|
8411
|
-
|
|
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";
|
|
8412
8349
|
}
|
|
8413
|
-
/**
|
|
8414
|
-
|
|
8415
|
-
if ("
|
|
8416
|
-
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";
|
|
8417
8354
|
return {
|
|
8418
|
-
|
|
8419
|
-
|
|
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 } : {}
|
|
8420
8359
|
};
|
|
8421
8360
|
}
|
|
8422
|
-
|
|
8423
|
-
|
|
8424
|
-
|
|
8425
|
-
|
|
8426
|
-
|
|
8427
|
-
|
|
8428
|
-
|
|
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)()
|
|
8429
8369
|
};
|
|
8430
|
-
return { region };
|
|
8431
|
-
}
|
|
8432
|
-
const SUFFIX_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
8433
|
-
/** A name that S3 would refuse — what the name builder never returns. */
|
|
8434
|
-
var InvalidBucketName = class extends require_errors.GkmError {
|
|
8435
|
-
bucket;
|
|
8436
|
-
reason;
|
|
8437
|
-
constructor(bucket, reason) {
|
|
8438
|
-
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.`);
|
|
8439
|
-
this.bucket = bucket;
|
|
8440
|
-
this.reason = reason;
|
|
8441
|
-
this.name = "InvalidBucketName";
|
|
8442
|
-
}
|
|
8443
|
-
};
|
|
8444
|
-
/** Lowercase `[a-z0-9-]`, runs of anything else one `-`, no edge dashes. */
|
|
8445
|
-
function part(value) {
|
|
8446
|
-
return (0, _geekmidas_manifest.kebabCase)(value).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
8447
|
-
}
|
|
8448
|
-
/**
|
|
8449
|
-
* `parts` joined by `-`, shortened to `max` characters by trimming the longest
|
|
8450
|
-
* part one character at a time — so a long construct id gives way before a
|
|
8451
|
-
* short stage name does, and every part keeps its start.
|
|
8452
|
-
*/
|
|
8453
|
-
function fitParts(parts, max) {
|
|
8454
|
-
const kept = parts.map(part).filter(Boolean);
|
|
8455
|
-
const length = () => kept.reduce((sum, p) => sum + p.length, 0) + kept.length - 1;
|
|
8456
|
-
while (length() > max) {
|
|
8457
|
-
let longest = 0;
|
|
8458
|
-
for (let i = 1; i < kept.length; i++) if (kept[i].length > kept[longest].length) longest = i;
|
|
8459
|
-
if (kept[longest].length <= 1) break;
|
|
8460
|
-
kept[longest] = kept[longest].slice(0, -1).replace(/-+$/, "");
|
|
8461
|
-
}
|
|
8462
|
-
return kept.join("-");
|
|
8463
8370
|
}
|
|
8464
|
-
/**
|
|
8465
|
-
function
|
|
8466
|
-
if (
|
|
8467
|
-
if (
|
|
8468
|
-
|
|
8469
|
-
if (name.startsWith("xn--") || name.startsWith("sthree-")) return "it starts with a prefix S3 reserves ('xn--', 'sthree-')";
|
|
8470
|
-
if (name.endsWith("-s3alias") || name.endsWith("--ol-s3")) return "it ends with a suffix S3 reserves ('-s3alias', '--ol-s3')";
|
|
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})`;
|
|
8471
8376
|
}
|
|
8472
|
-
|
|
8473
|
-
|
|
8474
|
-
|
|
8475
|
-
|
|
8476
|
-
|
|
8477
|
-
|
|
8478
|
-
|
|
8479
|
-
|
|
8480
|
-
|
|
8481
|
-
|
|
8482
|
-
|
|
8483
|
-
|
|
8484
|
-
|
|
8485
|
-
|
|
8486
|
-
|
|
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}`;
|
|
8384
|
+
}
|
|
8385
|
+
/** `dns-record:<fqdn>:<type>` */
|
|
8386
|
+
function dnsResourceKey(fqdn, type) {
|
|
8387
|
+
return `${DNS_RECORD_RESOURCE}:${fqdn.toLowerCase()}:${type.toUpperCase()}`;
|
|
8388
|
+
}
|
|
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
|
+
};
|
|
8487
8407
|
}
|
|
8488
|
-
/**
|
|
8489
|
-
function
|
|
8490
|
-
|
|
8491
|
-
|
|
8492
|
-
|
|
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);
|
|
8493
8416
|
}
|
|
8494
|
-
/**
|
|
8495
|
-
function
|
|
8496
|
-
|
|
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));
|
|
8497
8420
|
}
|
|
8498
8421
|
/**
|
|
8499
|
-
* The
|
|
8500
|
-
*
|
|
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.
|
|
8501
8425
|
*/
|
|
8502
|
-
function
|
|
8503
|
-
|
|
8504
|
-
"
|
|
8505
|
-
|
|
8506
|
-
input.stage,
|
|
8507
|
-
input.id
|
|
8508
|
-
], 64);
|
|
8509
|
-
}
|
|
8510
|
-
/** The path every user gkm creates is under, so it finds its own. */
|
|
8511
|
-
const IAM_USER_PATH = "/gkm/";
|
|
8512
|
-
/** The inline policy each user is given. */
|
|
8513
|
-
const IAM_POLICY_NAME = "gkm-bucket";
|
|
8514
|
-
//#endregion
|
|
8515
|
-
//#region src/providers/s3/origins.ts
|
|
8516
|
-
/**
|
|
8517
|
-
* Which browser origins may call a bucket: the stage's sites that depend on
|
|
8518
|
-
* an API using it.
|
|
8519
|
-
*
|
|
8520
|
-
* The same reading of the graph an API's own CORS origins are — its callers —
|
|
8521
|
-
* taken one step further: a site that calls an API which hands it a presigned
|
|
8522
|
-
* upload URL is a site whose browser PUTs to the bucket. A site that reads the
|
|
8523
|
-
* bucket (or a file server over it) itself is one too. Nothing is written
|
|
8524
|
-
* down: add a site that calls the API and the next provision run allows it.
|
|
8525
|
-
*/
|
|
8526
|
-
/** The keys a reader of this bucket holds: its URL, and each server's over it. */
|
|
8527
|
-
function bucketKeys(manifest, bucket) {
|
|
8528
|
-
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"))];
|
|
8529
|
-
}
|
|
8530
|
-
function bucketOrigins(input) {
|
|
8531
|
-
const { manifest, workspace, stage } = input;
|
|
8532
|
-
const keys = bucketKeys(manifest, input.bucket);
|
|
8533
|
-
const readers = Object.entries(manifest).filter(([, d]) => d.kind === "rest-api" || d.kind === "site").filter(([id]) => {
|
|
8534
|
-
const env = appEnvKeys(manifest, require_derive.appKey(id), input.runnables);
|
|
8535
|
-
return env !== void 0 && keys.some((key) => env.has(key));
|
|
8536
|
-
});
|
|
8537
|
-
const sites = /* @__PURE__ */ new Set();
|
|
8538
|
-
for (const [id, declaration] of readers) {
|
|
8539
|
-
if (declaration.kind === "site") {
|
|
8540
|
-
sites.add(id);
|
|
8541
|
-
continue;
|
|
8542
|
-
}
|
|
8543
|
-
for (const caller of (0, _geekmidas_manifest.dependentsOf)(manifest, id)) if (manifest[caller]?.kind === "site") sites.add(caller);
|
|
8544
|
-
}
|
|
8545
|
-
const origins = /* @__PURE__ */ new Set();
|
|
8546
|
-
const unresolved = [];
|
|
8547
|
-
for (const id of [...sites].sort()) {
|
|
8548
|
-
const name = require_derive.appKey(id);
|
|
8549
|
-
const app = workspace.apps[name];
|
|
8550
|
-
if (!app) {
|
|
8551
|
-
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);
|
|
8552
8430
|
continue;
|
|
8553
8431
|
}
|
|
8554
|
-
|
|
8555
|
-
|
|
8556
|
-
|
|
8557
|
-
|
|
8558
|
-
|
|
8559
|
-
|
|
8560
|
-
|
|
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
|
+
});
|
|
8561
8444
|
}
|
|
8562
|
-
|
|
8563
|
-
|
|
8564
|
-
|
|
8565
|
-
|
|
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));
|
|
8566
8457
|
}
|
|
8567
8458
|
//#endregion
|
|
8568
|
-
//#region src/
|
|
8569
|
-
const bucketArn = (bucket) => `arn:aws:s3:::${bucket}`;
|
|
8459
|
+
//#region src/docker/imageRef.ts
|
|
8570
8460
|
/**
|
|
8571
|
-
*
|
|
8572
|
-
* nothing else in the account.
|
|
8461
|
+
* An image reference, checked before it reaches `docker`.
|
|
8573
8462
|
*
|
|
8574
|
-
*
|
|
8575
|
-
*
|
|
8576
|
-
*
|
|
8577
|
-
|
|
8578
|
-
|
|
8579
|
-
|
|
8580
|
-
Version: "2012-10-17",
|
|
8581
|
-
Statement: [{
|
|
8582
|
-
Sid: "GkmObjects",
|
|
8583
|
-
Effect: "Allow",
|
|
8584
|
-
Action: [
|
|
8585
|
-
"s3:GetObject",
|
|
8586
|
-
"s3:PutObject",
|
|
8587
|
-
"s3:DeleteObject",
|
|
8588
|
-
"s3:AbortMultipartUpload",
|
|
8589
|
-
"s3:ListMultipartUploadParts",
|
|
8590
|
-
...options.versioning ? ["s3:GetObjectVersion"] : []
|
|
8591
|
-
],
|
|
8592
|
-
Resource: `${bucketArn(bucket)}/*`
|
|
8593
|
-
}, {
|
|
8594
|
-
Sid: "GkmBucket",
|
|
8595
|
-
Effect: "Allow",
|
|
8596
|
-
Action: [
|
|
8597
|
-
"s3:ListBucket",
|
|
8598
|
-
"s3:GetBucketLocation",
|
|
8599
|
-
"s3:ListBucketMultipartUploads"
|
|
8600
|
-
],
|
|
8601
|
-
Resource: bucketArn(bucket)
|
|
8602
|
-
}]
|
|
8603
|
-
};
|
|
8604
|
-
}
|
|
8605
|
-
/**
|
|
8606
|
-
* One open pattern as the resource it is public on. A `**` is S3's `*`; a
|
|
8607
|
-
* single `*` is too, which crosses `/` where the construct's own check does
|
|
8608
|
-
* not — the construct is the stricter of the two (see `bucketPolicy` in
|
|
8609
|
-
* 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.
|
|
8610
8469
|
*/
|
|
8611
|
-
|
|
8612
|
-
|
|
8613
|
-
|
|
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;
|
|
8614
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
|
+
};
|
|
8615
8521
|
/**
|
|
8616
|
-
*
|
|
8617
|
-
*
|
|
8618
|
-
* 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`.
|
|
8619
8526
|
*/
|
|
8620
|
-
function
|
|
8621
|
-
const
|
|
8622
|
-
|
|
8623
|
-
|
|
8624
|
-
|
|
8625
|
-
|
|
8626
|
-
|
|
8627
|
-
|
|
8628
|
-
}];
|
|
8629
|
-
if (open.length > 0) statements.push({
|
|
8630
|
-
Sid: "GkmOpenPaths",
|
|
8631
|
-
Effect: "Allow",
|
|
8632
|
-
Principal: "*",
|
|
8633
|
-
Action: "s3:GetObject",
|
|
8634
|
-
Resource: [...new Set(open.map((p) => openResource(bucket, p)))].sort()
|
|
8635
|
-
});
|
|
8636
|
-
return {
|
|
8637
|
-
Version: "2012-10-17",
|
|
8638
|
-
Statement: statements
|
|
8639
|
-
};
|
|
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;
|
|
8640
8535
|
}
|
|
8641
8536
|
/**
|
|
8642
|
-
*
|
|
8643
|
-
*
|
|
8644
|
-
* `RestrictPublicBuckets` off), and ACLs still may not.
|
|
8537
|
+
* Build one app's image, from the build root.
|
|
8538
|
+
* Uses BuildKit for cache mount support
|
|
8645
8539
|
*/
|
|
8646
|
-
function
|
|
8647
|
-
|
|
8648
|
-
|
|
8649
|
-
|
|
8650
|
-
|
|
8651
|
-
|
|
8652
|
-
|
|
8653
|
-
|
|
8654
|
-
|
|
8655
|
-
|
|
8656
|
-
|
|
8657
|
-
|
|
8658
|
-
|
|
8659
|
-
|
|
8660
|
-
|
|
8661
|
-
|
|
8662
|
-
|
|
8663
|
-
|
|
8664
|
-
|
|
8665
|
-
|
|
8666
|
-
|
|
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
|
+
}
|
|
8667
8562
|
}
|
|
8668
8563
|
/**
|
|
8669
|
-
*
|
|
8670
|
-
* one-element list, lists sorted — so one AWS reformatted reads as the same
|
|
8671
|
-
* document rather than as drift.
|
|
8564
|
+
* Push Docker image to registry
|
|
8672
8565
|
*/
|
|
8673
|
-
function
|
|
8674
|
-
|
|
8675
|
-
|
|
8676
|
-
|
|
8677
|
-
|
|
8678
|
-
|
|
8679
|
-
return key === "Statement" ? items : items.map((v) => JSON.stringify(v)).sort();
|
|
8680
|
-
}
|
|
8681
|
-
if (value && typeof value === "object") return Object.fromEntries(Object.keys(value).sort().map((k) => {
|
|
8682
|
-
const v = value[k];
|
|
8683
|
-
return [k, canonical(k === "Action" || k === "Resource" || k === "AWS" ? Array.isArray(v) ? v : [v] : v, k)];
|
|
8684
|
-
}));
|
|
8685
|
-
return value;
|
|
8686
|
-
}
|
|
8687
|
-
//#endregion
|
|
8688
|
-
//#region src/providers/s3/index.ts
|
|
8689
|
-
/** The tags gkm finds its own buckets and users by. */
|
|
8690
|
-
const TAG_PROJECT = "gkm:project";
|
|
8691
|
-
const TAG_STAGE = "gkm:stage";
|
|
8692
|
-
const TAG_CONSTRUCT = "gkm:construct";
|
|
8693
|
-
function bucketConstructs(manifest) {
|
|
8694
|
-
return Object.entries(manifest).filter(([, d]) => d.kind === "objects").map(([id, d]) => ({
|
|
8695
|
-
id,
|
|
8696
|
-
versioned: d.versioned === true,
|
|
8697
|
-
servers: Object.entries(manifest).filter(([, s]) => s.kind === "file-server" && s.of === id).map(([sid, s]) => ({
|
|
8698
|
-
id: sid,
|
|
8699
|
-
open: s.open ?? []
|
|
8700
|
-
})).sort((a, b) => a.id.localeCompare(b.id))
|
|
8701
|
-
})).sort((a, b) => a.id.localeCompare(b.id));
|
|
8702
|
-
}
|
|
8703
|
-
/**
|
|
8704
|
-
* The region the stage's buckets are created in: the entry's, the secrets
|
|
8705
|
-
* store's when it is in AWS, the credential's, `AWS_REGION`.
|
|
8706
|
-
*/
|
|
8707
|
-
function s3Region(ctx, env = process.env) {
|
|
8708
|
-
const store = ctx.workspace.secrets?.store;
|
|
8709
|
-
const storeRegion = store && typeof store === "object" && "region" in store ? store.region : void 0;
|
|
8710
|
-
const region = ctx.config.region ?? storeRegion ?? ctx.credential?.region ?? env.AWS_REGION ?? env.AWS_DEFAULT_REGION;
|
|
8711
|
-
if (!region) throw new require_workspace.S3RegionRequired(ctx.stage);
|
|
8712
|
-
return region;
|
|
8713
|
-
}
|
|
8714
|
-
/**
|
|
8715
|
-
* The address a file server over `bucket` serves on: the bucket's regional
|
|
8716
|
-
* virtual-host endpoint, which is what `FileServer.url(key)` appends a key to
|
|
8717
|
-
* — `https://<bucket>.s3.<region>.amazonaws.com/<key>`. Against an emulator,
|
|
8718
|
-
* its path-style address.
|
|
8719
|
-
*/
|
|
8720
|
-
function bucketPublicUrl(bucket, region, endpoint) {
|
|
8721
|
-
if (endpoint) return `${endpoint.replace(/\/+$/, "")}/${bucket}`;
|
|
8722
|
-
return `https://${bucket}.s3.${region}.amazonaws.com`;
|
|
8723
|
-
}
|
|
8724
|
-
/** What an AWS SDK error is called, whichever field carries it. */
|
|
8725
|
-
function errorName(error) {
|
|
8726
|
-
const e = error;
|
|
8727
|
-
return e?.Code ?? e?.name ?? "";
|
|
8728
|
-
}
|
|
8729
|
-
/** A read whose "there is none" answer is one of `missing`. */
|
|
8730
|
-
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}`);
|
|
8731
8572
|
try {
|
|
8732
|
-
|
|
8573
|
+
await require_credentials.run("docker", ["push", fullImageName], { cwd });
|
|
8574
|
+
logger.log(`✅ Docker image pushed: ${fullImageName}`);
|
|
8733
8575
|
} catch (error) {
|
|
8734
|
-
|
|
8735
|
-
throw error;
|
|
8576
|
+
throw new DockerPushFailed(fullImageName, error);
|
|
8736
8577
|
}
|
|
8737
8578
|
}
|
|
8738
|
-
|
|
8739
|
-
|
|
8740
|
-
|
|
8741
|
-
|
|
8742
|
-
|
|
8743
|
-
|
|
8744
|
-
|
|
8745
|
-
|
|
8746
|
-
|
|
8747
|
-
|
|
8748
|
-
|
|
8749
|
-
|
|
8750
|
-
|
|
8751
|
-
} : {}
|
|
8752
|
-
}),
|
|
8753
|
-
iam: new IAM.IAMClient(config),
|
|
8754
|
-
region,
|
|
8755
|
-
...endpoint ? { endpoint } : {}
|
|
8756
|
-
};
|
|
8757
|
-
};
|
|
8758
|
-
/** For tests: what makes the clients. */
|
|
8759
|
-
let makeClients = defaultClients;
|
|
8760
|
-
async function ensureS3(ctx) {
|
|
8761
|
-
const region = s3Region(ctx);
|
|
8762
|
-
const clients = await makeClients(ctx.credential, region);
|
|
8763
|
-
for (const construct of bucketConstructs(ctx.manifest)) await ensureBucket(ctx, clients, construct);
|
|
8764
|
-
}
|
|
8765
|
-
/** The keys gkm tags a bucket and a user with. */
|
|
8766
|
-
function identityTags(ctx, id) {
|
|
8767
|
-
return [
|
|
8768
|
-
{
|
|
8769
|
-
Key: TAG_PROJECT,
|
|
8770
|
-
Value: ctx.identity.key
|
|
8771
|
-
},
|
|
8772
|
-
{
|
|
8773
|
-
Key: TAG_STAGE,
|
|
8774
|
-
Value: ctx.stage
|
|
8775
|
-
},
|
|
8776
|
-
{
|
|
8777
|
-
Key: TAG_CONSTRUCT,
|
|
8778
|
-
Value: id
|
|
8779
|
-
}
|
|
8780
|
-
];
|
|
8781
|
-
}
|
|
8782
|
-
async function ensureBucket(ctx, clients, construct) {
|
|
8783
|
-
const { id } = construct;
|
|
8784
|
-
const nameInput = {
|
|
8785
|
-
scope: ctx.identity.scope,
|
|
8786
|
-
stage: ctx.stage,
|
|
8787
|
-
id: require_derive.appKey(id)
|
|
8788
|
-
};
|
|
8789
|
-
const versioning = ctx.config.versioning ?? construct.versioned;
|
|
8790
|
-
const open = [...new Set(construct.servers.flatMap((s) => s.open))].sort();
|
|
8791
|
-
const { bucket, exists } = await ensureBucketName(ctx, clients, id, bucketName(nameInput));
|
|
8792
|
-
await ensureBucketSettings(ctx, clients, {
|
|
8793
|
-
id,
|
|
8794
|
-
bucket,
|
|
8795
|
-
exists,
|
|
8796
|
-
versioning,
|
|
8797
|
-
open
|
|
8798
|
-
});
|
|
8799
|
-
await ensureAccessKey(ctx, clients, {
|
|
8800
|
-
id,
|
|
8801
|
-
bucket,
|
|
8802
|
-
...await ensureUser(ctx, clients, id, iamUserName(nameInput), userPolicy(bucket, { versioning }))
|
|
8803
|
-
});
|
|
8804
|
-
const publicUrl = bucketPublicUrl(bucket, clients.region, clients.endpoint);
|
|
8805
|
-
for (const server of construct.servers) {
|
|
8806
|
-
const key = (0, _geekmidas_manifest.provideKey)(server.id, "url");
|
|
8807
|
-
const entry = {
|
|
8808
|
-
key: `file-server:${server.id}`,
|
|
8809
|
-
type: "url"
|
|
8810
|
-
};
|
|
8811
|
-
const recorded = ctx.state.record(entry.key)?.id;
|
|
8812
|
-
const current = ctx.secrets[key];
|
|
8813
|
-
if (current === publicUrl) {
|
|
8814
|
-
await ctx.state.ready(entry, publicUrl);
|
|
8815
|
-
continue;
|
|
8816
|
-
}
|
|
8817
|
-
if (current !== void 0 && current !== recorded) {
|
|
8818
|
-
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)`);
|
|
8819
8592
|
continue;
|
|
8820
8593
|
}
|
|
8821
|
-
|
|
8822
|
-
|
|
8823
|
-
|
|
8824
|
-
|
|
8825
|
-
|
|
8826
|
-
|
|
8827
|
-
|
|
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
|
|
8828
8632
|
});
|
|
8829
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
|
+
};
|
|
8830
8650
|
}
|
|
8831
8651
|
/**
|
|
8832
|
-
*
|
|
8833
|
-
*
|
|
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.
|
|
8834
8655
|
*/
|
|
8835
|
-
|
|
8836
|
-
const
|
|
8837
|
-
|
|
8838
|
-
|
|
8839
|
-
|
|
8840
|
-
|
|
8841
|
-
type: "s3-bucket",
|
|
8842
|
-
data: {
|
|
8843
|
-
name,
|
|
8844
|
-
region
|
|
8845
|
-
}
|
|
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 } : {}
|
|
8846
8662
|
});
|
|
8847
|
-
|
|
8848
|
-
|
|
8849
|
-
|
|
8850
|
-
|
|
8851
|
-
return "ours";
|
|
8852
|
-
} catch (error) {
|
|
8853
|
-
const name = errorName(error);
|
|
8854
|
-
if (name === "NotFound" || name === "NoSuchBucket") return "none";
|
|
8855
|
-
if (name === "Forbidden" || name === "AccessDenied") return "taken";
|
|
8856
|
-
throw error;
|
|
8857
|
-
}
|
|
8858
|
-
};
|
|
8859
|
-
const create = async (name) => {
|
|
8860
|
-
if (ctx.dryRun) {
|
|
8861
|
-
const found = await probe(name);
|
|
8862
|
-
if (found === "none") {
|
|
8863
|
-
await ctx.change({
|
|
8864
|
-
construct: id,
|
|
8865
|
-
resource: `bucket ${name}`,
|
|
8866
|
-
change: `create in ${region}`
|
|
8867
|
-
}, async () => {});
|
|
8868
|
-
return "created";
|
|
8869
|
-
}
|
|
8870
|
-
return found;
|
|
8871
|
-
}
|
|
8872
|
-
await ctx.state.pending(entry(name));
|
|
8873
|
-
try {
|
|
8874
|
-
await s3.send(new S3.CreateBucketCommand({
|
|
8875
|
-
Bucket: name,
|
|
8876
|
-
...region === "us-east-1" ? {} : { CreateBucketConfiguration: { LocationConstraint: region } }
|
|
8877
|
-
}));
|
|
8878
|
-
} catch (error) {
|
|
8879
|
-
const code = errorName(error);
|
|
8880
|
-
if (code === "BucketAlreadyOwnedByYou") return "ours";
|
|
8881
|
-
if (code === "BucketAlreadyExists") return "taken";
|
|
8882
|
-
throw error;
|
|
8883
|
-
}
|
|
8884
|
-
await ctx.change({
|
|
8885
|
-
construct: id,
|
|
8886
|
-
resource: `bucket ${name}`,
|
|
8887
|
-
change: `create in ${region}`
|
|
8888
|
-
}, async () => {});
|
|
8889
|
-
return "created";
|
|
8890
|
-
};
|
|
8891
|
-
const adopt = async (name) => {
|
|
8892
|
-
const owner = (await readOr(() => s3.send(new S3.GetBucketTaggingCommand({ Bucket: name })), ["NoSuchTagSet", "NoSuchTagSetError"]))?.TagSet?.find((t) => t.Key === TAG_PROJECT)?.Value;
|
|
8893
|
-
if (owner && owner !== ctx.identity.key) throw new require_workspace.BucketOwnedByAnotherProject(name, owner, ctx.identity.key);
|
|
8894
|
-
};
|
|
8895
|
-
if (record?.status === "ready" && record.id) {
|
|
8896
|
-
const found = await probe(record.id);
|
|
8897
|
-
if (found === "ours") return {
|
|
8898
|
-
bucket: record.id,
|
|
8899
|
-
exists: true
|
|
8900
|
-
};
|
|
8901
|
-
if (found === "taken") throw new require_workspace.RecordedBucketUnreachable(id, record.id);
|
|
8902
|
-
const made = await create(record.id);
|
|
8903
|
-
if (made === "taken") throw new require_workspace.RecordedBucketUnreachable(id, record.id);
|
|
8904
|
-
if (!ctx.dryRun) await ctx.state.ready(entry(record.id), record.id);
|
|
8905
|
-
return {
|
|
8906
|
-
bucket: record.id,
|
|
8907
|
-
exists: made === "ours"
|
|
8908
|
-
};
|
|
8909
|
-
}
|
|
8910
|
-
const pending = record?.data?.name;
|
|
8911
|
-
const names = [
|
|
8912
|
-
...typeof pending === "string" && pending !== good ? [pending] : [],
|
|
8913
|
-
good,
|
|
8914
|
-
...Array.from({ length: 5 }, () => suffixedBucketName(good, randomSuffix()))
|
|
8915
|
-
];
|
|
8916
|
-
const tried = [];
|
|
8917
|
-
for (const name of names) {
|
|
8918
|
-
tried.push(name);
|
|
8919
|
-
const made = await create(name);
|
|
8920
|
-
if (made === "taken") continue;
|
|
8921
|
-
if (made === "ours") await adopt(name);
|
|
8922
|
-
if (!ctx.dryRun) await ctx.state.ready(entry(name), name);
|
|
8923
|
-
if (name !== good) ctx.log(` '${good}' is taken by another account; '${id}' is '${name}', recorded in the stage's state`);
|
|
8924
|
-
return {
|
|
8925
|
-
bucket: name,
|
|
8926
|
-
exists: made === "ours"
|
|
8927
|
-
};
|
|
8928
|
-
}
|
|
8929
|
-
throw new require_workspace.BucketNameUnavailable(id, tried.filter((n) => n !== pending));
|
|
8930
|
-
}
|
|
8931
|
-
/** Tags, Block Public Access, encryption, policy, versioning and CORS. */
|
|
8932
|
-
async function ensureBucketSettings(ctx, clients, input) {
|
|
8933
|
-
const { S3, s3 } = clients;
|
|
8934
|
-
const { id, bucket } = input;
|
|
8935
|
-
const Bucket = bucket;
|
|
8936
|
-
const fresh = ctx.dryRun && !input.exists;
|
|
8937
|
-
const read = async (fn, missing) => fresh ? void 0 : readOr(fn, missing);
|
|
8938
|
-
const set = (change, apply) => ctx.change({
|
|
8939
|
-
construct: id,
|
|
8940
|
-
resource: `bucket ${bucket}`,
|
|
8941
|
-
change
|
|
8942
|
-
}, () => apply().then(() => void 0));
|
|
8943
|
-
const tagged = await read(() => s3.send(new S3.GetBucketTaggingCommand({ Bucket })), ["NoSuchTagSet", "NoSuchTagSetError"]);
|
|
8944
|
-
const ours = identityTags(ctx, id);
|
|
8945
|
-
const others = (tagged?.TagSet ?? []).filter((t) => !ours.some((o) => o.Key === t.Key));
|
|
8946
|
-
if (ours.some((o) => !tagged?.TagSet?.some((t) => t.Key === o.Key && t.Value === o.Value))) await set("tag", () => s3.send(new S3.PutBucketTaggingCommand({
|
|
8947
|
-
Bucket,
|
|
8948
|
-
Tagging: { TagSet: [...others, ...ours].map((t) => ({
|
|
8949
|
-
Key: t.Key,
|
|
8950
|
-
Value: t.Value
|
|
8951
|
-
})) }
|
|
8952
|
-
})));
|
|
8953
|
-
const isOpen = input.open.length > 0;
|
|
8954
|
-
const block = publicAccessBlock(isOpen);
|
|
8955
|
-
const ensureBlock = async () => {
|
|
8956
|
-
const now = (await read(() => s3.send(new S3.GetPublicAccessBlockCommand({ Bucket })), ["NoSuchPublicAccessBlockConfiguration"]))?.PublicAccessBlockConfiguration;
|
|
8957
|
-
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({
|
|
8958
|
-
Bucket,
|
|
8959
|
-
PublicAccessBlockConfiguration: block
|
|
8960
|
-
})));
|
|
8961
|
-
};
|
|
8962
|
-
const policy = bucketPolicy(bucket, input.open);
|
|
8963
|
-
const ensurePolicy = async () => {
|
|
8964
|
-
const current = await read(() => s3.send(new S3.GetBucketPolicyCommand({ Bucket })), ["NoSuchBucketPolicy"]);
|
|
8965
|
-
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({
|
|
8966
|
-
Bucket,
|
|
8967
|
-
Policy: JSON.stringify(policy)
|
|
8968
|
-
})));
|
|
8969
|
-
};
|
|
8970
|
-
if (isOpen) {
|
|
8971
|
-
await ensureBlock();
|
|
8972
|
-
await ensurePolicy();
|
|
8973
|
-
} else {
|
|
8974
|
-
await ensurePolicy();
|
|
8975
|
-
await ensureBlock();
|
|
8976
|
-
}
|
|
8977
|
-
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({
|
|
8978
|
-
Bucket,
|
|
8979
|
-
ServerSideEncryptionConfiguration: { Rules: [{ ApplyServerSideEncryptionByDefault: { SSEAlgorithm: "AES256" } }] }
|
|
8980
|
-
})));
|
|
8981
|
-
const status = (await read(() => s3.send(new S3.GetBucketVersioningCommand({ Bucket })), []))?.Status;
|
|
8982
|
-
if (input.versioning && status !== "Enabled") await set("enable versioning", () => s3.send(new S3.PutBucketVersioningCommand({
|
|
8983
|
-
Bucket,
|
|
8984
|
-
VersioningConfiguration: { Status: "Enabled" }
|
|
8985
|
-
})));
|
|
8986
|
-
else if (!input.versioning && status === "Enabled") await set("suspend versioning", () => s3.send(new S3.PutBucketVersioningCommand({
|
|
8987
|
-
Bucket,
|
|
8988
|
-
VersioningConfiguration: { Status: "Suspended" }
|
|
8989
|
-
})));
|
|
8990
|
-
const { origins, unresolved } = bucketOrigins({
|
|
8991
|
-
workspace: ctx.workspace,
|
|
8992
|
-
manifest: ctx.manifest,
|
|
8993
|
-
runnables: ctx.runnables,
|
|
8994
|
-
stage: ctx.stage,
|
|
8995
|
-
bucket: id
|
|
8663
|
+
if (app.entry) return generateEntryDockerfile({
|
|
8664
|
+
...image,
|
|
8665
|
+
entry: app.entry,
|
|
8666
|
+
healthCheckPath: "/health"
|
|
8996
8667
|
});
|
|
8997
|
-
|
|
8998
|
-
|
|
8999
|
-
|
|
9000
|
-
|
|
9001
|
-
if (!(cors?.CORSRules?.length === 1 && canonicalCors(cors.CORSRules[0]) === canonicalCors(rule))) await set(`CORS for ${origins.join(", ")}`, () => s3.send(new S3.PutBucketCorsCommand({
|
|
9002
|
-
Bucket,
|
|
9003
|
-
CORSConfiguration: { CORSRules: [rule] }
|
|
9004
|
-
})));
|
|
9005
|
-
}
|
|
9006
|
-
function canonicalCors(rule) {
|
|
9007
|
-
const sorted = (list) => [...list ?? []].sort();
|
|
9008
|
-
return JSON.stringify({
|
|
9009
|
-
AllowedMethods: sorted(rule.AllowedMethods),
|
|
9010
|
-
AllowedOrigins: sorted(rule.AllowedOrigins),
|
|
9011
|
-
AllowedHeaders: sorted(rule.AllowedHeaders),
|
|
9012
|
-
ExposeHeaders: sorted(rule.ExposeHeaders),
|
|
9013
|
-
MaxAgeSeconds: rule.MaxAgeSeconds ?? null
|
|
8668
|
+
return generateBackendDockerfile({
|
|
8669
|
+
...image,
|
|
8670
|
+
healthCheckPath: "/health",
|
|
8671
|
+
...options.cache ? { cache: options.cache } : {}
|
|
9014
8672
|
});
|
|
9015
8673
|
}
|
|
9016
|
-
/**
|
|
9017
|
-
|
|
9018
|
-
|
|
9019
|
-
|
|
9020
|
-
|
|
9021
|
-
|
|
9022
|
-
|
|
9023
|
-
|
|
9024
|
-
|
|
9025
|
-
|
|
9026
|
-
|
|
9027
|
-
|
|
9028
|
-
|
|
9029
|
-
|
|
9030
|
-
|
|
9031
|
-
|
|
9032
|
-
|
|
9033
|
-
}, async () => {
|
|
9034
|
-
await ctx.state.pending(entry);
|
|
9035
|
-
await iam.send(new IAM.CreateUserCommand({
|
|
9036
|
-
UserName: user,
|
|
9037
|
-
Path: IAM_USER_PATH,
|
|
9038
|
-
Tags: identityTags(ctx, id)
|
|
9039
|
-
}));
|
|
9040
|
-
await ctx.state.ready(entry, user);
|
|
9041
|
-
});
|
|
9042
|
-
const exists = Boolean(found?.User) || !ctx.dryRun;
|
|
9043
|
-
const current = exists ? await readOr(() => iam.send(new IAM.GetUserPolicyCommand({
|
|
9044
|
-
UserName: user,
|
|
9045
|
-
PolicyName: IAM_POLICY_NAME
|
|
9046
|
-
})), ["NoSuchEntity", "NoSuchEntityException"]) : void 0;
|
|
9047
|
-
const document = current?.PolicyDocument ? JSON.parse(decodePolicy(current.PolicyDocument)) : void 0;
|
|
9048
|
-
if (!document || canonicalPolicy(document) !== canonicalPolicy(policy)) await ctx.change({
|
|
9049
|
-
construct: id,
|
|
9050
|
-
resource: `IAM user ${user}`,
|
|
9051
|
-
change: `policy ${IAM_POLICY_NAME}: this bucket only`
|
|
9052
|
-
}, async () => {
|
|
9053
|
-
await iam.send(new IAM.PutUserPolicyCommand({
|
|
9054
|
-
UserName: user,
|
|
9055
|
-
PolicyName: IAM_POLICY_NAME,
|
|
9056
|
-
PolicyDocument: JSON.stringify(policy)
|
|
9057
|
-
}));
|
|
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 } : {}
|
|
9058
8691
|
});
|
|
9059
|
-
return {
|
|
9060
|
-
user,
|
|
9061
|
-
exists
|
|
9062
|
-
};
|
|
9063
8692
|
}
|
|
9064
|
-
/**
|
|
9065
|
-
|
|
9066
|
-
|
|
9067
|
-
|
|
9068
|
-
|
|
9069
|
-
|
|
8693
|
+
/**
|
|
8694
|
+
* A site's Dockerfile, with an `ARG` for every key its bundler inlines.
|
|
8695
|
+
*
|
|
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.
|
|
8700
|
+
*/
|
|
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);
|
|
9070
8713
|
}
|
|
9071
8714
|
}
|
|
9072
8715
|
/**
|
|
9073
|
-
*
|
|
9074
|
-
*
|
|
9075
|
-
*
|
|
9076
|
-
*
|
|
9077
|
-
* stays active until the stage has deployed with the second, and the next
|
|
9078
|
-
* run after that deploy — or `--retire-old-keys` — deletes it.
|
|
9079
|
-
* - A key the secrets no longer hold (a store emptied by hand): replaced the
|
|
9080
|
-
* same way, since its secret cannot be read back.
|
|
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.
|
|
9081
8720
|
*/
|
|
9082
|
-
|
|
9083
|
-
|
|
9084
|
-
|
|
9085
|
-
|
|
9086
|
-
|
|
9087
|
-
|
|
9088
|
-
|
|
9089
|
-
|
|
9090
|
-
|
|
9091
|
-
let parsed;
|
|
9092
|
-
try {
|
|
9093
|
-
parsed = _geekmidas_storage_s3_url.parse(given);
|
|
9094
|
-
} catch {
|
|
9095
|
-
parsed = void 0;
|
|
9096
|
-
}
|
|
9097
|
-
if (parsed?.bucket !== bucket) throw new require_workspace.BucketKeySetElsewhere(urlKey, bucket, parsed?.bucket ?? given.replace(/\/\/[^@]*@/, "//"), ctx.stage);
|
|
9098
|
-
inUrl = parsed.accessKeyId;
|
|
9099
|
-
}
|
|
9100
|
-
const listed = input.exists ? (await iam.send(new IAM.ListAccessKeysCommand({ UserName: user }))).AccessKeyMetadata?.map((k) => k.AccessKeyId) ?? [] : [];
|
|
9101
|
-
let current = record?.status === "ready" ? record.id : void 0;
|
|
9102
|
-
let previous = typeof record?.data?.previous === "string" ? record.data.previous : void 0;
|
|
9103
|
-
const rotatedAt = typeof record?.data?.rotatedAt === "string" ? record.data.rotatedAt : void 0;
|
|
9104
|
-
const entry = (data = {}) => ({
|
|
9105
|
-
key: entryKey,
|
|
9106
|
-
type: "iam-access-key",
|
|
9107
|
-
data: {
|
|
9108
|
-
user,
|
|
9109
|
-
...data
|
|
9110
|
-
}
|
|
9111
|
-
});
|
|
9112
|
-
if (previous && current) {
|
|
9113
|
-
const deployed = rotatedAt !== void 0 && ctx.state.lastDeployedAt !== void 0 && ctx.state.lastDeployedAt > rotatedAt;
|
|
9114
|
-
if (ctx.retireOldKeys || deployed) {
|
|
9115
|
-
const old = previous;
|
|
9116
|
-
const keep = current;
|
|
9117
|
-
await ctx.change({
|
|
9118
|
-
construct: id,
|
|
9119
|
-
resource: `IAM user ${user}`,
|
|
9120
|
-
change: `delete the rotated-out access key ${old}`
|
|
9121
|
-
}, async () => {
|
|
9122
|
-
if (listed.includes(old)) await iam.send(new IAM.DeleteAccessKeyCommand({
|
|
9123
|
-
UserName: user,
|
|
9124
|
-
AccessKeyId: old
|
|
9125
|
-
}));
|
|
9126
|
-
await ctx.state.ready(entry(), keep);
|
|
9127
|
-
});
|
|
9128
|
-
previous = void 0;
|
|
9129
|
-
} 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)`);
|
|
9130
|
-
}
|
|
9131
|
-
if (!current && inUrl && listed.includes(inUrl)) {
|
|
9132
|
-
await ctx.state.ready(entry(), inUrl);
|
|
9133
|
-
current = inUrl;
|
|
9134
|
-
}
|
|
9135
|
-
if (current !== void 0 && listed.includes(current) && inUrl === current && !ctx.rotateKeys) return;
|
|
9136
|
-
if (ctx.rotateKeys && previous) throw new require_workspace.RotationInProgress(id, previous, ctx.stage);
|
|
9137
|
-
const outgoing = current && listed.includes(current) ? current : void 0;
|
|
9138
|
-
if (listed.length >= 2) throw new require_workspace.AccessKeyLimit(user, listed);
|
|
9139
|
-
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)";
|
|
9140
|
-
await ctx.change({
|
|
9141
|
-
construct: id,
|
|
9142
|
-
resource: `IAM user ${user}`,
|
|
9143
|
-
change: why
|
|
9144
|
-
}, async () => {
|
|
9145
|
-
await ctx.state.pending(entry(outgoing ? { previous: outgoing } : {}));
|
|
9146
|
-
const key = (await iam.send(new IAM.CreateAccessKeyCommand({ UserName: user }))).AccessKey;
|
|
9147
|
-
await ctx.writeSecrets({ [urlKey]: _geekmidas_storage_s3_url.build({
|
|
9148
|
-
bucket,
|
|
9149
|
-
region: clients.region,
|
|
9150
|
-
...clients.endpoint ? {
|
|
9151
|
-
endpoint: clients.endpoint,
|
|
9152
|
-
forcePathStyle: true
|
|
9153
|
-
} : {},
|
|
9154
|
-
accessKeyId: key.AccessKeyId,
|
|
9155
|
-
secretAccessKey: key.SecretAccessKey
|
|
9156
|
-
}) });
|
|
9157
|
-
await ctx.state.ready(entry(outgoing ? {
|
|
9158
|
-
previous: outgoing,
|
|
9159
|
-
rotatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
9160
|
-
} : {}), key.AccessKeyId);
|
|
9161
|
-
});
|
|
9162
|
-
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}`);
|
|
9163
|
-
}
|
|
9164
|
-
/**
|
|
9165
|
-
* Each provisioned bucket answers `HeadBucket` to the key in the stage's
|
|
9166
|
-
* secrets — the one the apps will use. A bucket whose URL carries no key is
|
|
9167
|
-
* not one this provider wrote, and the deploy's own check covers a URL that
|
|
9168
|
-
* is missing.
|
|
9169
|
-
*/
|
|
9170
|
-
async function verifyS3(ctx) {
|
|
9171
|
-
const S3 = await import("@aws-sdk/client-s3");
|
|
9172
|
-
for (const { id } of bucketConstructs(ctx.manifest)) {
|
|
9173
|
-
const url = ctx.secrets[(0, _geekmidas_manifest.provideKey)(id, "url")];
|
|
9174
|
-
if (url === void 0) continue;
|
|
9175
|
-
const address = _geekmidas_storage_s3_url.parse(url);
|
|
9176
|
-
if (!address.accessKeyId || !address.secretAccessKey) continue;
|
|
9177
|
-
const client = new S3.S3Client({
|
|
9178
|
-
region: address.region ?? ctx.config.region ?? "us-east-1",
|
|
9179
|
-
credentials: {
|
|
9180
|
-
accessKeyId: address.accessKeyId,
|
|
9181
|
-
secretAccessKey: address.secretAccessKey
|
|
9182
|
-
},
|
|
9183
|
-
...address.endpoint ? { endpoint: address.endpoint } : {},
|
|
9184
|
-
...address.forcePathStyle ? { forcePathStyle: true } : {}
|
|
9185
|
-
});
|
|
9186
|
-
try {
|
|
9187
|
-
await client.send(new S3.HeadBucketCommand({ Bucket: address.bucket }));
|
|
9188
|
-
} catch (error) {
|
|
9189
|
-
const name = errorName(error);
|
|
9190
|
-
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));
|
|
9191
|
-
} finally {
|
|
9192
|
-
client.destroy();
|
|
9193
|
-
}
|
|
9194
|
-
}
|
|
9195
|
-
}
|
|
9196
|
-
//#endregion
|
|
9197
|
-
//#region src/providers/registry.ts
|
|
9198
|
-
/**
|
|
9199
|
-
* The providers gkm ships, by kind and name — registered the way a deploy
|
|
9200
|
-
* target's built-ins are (`target/builtins.ts`). A provider from a package is
|
|
9201
|
-
* one more entry here once there is a loading story for it.
|
|
9202
|
-
*/
|
|
9203
|
-
const BUILTIN_PROVIDERS = { objects: { s3: {
|
|
9204
|
-
kind: "objects",
|
|
9205
|
-
name: "s3",
|
|
9206
|
-
check: require_workspace.checkS3Config,
|
|
9207
|
-
provisioning: require_workspace.S3_PROVISIONING,
|
|
9208
|
-
credentials: awsProvisioningCredentials,
|
|
9209
|
-
runtimeKeys(manifest) {
|
|
9210
|
-
return bucketConstructs(manifest).flatMap((b) => [(0, _geekmidas_manifest.provideKey)(b.id, "url"), ...b.servers.map((s) => (0, _geekmidas_manifest.provideKey)(s.id, "url"))]);
|
|
9211
|
-
},
|
|
9212
|
-
ensure: ensureS3,
|
|
9213
|
-
verify: verifyS3
|
|
9214
|
-
} } };
|
|
9215
|
-
/** The provider `kind` names `name`, if gkm has one. */
|
|
9216
|
-
function builtinProvider(kind, name) {
|
|
9217
|
-
return Object.hasOwn(BUILTIN_PROVIDERS[kind], name) ? BUILTIN_PROVIDERS[kind][name] : void 0;
|
|
9218
|
-
}
|
|
9219
|
-
//#endregion
|
|
9220
|
-
//#region src/providers/index.ts
|
|
9221
|
-
/** The constructs of a kind: `objects` is every bucket. */
|
|
9222
|
-
function constructsOf(manifest, kind) {
|
|
9223
|
-
return Object.entries(manifest).filter(([, d]) => d.kind === kind).map(([id]) => id).sort();
|
|
9224
|
-
}
|
|
8721
|
+
const REQUIRED_IGNORES = [
|
|
8722
|
+
"**/node_modules",
|
|
8723
|
+
".git",
|
|
8724
|
+
"**/dist",
|
|
8725
|
+
"**/.next",
|
|
8726
|
+
"**/.turbo",
|
|
8727
|
+
"**/.gkm",
|
|
8728
|
+
"**/.gkm/compose"
|
|
8729
|
+
];
|
|
9225
8730
|
/**
|
|
9226
|
-
*
|
|
9227
|
-
*
|
|
9228
|
-
* the
|
|
9229
|
-
*
|
|
9230
|
-
* @throws {StageProviderDisabled} for a declared kind the stage has none of
|
|
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.
|
|
9231
8734
|
*/
|
|
9232
|
-
async function
|
|
9233
|
-
|
|
9234
|
-
|
|
9235
|
-
|
|
9236
|
-
|
|
9237
|
-
const choice = require_workspace.stageProvider(input.workspace, kind, input.stage);
|
|
9238
|
-
if (choice.mode !== "provider") continue;
|
|
9239
|
-
const provider = builtinProvider(kind, choice.name);
|
|
9240
|
-
await provider.verify({
|
|
9241
|
-
workspace: input.workspace,
|
|
9242
|
-
manifest: input.manifest,
|
|
9243
|
-
stage: input.stage,
|
|
9244
|
-
config: choice.config,
|
|
9245
|
-
secrets: input.secrets
|
|
9246
|
-
});
|
|
9247
|
-
verified.push(`${kind}: ${provider.name}`);
|
|
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;
|
|
9248
8740
|
}
|
|
9249
|
-
|
|
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;
|
|
9250
8747
|
}
|
|
9251
8748
|
/**
|
|
9252
|
-
* `
|
|
9253
|
-
*
|
|
9254
|
-
*
|
|
9255
|
-
*
|
|
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.
|
|
9256
8754
|
*/
|
|
9257
|
-
async function
|
|
9258
|
-
|
|
9259
|
-
|
|
9260
|
-
|
|
9261
|
-
const log = options.log ?? ((line) => console.log(line));
|
|
9262
|
-
const named = PROVIDER_KINDS.some((kind) => require_workspace.stageProvider(workspace, kind, stage).mode === "provider");
|
|
9263
|
-
let found = options.manifest;
|
|
9264
|
-
let runnables = options.runnables ?? {};
|
|
9265
|
-
if (!found && named) {
|
|
9266
|
-
runnables = {};
|
|
9267
|
-
found = await require_discover.discover({
|
|
9268
|
-
patterns: constructGlobs(workspace),
|
|
9269
|
-
cwd: workspace.root,
|
|
9270
|
-
runnables
|
|
9271
|
-
});
|
|
9272
|
-
}
|
|
9273
|
-
const manifest = found ?? {};
|
|
9274
|
-
const reports = [];
|
|
9275
|
-
const configured = [];
|
|
9276
|
-
for (const kind of PROVIDER_KINDS) {
|
|
9277
|
-
const choice = require_workspace.stageProvider(workspace, kind, stage);
|
|
9278
|
-
if (choice.mode !== "provider") {
|
|
9279
|
-
reports.push({
|
|
9280
|
-
kind,
|
|
9281
|
-
mode: choice.mode,
|
|
9282
|
-
actions: []
|
|
9283
|
-
});
|
|
9284
|
-
continue;
|
|
9285
|
-
}
|
|
9286
|
-
const provider = builtinProvider(kind, choice.name);
|
|
9287
|
-
const report = {
|
|
9288
|
-
kind,
|
|
9289
|
-
mode: "provider",
|
|
9290
|
-
provider: provider.name,
|
|
9291
|
-
actions: []
|
|
9292
|
-
};
|
|
9293
|
-
reports.push(report);
|
|
9294
|
-
if (constructsOf(manifest, kind).length === 0) {
|
|
9295
|
-
log(` ${kind}: ${provider.name} — nothing declared to provision`);
|
|
9296
|
-
continue;
|
|
9297
|
-
}
|
|
9298
|
-
const credential = await provider.credentials({
|
|
9299
|
-
stage,
|
|
9300
|
-
env,
|
|
9301
|
-
...options.profile ? { profile: options.profile } : {},
|
|
9302
|
-
...options.home ? { home: options.home } : {}
|
|
9303
|
-
});
|
|
9304
|
-
report.credentials = credential !== void 0;
|
|
9305
|
-
if (credential === void 0) {
|
|
9306
|
-
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)}.`);
|
|
9307
|
-
continue;
|
|
9308
|
-
}
|
|
9309
|
-
configured.push({
|
|
9310
|
-
kind,
|
|
9311
|
-
provider,
|
|
9312
|
-
config: choice.config,
|
|
9313
|
-
credential,
|
|
9314
|
-
report
|
|
9315
|
-
});
|
|
9316
|
-
}
|
|
9317
|
-
if (configured.length === 0) return reports;
|
|
9318
|
-
const identity = deployIdentity(workspace, stage);
|
|
9319
|
-
const secretsStore = await secretsStoreFor(workspace, stage, {
|
|
9320
|
-
...options.profile ? { profile: options.profile } : {},
|
|
9321
|
-
...options.home ? { home: options.home } : {}
|
|
9322
|
-
});
|
|
9323
|
-
let secrets = (await secretsStore.read(stage))?.custom ?? {};
|
|
9324
|
-
const stateStore = await createStateStore({
|
|
9325
|
-
config: workspace.state,
|
|
9326
|
-
workspaceRoot: workspace.root,
|
|
9327
|
-
workspaceName: workspace.name
|
|
8755
|
+
async function writeConstructsCompose(workspace) {
|
|
8756
|
+
await reconcileWorkspace(workspace, {
|
|
8757
|
+
stage: workspace.stages.local,
|
|
8758
|
+
start: false
|
|
9328
8759
|
});
|
|
9329
|
-
|
|
9330
|
-
|
|
9331
|
-
const state = dryRun ? await readOnlyState(stateStore, stage) : journalState(await DeployJournal.open(stateStore, stage, () => createEmptyState(stage, "", "")));
|
|
9332
|
-
const writeSecrets = async (values) => {
|
|
9333
|
-
if (dryRun) return;
|
|
9334
|
-
const stored = await secretsStore.read(stage) ?? initStageSecrets(stage);
|
|
9335
|
-
const next = {
|
|
9336
|
-
...stored,
|
|
9337
|
-
custom: {
|
|
9338
|
-
...stored.custom,
|
|
9339
|
-
...values
|
|
9340
|
-
},
|
|
9341
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
9342
|
-
};
|
|
9343
|
-
await secretsStore.write(stage, next);
|
|
9344
|
-
secrets = next.custom;
|
|
9345
|
-
};
|
|
9346
|
-
for (const entry of configured) {
|
|
9347
|
-
log(` ${entry.kind}: ${entry.provider.name}${dryRun ? " (dry run)" : ""}`);
|
|
9348
|
-
await entry.provider.ensure({
|
|
9349
|
-
workspace,
|
|
9350
|
-
manifest,
|
|
9351
|
-
runnables,
|
|
9352
|
-
stage,
|
|
9353
|
-
identity,
|
|
9354
|
-
config: entry.config,
|
|
9355
|
-
credential: entry.credential,
|
|
9356
|
-
get secrets() {
|
|
9357
|
-
return secrets;
|
|
9358
|
-
},
|
|
9359
|
-
state,
|
|
9360
|
-
dryRun,
|
|
9361
|
-
rotateKeys: options.rotateKeys === true,
|
|
9362
|
-
retireOldKeys: options.retireOldKeys === true,
|
|
9363
|
-
async change(action, apply) {
|
|
9364
|
-
if (!dryRun) await apply();
|
|
9365
|
-
entry.report.actions.push(action);
|
|
9366
|
-
log(` ${dryRun ? "would " : ""}${action.change} — ${action.resource}`);
|
|
9367
|
-
},
|
|
9368
|
-
writeSecrets,
|
|
9369
|
-
log
|
|
9370
|
-
});
|
|
9371
|
-
if (entry.report.actions.length === 0) log(" up to date");
|
|
9372
|
-
}
|
|
9373
|
-
} finally {
|
|
9374
|
-
await lock?.release();
|
|
9375
|
-
}
|
|
9376
|
-
return reports;
|
|
9377
|
-
}
|
|
9378
|
-
/** The journal as `ensure()` sees it. */
|
|
9379
|
-
function journalState(journal) {
|
|
9380
|
-
return {
|
|
9381
|
-
record: (key) => journal.record(key),
|
|
9382
|
-
pending: (entry) => journal.pending(entry),
|
|
9383
|
-
ready: (entry, id) => journal.ready(entry, id),
|
|
9384
|
-
lastDeployedAt: journal.existed ? journal.state.lastDeployedAt : void 0
|
|
9385
|
-
};
|
|
8760
|
+
logger.log(` Generated: ${COMPOSE_PATH} (project root)`);
|
|
8761
|
+
return (0, node_path.join)(workspace.root, COMPOSE_PATH);
|
|
9386
8762
|
}
|
|
9387
|
-
|
|
9388
|
-
|
|
9389
|
-
|
|
9390
|
-
|
|
9391
|
-
|
|
9392
|
-
pending: async () => {},
|
|
9393
|
-
ready: async () => {},
|
|
9394
|
-
...stored ? { lastDeployedAt: stored.state.lastDeployedAt } : {}
|
|
9395
|
-
};
|
|
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`);
|
|
9396
8768
|
}
|
|
9397
8769
|
//#endregion
|
|
9398
8770
|
//#region src/secrets/stale.ts
|
|
@@ -9573,12 +8945,6 @@ Object.defineProperty(exports, "DEV_SERVICES", {
|
|
|
9573
8945
|
return DEV_SERVICES;
|
|
9574
8946
|
}
|
|
9575
8947
|
});
|
|
9576
|
-
Object.defineProperty(exports, "DeployJournal", {
|
|
9577
|
-
enumerable: true,
|
|
9578
|
-
get: function() {
|
|
9579
|
-
return DeployJournal;
|
|
9580
|
-
}
|
|
9581
|
-
});
|
|
9582
8948
|
Object.defineProperty(exports, "DeploySeedsFailed", {
|
|
9583
8949
|
enumerable: true,
|
|
9584
8950
|
get: function() {
|
|
@@ -9609,12 +8975,6 @@ Object.defineProperty(exports, "DockerPushFailed", {
|
|
|
9609
8975
|
return DockerPushFailed;
|
|
9610
8976
|
}
|
|
9611
8977
|
});
|
|
9612
|
-
Object.defineProperty(exports, "DocumentStateStore", {
|
|
9613
|
-
enumerable: true,
|
|
9614
|
-
get: function() {
|
|
9615
|
-
return DocumentStateStore;
|
|
9616
|
-
}
|
|
9617
|
-
});
|
|
9618
8978
|
Object.defineProperty(exports, "EMULATOR_ACCESS_KEY_ID", {
|
|
9619
8979
|
enumerable: true,
|
|
9620
8980
|
get: function() {
|
|
@@ -9639,6 +8999,24 @@ Object.defineProperty(exports, "FileSecretsStore", {
|
|
|
9639
8999
|
return FileSecretsStore;
|
|
9640
9000
|
}
|
|
9641
9001
|
});
|
|
9002
|
+
Object.defineProperty(exports, "HostNotPointingAtServer", {
|
|
9003
|
+
enumerable: true,
|
|
9004
|
+
get: function() {
|
|
9005
|
+
return HostNotPointingAtServer;
|
|
9006
|
+
}
|
|
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
|
+
});
|
|
9642
9020
|
Object.defineProperty(exports, "LEGACY_LOCAL_LOGIN", {
|
|
9643
9021
|
enumerable: true,
|
|
9644
9022
|
get: function() {
|
|
@@ -9675,6 +9053,12 @@ Object.defineProperty(exports, "NoDomainForStage", {
|
|
|
9675
9053
|
return NoDomainForStage;
|
|
9676
9054
|
}
|
|
9677
9055
|
});
|
|
9056
|
+
Object.defineProperty(exports, "PROVIDER_KINDS", {
|
|
9057
|
+
enumerable: true,
|
|
9058
|
+
get: function() {
|
|
9059
|
+
return PROVIDER_KINDS;
|
|
9060
|
+
}
|
|
9061
|
+
});
|
|
9678
9062
|
Object.defineProperty(exports, "QueueGenerator", {
|
|
9679
9063
|
enumerable: true,
|
|
9680
9064
|
get: function() {
|
|
@@ -9705,36 +9089,6 @@ Object.defineProperty(exports, "StageProviderDisabled", {
|
|
|
9705
9089
|
return StageProviderDisabled;
|
|
9706
9090
|
}
|
|
9707
9091
|
});
|
|
9708
|
-
Object.defineProperty(exports, "StageStateMissing", {
|
|
9709
|
-
enumerable: true,
|
|
9710
|
-
get: function() {
|
|
9711
|
-
return StageStateMissing;
|
|
9712
|
-
}
|
|
9713
|
-
});
|
|
9714
|
-
Object.defineProperty(exports, "StateLocked", {
|
|
9715
|
-
enumerable: true,
|
|
9716
|
-
get: function() {
|
|
9717
|
-
return StateLocked;
|
|
9718
|
-
}
|
|
9719
|
-
});
|
|
9720
|
-
Object.defineProperty(exports, "StateStoreBusy", {
|
|
9721
|
-
enumerable: true,
|
|
9722
|
-
get: function() {
|
|
9723
|
-
return StateStoreBusy;
|
|
9724
|
-
}
|
|
9725
|
-
});
|
|
9726
|
-
Object.defineProperty(exports, "StateStoreWithoutLocking", {
|
|
9727
|
-
enumerable: true,
|
|
9728
|
-
get: function() {
|
|
9729
|
-
return StateStoreWithoutLocking;
|
|
9730
|
-
}
|
|
9731
|
-
});
|
|
9732
|
-
Object.defineProperty(exports, "StateVersionConflict", {
|
|
9733
|
-
enumerable: true,
|
|
9734
|
-
get: function() {
|
|
9735
|
-
return StateVersionConflict;
|
|
9736
|
-
}
|
|
9737
|
-
});
|
|
9738
9092
|
Object.defineProperty(exports, "TELEMETRY_CONTAINER", {
|
|
9739
9093
|
enumerable: true,
|
|
9740
9094
|
get: function() {
|
|
@@ -9801,6 +9155,12 @@ Object.defineProperty(exports, "applyPostgres", {
|
|
|
9801
9155
|
return applyPostgres;
|
|
9802
9156
|
}
|
|
9803
9157
|
});
|
|
9158
|
+
Object.defineProperty(exports, "asDokployState", {
|
|
9159
|
+
enumerable: true,
|
|
9160
|
+
get: function() {
|
|
9161
|
+
return asDokployState;
|
|
9162
|
+
}
|
|
9163
|
+
});
|
|
9804
9164
|
Object.defineProperty(exports, "assertDevServicesFlag", {
|
|
9805
9165
|
enumerable: true,
|
|
9806
9166
|
get: function() {
|
|
@@ -9837,6 +9197,12 @@ Object.defineProperty(exports, "bucketClient", {
|
|
|
9837
9197
|
return bucketClient;
|
|
9838
9198
|
}
|
|
9839
9199
|
});
|
|
9200
|
+
Object.defineProperty(exports, "bucketName", {
|
|
9201
|
+
enumerable: true,
|
|
9202
|
+
get: function() {
|
|
9203
|
+
return bucketName;
|
|
9204
|
+
}
|
|
9205
|
+
});
|
|
9840
9206
|
Object.defineProperty(exports, "bucketPolicies", {
|
|
9841
9207
|
enumerable: true,
|
|
9842
9208
|
get: function() {
|
|
@@ -9867,10 +9233,10 @@ Object.defineProperty(exports, "constructGlobs", {
|
|
|
9867
9233
|
return constructGlobs;
|
|
9868
9234
|
}
|
|
9869
9235
|
});
|
|
9870
|
-
Object.defineProperty(exports, "
|
|
9236
|
+
Object.defineProperty(exports, "createComposeState", {
|
|
9871
9237
|
enumerable: true,
|
|
9872
9238
|
get: function() {
|
|
9873
|
-
return
|
|
9239
|
+
return createComposeState;
|
|
9874
9240
|
}
|
|
9875
9241
|
});
|
|
9876
9242
|
Object.defineProperty(exports, "createCredentialsPreload", {
|
|
@@ -9879,12 +9245,6 @@ Object.defineProperty(exports, "createCredentialsPreload", {
|
|
|
9879
9245
|
return createCredentialsPreload;
|
|
9880
9246
|
}
|
|
9881
9247
|
});
|
|
9882
|
-
Object.defineProperty(exports, "createDnsProvider", {
|
|
9883
|
-
enumerable: true,
|
|
9884
|
-
get: function() {
|
|
9885
|
-
return createDnsProvider;
|
|
9886
|
-
}
|
|
9887
|
-
});
|
|
9888
9248
|
Object.defineProperty(exports, "createEmptyState", {
|
|
9889
9249
|
enumerable: true,
|
|
9890
9250
|
get: function() {
|
|
@@ -9897,10 +9257,10 @@ Object.defineProperty(exports, "createEntryWrapper", {
|
|
|
9897
9257
|
return createEntryWrapper;
|
|
9898
9258
|
}
|
|
9899
9259
|
});
|
|
9900
|
-
Object.defineProperty(exports, "
|
|
9260
|
+
Object.defineProperty(exports, "currentActor", {
|
|
9901
9261
|
enumerable: true,
|
|
9902
9262
|
get: function() {
|
|
9903
|
-
return
|
|
9263
|
+
return currentActor;
|
|
9904
9264
|
}
|
|
9905
9265
|
});
|
|
9906
9266
|
Object.defineProperty(exports, "deployIdentity", {
|
|
@@ -9909,12 +9269,24 @@ Object.defineProperty(exports, "deployIdentity", {
|
|
|
9909
9269
|
return deployIdentity;
|
|
9910
9270
|
}
|
|
9911
9271
|
});
|
|
9272
|
+
Object.defineProperty(exports, "deploysWithCompose", {
|
|
9273
|
+
enumerable: true,
|
|
9274
|
+
get: function() {
|
|
9275
|
+
return deploysWithCompose;
|
|
9276
|
+
}
|
|
9277
|
+
});
|
|
9912
9278
|
Object.defineProperty(exports, "derivedContainers", {
|
|
9913
9279
|
enumerable: true,
|
|
9914
9280
|
get: function() {
|
|
9915
9281
|
return derivedContainers;
|
|
9916
9282
|
}
|
|
9917
9283
|
});
|
|
9284
|
+
Object.defineProperty(exports, "describeActor", {
|
|
9285
|
+
enumerable: true,
|
|
9286
|
+
get: function() {
|
|
9287
|
+
return describeActor;
|
|
9288
|
+
}
|
|
9289
|
+
});
|
|
9918
9290
|
Object.defineProperty(exports, "describeServices", {
|
|
9919
9291
|
enumerable: true,
|
|
9920
9292
|
get: function() {
|
|
@@ -9933,6 +9305,30 @@ Object.defineProperty(exports, "devServicesUsed", {
|
|
|
9933
9305
|
return devServicesUsed;
|
|
9934
9306
|
}
|
|
9935
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
|
+
});
|
|
9320
|
+
Object.defineProperty(exports, "dnsResources", {
|
|
9321
|
+
enumerable: true,
|
|
9322
|
+
get: function() {
|
|
9323
|
+
return dnsResources;
|
|
9324
|
+
}
|
|
9325
|
+
});
|
|
9326
|
+
Object.defineProperty(exports, "dns_exports", {
|
|
9327
|
+
enumerable: true,
|
|
9328
|
+
get: function() {
|
|
9329
|
+
return dns_exports;
|
|
9330
|
+
}
|
|
9331
|
+
});
|
|
9936
9332
|
Object.defineProperty(exports, "dockerCommand", {
|
|
9937
9333
|
enumerable: true,
|
|
9938
9334
|
get: function() {
|
|
@@ -9975,12 +9371,6 @@ Object.defineProperty(exports, "externalServices", {
|
|
|
9975
9371
|
return externalServices;
|
|
9976
9372
|
}
|
|
9977
9373
|
});
|
|
9978
|
-
Object.defineProperty(exports, "extractSubdomain", {
|
|
9979
|
-
enumerable: true,
|
|
9980
|
-
get: function() {
|
|
9981
|
-
return extractSubdomain;
|
|
9982
|
-
}
|
|
9983
|
-
});
|
|
9984
9374
|
Object.defineProperty(exports, "fakeKey", {
|
|
9985
9375
|
enumerable: true,
|
|
9986
9376
|
get: function() {
|
|
@@ -9999,12 +9389,6 @@ Object.defineProperty(exports, "findBuildRoot", {
|
|
|
9999
9389
|
return findBuildRoot;
|
|
10000
9390
|
}
|
|
10001
9391
|
});
|
|
10002
|
-
Object.defineProperty(exports, "findRootDomain", {
|
|
10003
|
-
enumerable: true,
|
|
10004
|
-
get: function() {
|
|
10005
|
-
return findRootDomain;
|
|
10006
|
-
}
|
|
10007
|
-
});
|
|
10008
9392
|
Object.defineProperty(exports, "forgetState", {
|
|
10009
9393
|
enumerable: true,
|
|
10010
9394
|
get: function() {
|
|
@@ -10035,6 +9419,12 @@ Object.defineProperty(exports, "hostFor", {
|
|
|
10035
9419
|
return hostFor;
|
|
10036
9420
|
}
|
|
10037
9421
|
});
|
|
9422
|
+
Object.defineProperty(exports, "iamUserName", {
|
|
9423
|
+
enumerable: true,
|
|
9424
|
+
get: function() {
|
|
9425
|
+
return iamUserName;
|
|
9426
|
+
}
|
|
9427
|
+
});
|
|
10038
9428
|
Object.defineProperty(exports, "imageBuildCredentials", {
|
|
10039
9429
|
enumerable: true,
|
|
10040
9430
|
get: function() {
|
|
@@ -10065,12 +9455,24 @@ Object.defineProperty(exports, "initStageSecrets", {
|
|
|
10065
9455
|
return initStageSecrets;
|
|
10066
9456
|
}
|
|
10067
9457
|
});
|
|
9458
|
+
Object.defineProperty(exports, "isCi", {
|
|
9459
|
+
enumerable: true,
|
|
9460
|
+
get: function() {
|
|
9461
|
+
return isCi;
|
|
9462
|
+
}
|
|
9463
|
+
});
|
|
10068
9464
|
Object.defineProperty(exports, "isImageBuild", {
|
|
10069
9465
|
enumerable: true,
|
|
10070
9466
|
get: function() {
|
|
10071
9467
|
return isImageBuild;
|
|
10072
9468
|
}
|
|
10073
9469
|
});
|
|
9470
|
+
Object.defineProperty(exports, "isLocalHost", {
|
|
9471
|
+
enumerable: true,
|
|
9472
|
+
get: function() {
|
|
9473
|
+
return isLocalHost;
|
|
9474
|
+
}
|
|
9475
|
+
});
|
|
10074
9476
|
Object.defineProperty(exports, "isMainFrontendApp", {
|
|
10075
9477
|
enumerable: true,
|
|
10076
9478
|
get: function() {
|
|
@@ -10173,18 +9575,6 @@ Object.defineProperty(exports, "networkEnv", {
|
|
|
10173
9575
|
return networkEnv;
|
|
10174
9576
|
}
|
|
10175
9577
|
});
|
|
10176
|
-
Object.defineProperty(exports, "newLockHolder", {
|
|
10177
|
-
enumerable: true,
|
|
10178
|
-
get: function() {
|
|
10179
|
-
return newLockHolder;
|
|
10180
|
-
}
|
|
10181
|
-
});
|
|
10182
|
-
Object.defineProperty(exports, "normalizeDnsConfig", {
|
|
10183
|
-
enumerable: true,
|
|
10184
|
-
get: function() {
|
|
10185
|
-
return normalizeDnsConfig;
|
|
10186
|
-
}
|
|
10187
|
-
});
|
|
10188
9578
|
Object.defineProperty(exports, "orchestrateDns", {
|
|
10189
9579
|
enumerable: true,
|
|
10190
9580
|
get: function() {
|
|
@@ -10275,16 +9665,16 @@ Object.defineProperty(exports, "provisionCommand", {
|
|
|
10275
9665
|
return provisionCommand;
|
|
10276
9666
|
}
|
|
10277
9667
|
});
|
|
10278
|
-
Object.defineProperty(exports, "
|
|
9668
|
+
Object.defineProperty(exports, "quoteIdentifier", {
|
|
10279
9669
|
enumerable: true,
|
|
10280
9670
|
get: function() {
|
|
10281
|
-
return
|
|
9671
|
+
return quoteIdentifier;
|
|
10282
9672
|
}
|
|
10283
9673
|
});
|
|
10284
|
-
Object.defineProperty(exports, "
|
|
9674
|
+
Object.defineProperty(exports, "randomSuffix", {
|
|
10285
9675
|
enumerable: true,
|
|
10286
9676
|
get: function() {
|
|
10287
|
-
return
|
|
9677
|
+
return randomSuffix;
|
|
10288
9678
|
}
|
|
10289
9679
|
});
|
|
10290
9680
|
Object.defineProperty(exports, "readKey", {
|
|
@@ -10305,6 +9695,12 @@ Object.defineProperty(exports, "reconcileWorkspace", {
|
|
|
10305
9695
|
return reconcileWorkspace;
|
|
10306
9696
|
}
|
|
10307
9697
|
});
|
|
9698
|
+
Object.defineProperty(exports, "recordDnsChanges", {
|
|
9699
|
+
enumerable: true,
|
|
9700
|
+
get: function() {
|
|
9701
|
+
return recordDnsChanges;
|
|
9702
|
+
}
|
|
9703
|
+
});
|
|
10308
9704
|
Object.defineProperty(exports, "recordRelease", {
|
|
10309
9705
|
enumerable: true,
|
|
10310
9706
|
get: function() {
|
|
@@ -10341,6 +9737,12 @@ Object.defineProperty(exports, "reportPlannedSeeds", {
|
|
|
10341
9737
|
return reportPlannedSeeds;
|
|
10342
9738
|
}
|
|
10343
9739
|
});
|
|
9740
|
+
Object.defineProperty(exports, "requiredServerAddress", {
|
|
9741
|
+
enumerable: true,
|
|
9742
|
+
get: function() {
|
|
9743
|
+
return requiredServerAddress;
|
|
9744
|
+
}
|
|
9745
|
+
});
|
|
10344
9746
|
Object.defineProperty(exports, "requiredStageKeys", {
|
|
10345
9747
|
enumerable: true,
|
|
10346
9748
|
get: function() {
|
|
@@ -10377,6 +9779,12 @@ Object.defineProperty(exports, "secretsStoreFor", {
|
|
|
10377
9779
|
return secretsStoreFor;
|
|
10378
9780
|
}
|
|
10379
9781
|
});
|
|
9782
|
+
Object.defineProperty(exports, "serverAddressHint", {
|
|
9783
|
+
enumerable: true,
|
|
9784
|
+
get: function() {
|
|
9785
|
+
return serverAddressHint;
|
|
9786
|
+
}
|
|
9787
|
+
});
|
|
10380
9788
|
Object.defineProperty(exports, "setApplicationId", {
|
|
10381
9789
|
enumerable: true,
|
|
10382
9790
|
get: function() {
|
|
@@ -10401,6 +9809,12 @@ Object.defineProperty(exports, "setPostgresBackupId", {
|
|
|
10401
9809
|
return setPostgresBackupId;
|
|
10402
9810
|
}
|
|
10403
9811
|
});
|
|
9812
|
+
Object.defineProperty(exports, "stackHosts", {
|
|
9813
|
+
enumerable: true,
|
|
9814
|
+
get: function() {
|
|
9815
|
+
return stackHosts;
|
|
9816
|
+
}
|
|
9817
|
+
});
|
|
10404
9818
|
Object.defineProperty(exports, "stackLogs", {
|
|
10405
9819
|
enumerable: true,
|
|
10406
9820
|
get: function() {
|
|
@@ -10413,6 +9827,18 @@ Object.defineProperty(exports, "stackRedis", {
|
|
|
10413
9827
|
return stackRedis;
|
|
10414
9828
|
}
|
|
10415
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
|
+
});
|
|
10416
9842
|
Object.defineProperty(exports, "stageProviderNotes", {
|
|
10417
9843
|
enumerable: true,
|
|
10418
9844
|
get: function() {
|
|
@@ -10443,6 +9869,12 @@ Object.defineProperty(exports, "store_exports", {
|
|
|
10443
9869
|
return store_exports;
|
|
10444
9870
|
}
|
|
10445
9871
|
});
|
|
9872
|
+
Object.defineProperty(exports, "suffixedBucketName", {
|
|
9873
|
+
enumerable: true,
|
|
9874
|
+
get: function() {
|
|
9875
|
+
return suffixedBucketName;
|
|
9876
|
+
}
|
|
9877
|
+
});
|
|
10446
9878
|
Object.defineProperty(exports, "suppliedOnly", {
|
|
10447
9879
|
enumerable: true,
|
|
10448
9880
|
get: function() {
|
|
@@ -10455,6 +9887,12 @@ Object.defineProperty(exports, "surfaceClientTraced", {
|
|
|
10455
9887
|
return surfaceClientTraced;
|
|
10456
9888
|
}
|
|
10457
9889
|
});
|
|
9890
|
+
Object.defineProperty(exports, "systemLookup", {
|
|
9891
|
+
enumerable: true,
|
|
9892
|
+
get: function() {
|
|
9893
|
+
return systemLookup;
|
|
9894
|
+
}
|
|
9895
|
+
});
|
|
10458
9896
|
Object.defineProperty(exports, "telemetryOf", {
|
|
10459
9897
|
enumerable: true,
|
|
10460
9898
|
get: function() {
|
|
@@ -10485,12 +9923,6 @@ Object.defineProperty(exports, "verifyDnsRecords", {
|
|
|
10485
9923
|
return verifyDnsRecords;
|
|
10486
9924
|
}
|
|
10487
9925
|
});
|
|
10488
|
-
Object.defineProperty(exports, "verifyStageProviders", {
|
|
10489
|
-
enumerable: true,
|
|
10490
|
-
get: function() {
|
|
10491
|
-
return verifyStageProviders;
|
|
10492
|
-
}
|
|
10493
|
-
});
|
|
10494
9926
|
Object.defineProperty(exports, "withCustomSecret", {
|
|
10495
9927
|
enumerable: true,
|
|
10496
9928
|
get: function() {
|
|
@@ -10558,4 +9990,4 @@ Object.defineProperty(exports, "workspace_exports", {
|
|
|
10558
9990
|
}
|
|
10559
9991
|
});
|
|
10560
9992
|
|
|
10561
|
-
//# sourceMappingURL=seeds-
|
|
9993
|
+
//# sourceMappingURL=seeds-BCg8X36S.cjs.map
|