@geekmidas/cli 10.0.0-alpha.93 → 10.0.0-alpha.94
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{GoDaddyProvider-Mi3vptSR.mjs → GoDaddyProvider-BRZUWD8z.mjs} +3 -3
- package/dist/{GoDaddyProvider-Mi3vptSR.mjs.map → GoDaddyProvider-BRZUWD8z.mjs.map} +1 -1
- package/dist/{GoDaddyProvider-BZb-V14e.cjs → GoDaddyProvider-DzguxU_a.cjs} +3 -3
- package/dist/{GoDaddyProvider-BZb-V14e.cjs.map → GoDaddyProvider-DzguxU_a.cjs.map} +1 -1
- package/dist/{HostingerProvider-1mqD6aG8.mjs → HostingerProvider-Cjh_2Ml6.mjs} +9 -4
- package/dist/{HostingerProvider-1mqD6aG8.mjs.map → HostingerProvider-Cjh_2Ml6.mjs.map} +1 -1
- package/dist/{HostingerProvider-C6t-PSNe.cjs → HostingerProvider-OdQmKXE3.cjs} +9 -4
- package/dist/HostingerProvider-OdQmKXE3.cjs.map +1 -0
- package/dist/{LegacyStateStore-CtwO7fwy.mjs → LegacyStateStore-BQZb6-j2.mjs} +3 -2
- package/dist/{LegacyStateStore-CtwO7fwy.mjs.map → LegacyStateStore-BQZb6-j2.mjs.map} +1 -1
- package/dist/{LegacyStateStore-BlYXQQSE.cjs → LegacyStateStore-D50dgUPc.cjs} +10 -9
- package/dist/{LegacyStateStore-BlYXQQSE.cjs.map → LegacyStateStore-D50dgUPc.cjs.map} +1 -1
- package/dist/{LocalStateStore-D40_SDmJ.mjs → LocalStateStore-BSaS2535.mjs} +2 -2
- package/dist/{LocalStateStore-D40_SDmJ.mjs.map → LocalStateStore-BSaS2535.mjs.map} +1 -1
- package/dist/{LocalStateStore-oX_zxj9V.cjs → LocalStateStore-EFBUjuMX.cjs} +8 -8
- package/dist/{LocalStateStore-oX_zxj9V.cjs.map → LocalStateStore-EFBUjuMX.cjs.map} +1 -1
- package/dist/{S3StateStore-DeI7jtvi.cjs → S3StateStore-C5mv8To3.cjs} +5 -5
- package/dist/{S3StateStore-DeI7jtvi.cjs.map → S3StateStore-C5mv8To3.cjs.map} +1 -1
- package/dist/{S3StateStore-BzS0IeSt.mjs → S3StateStore-CrFq4UZU.mjs} +2 -2
- package/dist/{S3StateStore-BzS0IeSt.mjs.map → S3StateStore-CrFq4UZU.mjs.map} +1 -1
- package/dist/{SSMStateStore-k_Ew7TWx.mjs → SSMStateStore-D3Z6mZnA.mjs} +2 -2
- package/dist/{SSMStateStore-k_Ew7TWx.mjs.map → SSMStateStore-D3Z6mZnA.mjs.map} +1 -1
- package/dist/{SSMStateStore-CaCtrzFL.cjs → SSMStateStore-DuZCH7_v.cjs} +9 -9
- package/dist/{SSMStateStore-CaCtrzFL.cjs.map → SSMStateStore-DuZCH7_v.cjs.map} +1 -1
- package/dist/{add-CP1podDH.mjs → add-C85isXFl.mjs} +4 -4
- package/dist/add-C85isXFl.mjs.map +1 -0
- package/dist/{add-VcyJnsPX.cjs → add-DsVDdvon.cjs} +4 -4
- package/dist/add-DsVDdvon.cjs.map +1 -0
- package/dist/{bundler-Dfsr6KXU.cjs → bundler-BEgUKc4j.cjs} +5 -5
- package/dist/{bundler-Dfsr6KXU.cjs.map → bundler-BEgUKc4j.cjs.map} +1 -1
- package/dist/{bundler-BWrh-AJH.mjs → bundler-BSHog8CH.mjs} +5 -5
- package/dist/{bundler-BWrh-AJH.mjs.map → bundler-BSHog8CH.mjs.map} +1 -1
- package/dist/{cli-B8otnZfX.mjs → cli-BsFenyaf.mjs} +9 -5
- package/dist/cli-BsFenyaf.mjs.map +1 -0
- package/dist/{cli-DL19w4OE.cjs → cli-BvBdv660.cjs} +9 -5
- package/dist/cli-BvBdv660.cjs.map +1 -0
- package/dist/{compose-BHVKLluX.cjs → compose-Cd09gW-n.cjs} +58 -19
- package/dist/compose-Cd09gW-n.cjs.map +1 -0
- package/dist/{compose-DrEB_4F-.mjs → compose-D2RFYd_f.mjs} +56 -17
- package/dist/compose-D2RFYd_f.mjs.map +1 -0
- package/dist/{config-DCCcrOSo.d.cts → config-DvHNMoOp.d.cts} +4 -4
- package/dist/{config-CHX5GOo3.d.mts.map → config-DvHNMoOp.d.cts.map} +1 -1
- package/dist/{config-CHX5GOo3.d.mts → config-Ngesg4O8.d.mts} +4 -4
- package/dist/{config-DCCcrOSo.d.cts.map → config-Ngesg4O8.d.mts.map} +1 -1
- package/dist/config.cjs +1 -1
- package/dist/config.d.cts +3 -3
- package/dist/config.d.mts +3 -3
- package/dist/config.mjs +1 -1
- package/dist/{credentials-QBu7VgaQ.cjs → credentials-D7XJypNd.cjs} +9 -3
- package/dist/{credentials-QBu7VgaQ.cjs.map → credentials-D7XJypNd.cjs.map} +1 -1
- package/dist/{credentials-C4KII3PD.mjs → credentials-DpfnUvIR.mjs} +9 -3
- package/dist/{credentials-C4KII3PD.mjs.map → credentials-DpfnUvIR.mjs.map} +1 -1
- package/dist/deploy/public.cjs +13 -13
- package/dist/deploy/public.d.cts +24 -9
- package/dist/deploy/public.d.cts.map +1 -1
- package/dist/deploy/public.d.mts +24 -9
- package/dist/deploy/public.d.mts.map +1 -1
- package/dist/deploy/public.mjs +10 -10
- package/dist/{deploy-C3Mhnmus.cjs → deploy-C911Akkn.cjs} +125 -11
- package/dist/deploy-C911Akkn.cjs.map +1 -0
- package/dist/{deploy-DYIyIUYd.mjs → deploy-CeJ5D0xn.mjs} +123 -9
- package/dist/deploy-CeJ5D0xn.mjs.map +1 -0
- package/dist/{discover-vk2r5giK.d.cts → discover-CTzOSGUF.d.cts} +2 -2
- package/dist/{discover-DqwlBVyY.d.mts.map → discover-CTzOSGUF.d.cts.map} +1 -1
- package/dist/{discover-DqwlBVyY.d.mts → discover-CihAtaoj.d.mts} +2 -2
- package/dist/{discover-vk2r5giK.d.cts.map → discover-CihAtaoj.d.mts.map} +1 -1
- package/dist/{dokploy-rivb1RFt.cjs → dokploy-CV7GcobZ.cjs} +2 -2
- package/dist/{dokploy-rivb1RFt.cjs.map → dokploy-CV7GcobZ.cjs.map} +1 -1
- package/dist/{dokploy-o4I6vb9W.mjs → dokploy-D_aat2Xj.mjs} +2 -2
- package/dist/{dokploy-o4I6vb9W.mjs.map → dokploy-D_aat2Xj.mjs.map} +1 -1
- package/dist/{encryption-D7zmwzWz.cjs → encryption-DFcivJaO.cjs} +3 -3
- package/dist/{encryption-D7zmwzWz.cjs.map → encryption-DFcivJaO.cjs.map} +1 -1
- package/dist/{encryption-B6ilrT7p.mjs → encryption-_4qKqtTx.mjs} +3 -3
- package/dist/{encryption-B6ilrT7p.mjs.map → encryption-_4qKqtTx.mjs.map} +1 -1
- package/dist/{engine-Cfx8fEZU.cjs → engine-Bv-LZpmj.cjs} +10 -9
- package/dist/{engine-Cfx8fEZU.cjs.map → engine-Bv-LZpmj.cjs.map} +1 -1
- package/dist/{engine-DjEcovm-.mjs → engine-DPMHThkg.mjs} +7 -6
- package/dist/{engine-DjEcovm-.mjs.map → engine-DPMHThkg.mjs.map} +1 -1
- package/dist/{github-zneTMvnO.cjs → github-Br42NMrW.cjs} +94 -6
- package/dist/github-Br42NMrW.cjs.map +1 -0
- package/dist/{github-CzBbs43W.mjs → github-xsDT19im.mjs} +94 -6
- package/dist/github-xsDT19im.mjs.map +1 -0
- package/dist/{index-BiYU7cUl.d.mts → index-BlpcClwM.d.mts} +2 -2
- package/dist/{index-BiYU7cUl.d.mts.map → index-BlpcClwM.d.mts.map} +1 -1
- package/dist/{index-Dm6V9bwh.d.cts → index-CwLvFqtH.d.cts} +2 -2
- package/dist/{index-Dm6V9bwh.d.cts.map → index-CwLvFqtH.d.cts.map} +1 -1
- package/dist/index.cjs +147 -116
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +143 -112
- package/dist/index.mjs.map +1 -1
- package/dist/{local-Otp74ak0.mjs → local-B7Zv1NoA.mjs} +2 -2
- package/dist/{local-Otp74ak0.mjs.map → local-B7Zv1NoA.mjs.map} +1 -1
- package/dist/{local-B61kUh0w.cjs → local-OPC9BCZt.cjs} +2 -2
- package/dist/{local-B61kUh0w.cjs.map → local-OPC9BCZt.cjs.map} +1 -1
- package/dist/{migrate-BaC4w0sb.mjs → migrate-B7evHv7O.mjs} +3 -3
- package/dist/{migrate-BaC4w0sb.mjs.map → migrate-B7evHv7O.mjs.map} +1 -1
- package/dist/{migrate-C8wTpakT.cjs → migrate-D5jUz20_.cjs} +3 -3
- package/dist/{migrate-C8wTpakT.cjs.map → migrate-D5jUz20_.cjs.map} +1 -1
- package/dist/{migrate-CsBZKALY.mjs → migrate-DhvFPGLi.mjs} +3 -3
- package/dist/{migrate-CsBZKALY.mjs.map → migrate-DhvFPGLi.mjs.map} +1 -1
- package/dist/{migrate-DFjbGnSr.cjs → migrate-SkWJotn5.cjs} +3 -3
- package/dist/{migrate-DFjbGnSr.cjs.map → migrate-SkWJotn5.cjs.map} +1 -1
- package/dist/openapi.cjs +1 -1
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.mjs +1 -1
- package/dist/providers-BFeP_mYw.cjs +1098 -0
- package/dist/providers-BFeP_mYw.cjs.map +1 -0
- package/dist/providers-sTj-BG0S.mjs +1079 -0
- package/dist/providers-sTj-BG0S.mjs.map +1 -0
- package/dist/reconcile/public.d.cts +2 -2
- package/dist/reconcile/public.d.mts +2 -2
- package/dist/{resolve-g8htuR80.cjs → resolve-CFEOtRsl.cjs} +4 -4
- package/dist/{resolve-g8htuR80.cjs.map → resolve-CFEOtRsl.cjs.map} +1 -1
- package/dist/{resolve-Ccjlpnd9.mjs → resolve-Dyd1S2wn.mjs} +4 -4
- package/dist/{resolve-Ccjlpnd9.mjs.map → resolve-Dyd1S2wn.mjs.map} +1 -1
- package/dist/{rollback--ympG9ss.cjs → rollback-D720SFhV.cjs} +11 -9
- package/dist/{rollback--ympG9ss.cjs.map → rollback-D720SFhV.cjs.map} +1 -1
- package/dist/{rollback-Drbg0OeR.mjs → rollback-NroNbGC0.mjs} +8 -6
- package/dist/{rollback-Drbg0OeR.mjs.map → rollback-NroNbGC0.mjs.map} +1 -1
- package/dist/{runtime-_wEVtshH.d.mts → runtime-BRCq6dQ7.d.mts} +2 -2
- package/dist/{runtime-D_J3ExJn.d.cts.map → runtime-BRCq6dQ7.d.mts.map} +1 -1
- package/dist/{runtime-D_J3ExJn.d.cts → runtime-VktAo_8B.d.cts} +2 -2
- package/dist/{runtime-_wEVtshH.d.mts.map → runtime-VktAo_8B.d.cts.map} +1 -1
- package/dist/{sandbox-CPJq1Kia.d.mts → sandbox-D1gd0iDz.d.cts} +2 -2
- package/dist/{sandbox-CPJq1Kia.d.mts.map → sandbox-D1gd0iDz.d.cts.map} +1 -1
- package/dist/{sandbox-JwuPAl7I.d.cts → sandbox-zHnRLCj0.d.mts} +2 -2
- package/dist/{sandbox-JwuPAl7I.d.cts.map → sandbox-zHnRLCj0.d.mts.map} +1 -1
- package/dist/{secrets-DbPmAS4k.mjs → secrets-CwLauUB_.mjs} +3 -3
- package/dist/secrets-CwLauUB_.mjs.map +1 -0
- package/dist/{secrets-DJteijZo.cjs → secrets-DAvi_O9g.cjs} +3 -3
- package/dist/secrets-DAvi_O9g.cjs.map +1 -0
- package/dist/{seeds-c-ByX9je.cjs → seeds-BCg8X36S.cjs} +1143 -2645
- package/dist/seeds-BCg8X36S.cjs.map +1 -0
- package/dist/{seeds-BSL55F6q.mjs → seeds-CTxTZa-R.mjs} +1289 -2760
- package/dist/seeds-CTxTZa-R.mjs.map +1 -0
- package/dist/{sniffer-Dq5BS98f.cjs → sniffer-C0z3NB2A.cjs} +572 -3
- package/dist/sniffer-C0z3NB2A.cjs.map +1 -0
- package/dist/{sniffer-BTtGAtm3.mjs → sniffer-DCcuzHz_.mjs} +496 -5
- package/dist/sniffer-DCcuzHz_.mjs.map +1 -0
- package/dist/{sst-CnKY-CSh.cjs → sst-DKNH243z.cjs} +3 -3
- package/dist/{sst-CnKY-CSh.cjs.map → sst-DKNH243z.cjs.map} +1 -1
- package/dist/{sst-BaRXp2Dd.mjs → sst-DRDSDwOk.mjs} +3 -3
- package/dist/{sst-BaRXp2Dd.mjs.map → sst-DRDSDwOk.mjs.map} +1 -1
- package/dist/{stack-1PeLJS2j.cjs → stack-B3fPDTBg.cjs} +3 -3
- package/dist/{stack-1PeLJS2j.cjs.map → stack-B3fPDTBg.cjs.map} +1 -1
- package/dist/{stack-Co4uZkAo.mjs → stack-NrdH5ngX.mjs} +3 -3
- package/dist/{stack-Co4uZkAo.mjs.map → stack-NrdH5ngX.mjs.map} +1 -1
- package/dist/{stages-BI8JvPzI.cjs → stages-CkqP1N4S.cjs} +16 -3
- package/dist/stages-CkqP1N4S.cjs.map +1 -0
- package/dist/{stages-DY-fQKmo.mjs → stages-veC4TcQe.mjs} +16 -3
- package/dist/stages-veC4TcQe.mjs.map +1 -0
- package/dist/target/public.cjs +1 -1
- package/dist/target/public.d.cts +2 -2
- package/dist/target/public.d.mts +2 -2
- package/dist/target/public.mjs +1 -1
- package/dist/{terminal-8XngdSTo.cjs → terminal-CkUYuAPd.cjs} +2 -2
- package/dist/{terminal-8XngdSTo.cjs.map → terminal-CkUYuAPd.cjs.map} +1 -1
- package/dist/{terminal-C4XRdwnU.mjs → terminal-DAHs2Zko.mjs} +2 -2
- package/dist/{terminal-C4XRdwnU.mjs.map → terminal-DAHs2Zko.mjs.map} +1 -1
- package/dist/{types-BibpiHKB.d.cts → types-DMoaFrjg.d.mts} +32 -9
- package/dist/types-DMoaFrjg.d.mts.map +1 -0
- package/dist/{types-B6lmD3en.d.mts → types-DXyaSmqu.d.cts} +33 -8
- package/dist/types-DXyaSmqu.d.cts.map +1 -0
- package/dist/workspace/index.cjs +1 -1
- package/dist/workspace/index.d.cts +2 -2
- package/dist/workspace/index.d.mts +2 -2
- package/dist/workspace/index.mjs +1 -1
- package/dist/{workspace-Bzw7Zs82.mjs → workspace-DWK1PTXP.mjs} +4 -4
- package/dist/workspace-DWK1PTXP.mjs.map +1 -0
- package/dist/{workspace-CHOB2Xg3.cjs → workspace-cSyi5nb1.cjs} +4 -4
- package/dist/workspace-cSyi5nb1.cjs.map +1 -0
- package/package.json +15 -15
- package/dist/HostingerProvider-C6t-PSNe.cjs.map +0 -1
- package/dist/add-CP1podDH.mjs.map +0 -1
- package/dist/add-VcyJnsPX.cjs.map +0 -1
- package/dist/cli-B8otnZfX.mjs.map +0 -1
- package/dist/cli-DL19w4OE.cjs.map +0 -1
- package/dist/compose-BHVKLluX.cjs.map +0 -1
- package/dist/compose-DrEB_4F-.mjs.map +0 -1
- package/dist/deploy-C3Mhnmus.cjs.map +0 -1
- package/dist/deploy-DYIyIUYd.mjs.map +0 -1
- package/dist/github-CzBbs43W.mjs.map +0 -1
- package/dist/github-zneTMvnO.cjs.map +0 -1
- package/dist/secrets-DJteijZo.cjs.map +0 -1
- package/dist/secrets-DbPmAS4k.mjs.map +0 -1
- package/dist/seeds-BSL55F6q.mjs.map +0 -1
- package/dist/seeds-c-ByX9je.cjs.map +0 -1
- package/dist/sniffer-BTtGAtm3.mjs.map +0 -1
- package/dist/sniffer-Dq5BS98f.cjs.map +0 -1
- package/dist/stages-BI8JvPzI.cjs.map +0 -1
- package/dist/stages-DY-fQKmo.mjs.map +0 -1
- package/dist/types-B6lmD3en.d.mts.map +0 -1
- package/dist/types-BibpiHKB.d.cts.map +0 -1
- package/dist/workspace-Bzw7Zs82.mjs.map +0 -1
- package/dist/workspace-CHOB2Xg3.cjs.map +0 -1
|
@@ -0,0 +1,1098 @@
|
|
|
1
|
+
const require_rolldown_runtime = require("./rolldown-runtime-C0BPl7ul.cjs");
|
|
2
|
+
const require_errors = require("./errors-DMC4Umsb.cjs");
|
|
3
|
+
const require_credentials = require("./credentials-D7XJypNd.cjs");
|
|
4
|
+
const require_derive = require("./derive-DAbo49sm.cjs");
|
|
5
|
+
const require_workspace = require("./workspace-cSyi5nb1.cjs");
|
|
6
|
+
const require_seeds = require("./seeds-BCg8X36S.cjs");
|
|
7
|
+
let _geekmidas_manifest = require("@geekmidas/manifest");
|
|
8
|
+
let _geekmidas_storage_s3_url = require("@geekmidas/storage/s3-url");
|
|
9
|
+
_geekmidas_storage_s3_url = require_rolldown_runtime.__toESM(_geekmidas_storage_s3_url, 1);
|
|
10
|
+
//#region src/deploy/journal.ts
|
|
11
|
+
var DeployJournal = class DeployJournal {
|
|
12
|
+
store;
|
|
13
|
+
stage;
|
|
14
|
+
state;
|
|
15
|
+
resources;
|
|
16
|
+
version;
|
|
17
|
+
existed;
|
|
18
|
+
constructor(store, stage, state, resources, version, existed) {
|
|
19
|
+
this.store = store;
|
|
20
|
+
this.stage = stage;
|
|
21
|
+
this.state = state;
|
|
22
|
+
this.resources = resources;
|
|
23
|
+
this.version = version;
|
|
24
|
+
this.existed = existed;
|
|
25
|
+
}
|
|
26
|
+
static async open(store, stage, initial, adopt = (stored) => stored) {
|
|
27
|
+
const stored = await store.read(stage);
|
|
28
|
+
if (stored) return new DeployJournal(store, stage, adopt(stored.state), stored.resources, stored.version, true);
|
|
29
|
+
const state = initial();
|
|
30
|
+
const version = await store.write(stage, state, { expectedVersion: null });
|
|
31
|
+
return new DeployJournal(store, stage, state, {}, version, false);
|
|
32
|
+
}
|
|
33
|
+
record(key) {
|
|
34
|
+
return this.resources[key];
|
|
35
|
+
}
|
|
36
|
+
/** The keys a previous run marked `pending` and never saw created. */
|
|
37
|
+
unfinished() {
|
|
38
|
+
return Object.values(this.resources).filter((record) => record.status === "pending").map((record) => record.key);
|
|
39
|
+
}
|
|
40
|
+
/** Records that `entry` is about to be created. */
|
|
41
|
+
async pending(entry) {
|
|
42
|
+
await this.put({
|
|
43
|
+
...entry,
|
|
44
|
+
status: "pending"
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Records `entry` as existing with `id`; a no-op if it already is, with
|
|
49
|
+
* the same data.
|
|
50
|
+
*/
|
|
51
|
+
async ready(entry, id) {
|
|
52
|
+
const current = this.resources[entry.key];
|
|
53
|
+
if (current?.status === "ready" && current.id === id && JSON.stringify(current.data) === JSON.stringify(entry.data)) return;
|
|
54
|
+
await this.put({
|
|
55
|
+
...entry,
|
|
56
|
+
status: "ready",
|
|
57
|
+
id
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The resource `entry` names: by its recorded id, else by looking it up,
|
|
62
|
+
* else created — with `pending` written before the create and `ready`
|
|
63
|
+
* after, so a run that dies in between leaves a record the next run
|
|
64
|
+
* resolves by looking the resource up rather than creating another.
|
|
65
|
+
*/
|
|
66
|
+
async ensure(entry, steps) {
|
|
67
|
+
const record = this.resources[entry.key];
|
|
68
|
+
let staleId;
|
|
69
|
+
if (record?.status === "ready" && record.id && steps.get) {
|
|
70
|
+
const known = await steps.get(record.id);
|
|
71
|
+
if (known) {
|
|
72
|
+
await this.ready(entry, steps.id(known));
|
|
73
|
+
return {
|
|
74
|
+
resource: known,
|
|
75
|
+
via: "recorded"
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
staleId = record.id;
|
|
79
|
+
}
|
|
80
|
+
const existing = await steps.find();
|
|
81
|
+
if (existing) {
|
|
82
|
+
await this.ready(entry, steps.id(existing));
|
|
83
|
+
return {
|
|
84
|
+
resource: existing,
|
|
85
|
+
via: record?.status === "pending" ? "resumed" : "found",
|
|
86
|
+
...staleId ? { staleId } : {}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
await this.pending(entry);
|
|
90
|
+
const created = await steps.create();
|
|
91
|
+
await this.ready(entry, steps.id(created));
|
|
92
|
+
return {
|
|
93
|
+
resource: created,
|
|
94
|
+
via: "created",
|
|
95
|
+
...staleId ? { staleId } : {}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/** Removes `key`'s record — a resource that no longer exists. */
|
|
99
|
+
async forget(key) {
|
|
100
|
+
if (!(key in this.resources)) return;
|
|
101
|
+
this.version = await this.store.deleteResource(this.stage, key, { expectedVersion: this.version });
|
|
102
|
+
const { [key]: _removed, ...resources } = this.resources;
|
|
103
|
+
this.resources = resources;
|
|
104
|
+
}
|
|
105
|
+
/** Every record, as the journal last wrote or read it. */
|
|
106
|
+
records() {
|
|
107
|
+
return this.resources;
|
|
108
|
+
}
|
|
109
|
+
/** Writes the stage's state, keeping its resource records. */
|
|
110
|
+
async save() {
|
|
111
|
+
this.state.lastDeployedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
112
|
+
this.version = await this.store.write(this.stage, this.state, { expectedVersion: this.version });
|
|
113
|
+
}
|
|
114
|
+
async put(record) {
|
|
115
|
+
this.version = await this.store.putResource(this.stage, record, { expectedVersion: this.version });
|
|
116
|
+
this.resources = {
|
|
117
|
+
...this.resources,
|
|
118
|
+
[record.key]: {
|
|
119
|
+
...record,
|
|
120
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
//#endregion
|
|
126
|
+
//#region src/providers/aws.ts
|
|
127
|
+
/**
|
|
128
|
+
* The AWS account a provider provisions in — the stage's, found the way the
|
|
129
|
+
* SSM and Secrets Manager stores find it.
|
|
130
|
+
*
|
|
131
|
+
* 1. `--profile`, resolved on its own: exported keys left over from another
|
|
132
|
+
* account never win over a profile someone named.
|
|
133
|
+
* 2. `AWS_PROFILE`, else `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY` — the
|
|
134
|
+
* deploy's own `aws` credential kind (`storedCredentials`).
|
|
135
|
+
* 3. The SDK's default chain: `~/.aws` default profile, SSO, a role. In CI,
|
|
136
|
+
* whatever `aws-actions/configure-aws-credentials` exported.
|
|
137
|
+
*
|
|
138
|
+
* None of them: the provider has no provisioning credentials, and the stage
|
|
139
|
+
* is treated as `external`.
|
|
140
|
+
*/
|
|
141
|
+
async function awsProvisioningCredentials(lookup) {
|
|
142
|
+
const region = lookup.env.AWS_REGION ?? lookup.env.AWS_DEFAULT_REGION;
|
|
143
|
+
const where = region ? { region } : {};
|
|
144
|
+
if (lookup.profile) return {
|
|
145
|
+
profile: lookup.profile,
|
|
146
|
+
...where
|
|
147
|
+
};
|
|
148
|
+
const stored = await require_credentials.storedCredentials({
|
|
149
|
+
env: lookup.env,
|
|
150
|
+
...lookup.home ? { home: lookup.home } : {}
|
|
151
|
+
}).get({
|
|
152
|
+
kind: "aws",
|
|
153
|
+
stage: lookup.stage
|
|
154
|
+
});
|
|
155
|
+
if (stored) return stored;
|
|
156
|
+
try {
|
|
157
|
+
const { fromNodeProviderChain } = await import("@aws-sdk/credential-providers");
|
|
158
|
+
await fromNodeProviderChain({
|
|
159
|
+
timeout: 1e3,
|
|
160
|
+
maxRetries: 0
|
|
161
|
+
})();
|
|
162
|
+
return {
|
|
163
|
+
chain: true,
|
|
164
|
+
...where
|
|
165
|
+
};
|
|
166
|
+
} catch {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* An S3-compatible endpoint the environment points the SDK at — the AWS
|
|
172
|
+
* emulator in tests. The SDK reads it itself; it is read here too, because
|
|
173
|
+
* an emulator takes path-style requests, and a key written for one has to
|
|
174
|
+
* say where it is.
|
|
175
|
+
*/
|
|
176
|
+
function awsEndpoint(env, service) {
|
|
177
|
+
return env[`AWS_ENDPOINT_URL_${service}`] ?? env.AWS_ENDPOINT_URL;
|
|
178
|
+
}
|
|
179
|
+
/** An SDK client's config for a provisioning credential. */
|
|
180
|
+
async function awsClientConfig(credential, region) {
|
|
181
|
+
if ("profile" in credential) {
|
|
182
|
+
const { fromIni } = await import("@aws-sdk/credential-providers");
|
|
183
|
+
return {
|
|
184
|
+
region,
|
|
185
|
+
credentials: fromIni({ profile: credential.profile })
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
if ("accessKeyId" in credential) return {
|
|
189
|
+
region,
|
|
190
|
+
credentials: {
|
|
191
|
+
accessKeyId: credential.accessKeyId,
|
|
192
|
+
secretAccessKey: credential.secretAccessKey,
|
|
193
|
+
...credential.sessionToken ? { sessionToken: credential.sessionToken } : {}
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
return { region };
|
|
197
|
+
}
|
|
198
|
+
//#endregion
|
|
199
|
+
//#region src/providers/s3/origins.ts
|
|
200
|
+
/**
|
|
201
|
+
* Which browser origins may call a bucket: the stage's sites that depend on
|
|
202
|
+
* an API using it.
|
|
203
|
+
*
|
|
204
|
+
* The same reading of the graph an API's own CORS origins are — its callers —
|
|
205
|
+
* taken one step further: a site that calls an API which hands it a presigned
|
|
206
|
+
* upload URL is a site whose browser PUTs to the bucket. A site that reads the
|
|
207
|
+
* bucket (or a file server over it) itself is one too. Nothing is written
|
|
208
|
+
* down: add a site that calls the API and the next provision run allows it.
|
|
209
|
+
*/
|
|
210
|
+
/** The keys a reader of this bucket holds: its URL, and each server's over it. */
|
|
211
|
+
function bucketKeys(manifest, bucket) {
|
|
212
|
+
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"))];
|
|
213
|
+
}
|
|
214
|
+
function bucketOrigins(input) {
|
|
215
|
+
const { manifest, workspace, stage } = input;
|
|
216
|
+
const keys = bucketKeys(manifest, input.bucket);
|
|
217
|
+
const readers = Object.entries(manifest).filter(([, d]) => d.kind === "rest-api" || d.kind === "site").filter(([id]) => {
|
|
218
|
+
const env = require_seeds.appEnvKeys(manifest, require_derive.appKey(id), input.runnables);
|
|
219
|
+
return env !== void 0 && keys.some((key) => env.has(key));
|
|
220
|
+
});
|
|
221
|
+
const sites = /* @__PURE__ */ new Set();
|
|
222
|
+
for (const [id, declaration] of readers) {
|
|
223
|
+
if (declaration.kind === "site") {
|
|
224
|
+
sites.add(id);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
for (const caller of (0, _geekmidas_manifest.dependentsOf)(manifest, id)) if (manifest[caller]?.kind === "site") sites.add(caller);
|
|
228
|
+
}
|
|
229
|
+
const origins = /* @__PURE__ */ new Set();
|
|
230
|
+
const unresolved = [];
|
|
231
|
+
for (const id of [...sites].sort()) {
|
|
232
|
+
const name = require_derive.appKey(id);
|
|
233
|
+
const app = workspace.apps[name];
|
|
234
|
+
if (!app) {
|
|
235
|
+
unresolved.push(name);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
const host = require_seeds.resolveHost(name, app, stage, workspace.domains, require_seeds.isMainFrontendApp(name, app, workspace.apps));
|
|
240
|
+
origins.add(`https://${host}`);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
if (!(error instanceof require_seeds.NoDomainForStage)) throw error;
|
|
243
|
+
unresolved.push(name);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
origins: [...origins].sort(),
|
|
248
|
+
unresolved
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
//#endregion
|
|
252
|
+
//#region src/providers/s3/policy.ts
|
|
253
|
+
const bucketArn = (bucket) => `arn:aws:s3:::${bucket}`;
|
|
254
|
+
/**
|
|
255
|
+
* What the app's key may do: objects in this bucket, and listing it — and
|
|
256
|
+
* nothing else in the account.
|
|
257
|
+
*
|
|
258
|
+
* Multipart uploads are in it because a presigned upload of a large file is
|
|
259
|
+
* one, and an abandoned one is cleaned up by the app. Reading old versions is
|
|
260
|
+
* added only when the bucket keeps them.
|
|
261
|
+
*/
|
|
262
|
+
function userPolicy(bucket, options = {}) {
|
|
263
|
+
return {
|
|
264
|
+
Version: "2012-10-17",
|
|
265
|
+
Statement: [{
|
|
266
|
+
Sid: "GkmObjects",
|
|
267
|
+
Effect: "Allow",
|
|
268
|
+
Action: [
|
|
269
|
+
"s3:GetObject",
|
|
270
|
+
"s3:PutObject",
|
|
271
|
+
"s3:DeleteObject",
|
|
272
|
+
"s3:AbortMultipartUpload",
|
|
273
|
+
"s3:ListMultipartUploadParts",
|
|
274
|
+
...options.versioning ? ["s3:GetObjectVersion"] : []
|
|
275
|
+
],
|
|
276
|
+
Resource: `${bucketArn(bucket)}/*`
|
|
277
|
+
}, {
|
|
278
|
+
Sid: "GkmBucket",
|
|
279
|
+
Effect: "Allow",
|
|
280
|
+
Action: [
|
|
281
|
+
"s3:ListBucket",
|
|
282
|
+
"s3:GetBucketLocation",
|
|
283
|
+
"s3:ListBucketMultipartUploads"
|
|
284
|
+
],
|
|
285
|
+
Resource: bucketArn(bucket)
|
|
286
|
+
}]
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* One open pattern as the resource it is public on. A `**` is S3's `*`; a
|
|
291
|
+
* single `*` is too, which crosses `/` where the construct's own check does
|
|
292
|
+
* not — the construct is the stricter of the two (see `bucketPolicy` in
|
|
293
|
+
* reconcile, which the local MinIO is given the same way).
|
|
294
|
+
*/
|
|
295
|
+
function openResource(bucket, pattern) {
|
|
296
|
+
const path = pattern.replace(/^\/+/, "").replace(/\*\*/g, "*");
|
|
297
|
+
return `${bucketArn(bucket)}/${path}`;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* The bucket's own policy: every request refused unless it came over TLS,
|
|
301
|
+
* and — only when a file server opens paths on it — anonymous `s3:GetObject`
|
|
302
|
+
* on exactly those prefixes.
|
|
303
|
+
*/
|
|
304
|
+
function bucketPolicy(bucket, open = []) {
|
|
305
|
+
const statements = [{
|
|
306
|
+
Sid: "GkmDenyInsecureTransport",
|
|
307
|
+
Effect: "Deny",
|
|
308
|
+
Principal: "*",
|
|
309
|
+
Action: "s3:*",
|
|
310
|
+
Resource: [bucketArn(bucket), `${bucketArn(bucket)}/*`],
|
|
311
|
+
Condition: { Bool: { "aws:SecureTransport": "false" } }
|
|
312
|
+
}];
|
|
313
|
+
if (open.length > 0) statements.push({
|
|
314
|
+
Sid: "GkmOpenPaths",
|
|
315
|
+
Effect: "Allow",
|
|
316
|
+
Principal: "*",
|
|
317
|
+
Action: "s3:GetObject",
|
|
318
|
+
Resource: [...new Set(open.map((p) => openResource(bucket, p)))].sort()
|
|
319
|
+
});
|
|
320
|
+
return {
|
|
321
|
+
Version: "2012-10-17",
|
|
322
|
+
Statement: statements
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Block Public Access. All four flags on, unless the bucket has open paths:
|
|
327
|
+
* then a *policy* may make it public (`BlockPublicPolicy`,
|
|
328
|
+
* `RestrictPublicBuckets` off), and ACLs still may not.
|
|
329
|
+
*/
|
|
330
|
+
function publicAccessBlock(open) {
|
|
331
|
+
return {
|
|
332
|
+
BlockPublicAcls: true,
|
|
333
|
+
IgnorePublicAcls: true,
|
|
334
|
+
BlockPublicPolicy: !open,
|
|
335
|
+
RestrictPublicBuckets: !open
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
function corsRule(origins) {
|
|
339
|
+
return {
|
|
340
|
+
AllowedMethods: [
|
|
341
|
+
"GET",
|
|
342
|
+
"HEAD",
|
|
343
|
+
"POST",
|
|
344
|
+
"PUT"
|
|
345
|
+
],
|
|
346
|
+
AllowedOrigins: [...new Set(origins)].sort(),
|
|
347
|
+
AllowedHeaders: ["*"],
|
|
348
|
+
ExposeHeaders: ["ETag"],
|
|
349
|
+
MaxAgeSeconds: 3e3
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* A policy document in one canonical form — keys sorted, a lone string a
|
|
354
|
+
* one-element list, lists sorted — so one AWS reformatted reads as the same
|
|
355
|
+
* document rather than as drift.
|
|
356
|
+
*/
|
|
357
|
+
function canonicalPolicy(value) {
|
|
358
|
+
return JSON.stringify(canonical(value));
|
|
359
|
+
}
|
|
360
|
+
function canonical(value, key) {
|
|
361
|
+
if (Array.isArray(value)) {
|
|
362
|
+
const items = value.map((v) => canonical(v));
|
|
363
|
+
return key === "Statement" ? items : items.map((v) => JSON.stringify(v)).sort();
|
|
364
|
+
}
|
|
365
|
+
if (value && typeof value === "object") return Object.fromEntries(Object.keys(value).sort().map((k) => {
|
|
366
|
+
const v = value[k];
|
|
367
|
+
return [k, canonical(k === "Action" || k === "Resource" || k === "AWS" ? Array.isArray(v) ? v : [v] : v, k)];
|
|
368
|
+
}));
|
|
369
|
+
return value;
|
|
370
|
+
}
|
|
371
|
+
//#endregion
|
|
372
|
+
//#region src/providers/s3/index.ts
|
|
373
|
+
/** The tags gkm finds its own buckets and users by. */
|
|
374
|
+
const TAG_PROJECT = "gkm:project";
|
|
375
|
+
const TAG_STAGE = "gkm:stage";
|
|
376
|
+
const TAG_CONSTRUCT = "gkm:construct";
|
|
377
|
+
function bucketConstructs(manifest) {
|
|
378
|
+
return Object.entries(manifest).filter(([, d]) => d.kind === "objects").map(([id, d]) => ({
|
|
379
|
+
id,
|
|
380
|
+
versioned: d.versioned === true,
|
|
381
|
+
servers: Object.entries(manifest).filter(([, s]) => s.kind === "file-server" && s.of === id).map(([sid, s]) => ({
|
|
382
|
+
id: sid,
|
|
383
|
+
open: s.open ?? []
|
|
384
|
+
})).sort((a, b) => a.id.localeCompare(b.id))
|
|
385
|
+
})).sort((a, b) => a.id.localeCompare(b.id));
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* The region the stage's buckets are created in: the entry's, the secrets
|
|
389
|
+
* store's when it is in AWS, the credential's, `AWS_REGION`.
|
|
390
|
+
*/
|
|
391
|
+
function s3Region(ctx, env = process.env) {
|
|
392
|
+
const store = ctx.workspace.secrets?.store;
|
|
393
|
+
const storeRegion = store && typeof store === "object" && "region" in store ? store.region : void 0;
|
|
394
|
+
const region = ctx.config.region ?? storeRegion ?? ctx.credential?.region ?? env.AWS_REGION ?? env.AWS_DEFAULT_REGION;
|
|
395
|
+
if (!region) throw new require_workspace.S3RegionRequired(ctx.stage);
|
|
396
|
+
return region;
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* The address a file server over `bucket` serves on: the bucket's regional
|
|
400
|
+
* virtual-host endpoint, which is what `FileServer.url(key)` appends a key to
|
|
401
|
+
* — `https://<bucket>.s3.<region>.amazonaws.com/<key>`. Against an emulator,
|
|
402
|
+
* its path-style address.
|
|
403
|
+
*/
|
|
404
|
+
function bucketPublicUrl(bucket, region, endpoint) {
|
|
405
|
+
if (endpoint) return `${endpoint.replace(/\/+$/, "")}/${bucket}`;
|
|
406
|
+
return `https://${bucket}.s3.${region}.amazonaws.com`;
|
|
407
|
+
}
|
|
408
|
+
/** What an AWS SDK error is called, whichever field carries it. */
|
|
409
|
+
function errorName(error) {
|
|
410
|
+
const e = error;
|
|
411
|
+
return e?.Code ?? e?.name ?? "";
|
|
412
|
+
}
|
|
413
|
+
/** A read whose "there is none" answer is one of `missing`. */
|
|
414
|
+
async function readOr(read, missing) {
|
|
415
|
+
try {
|
|
416
|
+
return await read();
|
|
417
|
+
} catch (error) {
|
|
418
|
+
if (missing.includes(errorName(error))) return void 0;
|
|
419
|
+
throw error;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
const defaultClients = async (credential, region) => {
|
|
423
|
+
const S3 = await import("@aws-sdk/client-s3");
|
|
424
|
+
const IAM = await import("@aws-sdk/client-iam");
|
|
425
|
+
const config = await awsClientConfig(credential, region);
|
|
426
|
+
const endpoint = awsEndpoint(process.env, "S3");
|
|
427
|
+
return {
|
|
428
|
+
S3,
|
|
429
|
+
IAM,
|
|
430
|
+
s3: new S3.S3Client({
|
|
431
|
+
...config,
|
|
432
|
+
...endpoint ? {
|
|
433
|
+
endpoint,
|
|
434
|
+
forcePathStyle: true
|
|
435
|
+
} : {}
|
|
436
|
+
}),
|
|
437
|
+
iam: new IAM.IAMClient(config),
|
|
438
|
+
region,
|
|
439
|
+
...endpoint ? { endpoint } : {}
|
|
440
|
+
};
|
|
441
|
+
};
|
|
442
|
+
/** For tests: what makes the clients. */
|
|
443
|
+
let makeClients = defaultClients;
|
|
444
|
+
async function ensureS3(ctx) {
|
|
445
|
+
const region = s3Region(ctx);
|
|
446
|
+
const clients = await makeClients(ctx.credential, region);
|
|
447
|
+
for (const construct of bucketConstructs(ctx.manifest)) await ensureBucket(ctx, clients, construct);
|
|
448
|
+
}
|
|
449
|
+
/** The keys gkm tags a bucket and a user with. */
|
|
450
|
+
function identityTags(ctx, id) {
|
|
451
|
+
return [
|
|
452
|
+
{
|
|
453
|
+
Key: TAG_PROJECT,
|
|
454
|
+
Value: ctx.identity.key
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
Key: TAG_STAGE,
|
|
458
|
+
Value: ctx.stage
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
Key: TAG_CONSTRUCT,
|
|
462
|
+
Value: id
|
|
463
|
+
}
|
|
464
|
+
];
|
|
465
|
+
}
|
|
466
|
+
async function ensureBucket(ctx, clients, construct) {
|
|
467
|
+
const { id } = construct;
|
|
468
|
+
const nameInput = {
|
|
469
|
+
scope: ctx.identity.scope,
|
|
470
|
+
stage: ctx.stage,
|
|
471
|
+
id: require_derive.appKey(id)
|
|
472
|
+
};
|
|
473
|
+
const versioning = ctx.config.versioning ?? construct.versioned;
|
|
474
|
+
const open = [...new Set(construct.servers.flatMap((s) => s.open))].sort();
|
|
475
|
+
const { bucket, exists } = await ensureBucketName(ctx, clients, id, require_seeds.bucketName(nameInput));
|
|
476
|
+
await ensureBucketSettings(ctx, clients, {
|
|
477
|
+
id,
|
|
478
|
+
bucket,
|
|
479
|
+
exists,
|
|
480
|
+
versioning,
|
|
481
|
+
open
|
|
482
|
+
});
|
|
483
|
+
await ensureAccessKey(ctx, clients, {
|
|
484
|
+
id,
|
|
485
|
+
bucket,
|
|
486
|
+
...await ensureUser(ctx, clients, id, require_seeds.iamUserName(nameInput), userPolicy(bucket, { versioning }))
|
|
487
|
+
});
|
|
488
|
+
const publicUrl = bucketPublicUrl(bucket, clients.region, clients.endpoint);
|
|
489
|
+
for (const server of construct.servers) {
|
|
490
|
+
const key = (0, _geekmidas_manifest.provideKey)(server.id, "url");
|
|
491
|
+
const entry = {
|
|
492
|
+
key: `file-server:${server.id}`,
|
|
493
|
+
type: "url"
|
|
494
|
+
};
|
|
495
|
+
const recorded = ctx.state.record(entry.key)?.id;
|
|
496
|
+
const current = ctx.secrets[key];
|
|
497
|
+
if (current === publicUrl) {
|
|
498
|
+
await ctx.state.ready(entry, publicUrl);
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
if (current !== void 0 && current !== recorded) {
|
|
502
|
+
ctx.log(` ${key} is set to ${current} by you, and kept — the bucket's own address is ${publicUrl}`);
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
await ctx.change({
|
|
506
|
+
construct: server.id,
|
|
507
|
+
resource: key,
|
|
508
|
+
change: `set to ${publicUrl}`
|
|
509
|
+
}, async () => {
|
|
510
|
+
await ctx.writeSecrets({ [key]: publicUrl });
|
|
511
|
+
await ctx.state.ready(entry, publicUrl);
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* The bucket's name: the recorded one, else the good name, else the good
|
|
517
|
+
* name with a random suffix — up to {@link SUFFIX_ATTEMPTS} times.
|
|
518
|
+
*/
|
|
519
|
+
async function ensureBucketName(ctx, clients, id, good) {
|
|
520
|
+
const { S3, s3, region } = clients;
|
|
521
|
+
const key = `s3-bucket:${id}`;
|
|
522
|
+
const record = ctx.state.record(key);
|
|
523
|
+
const entry = (name) => ({
|
|
524
|
+
key,
|
|
525
|
+
type: "s3-bucket",
|
|
526
|
+
data: {
|
|
527
|
+
name,
|
|
528
|
+
region
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
/** Whether this account holds `name`: yes, no, or someone else does. */
|
|
532
|
+
const probe = async (name) => {
|
|
533
|
+
try {
|
|
534
|
+
await s3.send(new S3.HeadBucketCommand({ Bucket: name }));
|
|
535
|
+
return "ours";
|
|
536
|
+
} catch (error) {
|
|
537
|
+
const name = errorName(error);
|
|
538
|
+
if (name === "NotFound" || name === "NoSuchBucket") return "none";
|
|
539
|
+
if (name === "Forbidden" || name === "AccessDenied") return "taken";
|
|
540
|
+
throw error;
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
const create = async (name) => {
|
|
544
|
+
if (ctx.dryRun) {
|
|
545
|
+
const found = await probe(name);
|
|
546
|
+
if (found === "none") {
|
|
547
|
+
await ctx.change({
|
|
548
|
+
construct: id,
|
|
549
|
+
resource: `bucket ${name}`,
|
|
550
|
+
change: `create in ${region}`
|
|
551
|
+
}, async () => {});
|
|
552
|
+
return "created";
|
|
553
|
+
}
|
|
554
|
+
return found;
|
|
555
|
+
}
|
|
556
|
+
await ctx.state.pending(entry(name));
|
|
557
|
+
try {
|
|
558
|
+
await s3.send(new S3.CreateBucketCommand({
|
|
559
|
+
Bucket: name,
|
|
560
|
+
...region === "us-east-1" ? {} : { CreateBucketConfiguration: { LocationConstraint: region } }
|
|
561
|
+
}));
|
|
562
|
+
} catch (error) {
|
|
563
|
+
const code = errorName(error);
|
|
564
|
+
if (code === "BucketAlreadyOwnedByYou") return "ours";
|
|
565
|
+
if (code === "BucketAlreadyExists") return "taken";
|
|
566
|
+
throw error;
|
|
567
|
+
}
|
|
568
|
+
await ctx.change({
|
|
569
|
+
construct: id,
|
|
570
|
+
resource: `bucket ${name}`,
|
|
571
|
+
change: `create in ${region}`
|
|
572
|
+
}, async () => {});
|
|
573
|
+
return "created";
|
|
574
|
+
};
|
|
575
|
+
const adopt = async (name) => {
|
|
576
|
+
const owner = (await readOr(() => s3.send(new S3.GetBucketTaggingCommand({ Bucket: name })), ["NoSuchTagSet", "NoSuchTagSetError"]))?.TagSet?.find((t) => t.Key === TAG_PROJECT)?.Value;
|
|
577
|
+
if (owner && owner !== ctx.identity.key) throw new require_workspace.BucketOwnedByAnotherProject(name, owner, ctx.identity.key);
|
|
578
|
+
};
|
|
579
|
+
if (record?.status === "ready" && record.id) {
|
|
580
|
+
const found = await probe(record.id);
|
|
581
|
+
if (found === "ours") return {
|
|
582
|
+
bucket: record.id,
|
|
583
|
+
exists: true
|
|
584
|
+
};
|
|
585
|
+
if (found === "taken") throw new require_workspace.RecordedBucketUnreachable(id, record.id);
|
|
586
|
+
const made = await create(record.id);
|
|
587
|
+
if (made === "taken") throw new require_workspace.RecordedBucketUnreachable(id, record.id);
|
|
588
|
+
if (!ctx.dryRun) await ctx.state.ready(entry(record.id), record.id);
|
|
589
|
+
return {
|
|
590
|
+
bucket: record.id,
|
|
591
|
+
exists: made === "ours"
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
const pending = record?.data?.name;
|
|
595
|
+
const names = [
|
|
596
|
+
...typeof pending === "string" && pending !== good ? [pending] : [],
|
|
597
|
+
good,
|
|
598
|
+
...Array.from({ length: 5 }, () => require_seeds.suffixedBucketName(good, require_seeds.randomSuffix()))
|
|
599
|
+
];
|
|
600
|
+
const tried = [];
|
|
601
|
+
for (const name of names) {
|
|
602
|
+
tried.push(name);
|
|
603
|
+
const made = await create(name);
|
|
604
|
+
if (made === "taken") continue;
|
|
605
|
+
if (made === "ours") await adopt(name);
|
|
606
|
+
if (!ctx.dryRun) await ctx.state.ready(entry(name), name);
|
|
607
|
+
if (name !== good) ctx.log(` '${good}' is taken by another account; '${id}' is '${name}', recorded in the stage's state`);
|
|
608
|
+
return {
|
|
609
|
+
bucket: name,
|
|
610
|
+
exists: made === "ours"
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
throw new require_workspace.BucketNameUnavailable(id, tried.filter((n) => n !== pending));
|
|
614
|
+
}
|
|
615
|
+
/** Tags, Block Public Access, encryption, policy, versioning and CORS. */
|
|
616
|
+
async function ensureBucketSettings(ctx, clients, input) {
|
|
617
|
+
const { S3, s3 } = clients;
|
|
618
|
+
const { id, bucket } = input;
|
|
619
|
+
const Bucket = bucket;
|
|
620
|
+
const fresh = ctx.dryRun && !input.exists;
|
|
621
|
+
const read = async (fn, missing) => fresh ? void 0 : readOr(fn, missing);
|
|
622
|
+
const set = (change, apply) => ctx.change({
|
|
623
|
+
construct: id,
|
|
624
|
+
resource: `bucket ${bucket}`,
|
|
625
|
+
change
|
|
626
|
+
}, () => apply().then(() => void 0));
|
|
627
|
+
const tagged = await read(() => s3.send(new S3.GetBucketTaggingCommand({ Bucket })), ["NoSuchTagSet", "NoSuchTagSetError"]);
|
|
628
|
+
const ours = identityTags(ctx, id);
|
|
629
|
+
const others = (tagged?.TagSet ?? []).filter((t) => !ours.some((o) => o.Key === t.Key));
|
|
630
|
+
if (ours.some((o) => !tagged?.TagSet?.some((t) => t.Key === o.Key && t.Value === o.Value))) await set("tag", () => s3.send(new S3.PutBucketTaggingCommand({
|
|
631
|
+
Bucket,
|
|
632
|
+
Tagging: { TagSet: [...others, ...ours].map((t) => ({
|
|
633
|
+
Key: t.Key,
|
|
634
|
+
Value: t.Value
|
|
635
|
+
})) }
|
|
636
|
+
})));
|
|
637
|
+
const isOpen = input.open.length > 0;
|
|
638
|
+
const block = publicAccessBlock(isOpen);
|
|
639
|
+
const ensureBlock = async () => {
|
|
640
|
+
const now = (await read(() => s3.send(new S3.GetPublicAccessBlockCommand({ Bucket })), ["NoSuchPublicAccessBlockConfiguration"]))?.PublicAccessBlockConfiguration;
|
|
641
|
+
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({
|
|
642
|
+
Bucket,
|
|
643
|
+
PublicAccessBlockConfiguration: block
|
|
644
|
+
})));
|
|
645
|
+
};
|
|
646
|
+
const policy = bucketPolicy(bucket, input.open);
|
|
647
|
+
const ensurePolicy = async () => {
|
|
648
|
+
const current = await read(() => s3.send(new S3.GetBucketPolicyCommand({ Bucket })), ["NoSuchBucketPolicy"]);
|
|
649
|
+
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({
|
|
650
|
+
Bucket,
|
|
651
|
+
Policy: JSON.stringify(policy)
|
|
652
|
+
})));
|
|
653
|
+
};
|
|
654
|
+
if (isOpen) {
|
|
655
|
+
await ensureBlock();
|
|
656
|
+
await ensurePolicy();
|
|
657
|
+
} else {
|
|
658
|
+
await ensurePolicy();
|
|
659
|
+
await ensureBlock();
|
|
660
|
+
}
|
|
661
|
+
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({
|
|
662
|
+
Bucket,
|
|
663
|
+
ServerSideEncryptionConfiguration: { Rules: [{ ApplyServerSideEncryptionByDefault: { SSEAlgorithm: "AES256" } }] }
|
|
664
|
+
})));
|
|
665
|
+
const status = (await read(() => s3.send(new S3.GetBucketVersioningCommand({ Bucket })), []))?.Status;
|
|
666
|
+
if (input.versioning && status !== "Enabled") await set("enable versioning", () => s3.send(new S3.PutBucketVersioningCommand({
|
|
667
|
+
Bucket,
|
|
668
|
+
VersioningConfiguration: { Status: "Enabled" }
|
|
669
|
+
})));
|
|
670
|
+
else if (!input.versioning && status === "Enabled") await set("suspend versioning", () => s3.send(new S3.PutBucketVersioningCommand({
|
|
671
|
+
Bucket,
|
|
672
|
+
VersioningConfiguration: { Status: "Suspended" }
|
|
673
|
+
})));
|
|
674
|
+
const { origins, unresolved } = bucketOrigins({
|
|
675
|
+
workspace: ctx.workspace,
|
|
676
|
+
manifest: ctx.manifest,
|
|
677
|
+
runnables: ctx.runnables,
|
|
678
|
+
stage: ctx.stage,
|
|
679
|
+
bucket: id
|
|
680
|
+
});
|
|
681
|
+
if (unresolved.length > 0) ctx.log(` ${unresolved.join(", ")} ${unresolved.length === 1 ? "has" : "have"} no address on '${ctx.stage}' (domains.${ctx.stage}), so ${unresolved.length === 1 ? "is" : "are"} not in '${bucket}''s CORS`);
|
|
682
|
+
if (origins.length === 0) return;
|
|
683
|
+
const rule = corsRule(origins);
|
|
684
|
+
const cors = await read(() => s3.send(new S3.GetBucketCorsCommand({ Bucket })), ["NoSuchCORSConfiguration"]);
|
|
685
|
+
if (!(cors?.CORSRules?.length === 1 && canonicalCors(cors.CORSRules[0]) === canonicalCors(rule))) await set(`CORS for ${origins.join(", ")}`, () => s3.send(new S3.PutBucketCorsCommand({
|
|
686
|
+
Bucket,
|
|
687
|
+
CORSConfiguration: { CORSRules: [rule] }
|
|
688
|
+
})));
|
|
689
|
+
}
|
|
690
|
+
function canonicalCors(rule) {
|
|
691
|
+
const sorted = (list) => [...list ?? []].sort();
|
|
692
|
+
return JSON.stringify({
|
|
693
|
+
AllowedMethods: sorted(rule.AllowedMethods),
|
|
694
|
+
AllowedOrigins: sorted(rule.AllowedOrigins),
|
|
695
|
+
AllowedHeaders: sorted(rule.AllowedHeaders),
|
|
696
|
+
ExposeHeaders: sorted(rule.ExposeHeaders),
|
|
697
|
+
MaxAgeSeconds: rule.MaxAgeSeconds ?? null
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
/** The bucket's IAM user and its inline policy. */
|
|
701
|
+
async function ensureUser(ctx, clients, id, user, policy) {
|
|
702
|
+
const { IAM, iam } = clients;
|
|
703
|
+
const entry = {
|
|
704
|
+
key: `iam-user:${id}`,
|
|
705
|
+
type: "iam-user"
|
|
706
|
+
};
|
|
707
|
+
const found = await readOr(() => iam.send(new IAM.GetUserCommand({ UserName: user })), ["NoSuchEntity", "NoSuchEntityException"]);
|
|
708
|
+
if (found?.User) {
|
|
709
|
+
const tags = await iam.send(new IAM.ListUserTagsCommand({ UserName: user }));
|
|
710
|
+
const tag = (key) => tags.Tags?.find((t) => t.Key === key)?.Value;
|
|
711
|
+
if (found.User.Path !== "/gkm/" || tag("gkm:project") !== ctx.identity.key || tag("gkm:stage") !== ctx.stage) throw new require_workspace.IamUserNotOwned(user, ctx.identity.key, ctx.stage);
|
|
712
|
+
await ctx.state.ready(entry, user);
|
|
713
|
+
} else await ctx.change({
|
|
714
|
+
construct: id,
|
|
715
|
+
resource: `IAM user ${user}`,
|
|
716
|
+
change: `create under ${require_seeds.IAM_USER_PATH}`
|
|
717
|
+
}, async () => {
|
|
718
|
+
await ctx.state.pending(entry);
|
|
719
|
+
await iam.send(new IAM.CreateUserCommand({
|
|
720
|
+
UserName: user,
|
|
721
|
+
Path: require_seeds.IAM_USER_PATH,
|
|
722
|
+
Tags: identityTags(ctx, id)
|
|
723
|
+
}));
|
|
724
|
+
await ctx.state.ready(entry, user);
|
|
725
|
+
});
|
|
726
|
+
const exists = Boolean(found?.User) || !ctx.dryRun;
|
|
727
|
+
const current = exists ? await readOr(() => iam.send(new IAM.GetUserPolicyCommand({
|
|
728
|
+
UserName: user,
|
|
729
|
+
PolicyName: require_seeds.IAM_POLICY_NAME
|
|
730
|
+
})), ["NoSuchEntity", "NoSuchEntityException"]) : void 0;
|
|
731
|
+
const document = current?.PolicyDocument ? JSON.parse(decodePolicy(current.PolicyDocument)) : void 0;
|
|
732
|
+
if (!document || canonicalPolicy(document) !== canonicalPolicy(policy)) await ctx.change({
|
|
733
|
+
construct: id,
|
|
734
|
+
resource: `IAM user ${user}`,
|
|
735
|
+
change: `policy ${require_seeds.IAM_POLICY_NAME}: this bucket only`
|
|
736
|
+
}, async () => {
|
|
737
|
+
await iam.send(new IAM.PutUserPolicyCommand({
|
|
738
|
+
UserName: user,
|
|
739
|
+
PolicyName: require_seeds.IAM_POLICY_NAME,
|
|
740
|
+
PolicyDocument: JSON.stringify(policy)
|
|
741
|
+
}));
|
|
742
|
+
});
|
|
743
|
+
return {
|
|
744
|
+
user,
|
|
745
|
+
exists
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
/** IAM returns a policy document URL-encoded; an emulator may not. */
|
|
749
|
+
function decodePolicy(document) {
|
|
750
|
+
try {
|
|
751
|
+
return decodeURIComponent(document);
|
|
752
|
+
} catch {
|
|
753
|
+
return document;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* The user's access key, in the stage's secrets as the bucket's URL.
|
|
758
|
+
*
|
|
759
|
+
* - First run: one key, written into the stage's secrets.
|
|
760
|
+
* - `--rotate-keys`: a second key, written in place of the first; the first
|
|
761
|
+
* stays active until the stage has deployed with the second, and the next
|
|
762
|
+
* run after that deploy — or `--retire-old-keys` — deletes it.
|
|
763
|
+
* - A key the secrets no longer hold (a store emptied by hand): replaced the
|
|
764
|
+
* same way, since its secret cannot be read back.
|
|
765
|
+
*/
|
|
766
|
+
async function ensureAccessKey(ctx, clients, input) {
|
|
767
|
+
const { IAM, iam } = clients;
|
|
768
|
+
const { id, bucket, user } = input;
|
|
769
|
+
const urlKey = (0, _geekmidas_manifest.provideKey)(id, "url");
|
|
770
|
+
const entryKey = `iam-access-key:${id}`;
|
|
771
|
+
const record = ctx.state.record(entryKey);
|
|
772
|
+
const given = ctx.secrets[urlKey];
|
|
773
|
+
let inUrl;
|
|
774
|
+
if (given !== void 0) {
|
|
775
|
+
let parsed;
|
|
776
|
+
try {
|
|
777
|
+
parsed = _geekmidas_storage_s3_url.parse(given);
|
|
778
|
+
} catch {
|
|
779
|
+
parsed = void 0;
|
|
780
|
+
}
|
|
781
|
+
if (parsed?.bucket !== bucket) throw new require_workspace.BucketKeySetElsewhere(urlKey, bucket, parsed?.bucket ?? given.replace(/\/\/[^@]*@/, "//"), ctx.stage);
|
|
782
|
+
inUrl = parsed.accessKeyId;
|
|
783
|
+
}
|
|
784
|
+
const listed = input.exists ? (await iam.send(new IAM.ListAccessKeysCommand({ UserName: user }))).AccessKeyMetadata?.map((k) => k.AccessKeyId) ?? [] : [];
|
|
785
|
+
let current = record?.status === "ready" ? record.id : void 0;
|
|
786
|
+
let previous = typeof record?.data?.previous === "string" ? record.data.previous : void 0;
|
|
787
|
+
const rotatedAt = typeof record?.data?.rotatedAt === "string" ? record.data.rotatedAt : void 0;
|
|
788
|
+
const entry = (data = {}) => ({
|
|
789
|
+
key: entryKey,
|
|
790
|
+
type: "iam-access-key",
|
|
791
|
+
data: {
|
|
792
|
+
user,
|
|
793
|
+
...data
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
if (previous && current) {
|
|
797
|
+
const deployed = rotatedAt !== void 0 && ctx.state.lastDeployedAt !== void 0 && ctx.state.lastDeployedAt > rotatedAt;
|
|
798
|
+
if (ctx.retireOldKeys || deployed) {
|
|
799
|
+
const old = previous;
|
|
800
|
+
const keep = current;
|
|
801
|
+
await ctx.change({
|
|
802
|
+
construct: id,
|
|
803
|
+
resource: `IAM user ${user}`,
|
|
804
|
+
change: `delete the rotated-out access key ${old}`
|
|
805
|
+
}, async () => {
|
|
806
|
+
if (listed.includes(old)) await iam.send(new IAM.DeleteAccessKeyCommand({
|
|
807
|
+
UserName: user,
|
|
808
|
+
AccessKeyId: old
|
|
809
|
+
}));
|
|
810
|
+
await ctx.state.ready(entry(), keep);
|
|
811
|
+
});
|
|
812
|
+
previous = void 0;
|
|
813
|
+
} else ctx.log(` ${id}: the old access key ${previous} stays active until '${ctx.stage}' is deployed with the new one; the next deploy after that deletes it (or pass --retire-old-keys)`);
|
|
814
|
+
}
|
|
815
|
+
if (!current && inUrl && listed.includes(inUrl)) {
|
|
816
|
+
await ctx.state.ready(entry(), inUrl);
|
|
817
|
+
current = inUrl;
|
|
818
|
+
}
|
|
819
|
+
if (current !== void 0 && listed.includes(current) && inUrl === current && !ctx.rotateKeys) return;
|
|
820
|
+
if (ctx.rotateKeys && previous) throw new require_workspace.RotationInProgress(id, previous, ctx.stage);
|
|
821
|
+
const outgoing = current && listed.includes(current) ? current : void 0;
|
|
822
|
+
if (listed.length >= 2) throw new require_workspace.AccessKeyLimit(user, listed);
|
|
823
|
+
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)";
|
|
824
|
+
await ctx.change({
|
|
825
|
+
construct: id,
|
|
826
|
+
resource: `IAM user ${user}`,
|
|
827
|
+
change: why
|
|
828
|
+
}, async () => {
|
|
829
|
+
await ctx.state.pending(entry(outgoing ? { previous: outgoing } : {}));
|
|
830
|
+
const key = (await iam.send(new IAM.CreateAccessKeyCommand({ UserName: user }))).AccessKey;
|
|
831
|
+
await ctx.writeSecrets({ [urlKey]: _geekmidas_storage_s3_url.build({
|
|
832
|
+
bucket,
|
|
833
|
+
region: clients.region,
|
|
834
|
+
...clients.endpoint ? {
|
|
835
|
+
endpoint: clients.endpoint,
|
|
836
|
+
forcePathStyle: true
|
|
837
|
+
} : {},
|
|
838
|
+
accessKeyId: key.AccessKeyId,
|
|
839
|
+
secretAccessKey: key.SecretAccessKey
|
|
840
|
+
}) });
|
|
841
|
+
await ctx.state.ready(entry(outgoing ? {
|
|
842
|
+
previous: outgoing,
|
|
843
|
+
rotatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
844
|
+
} : {}), key.AccessKeyId);
|
|
845
|
+
});
|
|
846
|
+
if (outgoing) ctx.log(` ${urlKey} holds the new key. This deploy moves '${ctx.stage}' onto it; the next deploy after it deletes the old key ${outgoing}`);
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Each provisioned bucket answers `HeadBucket` to the key in the stage's
|
|
850
|
+
* secrets — the one the apps will use. A bucket whose URL carries no key is
|
|
851
|
+
* not one this provider wrote, and the deploy's own check covers a URL that
|
|
852
|
+
* is missing.
|
|
853
|
+
*/
|
|
854
|
+
async function verifyS3(ctx) {
|
|
855
|
+
const S3 = await import("@aws-sdk/client-s3");
|
|
856
|
+
for (const { id } of bucketConstructs(ctx.manifest)) {
|
|
857
|
+
const url = ctx.secrets[(0, _geekmidas_manifest.provideKey)(id, "url")];
|
|
858
|
+
if (url === void 0) continue;
|
|
859
|
+
const address = _geekmidas_storage_s3_url.parse(url);
|
|
860
|
+
if (!address.accessKeyId || !address.secretAccessKey) continue;
|
|
861
|
+
const client = new S3.S3Client({
|
|
862
|
+
region: address.region ?? ctx.config.region ?? "us-east-1",
|
|
863
|
+
credentials: {
|
|
864
|
+
accessKeyId: address.accessKeyId,
|
|
865
|
+
secretAccessKey: address.secretAccessKey
|
|
866
|
+
},
|
|
867
|
+
...address.endpoint ? { endpoint: address.endpoint } : {},
|
|
868
|
+
...address.forcePathStyle ? { forcePathStyle: true } : {}
|
|
869
|
+
});
|
|
870
|
+
try {
|
|
871
|
+
await client.send(new S3.HeadBucketCommand({ Bucket: address.bucket }));
|
|
872
|
+
} catch (error) {
|
|
873
|
+
const name = errorName(error);
|
|
874
|
+
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));
|
|
875
|
+
} finally {
|
|
876
|
+
client.destroy();
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
//#endregion
|
|
881
|
+
//#region src/providers/registry.ts
|
|
882
|
+
/**
|
|
883
|
+
* The providers gkm ships, by kind and name — registered the way a deploy
|
|
884
|
+
* target's built-ins are (`target/builtins.ts`). A provider from a package is
|
|
885
|
+
* one more entry here once there is a loading story for it.
|
|
886
|
+
*/
|
|
887
|
+
const BUILTIN_PROVIDERS = { objects: { s3: {
|
|
888
|
+
kind: "objects",
|
|
889
|
+
name: "s3",
|
|
890
|
+
check: require_workspace.checkS3Config,
|
|
891
|
+
provisioning: require_workspace.S3_PROVISIONING,
|
|
892
|
+
credentials: awsProvisioningCredentials,
|
|
893
|
+
runtimeKeys(manifest) {
|
|
894
|
+
return bucketConstructs(manifest).flatMap((b) => [(0, _geekmidas_manifest.provideKey)(b.id, "url"), ...b.servers.map((s) => (0, _geekmidas_manifest.provideKey)(s.id, "url"))]);
|
|
895
|
+
},
|
|
896
|
+
ensure: ensureS3,
|
|
897
|
+
verify: verifyS3
|
|
898
|
+
} } };
|
|
899
|
+
/** The provider `kind` names `name`, if gkm has one. */
|
|
900
|
+
function builtinProvider(kind, name) {
|
|
901
|
+
return Object.hasOwn(BUILTIN_PROVIDERS[kind], name) ? BUILTIN_PROVIDERS[kind][name] : void 0;
|
|
902
|
+
}
|
|
903
|
+
//#endregion
|
|
904
|
+
//#region src/providers/index.ts
|
|
905
|
+
/** The constructs of a kind: `objects` is every bucket. */
|
|
906
|
+
function constructsOf(manifest, kind) {
|
|
907
|
+
return Object.entries(manifest).filter(([, d]) => d.kind === kind).map(([id]) => id).sort();
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* What every deploy runs: each provider's `verify()` — the cheap check that
|
|
911
|
+
* what it created is still there and the stage's key reaches it. Nothing on
|
|
912
|
+
* the local stage, and nothing for a kind on `external`.
|
|
913
|
+
*
|
|
914
|
+
* @throws {StageProviderDisabled} for a declared kind the stage has none of
|
|
915
|
+
*/
|
|
916
|
+
async function verifyStageProviders(input) {
|
|
917
|
+
require_seeds.assertStageProvidersEnabled(input.workspace, input.manifest, input.stage);
|
|
918
|
+
const verified = [];
|
|
919
|
+
for (const kind of require_seeds.PROVIDER_KINDS) {
|
|
920
|
+
if (constructsOf(input.manifest, kind).length === 0) continue;
|
|
921
|
+
const choice = require_workspace.stageProvider(input.workspace, kind, input.stage);
|
|
922
|
+
if (choice.mode !== "provider") continue;
|
|
923
|
+
const provider = builtinProvider(kind, choice.name);
|
|
924
|
+
await provider.verify({
|
|
925
|
+
workspace: input.workspace,
|
|
926
|
+
manifest: input.manifest,
|
|
927
|
+
stage: input.stage,
|
|
928
|
+
config: choice.config,
|
|
929
|
+
secrets: input.secrets
|
|
930
|
+
});
|
|
931
|
+
verified.push(`${kind}: ${provider.name}`);
|
|
932
|
+
}
|
|
933
|
+
return verified;
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
936
|
+
* A stage whose provider creates its resources, and no credentials to create
|
|
937
|
+
* them with. Raised before anything is created, built or started.
|
|
938
|
+
*/
|
|
939
|
+
var ProviderCredentialsMissing = class extends require_errors.GkmError {
|
|
940
|
+
stage;
|
|
941
|
+
kind;
|
|
942
|
+
provider;
|
|
943
|
+
ids;
|
|
944
|
+
env;
|
|
945
|
+
describe;
|
|
946
|
+
login;
|
|
947
|
+
constructor(stage, kind, provider, ids, env, describe, login) {
|
|
948
|
+
super(`deploy.${kind}.${stage} is ${provider}, so the deploy creates what ${ids.join(", ")} ${ids.length === 1 ? "is" : "are"} backed by on '${stage}' and writes ${ids.length === 1 ? "its key" : "their keys"} into the stage's secrets — and it found no ${provider} credentials to do it with. On this machine, supply ${describe}` + (login ? `, or run gkm login --provider ${login}` : "") + `. In CI, the step that runs the deploy (or gkm deploy --resources-only) needs them: the '${stage}' environment's role (AWS_ROLE_ARN, from gkm deploy:github), assumed before that step. With the keys set by hand instead, set deploy.${kind}.${stage} to 'external'.`);
|
|
949
|
+
this.stage = stage;
|
|
950
|
+
this.kind = kind;
|
|
951
|
+
this.provider = provider;
|
|
952
|
+
this.ids = ids;
|
|
953
|
+
this.env = env;
|
|
954
|
+
this.describe = describe;
|
|
955
|
+
this.login = login;
|
|
956
|
+
this.name = "ProviderCredentialsMissing";
|
|
957
|
+
}
|
|
958
|
+
};
|
|
959
|
+
/**
|
|
960
|
+
* `ensure()` for every provider the stage configures — find or create, fix
|
|
961
|
+
* drift, record in the stage's state, write the runtime keys into its
|
|
962
|
+
* secrets. Run by the deploy, which holds the stage's lock. A provider with
|
|
963
|
+
* no provisioning credentials stops the run with
|
|
964
|
+
* {@link ProviderCredentialsMissing} before anything is created.
|
|
965
|
+
*/
|
|
966
|
+
async function provisionStage(options) {
|
|
967
|
+
const { workspace, stage, manifest } = options;
|
|
968
|
+
const dryRun = options.dryRun === true;
|
|
969
|
+
const env = options.env ?? process.env;
|
|
970
|
+
const log = options.log ?? ((line) => console.log(line));
|
|
971
|
+
const runnables = options.runnables ?? {};
|
|
972
|
+
const reports = [];
|
|
973
|
+
const configured = [];
|
|
974
|
+
for (const kind of require_seeds.PROVIDER_KINDS) {
|
|
975
|
+
const choice = require_workspace.stageProvider(workspace, kind, stage);
|
|
976
|
+
if (choice.mode !== "provider") {
|
|
977
|
+
reports.push({
|
|
978
|
+
kind,
|
|
979
|
+
mode: choice.mode,
|
|
980
|
+
actions: [],
|
|
981
|
+
planned: []
|
|
982
|
+
});
|
|
983
|
+
continue;
|
|
984
|
+
}
|
|
985
|
+
const provider = builtinProvider(kind, choice.name);
|
|
986
|
+
const report = {
|
|
987
|
+
kind,
|
|
988
|
+
mode: "provider",
|
|
989
|
+
provider: provider.name,
|
|
990
|
+
actions: [],
|
|
991
|
+
planned: []
|
|
992
|
+
};
|
|
993
|
+
reports.push(report);
|
|
994
|
+
const ids = constructsOf(manifest, kind);
|
|
995
|
+
if (ids.length === 0) continue;
|
|
996
|
+
const credential = await provider.credentials({
|
|
997
|
+
stage,
|
|
998
|
+
env,
|
|
999
|
+
...options.profile ? { profile: options.profile } : {},
|
|
1000
|
+
...options.home ? { home: options.home } : {}
|
|
1001
|
+
});
|
|
1002
|
+
if (credential === void 0) throw new ProviderCredentialsMissing(stage, kind, provider.name, ids, provider.provisioning.env, provider.provisioning.describe, provider.provisioning.login);
|
|
1003
|
+
configured.push({
|
|
1004
|
+
kind,
|
|
1005
|
+
provider,
|
|
1006
|
+
config: choice.config,
|
|
1007
|
+
credential,
|
|
1008
|
+
report
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
if (configured.length === 0) return reports;
|
|
1012
|
+
const identity = require_seeds.deployIdentity(workspace, stage);
|
|
1013
|
+
let secrets = (await options.secrets.read())?.custom ?? {};
|
|
1014
|
+
for (const entry of configured) entry.report.planned = entry.provider.runtimeKeys(manifest).filter((key) => secrets[key] === void 0);
|
|
1015
|
+
const state = dryRun ? await readOnlyState(options.state, stage) : journalState(await DeployJournal.open(options.state, stage, () => require_seeds.deploysWithCompose(workspace) ? require_seeds.createComposeState(stage) : require_seeds.createEmptyState(stage, "", "")));
|
|
1016
|
+
const writeSecrets = async (values) => {
|
|
1017
|
+
if (dryRun) return;
|
|
1018
|
+
const stored = await options.secrets.read() ?? require_seeds.initStageSecrets(stage);
|
|
1019
|
+
const next = {
|
|
1020
|
+
...stored,
|
|
1021
|
+
custom: {
|
|
1022
|
+
...stored.custom,
|
|
1023
|
+
...values
|
|
1024
|
+
},
|
|
1025
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1026
|
+
};
|
|
1027
|
+
await options.secrets.write(next);
|
|
1028
|
+
secrets = next.custom;
|
|
1029
|
+
};
|
|
1030
|
+
for (const entry of configured) {
|
|
1031
|
+
log(`☁️ ${entry.kind}: ${entry.provider.name}${dryRun ? " (dry run)" : ""}`);
|
|
1032
|
+
await entry.provider.ensure({
|
|
1033
|
+
workspace,
|
|
1034
|
+
manifest,
|
|
1035
|
+
runnables,
|
|
1036
|
+
stage,
|
|
1037
|
+
identity,
|
|
1038
|
+
config: entry.config,
|
|
1039
|
+
credential: entry.credential,
|
|
1040
|
+
get secrets() {
|
|
1041
|
+
return secrets;
|
|
1042
|
+
},
|
|
1043
|
+
state,
|
|
1044
|
+
dryRun,
|
|
1045
|
+
rotateKeys: options.rotateKeys === true,
|
|
1046
|
+
retireOldKeys: options.retireOldKeys === true,
|
|
1047
|
+
async change(action, apply) {
|
|
1048
|
+
if (!dryRun) await apply();
|
|
1049
|
+
entry.report.actions.push(action);
|
|
1050
|
+
log(` ${dryRun ? "would " : ""}${action.change} — ${action.resource}`);
|
|
1051
|
+
},
|
|
1052
|
+
writeSecrets,
|
|
1053
|
+
log
|
|
1054
|
+
});
|
|
1055
|
+
if (entry.report.actions.length === 0) log(" up to date");
|
|
1056
|
+
}
|
|
1057
|
+
return reports;
|
|
1058
|
+
}
|
|
1059
|
+
/** The journal as `ensure()` sees it. */
|
|
1060
|
+
function journalState(journal) {
|
|
1061
|
+
return {
|
|
1062
|
+
record: (key) => journal.record(key),
|
|
1063
|
+
pending: (entry) => journal.pending(entry),
|
|
1064
|
+
ready: (entry, id) => journal.ready(entry, id),
|
|
1065
|
+
lastDeployedAt: journal.existed ? journal.state.lastDeployedAt : void 0
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
/** What a dry run reads, and the writes it does not make. */
|
|
1069
|
+
async function readOnlyState(store, stage) {
|
|
1070
|
+
const stored = await store.read(stage);
|
|
1071
|
+
return {
|
|
1072
|
+
record: (key) => stored?.resources[key],
|
|
1073
|
+
pending: async () => {},
|
|
1074
|
+
ready: async () => {},
|
|
1075
|
+
...stored ? { lastDeployedAt: stored.state.lastDeployedAt } : {}
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
//#endregion
|
|
1079
|
+
Object.defineProperty(exports, "DeployJournal", {
|
|
1080
|
+
enumerable: true,
|
|
1081
|
+
get: function() {
|
|
1082
|
+
return DeployJournal;
|
|
1083
|
+
}
|
|
1084
|
+
});
|
|
1085
|
+
Object.defineProperty(exports, "provisionStage", {
|
|
1086
|
+
enumerable: true,
|
|
1087
|
+
get: function() {
|
|
1088
|
+
return provisionStage;
|
|
1089
|
+
}
|
|
1090
|
+
});
|
|
1091
|
+
Object.defineProperty(exports, "verifyStageProviders", {
|
|
1092
|
+
enumerable: true,
|
|
1093
|
+
get: function() {
|
|
1094
|
+
return verifyStageProviders;
|
|
1095
|
+
}
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
//# sourceMappingURL=providers-BFeP_mYw.cjs.map
|