@geekmidas/cli 10.0.0-alpha.85 → 10.0.0-alpha.87
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/{HostingerProvider-dxkwynSs.cjs → HostingerProvider-BCB1QJj1.cjs} +2 -2
- package/dist/{HostingerProvider-dxkwynSs.cjs.map → HostingerProvider-BCB1QJj1.cjs.map} +1 -1
- package/dist/{HostingerProvider-D85ayzTi.mjs → HostingerProvider-CVc3rLPB.mjs} +2 -2
- package/dist/{HostingerProvider-D85ayzTi.mjs.map → HostingerProvider-CVc3rLPB.mjs.map} +1 -1
- package/dist/{LegacyStateStore-CJ2o18nT.mjs → LegacyStateStore-B0R4LlX_.mjs} +2 -2
- package/dist/{LegacyStateStore-CJ2o18nT.mjs.map → LegacyStateStore-B0R4LlX_.mjs.map} +1 -1
- package/dist/{LegacyStateStore-xGxPwP-m.cjs → LegacyStateStore-CWG_HVC0.cjs} +7 -7
- package/dist/{LegacyStateStore-xGxPwP-m.cjs.map → LegacyStateStore-CWG_HVC0.cjs.map} +1 -1
- package/dist/{LocalStateStore-CkjFD-4b.cjs → LocalStateStore-CLwa2DpK.cjs} +8 -8
- package/dist/{LocalStateStore-CkjFD-4b.cjs.map → LocalStateStore-CLwa2DpK.cjs.map} +1 -1
- package/dist/{LocalStateStore-ByVjUIvS.mjs → LocalStateStore-Q4vXg2ua.mjs} +2 -2
- package/dist/{LocalStateStore-ByVjUIvS.mjs.map → LocalStateStore-Q4vXg2ua.mjs.map} +1 -1
- package/dist/{S3StateStore-iqMezbES.cjs → S3StateStore-Cea9_5gF.cjs} +5 -5
- package/dist/{S3StateStore-iqMezbES.cjs.map → S3StateStore-Cea9_5gF.cjs.map} +1 -1
- package/dist/{S3StateStore-kpFlGVLE.mjs → S3StateStore-CksKYKRa.mjs} +2 -2
- package/dist/{S3StateStore-kpFlGVLE.mjs.map → S3StateStore-CksKYKRa.mjs.map} +1 -1
- package/dist/{SSMStateStore-Bkz5ztfo.mjs → SSMStateStore-BszSCAS2.mjs} +2 -2
- package/dist/{SSMStateStore-Bkz5ztfo.mjs.map → SSMStateStore-BszSCAS2.mjs.map} +1 -1
- package/dist/{SSMStateStore-Z2OMVCtq.cjs → SSMStateStore-aBOTF018.cjs} +9 -9
- package/dist/{SSMStateStore-Z2OMVCtq.cjs.map → SSMStateStore-aBOTF018.cjs.map} +1 -1
- package/dist/{add-CM7RLj89.cjs → add-5MZAXybv.cjs} +8 -5
- package/dist/add-5MZAXybv.cjs.map +1 -0
- package/dist/{add-jpX7M8RB.mjs → add-D_jNqnfZ.mjs} +8 -5
- package/dist/add-D_jNqnfZ.mjs.map +1 -0
- package/dist/{bundler-lEBH8ENP.cjs → bundler-1REMhKps.cjs} +5 -5
- package/dist/{bundler-lEBH8ENP.cjs.map → bundler-1REMhKps.cjs.map} +1 -1
- package/dist/{bundler-CaAKsbcN.mjs → bundler-BMo-JP5s.mjs} +5 -5
- package/dist/{bundler-CaAKsbcN.mjs.map → bundler-BMo-JP5s.mjs.map} +1 -1
- package/dist/{cli-BV3jzbk0.mjs → cli-7edNFwGk.mjs} +5 -13
- package/dist/cli-7edNFwGk.mjs.map +1 -0
- package/dist/{cli-DQCBcNlN.cjs → cli-BL8ZHsLQ.cjs} +5 -13
- package/dist/cli-BL8ZHsLQ.cjs.map +1 -0
- package/dist/{compose-CQK6StuE.mjs → compose-DbJhRQlh.mjs} +24 -11
- package/dist/compose-DbJhRQlh.mjs.map +1 -0
- package/dist/{compose-CSR0yEJm.cjs → compose-xw86kVYi.cjs} +34 -21
- package/dist/compose-xw86kVYi.cjs.map +1 -0
- package/dist/{config-BzIfP8a-.d.cts → config-CpjsVxru.d.cts} +3 -3
- package/dist/{config-BzIfP8a-.d.cts.map → config-CpjsVxru.d.cts.map} +1 -1
- package/dist/{config-DhdzmndX.d.mts → config-DrCAyoH5.d.mts} +3 -3
- package/dist/{config-DhdzmndX.d.mts.map → config-DrCAyoH5.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-pAtB70k3.mjs → credentials-BgJ5mS_d.mjs} +91 -181
- package/dist/credentials-BgJ5mS_d.mjs.map +1 -0
- package/dist/{credentials-BVs7mrtw.cjs → credentials-tr26-KML.cjs} +122 -188
- package/dist/credentials-tr26-KML.cjs.map +1 -0
- package/dist/deploy/public.cjs +24 -20
- package/dist/deploy/public.cjs.map +1 -1
- package/dist/deploy/public.d.cts +95 -13
- package/dist/deploy/public.d.cts.map +1 -1
- package/dist/deploy/public.d.mts +95 -13
- package/dist/deploy/public.d.mts.map +1 -1
- package/dist/deploy/public.mjs +11 -10
- package/dist/deploy/public.mjs.map +1 -1
- package/dist/{deploy-BuU117CA.mjs → deploy-BTfmXd3g.mjs} +9 -9
- package/dist/deploy-BTfmXd3g.mjs.map +1 -0
- package/dist/{deploy-Bw2Mm3x3.cjs → deploy-CtbZj4zN.cjs} +13 -13
- package/dist/deploy-CtbZj4zN.cjs.map +1 -0
- package/dist/{dokploy-C81J1MVK.cjs → dokploy-TGk3oPwr.cjs} +2 -2
- package/dist/{dokploy-C81J1MVK.cjs.map → dokploy-TGk3oPwr.cjs.map} +1 -1
- package/dist/{dokploy-DWALWk7u.mjs → dokploy-r69yZaIt.mjs} +2 -2
- package/dist/{dokploy-DWALWk7u.mjs.map → dokploy-r69yZaIt.mjs.map} +1 -1
- package/dist/{encryption-DpAum8W7.mjs → encryption-C-8Tt5Hh.mjs} +3 -3
- package/dist/{encryption-DpAum8W7.mjs.map → encryption-C-8Tt5Hh.mjs.map} +1 -1
- package/dist/{encryption-B8h8Tna0.cjs → encryption-CDn7BH5O.cjs} +3 -3
- package/dist/{encryption-B8h8Tna0.cjs.map → encryption-CDn7BH5O.cjs.map} +1 -1
- package/dist/{engine-B01fjhLt.cjs → engine-Bt5k_UMq.cjs} +33 -23
- package/dist/engine-Bt5k_UMq.cjs.map +1 -0
- package/dist/{engine-BXCtEhSH.mjs → engine-jAINgiOB.mjs} +28 -18
- package/dist/engine-jAINgiOB.mjs.map +1 -0
- package/dist/{github-8B68qrLs.mjs → github-CZHAtwPT.mjs} +3 -3
- package/dist/{github-8B68qrLs.mjs.map → github-CZHAtwPT.mjs.map} +1 -1
- package/dist/{github-Cis9tYur.cjs → github-DFhuJQlp.cjs} +3 -3
- package/dist/{github-Cis9tYur.cjs.map → github-DFhuJQlp.cjs.map} +1 -1
- package/dist/{index-f77HdqN5.d.mts → index-MnUJuUlj.d.mts} +2 -2
- package/dist/{index-D1ODeM9s.d.cts.map → index-MnUJuUlj.d.mts.map} +1 -1
- package/dist/{index-D1ODeM9s.d.cts → index-elsLJTUv.d.cts} +2 -2
- package/dist/{index-f77HdqN5.d.mts.map → index-elsLJTUv.d.cts.map} +1 -1
- package/dist/index.cjs +106 -68
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +100 -62
- package/dist/index.mjs.map +1 -1
- package/dist/local-Ciq-bTPr.mjs +224 -0
- package/dist/local-Ciq-bTPr.mjs.map +1 -0
- package/dist/local-Cq8qdgNX.cjs +241 -0
- package/dist/local-Cq8qdgNX.cjs.map +1 -0
- package/dist/{migrate-CPWn7A51.cjs → migrate-B16qgYTn.cjs} +3 -3
- package/dist/{migrate-CPWn7A51.cjs.map → migrate-B16qgYTn.cjs.map} +1 -1
- package/dist/{migrate-BBCp5blp.cjs → migrate-B1rVlwj1.cjs} +3 -3
- package/dist/{migrate-BBCp5blp.cjs.map → migrate-B1rVlwj1.cjs.map} +1 -1
- package/dist/{migrate-sDq7NApJ.mjs → migrate-Dn7nK6sh.mjs} +3 -3
- package/dist/{migrate-sDq7NApJ.mjs.map → migrate-Dn7nK6sh.mjs.map} +1 -1
- package/dist/{migrate-BeTWLSeC.mjs → migrate-ggkPVJRC.mjs} +3 -3
- package/dist/{migrate-BeTWLSeC.mjs.map → migrate-ggkPVJRC.mjs.map} +1 -1
- package/dist/openapi.cjs +1 -1
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.mjs +1 -1
- package/dist/reconcile/public.d.cts +1 -1
- package/dist/reconcile/public.d.mts +1 -1
- package/dist/{resolve-CXbwJ_iu.mjs → resolve-BSke45k6.mjs} +4 -4
- package/dist/{resolve-CXbwJ_iu.mjs.map → resolve-BSke45k6.mjs.map} +1 -1
- package/dist/{resolve-CztIqBOI.cjs → resolve-BXeXzpmU.cjs} +4 -4
- package/dist/{resolve-CztIqBOI.cjs.map → resolve-BXeXzpmU.cjs.map} +1 -1
- package/dist/{rollback-ByyFE5lV.mjs → rollback-CEE6dse4.mjs} +6 -6
- package/dist/{rollback-ByyFE5lV.mjs.map → rollback-CEE6dse4.mjs.map} +1 -1
- package/dist/{rollback-CyIJ4xGJ.cjs → rollback-DFMiBWf0.cjs} +8 -8
- package/dist/{rollback-CyIJ4xGJ.cjs.map → rollback-DFMiBWf0.cjs.map} +1 -1
- package/dist/{runtime-CNu2C2hB.d.mts → runtime-D48ZKk8_.d.cts} +2 -2
- package/dist/{runtime-CNu2C2hB.d.mts.map → runtime-D48ZKk8_.d.cts.map} +1 -1
- package/dist/{runtime-DMLgL26v.d.cts → runtime-HabiNBtJ.d.mts} +2 -2
- package/dist/{runtime-DMLgL26v.d.cts.map → runtime-HabiNBtJ.d.mts.map} +1 -1
- package/dist/{seeds-9C5CkDon.mjs → seeds-BYdrtZsC.mjs} +1525 -53
- package/dist/seeds-BYdrtZsC.mjs.map +1 -0
- package/dist/{seeds-B_Beuv0o.cjs → seeds-CsI0A_Ef.cjs} +1628 -65
- package/dist/seeds-CsI0A_Ef.cjs.map +1 -0
- package/dist/{sniffer-DgBkaLAu.mjs → sniffer-B0EU73bj.mjs} +5 -360
- package/dist/sniffer-B0EU73bj.mjs.map +1 -0
- package/dist/{sniffer-3fA0iExx.cjs → sniffer-DJYupBeK.cjs} +4 -413
- package/dist/sniffer-DJYupBeK.cjs.map +1 -0
- package/dist/{sst-DRi3H5TB.mjs → sst-D1Qj4sIs.mjs} +3 -3
- package/dist/{sst-DRi3H5TB.mjs.map → sst-D1Qj4sIs.mjs.map} +1 -1
- package/dist/{sst-hBrKouBy.cjs → sst-DAqUoZKG.cjs} +5 -5
- package/dist/{sst-hBrKouBy.cjs.map → sst-DAqUoZKG.cjs.map} +1 -1
- package/dist/{stages-CS1sjcRb.cjs → stages-Brn3vcdy.cjs} +2 -2
- package/dist/{stages-CS1sjcRb.cjs.map → stages-Brn3vcdy.cjs.map} +1 -1
- package/dist/{stages-CSXt7N2b.mjs → stages-DoHHz6pS.mjs} +2 -2
- package/dist/{stages-CSXt7N2b.mjs.map → stages-DoHHz6pS.mjs.map} +1 -1
- 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-BdvrmkaC.mjs → terminal-BTbZlrFV.mjs} +2 -2
- package/dist/{terminal-BdvrmkaC.mjs.map → terminal-BTbZlrFV.mjs.map} +1 -1
- package/dist/{terminal-BL5E0xbR.cjs → terminal-COv23heb.cjs} +2 -2
- package/dist/{terminal-BL5E0xbR.cjs.map → terminal-COv23heb.cjs.map} +1 -1
- package/dist/{types-BZn-8ssD.d.mts → types-0oAyFOfR.d.cts} +48 -49
- package/dist/types-0oAyFOfR.d.cts.map +1 -0
- package/dist/{types-DiZj6hqp.d.cts → types-BZ01tZs-.d.mts} +47 -50
- package/dist/types-BZ01tZs-.d.mts.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-Bic_R3tz.cjs → workspace-CT0JM3Y8.cjs} +333 -1
- package/dist/workspace-CT0JM3Y8.cjs.map +1 -0
- package/dist/{workspace-Drz4jsvy.mjs → workspace-D2CYLMCW.mjs} +250 -2
- package/dist/workspace-D2CYLMCW.mjs.map +1 -0
- package/package.json +15 -15
- package/dist/add-CM7RLj89.cjs.map +0 -1
- package/dist/add-jpX7M8RB.mjs.map +0 -1
- package/dist/cli-BV3jzbk0.mjs.map +0 -1
- package/dist/cli-DQCBcNlN.cjs.map +0 -1
- package/dist/compose-CQK6StuE.mjs.map +0 -1
- package/dist/compose-CSR0yEJm.cjs.map +0 -1
- package/dist/credentials-BVs7mrtw.cjs.map +0 -1
- package/dist/credentials-pAtB70k3.mjs.map +0 -1
- package/dist/deploy-BuU117CA.mjs.map +0 -1
- package/dist/deploy-Bw2Mm3x3.cjs.map +0 -1
- package/dist/engine-B01fjhLt.cjs.map +0 -1
- package/dist/engine-BXCtEhSH.mjs.map +0 -1
- package/dist/run-BJLIerMM.mjs +0 -133
- package/dist/run-BJLIerMM.mjs.map +0 -1
- package/dist/run-fVZ-rLnc.cjs +0 -174
- package/dist/run-fVZ-rLnc.cjs.map +0 -1
- package/dist/seeds-9C5CkDon.mjs.map +0 -1
- package/dist/seeds-B_Beuv0o.cjs.map +0 -1
- package/dist/sniffer-3fA0iExx.cjs.map +0 -1
- package/dist/sniffer-DgBkaLAu.mjs.map +0 -1
- package/dist/types-BZn-8ssD.d.mts.map +0 -1
- package/dist/types-DiZj6hqp.d.cts.map +0 -1
- package/dist/workspace-Bic_R3tz.cjs.map +0 -1
- package/dist/workspace-Drz4jsvy.mjs.map +0 -1
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
const require_rolldown_runtime = require("./rolldown-runtime-C0BPl7ul.cjs");
|
|
2
|
-
const
|
|
2
|
+
const require_credentials = require("./credentials-tr26-KML.cjs");
|
|
3
3
|
const require_discover = require("./discover-D1It5r_h.cjs");
|
|
4
4
|
const require_derive = require("./derive-DAbo49sm.cjs");
|
|
5
|
-
const require_workspace = require("./workspace-
|
|
5
|
+
const require_workspace = require("./workspace-CT0JM3Y8.cjs");
|
|
6
6
|
const require_config = require("./config.cjs");
|
|
7
7
|
const require_EndpointGenerator = require("./EndpointGenerator-CwLjBCFE.cjs");
|
|
8
8
|
const require_SubscriberGenerator = require("./SubscriberGenerator-BVn7cQ-2.cjs");
|
|
9
|
-
const require_resolve = require("./resolve-
|
|
9
|
+
const require_resolve = require("./resolve-BXeXzpmU.cjs");
|
|
10
10
|
let node_path = require("node:path");
|
|
11
11
|
let node_fs = require("node:fs");
|
|
12
12
|
let node_fs_promises = require("node:fs/promises");
|
|
@@ -31,6 +31,8 @@ let _aws_sdk_client_sns = require("@aws-sdk/client-sns");
|
|
|
31
31
|
let _aws_sdk_client_sqs = require("@aws-sdk/client-sqs");
|
|
32
32
|
let pg = require("pg");
|
|
33
33
|
let _geekmidas_cache_postgres = require("@geekmidas/cache/postgres");
|
|
34
|
+
let _geekmidas_storage_s3_url = require("@geekmidas/storage/s3-url");
|
|
35
|
+
_geekmidas_storage_s3_url = require_rolldown_runtime.__toESM(_geekmidas_storage_s3_url, 1);
|
|
34
36
|
//#region src/workspace/rootSite.ts
|
|
35
37
|
/**
|
|
36
38
|
* Which site the base domain points at — deployed and locally alike.
|
|
@@ -602,7 +604,7 @@ function folderName(path) {
|
|
|
602
604
|
function getKeystoreDir(project) {
|
|
603
605
|
if (!PROJECT_KEY.test(project.key)) throw new KeystoreProjectInvalid(project.key);
|
|
604
606
|
const [namespace, name] = project.key.split("/");
|
|
605
|
-
return (0, node_path.join)(project.home ??
|
|
607
|
+
return (0, node_path.join)(project.home ?? require_credentials.gkmHome(), "keys", namespace, name);
|
|
606
608
|
}
|
|
607
609
|
/** Get the path to a stage's encryption key. */
|
|
608
610
|
function getKeyPath(stage, project) {
|
|
@@ -619,7 +621,7 @@ function legacyKeyPaths(stage, project) {
|
|
|
619
621
|
key,
|
|
620
622
|
home: project.home
|
|
621
623
|
}));
|
|
622
|
-
const homes = [.../* @__PURE__ */ new Set([project.home ??
|
|
624
|
+
const homes = [.../* @__PURE__ */ new Set([project.home ?? require_credentials.gkmHome(), (0, node_path.join)((0, node_os.homedir)(), ".gkm")])];
|
|
623
625
|
const names = (project.legacy ?? []).filter(isLegacyName);
|
|
624
626
|
return [...aliases, ...names.flatMap((name) => homes.map((home) => (0, node_path.join)(home, name, `${stage}.key`)))];
|
|
625
627
|
}
|
|
@@ -4877,7 +4879,7 @@ function bucketPolicies(plan) {
|
|
|
4877
4879
|
* client, can be admitted by the policy where the client would have said no.
|
|
4878
4880
|
* Prefer `**` where crossing segments is what you meant.
|
|
4879
4881
|
*/
|
|
4880
|
-
function bucketPolicy(bucket, open) {
|
|
4882
|
+
function bucketPolicy$1(bucket, open) {
|
|
4881
4883
|
return JSON.stringify({
|
|
4882
4884
|
Version: "2012-10-17",
|
|
4883
4885
|
Statement: [{
|
|
@@ -4959,7 +4961,7 @@ async function databasesExist(client, names) {
|
|
|
4959
4961
|
async function applyPolicies(client, policies) {
|
|
4960
4962
|
const applied = [];
|
|
4961
4963
|
for (const { bucket, open } of policies) {
|
|
4962
|
-
const wanted = bucketPolicy(bucket, open);
|
|
4964
|
+
const wanted = bucketPolicy$1(bucket, open);
|
|
4963
4965
|
const describe = `open paths on ${bucket}`;
|
|
4964
4966
|
if (equivalent(await client.policy(bucket), wanted)) {
|
|
4965
4967
|
applied.push({
|
|
@@ -5578,7 +5580,7 @@ function localCredentialsPath(project) {
|
|
|
5578
5580
|
const keys = getKeystoreDir(project);
|
|
5579
5581
|
const name = keys.split(/[\\/]/).at(-1);
|
|
5580
5582
|
const namespace = keys.split(/[\\/]/).at(-2);
|
|
5581
|
-
return (0, node_path.join)(project.home ??
|
|
5583
|
+
return (0, node_path.join)(project.home ?? require_credentials.gkmHome(), "local", namespace, `${name}.json`);
|
|
5582
5584
|
}
|
|
5583
5585
|
/** The local stage's logins file is there, and its key is not. */
|
|
5584
5586
|
var LocalCredentialsKeyMissing = class extends Error {
|
|
@@ -6607,15 +6609,21 @@ function isUnder(file, dir) {
|
|
|
6607
6609
|
* chain applies — the stage's shared `AWS_ACCESS_KEY_ID`/
|
|
6608
6610
|
* `AWS_SECRET_ACCESS_KEY` when it set them, or a role.
|
|
6609
6611
|
*
|
|
6610
|
-
* `--allow-dev-services
|
|
6611
|
-
*
|
|
6612
|
-
*
|
|
6613
|
-
*
|
|
6612
|
+
* `--allow-dev-services` is the way out for a stage that is not production —
|
|
6613
|
+
* a preview, a demo: every construct the stage does not account for gets the
|
|
6614
|
+
* dev service the local stage runs for it — a bucket MinIO, mail Mailpit —
|
|
6615
|
+
* and the target derives the keys from it. What the stage does account for
|
|
6616
|
+
* still wins: a key set in its secrets, or a provider (`deploy.objects`)
|
|
6617
|
+
* that backs the kind. Every run that uses one says so, loudly.
|
|
6614
6618
|
*
|
|
6615
6619
|
* Pure: what each target runs is decided here from declarations and the
|
|
6616
6620
|
* stage's values, and the targets do the running.
|
|
6617
6621
|
*/
|
|
6618
|
-
/**
|
|
6622
|
+
/**
|
|
6623
|
+
* The dev services a deployed stage can run — one per kind that has one on
|
|
6624
|
+
* every server target: a bucket's MinIO and mail's Mailpit. A cache or a
|
|
6625
|
+
* database is not here because a server stage runs its own anyway.
|
|
6626
|
+
*/
|
|
6619
6627
|
const DEV_SERVICES = ["minio", "mailpit"];
|
|
6620
6628
|
/**
|
|
6621
6629
|
* The S3 client's shared key pair. Optional: a bucket whose URL carries its own
|
|
@@ -6624,27 +6632,45 @@ const DEV_SERVICES = ["minio", "mailpit"];
|
|
|
6624
6632
|
* chain finds — a role.
|
|
6625
6633
|
*/
|
|
6626
6634
|
const STORAGE_KEY_PAIR = ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"];
|
|
6627
|
-
/** A `--allow-dev-services` value that is not a dev service. */
|
|
6628
|
-
var UnknownDevService = class extends Error {
|
|
6629
|
-
value;
|
|
6630
|
-
constructor(value) {
|
|
6631
|
-
super(`'${value}' is not a dev service. --allow-dev-services takes a comma-separated list of ${DEV_SERVICES.join(", ")}: --allow-dev-services ${DEV_SERVICES.join(",")}`);
|
|
6632
|
-
this.value = value;
|
|
6633
|
-
this.name = "UnknownDevService";
|
|
6634
|
-
}
|
|
6635
|
-
};
|
|
6636
6635
|
/** Dev services asked of a target that runs no containers. */
|
|
6637
6636
|
var DevServicesNeedServerTarget = class extends Error {
|
|
6638
6637
|
target;
|
|
6639
|
-
|
|
6640
|
-
|
|
6641
|
-
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 ${services.join(",")}.`);
|
|
6638
|
+
constructor(target) {
|
|
6639
|
+
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.`);
|
|
6642
6640
|
this.target = target;
|
|
6643
|
-
this.services = services;
|
|
6644
6641
|
this.name = "DevServicesNeedServerTarget";
|
|
6645
6642
|
}
|
|
6646
6643
|
};
|
|
6647
6644
|
/**
|
|
6645
|
+
* `--allow-dev-services` given a value — the list it used to take. It is a
|
|
6646
|
+
* switch now: every construct the stage does not account for gets its dev
|
|
6647
|
+
* service.
|
|
6648
|
+
*/
|
|
6649
|
+
var AllowDevServicesTakesNoValue = class extends Error {
|
|
6650
|
+
value;
|
|
6651
|
+
constructor(value) {
|
|
6652
|
+
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.`);
|
|
6653
|
+
this.value = value;
|
|
6654
|
+
this.name = "AllowDevServicesTakesNoValue";
|
|
6655
|
+
}
|
|
6656
|
+
};
|
|
6657
|
+
/**
|
|
6658
|
+
* Refuses `--allow-dev-services <value>` and `--allow-dev-services=<value>` on
|
|
6659
|
+
* a command line, with the reason, before the parser calls the value an
|
|
6660
|
+
* excess argument. `gkm deploy` and `gkm compose` take no positional
|
|
6661
|
+
* argument, so whatever follows the flag and is not a flag was meant for it.
|
|
6662
|
+
*
|
|
6663
|
+
* @throws {AllowDevServicesTakesNoValue}
|
|
6664
|
+
*/
|
|
6665
|
+
function assertDevServicesFlag(argv) {
|
|
6666
|
+
for (let i = 0; i < argv.length; i++) {
|
|
6667
|
+
const arg = argv[i];
|
|
6668
|
+
if (arg.startsWith("--allow-dev-services=")) throw new AllowDevServicesTakesNoValue(arg.slice(arg.indexOf("=") + 1));
|
|
6669
|
+
const next = argv[i + 1];
|
|
6670
|
+
if (arg === "--allow-dev-services" && next && !next.startsWith("-")) throw new AllowDevServicesTakesNoValue(next);
|
|
6671
|
+
}
|
|
6672
|
+
}
|
|
6673
|
+
/**
|
|
6648
6674
|
* A deployed stage with mail or object storage that its secrets do not
|
|
6649
6675
|
* configure — every missing key, across every app, at once.
|
|
6650
6676
|
*/
|
|
@@ -6652,33 +6678,18 @@ var ExternalServicesNotConfigured = class extends Error {
|
|
|
6652
6678
|
stage;
|
|
6653
6679
|
missing;
|
|
6654
6680
|
constructor(stage, missing) {
|
|
6655
|
-
const
|
|
6681
|
+
const standIns = missing.some((m) => m.service !== void 0);
|
|
6656
6682
|
const lines = missing.map((m) => {
|
|
6657
6683
|
const by = m.apps?.length ? `, read by ${m.apps.join(", ")}` : "";
|
|
6658
|
-
return ` gkm secrets:set ${m.key} '${m.example}' --stage ${stage}\n ${m.what}${by}` + (m.note ? `\n ${m.note}` : "");
|
|
6684
|
+
return ` gkm secrets:set ${m.key} '${m.example}' --stage ${stage}\n ${m.what}${by}` + (m.note ? `\n ${m.note}` : "") + (m.hint ? `\n ${m.hint}` : "");
|
|
6659
6685
|
});
|
|
6660
|
-
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\
|
|
6686
|
+
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}`);
|
|
6661
6687
|
this.stage = stage;
|
|
6662
6688
|
this.missing = missing;
|
|
6663
6689
|
this.name = "ExternalServicesNotConfigured";
|
|
6664
6690
|
}
|
|
6665
6691
|
};
|
|
6666
6692
|
/**
|
|
6667
|
-
* The dev services a value names: `'minio,mailpit'`, or a list of them.
|
|
6668
|
-
*
|
|
6669
|
-
* @throws {UnknownDevService} for anything that is not one
|
|
6670
|
-
*/
|
|
6671
|
-
function parseDevServices(value) {
|
|
6672
|
-
if (value === void 0) return [];
|
|
6673
|
-
const items = (typeof value === "string" ? [value] : value).flatMap((item) => item.split(",")).map((item) => item.trim()).filter(Boolean);
|
|
6674
|
-
const services = /* @__PURE__ */ new Set();
|
|
6675
|
-
for (const item of items) {
|
|
6676
|
-
if (!DEV_SERVICES.includes(item)) throw new UnknownDevService(item);
|
|
6677
|
-
services.add(item);
|
|
6678
|
-
}
|
|
6679
|
-
return [...services].sort();
|
|
6680
|
-
}
|
|
6681
|
-
/**
|
|
6682
6693
|
* Every mail and storage construct a manifest declares, with the apps that
|
|
6683
6694
|
* depend on each — what a target that provisions the whole manifest checks.
|
|
6684
6695
|
*/
|
|
@@ -6720,6 +6731,8 @@ function requiredStageKeys(input) {
|
|
|
6720
6731
|
const apps = (d) => d.apps?.length ? { apps: [...d.apps].sort() } : {};
|
|
6721
6732
|
if (!input.local) {
|
|
6722
6733
|
const services = byId(input.services);
|
|
6734
|
+
const objects = input.providers?.objects;
|
|
6735
|
+
const storage = objects?.accounted ? objects.hint ? { hint: objects.hint } : {} : { service: "minio" };
|
|
6723
6736
|
for (const d of services.filter((d) => d.kind === "email")) {
|
|
6724
6737
|
add({
|
|
6725
6738
|
key: (0, _geekmidas_manifest.provideKey)(d.id, "url"),
|
|
@@ -6747,7 +6760,7 @@ function requiredStageKeys(input) {
|
|
|
6747
6760
|
what: `the bucket '${d.id}' — S3, R2, or any S3-compatible store (add &endpoint=… for one that is not S3)`,
|
|
6748
6761
|
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`,
|
|
6749
6762
|
example: `s3://${require_derive.appKey(d.id)}?region=eu-west-1`,
|
|
6750
|
-
|
|
6763
|
+
...storage,
|
|
6751
6764
|
...apps(d)
|
|
6752
6765
|
});
|
|
6753
6766
|
for (const d of services.filter((d) => d.kind === "file-server")) add({
|
|
@@ -6756,7 +6769,7 @@ function requiredStageKeys(input) {
|
|
|
6756
6769
|
kind: "file-server",
|
|
6757
6770
|
what: `the public address '${d.id}' serves its bucket on — a CDN or the bucket's own domain`,
|
|
6758
6771
|
example: `https://${require_derive.appKey(d.id)}.${domain}`,
|
|
6759
|
-
|
|
6772
|
+
...storage,
|
|
6760
6773
|
...d.of ? { of: d.of } : {},
|
|
6761
6774
|
...apps(d)
|
|
6762
6775
|
});
|
|
@@ -6805,20 +6818,23 @@ function externalServices(input) {
|
|
|
6805
6818
|
const required = requiredStageKeys({
|
|
6806
6819
|
local: false,
|
|
6807
6820
|
services: input.declarations,
|
|
6808
|
-
...input.domain ? { domain: input.domain } : {}
|
|
6821
|
+
...input.domain ? { domain: input.domain } : {},
|
|
6822
|
+
...input.providers ? { providers: input.providers } : {}
|
|
6809
6823
|
});
|
|
6810
|
-
const
|
|
6811
|
-
const
|
|
6824
|
+
const standIn = (k) => allow && k.service !== void 0 && !has(k.key);
|
|
6825
|
+
const mailpit = required.filter((k) => k.kind === "email" && k.key === (0, _geekmidas_manifest.provideKey)(k.id, "url")).filter(standIn).map((k) => k.id);
|
|
6826
|
+
const minio = required.filter((k) => k.kind === "bucket").filter(standIn).map((k) => k.id);
|
|
6812
6827
|
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);
|
|
6813
6828
|
return {
|
|
6814
|
-
missing: required.filter((k) => !has(k.key) && !covered(k)).map(({ key, id, what, note, example, service, apps }) => ({
|
|
6829
|
+
missing: required.filter((k) => !has(k.key) && !covered(k)).map(({ key, id, what, note, example, service, apps, hint }) => ({
|
|
6815
6830
|
key,
|
|
6816
6831
|
id,
|
|
6817
6832
|
what,
|
|
6818
6833
|
...note ? { note } : {},
|
|
6819
6834
|
example,
|
|
6820
|
-
service,
|
|
6821
|
-
...apps ? { apps } : {}
|
|
6835
|
+
...service ? { service } : {},
|
|
6836
|
+
...apps ? { apps } : {},
|
|
6837
|
+
...hint ? { hint } : {}
|
|
6822
6838
|
})),
|
|
6823
6839
|
minio,
|
|
6824
6840
|
mailpit
|
|
@@ -6867,6 +6883,360 @@ function reportDevServices(ctx, uses) {
|
|
|
6867
6883
|
}
|
|
6868
6884
|
}
|
|
6869
6885
|
//#endregion
|
|
6886
|
+
//#region src/deploy/StateProvider.ts
|
|
6887
|
+
/**
|
|
6888
|
+
* Check if value is a StateProvider implementation.
|
|
6889
|
+
*/
|
|
6890
|
+
function isStateProvider(value) {
|
|
6891
|
+
return typeof value === "object" && value !== null && typeof value.read === "function" && typeof value.write === "function";
|
|
6892
|
+
}
|
|
6893
|
+
//#endregion
|
|
6894
|
+
//#region src/deploy/StateStore.ts
|
|
6895
|
+
/**
|
|
6896
|
+
* Deploy state store.
|
|
6897
|
+
*
|
|
6898
|
+
* `StateProvider` reads and writes a whole stage at once, with nothing to stop
|
|
6899
|
+
* two runs of the same stage from overwriting each other and nowhere to record
|
|
6900
|
+
* a resource the moment it exists. A `StateStore` adds the three things a
|
|
6901
|
+
* deploy needs to be safe against crashes and concurrent runs:
|
|
6902
|
+
*
|
|
6903
|
+
* - `lock(stage)`: one run per stage. A second gets `StateLocked`, naming the
|
|
6904
|
+
* holder, instead of racing the first.
|
|
6905
|
+
* - Versioned `read` and conditional `write`: a write names the version it was
|
|
6906
|
+
* based on, and raises `StateVersionConflict` when the stored state moved on
|
|
6907
|
+
* underneath it, so nothing is lost silently even without the lock.
|
|
6908
|
+
* - Per-resource records: `putResource` journals one resource at a time, so a
|
|
6909
|
+
* run that dies half way leaves behind the ids of everything it created.
|
|
6910
|
+
*
|
|
6911
|
+
* Built-in stores: `LocalStateStore`, `SSMStateStore`, `S3StateStore`. A
|
|
6912
|
+
* custom `StateProvider` is wrapped by `LegacyStateStore`, which keeps it
|
|
6913
|
+
* working but cannot lock.
|
|
6914
|
+
*/
|
|
6915
|
+
var StateLocked = class extends Error {
|
|
6916
|
+
stage;
|
|
6917
|
+
holder;
|
|
6918
|
+
location;
|
|
6919
|
+
constructor(stage, holder, location) {
|
|
6920
|
+
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";
|
|
6921
|
+
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}\`.`);
|
|
6922
|
+
this.stage = stage;
|
|
6923
|
+
this.holder = holder;
|
|
6924
|
+
this.location = location;
|
|
6925
|
+
this.name = "StateLocked";
|
|
6926
|
+
}
|
|
6927
|
+
};
|
|
6928
|
+
var StateVersionConflict = class extends Error {
|
|
6929
|
+
stage;
|
|
6930
|
+
expectedVersion;
|
|
6931
|
+
actualVersion;
|
|
6932
|
+
constructor(stage, expectedVersion, actualVersion) {
|
|
6933
|
+
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.`);
|
|
6934
|
+
this.stage = stage;
|
|
6935
|
+
this.expectedVersion = expectedVersion;
|
|
6936
|
+
this.actualVersion = actualVersion;
|
|
6937
|
+
this.name = "StateVersionConflict";
|
|
6938
|
+
}
|
|
6939
|
+
};
|
|
6940
|
+
var StageStateMissing = class extends Error {
|
|
6941
|
+
stage;
|
|
6942
|
+
constructor(stage) {
|
|
6943
|
+
super(`There is no deploy state for stage '${stage}' to record a resource in. Write the stage's state first (write with expectedVersion: null).`);
|
|
6944
|
+
this.stage = stage;
|
|
6945
|
+
this.name = "StageStateMissing";
|
|
6946
|
+
}
|
|
6947
|
+
};
|
|
6948
|
+
var StateUnreadable = class extends Error {
|
|
6949
|
+
stage;
|
|
6950
|
+
location;
|
|
6951
|
+
constructor(stage, location, cause) {
|
|
6952
|
+
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 });
|
|
6953
|
+
this.stage = stage;
|
|
6954
|
+
this.location = location;
|
|
6955
|
+
this.name = "StateUnreadable";
|
|
6956
|
+
}
|
|
6957
|
+
};
|
|
6958
|
+
var StateSchemaTooNew = class extends Error {
|
|
6959
|
+
stage;
|
|
6960
|
+
schemaVersion;
|
|
6961
|
+
constructor(stage, schemaVersion) {
|
|
6962
|
+
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.`);
|
|
6963
|
+
this.stage = stage;
|
|
6964
|
+
this.schemaVersion = schemaVersion;
|
|
6965
|
+
this.name = "StateSchemaTooNew";
|
|
6966
|
+
}
|
|
6967
|
+
};
|
|
6968
|
+
/**
|
|
6969
|
+
* Not thrown: emitted (once per store) when a custom `StateProvider` stands
|
|
6970
|
+
* in for a store, so a deploy that cannot be protected from a concurrent one
|
|
6971
|
+
* says so.
|
|
6972
|
+
*/
|
|
6973
|
+
var StateStoreWithoutLocking = class extends Error {
|
|
6974
|
+
provider;
|
|
6975
|
+
constructor(provider) {
|
|
6976
|
+
super(`The custom state provider '${provider}' has no lock and no versions, so two deploys of the same stage can still overwrite each other. Implement StateStore (lock, versioned read, conditional write) or use the 'local', 'ssm' or 's3' state provider.`);
|
|
6977
|
+
this.provider = provider;
|
|
6978
|
+
this.name = "StateStoreWithoutLocking";
|
|
6979
|
+
}
|
|
6980
|
+
};
|
|
6981
|
+
var StateStoreNeedsWorkspaceName = class extends Error {
|
|
6982
|
+
provider;
|
|
6983
|
+
constructor(provider) {
|
|
6984
|
+
super(`The '${provider}' state provider keys state by workspace name. Set "name" in gkm.config.ts.`);
|
|
6985
|
+
this.provider = provider;
|
|
6986
|
+
this.name = "StateStoreNeedsWorkspaceName";
|
|
6987
|
+
}
|
|
6988
|
+
};
|
|
6989
|
+
var UnknownStateProvider = class extends Error {
|
|
6990
|
+
config;
|
|
6991
|
+
constructor(config) {
|
|
6992
|
+
super(`Unknown state provider ${JSON.stringify(config)}. Use 'local', 'ssm', 's3', or an object implementing StateStore or StateProvider.`);
|
|
6993
|
+
this.config = config;
|
|
6994
|
+
this.name = "UnknownStateProvider";
|
|
6995
|
+
}
|
|
6996
|
+
};
|
|
6997
|
+
/** A lock or write that could not get at the state backend in time. */
|
|
6998
|
+
var StateStoreBusy = class extends Error {
|
|
6999
|
+
stage;
|
|
7000
|
+
location;
|
|
7001
|
+
constructor(stage, location) {
|
|
7002
|
+
super(`Timed out waiting to write deploy state for stage '${stage}' (${location}). If no other gkm process is running, remove ${location}.`);
|
|
7003
|
+
this.stage = stage;
|
|
7004
|
+
this.location = location;
|
|
7005
|
+
this.name = "StateStoreBusy";
|
|
7006
|
+
}
|
|
7007
|
+
};
|
|
7008
|
+
function decodeDocument(stage, body, location) {
|
|
7009
|
+
let parsed;
|
|
7010
|
+
try {
|
|
7011
|
+
parsed = JSON.parse(body);
|
|
7012
|
+
} catch (error) {
|
|
7013
|
+
throw new StateUnreadable(stage, location, error);
|
|
7014
|
+
}
|
|
7015
|
+
if (typeof parsed !== "object" || parsed === null) throw new StateUnreadable(stage, location, parsed);
|
|
7016
|
+
const schemaVersion = parsed.schemaVersion;
|
|
7017
|
+
if (schemaVersion === void 0) return {
|
|
7018
|
+
schemaVersion: 1,
|
|
7019
|
+
state: parsed
|
|
7020
|
+
};
|
|
7021
|
+
if (schemaVersion === 2) return {
|
|
7022
|
+
schemaVersion: 2,
|
|
7023
|
+
document: parsed
|
|
7024
|
+
};
|
|
7025
|
+
throw new StateSchemaTooNew(stage, schemaVersion);
|
|
7026
|
+
}
|
|
7027
|
+
function encodeDocument(document) {
|
|
7028
|
+
return JSON.stringify(document, null, 2);
|
|
7029
|
+
}
|
|
7030
|
+
/**
|
|
7031
|
+
* v1 state as a v2 document. The ids v1 kept are seeded as `ready` records,
|
|
7032
|
+
* so the first journaled deploy after an upgrade adopts them rather than
|
|
7033
|
+
* treating them as unknown.
|
|
7034
|
+
*/
|
|
7035
|
+
function migrateV1(stage, state) {
|
|
7036
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7037
|
+
const resources = {};
|
|
7038
|
+
const seed = (type, name, id) => {
|
|
7039
|
+
if (!id) return;
|
|
7040
|
+
const key = name ? `${type}:${name}` : type;
|
|
7041
|
+
resources[key] = {
|
|
7042
|
+
key,
|
|
7043
|
+
type,
|
|
7044
|
+
id,
|
|
7045
|
+
status: "ready",
|
|
7046
|
+
updatedAt: now
|
|
7047
|
+
};
|
|
7048
|
+
};
|
|
7049
|
+
seed("project", null, state.projectId);
|
|
7050
|
+
seed("environment", null, state.environmentId);
|
|
7051
|
+
for (const [app, id] of Object.entries(state.applications ?? {})) seed("application", app, id);
|
|
7052
|
+
seed("postgres", null, state.services?.postgresId);
|
|
7053
|
+
seed("redis", null, state.services?.redisId);
|
|
7054
|
+
seed("backup-destination", null, state.backups?.destinationId);
|
|
7055
|
+
return {
|
|
7056
|
+
schemaVersion: 2,
|
|
7057
|
+
stage,
|
|
7058
|
+
serial: 1,
|
|
7059
|
+
state,
|
|
7060
|
+
resources,
|
|
7061
|
+
updatedAt: now
|
|
7062
|
+
};
|
|
7063
|
+
}
|
|
7064
|
+
function newLockHolder(options) {
|
|
7065
|
+
let owner = "unknown";
|
|
7066
|
+
try {
|
|
7067
|
+
owner = (0, node_os.userInfo)().username;
|
|
7068
|
+
} catch {}
|
|
7069
|
+
return {
|
|
7070
|
+
id: (0, node_crypto.randomUUID)(),
|
|
7071
|
+
owner,
|
|
7072
|
+
host: (0, node_os.hostname)(),
|
|
7073
|
+
pid: process.pid,
|
|
7074
|
+
acquiredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7075
|
+
operation: options?.operation
|
|
7076
|
+
};
|
|
7077
|
+
}
|
|
7078
|
+
function contentVersion(body) {
|
|
7079
|
+
return (0, node_crypto.createHash)("sha256").update(body).digest("hex").slice(0, 16);
|
|
7080
|
+
}
|
|
7081
|
+
/**
|
|
7082
|
+
* A store over a backend that can read and conditionally write one blob per
|
|
7083
|
+
* stage, and create a lock object only if it does not exist. Migration and
|
|
7084
|
+
* resource records are the same for every such backend, so they live here.
|
|
7085
|
+
*/
|
|
7086
|
+
var DocumentStateStore = class {
|
|
7087
|
+
async lock(stage, options) {
|
|
7088
|
+
const holder = newLockHolder(options);
|
|
7089
|
+
await this.createLock(stage, holder);
|
|
7090
|
+
let released = false;
|
|
7091
|
+
return {
|
|
7092
|
+
stage,
|
|
7093
|
+
holder,
|
|
7094
|
+
release: async () => {
|
|
7095
|
+
if (released) return;
|
|
7096
|
+
released = true;
|
|
7097
|
+
await this.removeLock(stage, holder);
|
|
7098
|
+
}
|
|
7099
|
+
};
|
|
7100
|
+
}
|
|
7101
|
+
async forceUnlock(stage) {
|
|
7102
|
+
const holder = await this.readLock(stage);
|
|
7103
|
+
await this.removeLock(stage, null);
|
|
7104
|
+
return holder;
|
|
7105
|
+
}
|
|
7106
|
+
async read(stage) {
|
|
7107
|
+
const current = await this.readDocument(stage);
|
|
7108
|
+
if (!current) return null;
|
|
7109
|
+
return {
|
|
7110
|
+
state: current.document.state,
|
|
7111
|
+
resources: current.document.resources,
|
|
7112
|
+
version: current.version
|
|
7113
|
+
};
|
|
7114
|
+
}
|
|
7115
|
+
async write(stage, state, options) {
|
|
7116
|
+
if (options.expectedVersion === null) {
|
|
7117
|
+
const document = {
|
|
7118
|
+
schemaVersion: 2,
|
|
7119
|
+
stage,
|
|
7120
|
+
serial: 1,
|
|
7121
|
+
state,
|
|
7122
|
+
resources: {},
|
|
7123
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7124
|
+
};
|
|
7125
|
+
return this.writeRaw(stage, encodeDocument(document), null);
|
|
7126
|
+
}
|
|
7127
|
+
return this.mutate(stage, options.expectedVersion, (document) => ({
|
|
7128
|
+
...document,
|
|
7129
|
+
state
|
|
7130
|
+
}));
|
|
7131
|
+
}
|
|
7132
|
+
async putResource(stage, record, options) {
|
|
7133
|
+
return this.mutate(stage, options?.expectedVersion, (document) => ({
|
|
7134
|
+
...document,
|
|
7135
|
+
resources: {
|
|
7136
|
+
...document.resources,
|
|
7137
|
+
[record.key]: {
|
|
7138
|
+
...record,
|
|
7139
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7140
|
+
}
|
|
7141
|
+
}
|
|
7142
|
+
}));
|
|
7143
|
+
}
|
|
7144
|
+
async deleteResource(stage, key, options) {
|
|
7145
|
+
return this.mutate(stage, options?.expectedVersion, (document) => {
|
|
7146
|
+
const { [key]: _removed, ...resources } = document.resources;
|
|
7147
|
+
return {
|
|
7148
|
+
...document,
|
|
7149
|
+
resources
|
|
7150
|
+
};
|
|
7151
|
+
});
|
|
7152
|
+
}
|
|
7153
|
+
async mutate(stage, expectedVersion, change) {
|
|
7154
|
+
const current = await this.readDocument(stage);
|
|
7155
|
+
if (!current) {
|
|
7156
|
+
if (expectedVersion !== void 0) throw new StateVersionConflict(stage, expectedVersion, null);
|
|
7157
|
+
throw new StageStateMissing(stage);
|
|
7158
|
+
}
|
|
7159
|
+
if (expectedVersion !== void 0 && current.version !== expectedVersion) throw new StateVersionConflict(stage, expectedVersion, current.version);
|
|
7160
|
+
const next = change(current.document);
|
|
7161
|
+
next.serial = current.document.serial + 1;
|
|
7162
|
+
next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7163
|
+
return this.writeRaw(stage, encodeDocument(next), current.version);
|
|
7164
|
+
}
|
|
7165
|
+
/**
|
|
7166
|
+
* Reads the stage's document, migrating v1 in place on the way. The
|
|
7167
|
+
* migration is itself a conditional write: if another run migrated (or
|
|
7168
|
+
* wrote) first, its result is read instead.
|
|
7169
|
+
*/
|
|
7170
|
+
async readDocument(stage, attempt = 0) {
|
|
7171
|
+
const raw = await this.readRaw(stage);
|
|
7172
|
+
if (!raw) return null;
|
|
7173
|
+
const decoded = decodeDocument(stage, raw.body, this.location(stage));
|
|
7174
|
+
if (decoded.schemaVersion === 2) return {
|
|
7175
|
+
document: decoded.document,
|
|
7176
|
+
version: raw.version
|
|
7177
|
+
};
|
|
7178
|
+
await this.writeV1Backup(stage, raw.body);
|
|
7179
|
+
const document = migrateV1(stage, decoded.state);
|
|
7180
|
+
try {
|
|
7181
|
+
return {
|
|
7182
|
+
document,
|
|
7183
|
+
version: await this.writeRaw(stage, encodeDocument(document), raw.version)
|
|
7184
|
+
};
|
|
7185
|
+
} catch (error) {
|
|
7186
|
+
if (error instanceof StateVersionConflict && attempt < 2) return this.readDocument(stage, attempt + 1);
|
|
7187
|
+
throw error;
|
|
7188
|
+
}
|
|
7189
|
+
}
|
|
7190
|
+
};
|
|
7191
|
+
function isStateStore(value) {
|
|
7192
|
+
return typeof value === "object" && value !== null && typeof value.lock === "function" && typeof value.read === "function" && typeof value.write === "function" && typeof value.putResource === "function";
|
|
7193
|
+
}
|
|
7194
|
+
/**
|
|
7195
|
+
* The store for a workspace's `state` config:
|
|
7196
|
+
*
|
|
7197
|
+
* - none / `'local'`: `LocalStateStore` under `.gkm/`
|
|
7198
|
+
* - `'ssm'`: `SSMStateStore`, with no local cache — SSM is read directly, so
|
|
7199
|
+
* a stale local copy can never win over it
|
|
7200
|
+
* - `'s3'`: `S3StateStore`
|
|
7201
|
+
* - a `StateStore` object: used as is
|
|
7202
|
+
* - a `StateProvider` object: wrapped by `LegacyStateStore`
|
|
7203
|
+
*/
|
|
7204
|
+
async function createStateStore(options) {
|
|
7205
|
+
const { config, workspaceRoot, workspaceName } = options;
|
|
7206
|
+
if (!config || config.provider === "local") {
|
|
7207
|
+
const { LocalStateStore } = await Promise.resolve().then(() => require("./LocalStateStore-CLwa2DpK.cjs")).then((n) => n.LocalStateStore_exports);
|
|
7208
|
+
return new LocalStateStore(workspaceRoot);
|
|
7209
|
+
}
|
|
7210
|
+
if (isStateStore(config.provider)) return config.provider;
|
|
7211
|
+
if (isStateProvider(config.provider)) {
|
|
7212
|
+
const { LegacyStateStore } = await Promise.resolve().then(() => require("./LegacyStateStore-CWG_HVC0.cjs"));
|
|
7213
|
+
return new LegacyStateStore(config.provider, { warn: options.warn });
|
|
7214
|
+
}
|
|
7215
|
+
if (config.provider === "ssm") {
|
|
7216
|
+
if (!workspaceName) throw new StateStoreNeedsWorkspaceName("ssm");
|
|
7217
|
+
const { SSMStateStore } = await Promise.resolve().then(() => require("./SSMStateStore-aBOTF018.cjs"));
|
|
7218
|
+
const ssm = config;
|
|
7219
|
+
return SSMStateStore.create({
|
|
7220
|
+
workspaceName,
|
|
7221
|
+
region: ssm.region,
|
|
7222
|
+
profile: ssm.profile
|
|
7223
|
+
});
|
|
7224
|
+
}
|
|
7225
|
+
if (config.provider === "s3") {
|
|
7226
|
+
if (!workspaceName) throw new StateStoreNeedsWorkspaceName("s3");
|
|
7227
|
+
const { S3StateStore } = await Promise.resolve().then(() => require("./S3StateStore-Cea9_5gF.cjs"));
|
|
7228
|
+
const s3 = config;
|
|
7229
|
+
return S3StateStore.create({
|
|
7230
|
+
workspaceName,
|
|
7231
|
+
bucket: s3.bucket,
|
|
7232
|
+
prefix: s3.prefix,
|
|
7233
|
+
region: s3.region,
|
|
7234
|
+
profile: s3.profile
|
|
7235
|
+
});
|
|
7236
|
+
}
|
|
7237
|
+
throw new UnknownStateProvider(config);
|
|
7238
|
+
}
|
|
7239
|
+
//#endregion
|
|
6870
7240
|
//#region src/docker/imageRef.ts
|
|
6871
7241
|
/**
|
|
6872
7242
|
* An image reference, checked before it reaches `docker`.
|
|
@@ -6954,7 +7324,7 @@ async function buildDockerImage(app, buildRoot, options) {
|
|
|
6954
7324
|
const fullImageName = validateImageRef(registry ? `${registry}/${app.imageName}:${tag}` : `${app.imageName}:${tag}`);
|
|
6955
7325
|
logger.log(`\n🐳 Building Docker image: ${fullImageName}`);
|
|
6956
7326
|
try {
|
|
6957
|
-
await
|
|
7327
|
+
await require_credentials.run("docker", [
|
|
6958
7328
|
"build",
|
|
6959
7329
|
`--file=${(0, node_path.relative)(buildRoot, app.dockerfile)}`,
|
|
6960
7330
|
`--tag=${fullImageName}`,
|
|
@@ -6981,7 +7351,7 @@ async function pushDockerImage(imageName, options, cwd) {
|
|
|
6981
7351
|
const fullImageName = validateImageRef(`${registry}/${imageName}:${tag}`);
|
|
6982
7352
|
logger.log(`\n🚀 Pushing Docker image: ${fullImageName}`);
|
|
6983
7353
|
try {
|
|
6984
|
-
await
|
|
7354
|
+
await require_credentials.run("docker", ["push", fullImageName], { cwd });
|
|
6985
7355
|
logger.log(`✅ Docker image pushed: ${fullImageName}`);
|
|
6986
7356
|
} catch (error) {
|
|
6987
7357
|
throw new DockerPushFailed(fullImageName, error);
|
|
@@ -7178,6 +7548,71 @@ function printRunInstructions(workspace) {
|
|
|
7178
7548
|
logger.log(` docker compose -f ${COMPOSE_PATH}${project} --profile apps up --build`);
|
|
7179
7549
|
}
|
|
7180
7550
|
//#endregion
|
|
7551
|
+
//#region src/providers/types.ts
|
|
7552
|
+
/** The manifest kinds a stage can name a provider for. */
|
|
7553
|
+
const PROVIDER_KINDS = ["objects"];
|
|
7554
|
+
//#endregion
|
|
7555
|
+
//#region src/providers/notes.ts
|
|
7556
|
+
/** The constructs of a kind: `objects` is every bucket. */
|
|
7557
|
+
function constructsOf$1(manifest, kind) {
|
|
7558
|
+
return Object.entries(manifest).filter(([, d]) => d.kind === kind).map(([id]) => id).sort();
|
|
7559
|
+
}
|
|
7560
|
+
/** A stage that names `false` for a kind, and declares constructs of it. */
|
|
7561
|
+
var StageProviderDisabled = class extends Error {
|
|
7562
|
+
kind;
|
|
7563
|
+
stage;
|
|
7564
|
+
ids;
|
|
7565
|
+
constructor(kind, stage, ids) {
|
|
7566
|
+
super(`deploy.${kind}.${stage} is false — the stage has no ${kind} — and ${ids.join(", ")} ${ids.length === 1 ? "is" : "are"} declared. Name what backs ${ids.length === 1 ? "it" : "them"} on '${stage}' (deploy.${kind}.${stage}: 'external', or a provider), or remove the ${ids.length === 1 ? "construct" : "constructs"}.`);
|
|
7567
|
+
this.kind = kind;
|
|
7568
|
+
this.stage = stage;
|
|
7569
|
+
this.ids = ids;
|
|
7570
|
+
this.name = "StageProviderDisabled";
|
|
7571
|
+
}
|
|
7572
|
+
};
|
|
7573
|
+
/** The command a stage's providers are run with. */
|
|
7574
|
+
function provisionCommand(stage) {
|
|
7575
|
+
return `gkm setup --stage ${stage}`;
|
|
7576
|
+
}
|
|
7577
|
+
/**
|
|
7578
|
+
* The line a stage missing a key a provider would write is given: what
|
|
7579
|
+
* creates it, and with which credentials. Undefined when no provider backs
|
|
7580
|
+
* the kind on the stage.
|
|
7581
|
+
*/
|
|
7582
|
+
function provisionHint(workspace, kind, stage) {
|
|
7583
|
+
const choice = require_workspace.stageProvider(workspace, kind, stage);
|
|
7584
|
+
if (choice.mode !== "provider") return void 0;
|
|
7585
|
+
const provisioning = require_workspace.provisioningOf(kind, choice.name);
|
|
7586
|
+
if (!provisioning) return void 0;
|
|
7587
|
+
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}` : "");
|
|
7588
|
+
}
|
|
7589
|
+
/**
|
|
7590
|
+
* What a deploy's checks read off the stage's providers: whether each kind is
|
|
7591
|
+
* accounted for — a provider, or `false` — so no dev service stands in for
|
|
7592
|
+
* it, and what creates a missing key of it.
|
|
7593
|
+
*/
|
|
7594
|
+
function stageProviderNotes(workspace, stage) {
|
|
7595
|
+
if (require_workspace.stageProvider(workspace, "objects", stage).mode === "external") return {};
|
|
7596
|
+
const hint = provisionHint(workspace, "objects", stage);
|
|
7597
|
+
return { objects: {
|
|
7598
|
+
accounted: true,
|
|
7599
|
+
...hint ? { hint } : {}
|
|
7600
|
+
} };
|
|
7601
|
+
}
|
|
7602
|
+
/**
|
|
7603
|
+
* A kind the stage set to `false`, declared anyway — refused before anything
|
|
7604
|
+
* else is checked. Pure: what a stack is composed from.
|
|
7605
|
+
*
|
|
7606
|
+
* @throws {StageProviderDisabled}
|
|
7607
|
+
*/
|
|
7608
|
+
function assertStageProvidersEnabled(workspace, manifest, stage) {
|
|
7609
|
+
for (const kind of PROVIDER_KINDS) {
|
|
7610
|
+
const ids = constructsOf$1(manifest, kind);
|
|
7611
|
+
if (ids.length === 0) continue;
|
|
7612
|
+
if (require_workspace.stageProvider(workspace, kind, stage).mode === "none") throw new StageProviderDisabled(kind, stage, ids);
|
|
7613
|
+
}
|
|
7614
|
+
}
|
|
7615
|
+
//#endregion
|
|
7181
7616
|
//#region src/deploy/journal.ts
|
|
7182
7617
|
var DeployJournal = class DeployJournal {
|
|
7183
7618
|
store;
|
|
@@ -7434,6 +7869,1044 @@ function setPostgresBackupId(state, backupId) {
|
|
|
7434
7869
|
if (state.backups) state.backups.postgresBackupId = backupId;
|
|
7435
7870
|
}
|
|
7436
7871
|
//#endregion
|
|
7872
|
+
//#region src/providers/aws.ts
|
|
7873
|
+
/**
|
|
7874
|
+
* The AWS account a provider provisions in — the stage's, found the way the
|
|
7875
|
+
* SSM and Secrets Manager stores find it.
|
|
7876
|
+
*
|
|
7877
|
+
* 1. `--profile`, resolved on its own: exported keys left over from another
|
|
7878
|
+
* account never win over a profile someone named.
|
|
7879
|
+
* 2. `AWS_PROFILE`, else `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY` — the
|
|
7880
|
+
* deploy's own `aws` credential kind (`storedCredentials`).
|
|
7881
|
+
* 3. The SDK's default chain: `~/.aws` default profile, SSO, a role. In CI,
|
|
7882
|
+
* whatever `aws-actions/configure-aws-credentials` exported.
|
|
7883
|
+
*
|
|
7884
|
+
* None of them: the provider has no provisioning credentials, and the stage
|
|
7885
|
+
* is treated as `external`.
|
|
7886
|
+
*/
|
|
7887
|
+
async function awsProvisioningCredentials(lookup) {
|
|
7888
|
+
const region = lookup.env.AWS_REGION ?? lookup.env.AWS_DEFAULT_REGION;
|
|
7889
|
+
const where = region ? { region } : {};
|
|
7890
|
+
if (lookup.profile) return {
|
|
7891
|
+
profile: lookup.profile,
|
|
7892
|
+
...where
|
|
7893
|
+
};
|
|
7894
|
+
const stored = await require_credentials.storedCredentials({
|
|
7895
|
+
env: lookup.env,
|
|
7896
|
+
...lookup.home ? { home: lookup.home } : {}
|
|
7897
|
+
}).get({
|
|
7898
|
+
kind: "aws",
|
|
7899
|
+
stage: lookup.stage
|
|
7900
|
+
});
|
|
7901
|
+
if (stored) return stored;
|
|
7902
|
+
try {
|
|
7903
|
+
const { fromNodeProviderChain } = await import("@aws-sdk/credential-providers");
|
|
7904
|
+
await fromNodeProviderChain({
|
|
7905
|
+
timeout: 1e3,
|
|
7906
|
+
maxRetries: 0
|
|
7907
|
+
})();
|
|
7908
|
+
return {
|
|
7909
|
+
chain: true,
|
|
7910
|
+
...where
|
|
7911
|
+
};
|
|
7912
|
+
} catch {
|
|
7913
|
+
return;
|
|
7914
|
+
}
|
|
7915
|
+
}
|
|
7916
|
+
/**
|
|
7917
|
+
* An S3-compatible endpoint the environment points the SDK at — the AWS
|
|
7918
|
+
* emulator in tests. The SDK reads it itself; it is read here too, because
|
|
7919
|
+
* an emulator takes path-style requests, and a key written for one has to
|
|
7920
|
+
* say where it is.
|
|
7921
|
+
*/
|
|
7922
|
+
function awsEndpoint(env, service) {
|
|
7923
|
+
return env[`AWS_ENDPOINT_URL_${service}`] ?? env.AWS_ENDPOINT_URL;
|
|
7924
|
+
}
|
|
7925
|
+
/** An SDK client's config for a provisioning credential. */
|
|
7926
|
+
async function awsClientConfig(credential, region) {
|
|
7927
|
+
if ("profile" in credential) {
|
|
7928
|
+
const { fromIni } = await import("@aws-sdk/credential-providers");
|
|
7929
|
+
return {
|
|
7930
|
+
region,
|
|
7931
|
+
credentials: fromIni({ profile: credential.profile })
|
|
7932
|
+
};
|
|
7933
|
+
}
|
|
7934
|
+
if ("accessKeyId" in credential) return {
|
|
7935
|
+
region,
|
|
7936
|
+
credentials: {
|
|
7937
|
+
accessKeyId: credential.accessKeyId,
|
|
7938
|
+
secretAccessKey: credential.secretAccessKey,
|
|
7939
|
+
...credential.sessionToken ? { sessionToken: credential.sessionToken } : {}
|
|
7940
|
+
}
|
|
7941
|
+
};
|
|
7942
|
+
return { region };
|
|
7943
|
+
}
|
|
7944
|
+
const SUFFIX_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
7945
|
+
/** A name that S3 would refuse — what the name builder never returns. */
|
|
7946
|
+
var InvalidBucketName = class extends Error {
|
|
7947
|
+
bucket;
|
|
7948
|
+
reason;
|
|
7949
|
+
constructor(bucket, reason) {
|
|
7950
|
+
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.`);
|
|
7951
|
+
this.bucket = bucket;
|
|
7952
|
+
this.reason = reason;
|
|
7953
|
+
this.name = "InvalidBucketName";
|
|
7954
|
+
}
|
|
7955
|
+
};
|
|
7956
|
+
/** Lowercase `[a-z0-9-]`, runs of anything else one `-`, no edge dashes. */
|
|
7957
|
+
function part(value) {
|
|
7958
|
+
return (0, _geekmidas_manifest.kebabCase)(value).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
7959
|
+
}
|
|
7960
|
+
/**
|
|
7961
|
+
* `parts` joined by `-`, shortened to `max` characters by trimming the longest
|
|
7962
|
+
* part one character at a time — so a long construct id gives way before a
|
|
7963
|
+
* short stage name does, and every part keeps its start.
|
|
7964
|
+
*/
|
|
7965
|
+
function fitParts(parts, max) {
|
|
7966
|
+
const kept = parts.map(part).filter(Boolean);
|
|
7967
|
+
const length = () => kept.reduce((sum, p) => sum + p.length, 0) + kept.length - 1;
|
|
7968
|
+
while (length() > max) {
|
|
7969
|
+
let longest = 0;
|
|
7970
|
+
for (let i = 1; i < kept.length; i++) if (kept[i].length > kept[longest].length) longest = i;
|
|
7971
|
+
if (kept[longest].length <= 1) break;
|
|
7972
|
+
kept[longest] = kept[longest].slice(0, -1).replace(/-+$/, "");
|
|
7973
|
+
}
|
|
7974
|
+
return kept.join("-");
|
|
7975
|
+
}
|
|
7976
|
+
/** Why `name` is not a valid bucket name, or undefined when it is. */
|
|
7977
|
+
function bucketNameProblem(name) {
|
|
7978
|
+
if (name.length < 3 || name.length > 63) return `it is ${name.length} characters, and S3 takes 3 to 63`;
|
|
7979
|
+
if (!/^[a-z0-9-]+$/.test(name)) return "it may hold only lowercase letters, digits and -";
|
|
7980
|
+
if (!/^[a-z0-9].*[a-z0-9]$/.test(name)) return "it must start and end with a letter or a digit";
|
|
7981
|
+
if (name.startsWith("xn--") || name.startsWith("sthree-")) return "it starts with a prefix S3 reserves ('xn--', 'sthree-')";
|
|
7982
|
+
if (name.endsWith("-s3alias") || name.endsWith("--ol-s3")) return "it ends with a suffix S3 reserves ('-s3alias', '--ol-s3')";
|
|
7983
|
+
}
|
|
7984
|
+
/**
|
|
7985
|
+
* The good name: `<namespace>-<project>-<stage>-<bucket id>`, lowercased, no
|
|
7986
|
+
* dots, shortened to leave room for a clash's suffix.
|
|
7987
|
+
*
|
|
7988
|
+
* @throws {InvalidBucketName} when nothing usable is left
|
|
7989
|
+
*/
|
|
7990
|
+
function bucketName(input) {
|
|
7991
|
+
const name = fitParts([
|
|
7992
|
+
input.scope,
|
|
7993
|
+
input.stage,
|
|
7994
|
+
input.id
|
|
7995
|
+
], 56);
|
|
7996
|
+
const problem = bucketNameProblem(name);
|
|
7997
|
+
if (problem) throw new InvalidBucketName(name, problem);
|
|
7998
|
+
return name;
|
|
7999
|
+
}
|
|
8000
|
+
/** Six random `[a-z0-9]` characters. */
|
|
8001
|
+
function randomSuffix(random = node_crypto.randomInt) {
|
|
8002
|
+
let suffix = "";
|
|
8003
|
+
for (let i = 0; i < 6; i++) suffix += SUFFIX_ALPHABET[random(36)];
|
|
8004
|
+
return suffix;
|
|
8005
|
+
}
|
|
8006
|
+
/** The good name with a clash's suffix. */
|
|
8007
|
+
function suffixedBucketName(good, suffix) {
|
|
8008
|
+
return `${good}-${suffix}`;
|
|
8009
|
+
}
|
|
8010
|
+
/**
|
|
8011
|
+
* The IAM user a bucket is reached with on a stage:
|
|
8012
|
+
* `gkm-<project>-<stage>-<bucket id>`, at most 64 characters.
|
|
8013
|
+
*/
|
|
8014
|
+
function iamUserName(input) {
|
|
8015
|
+
return fitParts([
|
|
8016
|
+
"gkm",
|
|
8017
|
+
input.scope,
|
|
8018
|
+
input.stage,
|
|
8019
|
+
input.id
|
|
8020
|
+
], 64);
|
|
8021
|
+
}
|
|
8022
|
+
/** The path every user gkm creates is under, so it finds its own. */
|
|
8023
|
+
const IAM_USER_PATH = "/gkm/";
|
|
8024
|
+
/** The inline policy each user is given. */
|
|
8025
|
+
const IAM_POLICY_NAME = "gkm-bucket";
|
|
8026
|
+
//#endregion
|
|
8027
|
+
//#region src/providers/s3/origins.ts
|
|
8028
|
+
/**
|
|
8029
|
+
* Which browser origins may call a bucket: the stage's sites that depend on
|
|
8030
|
+
* an API using it.
|
|
8031
|
+
*
|
|
8032
|
+
* The same reading of the graph an API's own CORS origins are — its callers —
|
|
8033
|
+
* taken one step further: a site that calls an API which hands it a presigned
|
|
8034
|
+
* upload URL is a site whose browser PUTs to the bucket. A site that reads the
|
|
8035
|
+
* bucket (or a file server over it) itself is one too. Nothing is written
|
|
8036
|
+
* down: add a site that calls the API and the next provision run allows it.
|
|
8037
|
+
*/
|
|
8038
|
+
/** The keys a reader of this bucket holds: its URL, and each server's over it. */
|
|
8039
|
+
function bucketKeys(manifest, bucket) {
|
|
8040
|
+
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"))];
|
|
8041
|
+
}
|
|
8042
|
+
function bucketOrigins(input) {
|
|
8043
|
+
const { manifest, workspace, stage } = input;
|
|
8044
|
+
const keys = bucketKeys(manifest, input.bucket);
|
|
8045
|
+
const readers = Object.entries(manifest).filter(([, d]) => d.kind === "rest-api" || d.kind === "site").filter(([id]) => {
|
|
8046
|
+
const env = appEnvKeys(manifest, require_derive.appKey(id), input.runnables);
|
|
8047
|
+
return env !== void 0 && keys.some((key) => env.has(key));
|
|
8048
|
+
});
|
|
8049
|
+
const sites = /* @__PURE__ */ new Set();
|
|
8050
|
+
for (const [id, declaration] of readers) {
|
|
8051
|
+
if (declaration.kind === "site") {
|
|
8052
|
+
sites.add(id);
|
|
8053
|
+
continue;
|
|
8054
|
+
}
|
|
8055
|
+
for (const caller of (0, _geekmidas_manifest.dependentsOf)(manifest, id)) if (manifest[caller]?.kind === "site") sites.add(caller);
|
|
8056
|
+
}
|
|
8057
|
+
const origins = /* @__PURE__ */ new Set();
|
|
8058
|
+
const unresolved = [];
|
|
8059
|
+
for (const id of [...sites].sort()) {
|
|
8060
|
+
const name = require_derive.appKey(id);
|
|
8061
|
+
const app = workspace.apps[name];
|
|
8062
|
+
if (!app) {
|
|
8063
|
+
unresolved.push(name);
|
|
8064
|
+
continue;
|
|
8065
|
+
}
|
|
8066
|
+
try {
|
|
8067
|
+
const host = resolveHost(name, app, stage, workspace.deploy?.domains, isMainFrontendApp(name, app, workspace.apps));
|
|
8068
|
+
origins.add(`https://${host}`);
|
|
8069
|
+
} catch (error) {
|
|
8070
|
+
if (!(error instanceof NoDomainForStage)) throw error;
|
|
8071
|
+
unresolved.push(name);
|
|
8072
|
+
}
|
|
8073
|
+
}
|
|
8074
|
+
return {
|
|
8075
|
+
origins: [...origins].sort(),
|
|
8076
|
+
unresolved
|
|
8077
|
+
};
|
|
8078
|
+
}
|
|
8079
|
+
//#endregion
|
|
8080
|
+
//#region src/providers/s3/policy.ts
|
|
8081
|
+
const bucketArn = (bucket) => `arn:aws:s3:::${bucket}`;
|
|
8082
|
+
/**
|
|
8083
|
+
* What the app's key may do: objects in this bucket, and listing it — and
|
|
8084
|
+
* nothing else in the account.
|
|
8085
|
+
*
|
|
8086
|
+
* Multipart uploads are in it because a presigned upload of a large file is
|
|
8087
|
+
* one, and an abandoned one is cleaned up by the app. Reading old versions is
|
|
8088
|
+
* added only when the bucket keeps them.
|
|
8089
|
+
*/
|
|
8090
|
+
function userPolicy(bucket, options = {}) {
|
|
8091
|
+
return {
|
|
8092
|
+
Version: "2012-10-17",
|
|
8093
|
+
Statement: [{
|
|
8094
|
+
Sid: "GkmObjects",
|
|
8095
|
+
Effect: "Allow",
|
|
8096
|
+
Action: [
|
|
8097
|
+
"s3:GetObject",
|
|
8098
|
+
"s3:PutObject",
|
|
8099
|
+
"s3:DeleteObject",
|
|
8100
|
+
"s3:AbortMultipartUpload",
|
|
8101
|
+
"s3:ListMultipartUploadParts",
|
|
8102
|
+
...options.versioning ? ["s3:GetObjectVersion"] : []
|
|
8103
|
+
],
|
|
8104
|
+
Resource: `${bucketArn(bucket)}/*`
|
|
8105
|
+
}, {
|
|
8106
|
+
Sid: "GkmBucket",
|
|
8107
|
+
Effect: "Allow",
|
|
8108
|
+
Action: [
|
|
8109
|
+
"s3:ListBucket",
|
|
8110
|
+
"s3:GetBucketLocation",
|
|
8111
|
+
"s3:ListBucketMultipartUploads"
|
|
8112
|
+
],
|
|
8113
|
+
Resource: bucketArn(bucket)
|
|
8114
|
+
}]
|
|
8115
|
+
};
|
|
8116
|
+
}
|
|
8117
|
+
/**
|
|
8118
|
+
* One open pattern as the resource it is public on. A `**` is S3's `*`; a
|
|
8119
|
+
* single `*` is too, which crosses `/` where the construct's own check does
|
|
8120
|
+
* not — the construct is the stricter of the two (see `bucketPolicy` in
|
|
8121
|
+
* reconcile, which the local MinIO is given the same way).
|
|
8122
|
+
*/
|
|
8123
|
+
function openResource(bucket, pattern) {
|
|
8124
|
+
const path = pattern.replace(/^\/+/, "").replace(/\*\*/g, "*");
|
|
8125
|
+
return `${bucketArn(bucket)}/${path}`;
|
|
8126
|
+
}
|
|
8127
|
+
/**
|
|
8128
|
+
* The bucket's own policy: every request refused unless it came over TLS,
|
|
8129
|
+
* and — only when a file server opens paths on it — anonymous `s3:GetObject`
|
|
8130
|
+
* on exactly those prefixes.
|
|
8131
|
+
*/
|
|
8132
|
+
function bucketPolicy(bucket, open = []) {
|
|
8133
|
+
const statements = [{
|
|
8134
|
+
Sid: "GkmDenyInsecureTransport",
|
|
8135
|
+
Effect: "Deny",
|
|
8136
|
+
Principal: "*",
|
|
8137
|
+
Action: "s3:*",
|
|
8138
|
+
Resource: [bucketArn(bucket), `${bucketArn(bucket)}/*`],
|
|
8139
|
+
Condition: { Bool: { "aws:SecureTransport": "false" } }
|
|
8140
|
+
}];
|
|
8141
|
+
if (open.length > 0) statements.push({
|
|
8142
|
+
Sid: "GkmOpenPaths",
|
|
8143
|
+
Effect: "Allow",
|
|
8144
|
+
Principal: "*",
|
|
8145
|
+
Action: "s3:GetObject",
|
|
8146
|
+
Resource: [...new Set(open.map((p) => openResource(bucket, p)))].sort()
|
|
8147
|
+
});
|
|
8148
|
+
return {
|
|
8149
|
+
Version: "2012-10-17",
|
|
8150
|
+
Statement: statements
|
|
8151
|
+
};
|
|
8152
|
+
}
|
|
8153
|
+
/**
|
|
8154
|
+
* Block Public Access. All four flags on, unless the bucket has open paths:
|
|
8155
|
+
* then a *policy* may make it public (`BlockPublicPolicy`,
|
|
8156
|
+
* `RestrictPublicBuckets` off), and ACLs still may not.
|
|
8157
|
+
*/
|
|
8158
|
+
function publicAccessBlock(open) {
|
|
8159
|
+
return {
|
|
8160
|
+
BlockPublicAcls: true,
|
|
8161
|
+
IgnorePublicAcls: true,
|
|
8162
|
+
BlockPublicPolicy: !open,
|
|
8163
|
+
RestrictPublicBuckets: !open
|
|
8164
|
+
};
|
|
8165
|
+
}
|
|
8166
|
+
function corsRule(origins) {
|
|
8167
|
+
return {
|
|
8168
|
+
AllowedMethods: [
|
|
8169
|
+
"GET",
|
|
8170
|
+
"HEAD",
|
|
8171
|
+
"POST",
|
|
8172
|
+
"PUT"
|
|
8173
|
+
],
|
|
8174
|
+
AllowedOrigins: [...new Set(origins)].sort(),
|
|
8175
|
+
AllowedHeaders: ["*"],
|
|
8176
|
+
ExposeHeaders: ["ETag"],
|
|
8177
|
+
MaxAgeSeconds: 3e3
|
|
8178
|
+
};
|
|
8179
|
+
}
|
|
8180
|
+
/**
|
|
8181
|
+
* A policy document in one canonical form — keys sorted, a lone string a
|
|
8182
|
+
* one-element list, lists sorted — so one AWS reformatted reads as the same
|
|
8183
|
+
* document rather than as drift.
|
|
8184
|
+
*/
|
|
8185
|
+
function canonicalPolicy(value) {
|
|
8186
|
+
return JSON.stringify(canonical(value));
|
|
8187
|
+
}
|
|
8188
|
+
function canonical(value, key) {
|
|
8189
|
+
if (Array.isArray(value)) {
|
|
8190
|
+
const items = value.map((v) => canonical(v));
|
|
8191
|
+
return key === "Statement" ? items : items.map((v) => JSON.stringify(v)).sort();
|
|
8192
|
+
}
|
|
8193
|
+
if (value && typeof value === "object") return Object.fromEntries(Object.keys(value).sort().map((k) => {
|
|
8194
|
+
const v = value[k];
|
|
8195
|
+
return [k, canonical(k === "Action" || k === "Resource" || k === "AWS" ? Array.isArray(v) ? v : [v] : v, k)];
|
|
8196
|
+
}));
|
|
8197
|
+
return value;
|
|
8198
|
+
}
|
|
8199
|
+
//#endregion
|
|
8200
|
+
//#region src/providers/s3/index.ts
|
|
8201
|
+
/** The tags gkm finds its own buckets and users by. */
|
|
8202
|
+
const TAG_PROJECT = "gkm:project";
|
|
8203
|
+
const TAG_STAGE = "gkm:stage";
|
|
8204
|
+
const TAG_CONSTRUCT = "gkm:construct";
|
|
8205
|
+
function bucketConstructs(manifest) {
|
|
8206
|
+
return Object.entries(manifest).filter(([, d]) => d.kind === "objects").map(([id, d]) => ({
|
|
8207
|
+
id,
|
|
8208
|
+
versioned: d.versioned === true,
|
|
8209
|
+
servers: Object.entries(manifest).filter(([, s]) => s.kind === "file-server" && s.of === id).map(([sid, s]) => ({
|
|
8210
|
+
id: sid,
|
|
8211
|
+
open: s.open ?? []
|
|
8212
|
+
})).sort((a, b) => a.id.localeCompare(b.id))
|
|
8213
|
+
})).sort((a, b) => a.id.localeCompare(b.id));
|
|
8214
|
+
}
|
|
8215
|
+
/**
|
|
8216
|
+
* The region the stage's buckets are created in: the entry's, the secrets
|
|
8217
|
+
* store's when it is in AWS, the credential's, `AWS_REGION`.
|
|
8218
|
+
*/
|
|
8219
|
+
function s3Region(ctx, env = process.env) {
|
|
8220
|
+
const store = ctx.workspace.secrets?.store;
|
|
8221
|
+
const storeRegion = store && typeof store === "object" && "region" in store ? store.region : void 0;
|
|
8222
|
+
const region = ctx.config.region ?? storeRegion ?? ctx.credential?.region ?? env.AWS_REGION ?? env.AWS_DEFAULT_REGION;
|
|
8223
|
+
if (!region) throw new require_workspace.S3RegionRequired(ctx.stage);
|
|
8224
|
+
return region;
|
|
8225
|
+
}
|
|
8226
|
+
/**
|
|
8227
|
+
* The address a file server over `bucket` serves on: the bucket's regional
|
|
8228
|
+
* virtual-host endpoint, which is what `FileServer.url(key)` appends a key to
|
|
8229
|
+
* — `https://<bucket>.s3.<region>.amazonaws.com/<key>`. Against an emulator,
|
|
8230
|
+
* its path-style address.
|
|
8231
|
+
*/
|
|
8232
|
+
function bucketPublicUrl(bucket, region, endpoint) {
|
|
8233
|
+
if (endpoint) return `${endpoint.replace(/\/+$/, "")}/${bucket}`;
|
|
8234
|
+
return `https://${bucket}.s3.${region}.amazonaws.com`;
|
|
8235
|
+
}
|
|
8236
|
+
/** What an AWS SDK error is called, whichever field carries it. */
|
|
8237
|
+
function errorName(error) {
|
|
8238
|
+
const e = error;
|
|
8239
|
+
return e?.Code ?? e?.name ?? "";
|
|
8240
|
+
}
|
|
8241
|
+
/** A read whose "there is none" answer is one of `missing`. */
|
|
8242
|
+
async function readOr(read, missing) {
|
|
8243
|
+
try {
|
|
8244
|
+
return await read();
|
|
8245
|
+
} catch (error) {
|
|
8246
|
+
if (missing.includes(errorName(error))) return void 0;
|
|
8247
|
+
throw error;
|
|
8248
|
+
}
|
|
8249
|
+
}
|
|
8250
|
+
const defaultClients = async (credential, region) => {
|
|
8251
|
+
const S3 = await import("@aws-sdk/client-s3");
|
|
8252
|
+
const IAM = await import("@aws-sdk/client-iam");
|
|
8253
|
+
const config = await awsClientConfig(credential, region);
|
|
8254
|
+
const endpoint = awsEndpoint(process.env, "S3");
|
|
8255
|
+
return {
|
|
8256
|
+
S3,
|
|
8257
|
+
IAM,
|
|
8258
|
+
s3: new S3.S3Client({
|
|
8259
|
+
...config,
|
|
8260
|
+
...endpoint ? {
|
|
8261
|
+
endpoint,
|
|
8262
|
+
forcePathStyle: true
|
|
8263
|
+
} : {}
|
|
8264
|
+
}),
|
|
8265
|
+
iam: new IAM.IAMClient(config),
|
|
8266
|
+
region,
|
|
8267
|
+
...endpoint ? { endpoint } : {}
|
|
8268
|
+
};
|
|
8269
|
+
};
|
|
8270
|
+
/** For tests: what makes the clients. */
|
|
8271
|
+
let makeClients = defaultClients;
|
|
8272
|
+
async function ensureS3(ctx) {
|
|
8273
|
+
const region = s3Region(ctx);
|
|
8274
|
+
const clients = await makeClients(ctx.credential, region);
|
|
8275
|
+
for (const construct of bucketConstructs(ctx.manifest)) await ensureBucket(ctx, clients, construct);
|
|
8276
|
+
}
|
|
8277
|
+
/** The keys gkm tags a bucket and a user with. */
|
|
8278
|
+
function identityTags(ctx, id) {
|
|
8279
|
+
return [
|
|
8280
|
+
{
|
|
8281
|
+
Key: TAG_PROJECT,
|
|
8282
|
+
Value: ctx.identity.key
|
|
8283
|
+
},
|
|
8284
|
+
{
|
|
8285
|
+
Key: TAG_STAGE,
|
|
8286
|
+
Value: ctx.stage
|
|
8287
|
+
},
|
|
8288
|
+
{
|
|
8289
|
+
Key: TAG_CONSTRUCT,
|
|
8290
|
+
Value: id
|
|
8291
|
+
}
|
|
8292
|
+
];
|
|
8293
|
+
}
|
|
8294
|
+
async function ensureBucket(ctx, clients, construct) {
|
|
8295
|
+
const { id } = construct;
|
|
8296
|
+
const nameInput = {
|
|
8297
|
+
scope: ctx.identity.scope,
|
|
8298
|
+
stage: ctx.stage,
|
|
8299
|
+
id: require_derive.appKey(id)
|
|
8300
|
+
};
|
|
8301
|
+
const versioning = ctx.config.versioning ?? construct.versioned;
|
|
8302
|
+
const open = [...new Set(construct.servers.flatMap((s) => s.open))].sort();
|
|
8303
|
+
const { bucket, exists } = await ensureBucketName(ctx, clients, id, bucketName(nameInput));
|
|
8304
|
+
await ensureBucketSettings(ctx, clients, {
|
|
8305
|
+
id,
|
|
8306
|
+
bucket,
|
|
8307
|
+
exists,
|
|
8308
|
+
versioning,
|
|
8309
|
+
open
|
|
8310
|
+
});
|
|
8311
|
+
await ensureAccessKey(ctx, clients, {
|
|
8312
|
+
id,
|
|
8313
|
+
bucket,
|
|
8314
|
+
...await ensureUser(ctx, clients, id, iamUserName(nameInput), userPolicy(bucket, { versioning }))
|
|
8315
|
+
});
|
|
8316
|
+
const publicUrl = bucketPublicUrl(bucket, clients.region, clients.endpoint);
|
|
8317
|
+
for (const server of construct.servers) {
|
|
8318
|
+
const key = (0, _geekmidas_manifest.provideKey)(server.id, "url");
|
|
8319
|
+
const entry = {
|
|
8320
|
+
key: `file-server:${server.id}`,
|
|
8321
|
+
type: "url"
|
|
8322
|
+
};
|
|
8323
|
+
const recorded = ctx.state.record(entry.key)?.id;
|
|
8324
|
+
const current = ctx.secrets[key];
|
|
8325
|
+
if (current === publicUrl) {
|
|
8326
|
+
await ctx.state.ready(entry, publicUrl);
|
|
8327
|
+
continue;
|
|
8328
|
+
}
|
|
8329
|
+
if (current !== void 0 && current !== recorded) {
|
|
8330
|
+
ctx.log(` ${key} is set to ${current} by you, and kept — the bucket's own address is ${publicUrl}`);
|
|
8331
|
+
continue;
|
|
8332
|
+
}
|
|
8333
|
+
await ctx.change({
|
|
8334
|
+
construct: server.id,
|
|
8335
|
+
resource: key,
|
|
8336
|
+
change: `set to ${publicUrl}`
|
|
8337
|
+
}, async () => {
|
|
8338
|
+
await ctx.writeSecrets({ [key]: publicUrl });
|
|
8339
|
+
await ctx.state.ready(entry, publicUrl);
|
|
8340
|
+
});
|
|
8341
|
+
}
|
|
8342
|
+
}
|
|
8343
|
+
/**
|
|
8344
|
+
* The bucket's name: the recorded one, else the good name, else the good
|
|
8345
|
+
* name with a random suffix — up to {@link SUFFIX_ATTEMPTS} times.
|
|
8346
|
+
*/
|
|
8347
|
+
async function ensureBucketName(ctx, clients, id, good) {
|
|
8348
|
+
const { S3, s3, region } = clients;
|
|
8349
|
+
const key = `s3-bucket:${id}`;
|
|
8350
|
+
const record = ctx.state.record(key);
|
|
8351
|
+
const entry = (name) => ({
|
|
8352
|
+
key,
|
|
8353
|
+
type: "s3-bucket",
|
|
8354
|
+
data: {
|
|
8355
|
+
name,
|
|
8356
|
+
region
|
|
8357
|
+
}
|
|
8358
|
+
});
|
|
8359
|
+
/** Whether this account holds `name`: yes, no, or someone else does. */
|
|
8360
|
+
const probe = async (name) => {
|
|
8361
|
+
try {
|
|
8362
|
+
await s3.send(new S3.HeadBucketCommand({ Bucket: name }));
|
|
8363
|
+
return "ours";
|
|
8364
|
+
} catch (error) {
|
|
8365
|
+
const name = errorName(error);
|
|
8366
|
+
if (name === "NotFound" || name === "NoSuchBucket") return "none";
|
|
8367
|
+
if (name === "Forbidden" || name === "AccessDenied") return "taken";
|
|
8368
|
+
throw error;
|
|
8369
|
+
}
|
|
8370
|
+
};
|
|
8371
|
+
const create = async (name) => {
|
|
8372
|
+
if (ctx.dryRun) {
|
|
8373
|
+
const found = await probe(name);
|
|
8374
|
+
if (found === "none") {
|
|
8375
|
+
await ctx.change({
|
|
8376
|
+
construct: id,
|
|
8377
|
+
resource: `bucket ${name}`,
|
|
8378
|
+
change: `create in ${region}`
|
|
8379
|
+
}, async () => {});
|
|
8380
|
+
return "created";
|
|
8381
|
+
}
|
|
8382
|
+
return found;
|
|
8383
|
+
}
|
|
8384
|
+
await ctx.state.pending(entry(name));
|
|
8385
|
+
try {
|
|
8386
|
+
await s3.send(new S3.CreateBucketCommand({
|
|
8387
|
+
Bucket: name,
|
|
8388
|
+
...region === "us-east-1" ? {} : { CreateBucketConfiguration: { LocationConstraint: region } }
|
|
8389
|
+
}));
|
|
8390
|
+
} catch (error) {
|
|
8391
|
+
const code = errorName(error);
|
|
8392
|
+
if (code === "BucketAlreadyOwnedByYou") return "ours";
|
|
8393
|
+
if (code === "BucketAlreadyExists") return "taken";
|
|
8394
|
+
throw error;
|
|
8395
|
+
}
|
|
8396
|
+
await ctx.change({
|
|
8397
|
+
construct: id,
|
|
8398
|
+
resource: `bucket ${name}`,
|
|
8399
|
+
change: `create in ${region}`
|
|
8400
|
+
}, async () => {});
|
|
8401
|
+
return "created";
|
|
8402
|
+
};
|
|
8403
|
+
const adopt = async (name) => {
|
|
8404
|
+
const owner = (await readOr(() => s3.send(new S3.GetBucketTaggingCommand({ Bucket: name })), ["NoSuchTagSet", "NoSuchTagSetError"]))?.TagSet?.find((t) => t.Key === TAG_PROJECT)?.Value;
|
|
8405
|
+
if (owner && owner !== ctx.identity.key) throw new require_workspace.BucketOwnedByAnotherProject(name, owner, ctx.identity.key);
|
|
8406
|
+
};
|
|
8407
|
+
if (record?.status === "ready" && record.id) {
|
|
8408
|
+
const found = await probe(record.id);
|
|
8409
|
+
if (found === "ours") return {
|
|
8410
|
+
bucket: record.id,
|
|
8411
|
+
exists: true
|
|
8412
|
+
};
|
|
8413
|
+
if (found === "taken") throw new require_workspace.RecordedBucketUnreachable(id, record.id);
|
|
8414
|
+
const made = await create(record.id);
|
|
8415
|
+
if (made === "taken") throw new require_workspace.RecordedBucketUnreachable(id, record.id);
|
|
8416
|
+
if (!ctx.dryRun) await ctx.state.ready(entry(record.id), record.id);
|
|
8417
|
+
return {
|
|
8418
|
+
bucket: record.id,
|
|
8419
|
+
exists: made === "ours"
|
|
8420
|
+
};
|
|
8421
|
+
}
|
|
8422
|
+
const pending = record?.data?.name;
|
|
8423
|
+
const names = [
|
|
8424
|
+
...typeof pending === "string" && pending !== good ? [pending] : [],
|
|
8425
|
+
good,
|
|
8426
|
+
...Array.from({ length: 5 }, () => suffixedBucketName(good, randomSuffix()))
|
|
8427
|
+
];
|
|
8428
|
+
const tried = [];
|
|
8429
|
+
for (const name of names) {
|
|
8430
|
+
tried.push(name);
|
|
8431
|
+
const made = await create(name);
|
|
8432
|
+
if (made === "taken") continue;
|
|
8433
|
+
if (made === "ours") await adopt(name);
|
|
8434
|
+
if (!ctx.dryRun) await ctx.state.ready(entry(name), name);
|
|
8435
|
+
if (name !== good) ctx.log(` '${good}' is taken by another account; '${id}' is '${name}', recorded in the stage's state`);
|
|
8436
|
+
return {
|
|
8437
|
+
bucket: name,
|
|
8438
|
+
exists: made === "ours"
|
|
8439
|
+
};
|
|
8440
|
+
}
|
|
8441
|
+
throw new require_workspace.BucketNameUnavailable(id, tried.filter((n) => n !== pending));
|
|
8442
|
+
}
|
|
8443
|
+
/** Tags, Block Public Access, encryption, policy, versioning and CORS. */
|
|
8444
|
+
async function ensureBucketSettings(ctx, clients, input) {
|
|
8445
|
+
const { S3, s3 } = clients;
|
|
8446
|
+
const { id, bucket } = input;
|
|
8447
|
+
const Bucket = bucket;
|
|
8448
|
+
const fresh = ctx.dryRun && !input.exists;
|
|
8449
|
+
const read = async (fn, missing) => fresh ? void 0 : readOr(fn, missing);
|
|
8450
|
+
const set = (change, apply) => ctx.change({
|
|
8451
|
+
construct: id,
|
|
8452
|
+
resource: `bucket ${bucket}`,
|
|
8453
|
+
change
|
|
8454
|
+
}, () => apply().then(() => void 0));
|
|
8455
|
+
const tagged = await read(() => s3.send(new S3.GetBucketTaggingCommand({ Bucket })), ["NoSuchTagSet", "NoSuchTagSetError"]);
|
|
8456
|
+
const ours = identityTags(ctx, id);
|
|
8457
|
+
const others = (tagged?.TagSet ?? []).filter((t) => !ours.some((o) => o.Key === t.Key));
|
|
8458
|
+
if (ours.some((o) => !tagged?.TagSet?.some((t) => t.Key === o.Key && t.Value === o.Value))) await set("tag", () => s3.send(new S3.PutBucketTaggingCommand({
|
|
8459
|
+
Bucket,
|
|
8460
|
+
Tagging: { TagSet: [...others, ...ours].map((t) => ({
|
|
8461
|
+
Key: t.Key,
|
|
8462
|
+
Value: t.Value
|
|
8463
|
+
})) }
|
|
8464
|
+
})));
|
|
8465
|
+
const isOpen = input.open.length > 0;
|
|
8466
|
+
const block = publicAccessBlock(isOpen);
|
|
8467
|
+
const ensureBlock = async () => {
|
|
8468
|
+
const now = (await read(() => s3.send(new S3.GetPublicAccessBlockCommand({ Bucket })), ["NoSuchPublicAccessBlockConfiguration"]))?.PublicAccessBlockConfiguration;
|
|
8469
|
+
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({
|
|
8470
|
+
Bucket,
|
|
8471
|
+
PublicAccessBlockConfiguration: block
|
|
8472
|
+
})));
|
|
8473
|
+
};
|
|
8474
|
+
const policy = bucketPolicy(bucket, input.open);
|
|
8475
|
+
const ensurePolicy = async () => {
|
|
8476
|
+
const current = await read(() => s3.send(new S3.GetBucketPolicyCommand({ Bucket })), ["NoSuchBucketPolicy"]);
|
|
8477
|
+
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({
|
|
8478
|
+
Bucket,
|
|
8479
|
+
Policy: JSON.stringify(policy)
|
|
8480
|
+
})));
|
|
8481
|
+
};
|
|
8482
|
+
if (isOpen) {
|
|
8483
|
+
await ensureBlock();
|
|
8484
|
+
await ensurePolicy();
|
|
8485
|
+
} else {
|
|
8486
|
+
await ensurePolicy();
|
|
8487
|
+
await ensureBlock();
|
|
8488
|
+
}
|
|
8489
|
+
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({
|
|
8490
|
+
Bucket,
|
|
8491
|
+
ServerSideEncryptionConfiguration: { Rules: [{ ApplyServerSideEncryptionByDefault: { SSEAlgorithm: "AES256" } }] }
|
|
8492
|
+
})));
|
|
8493
|
+
const status = (await read(() => s3.send(new S3.GetBucketVersioningCommand({ Bucket })), []))?.Status;
|
|
8494
|
+
if (input.versioning && status !== "Enabled") await set("enable versioning", () => s3.send(new S3.PutBucketVersioningCommand({
|
|
8495
|
+
Bucket,
|
|
8496
|
+
VersioningConfiguration: { Status: "Enabled" }
|
|
8497
|
+
})));
|
|
8498
|
+
else if (!input.versioning && status === "Enabled") await set("suspend versioning", () => s3.send(new S3.PutBucketVersioningCommand({
|
|
8499
|
+
Bucket,
|
|
8500
|
+
VersioningConfiguration: { Status: "Suspended" }
|
|
8501
|
+
})));
|
|
8502
|
+
const { origins, unresolved } = bucketOrigins({
|
|
8503
|
+
workspace: ctx.workspace,
|
|
8504
|
+
manifest: ctx.manifest,
|
|
8505
|
+
runnables: ctx.runnables,
|
|
8506
|
+
stage: ctx.stage,
|
|
8507
|
+
bucket: id
|
|
8508
|
+
});
|
|
8509
|
+
if (unresolved.length > 0) ctx.log(` ${unresolved.join(", ")} ${unresolved.length === 1 ? "has" : "have"} no address on '${ctx.stage}' (deploy.domains.${ctx.stage}), so ${unresolved.length === 1 ? "is" : "are"} not in '${bucket}''s CORS`);
|
|
8510
|
+
if (origins.length === 0) return;
|
|
8511
|
+
const rule = corsRule(origins);
|
|
8512
|
+
const cors = await read(() => s3.send(new S3.GetBucketCorsCommand({ Bucket })), ["NoSuchCORSConfiguration"]);
|
|
8513
|
+
if (!(cors?.CORSRules?.length === 1 && canonicalCors(cors.CORSRules[0]) === canonicalCors(rule))) await set(`CORS for ${origins.join(", ")}`, () => s3.send(new S3.PutBucketCorsCommand({
|
|
8514
|
+
Bucket,
|
|
8515
|
+
CORSConfiguration: { CORSRules: [rule] }
|
|
8516
|
+
})));
|
|
8517
|
+
}
|
|
8518
|
+
function canonicalCors(rule) {
|
|
8519
|
+
const sorted = (list) => [...list ?? []].sort();
|
|
8520
|
+
return JSON.stringify({
|
|
8521
|
+
AllowedMethods: sorted(rule.AllowedMethods),
|
|
8522
|
+
AllowedOrigins: sorted(rule.AllowedOrigins),
|
|
8523
|
+
AllowedHeaders: sorted(rule.AllowedHeaders),
|
|
8524
|
+
ExposeHeaders: sorted(rule.ExposeHeaders),
|
|
8525
|
+
MaxAgeSeconds: rule.MaxAgeSeconds ?? null
|
|
8526
|
+
});
|
|
8527
|
+
}
|
|
8528
|
+
/** The bucket's IAM user and its inline policy. */
|
|
8529
|
+
async function ensureUser(ctx, clients, id, user, policy) {
|
|
8530
|
+
const { IAM, iam } = clients;
|
|
8531
|
+
const entry = {
|
|
8532
|
+
key: `iam-user:${id}`,
|
|
8533
|
+
type: "iam-user"
|
|
8534
|
+
};
|
|
8535
|
+
const found = await readOr(() => iam.send(new IAM.GetUserCommand({ UserName: user })), ["NoSuchEntity", "NoSuchEntityException"]);
|
|
8536
|
+
if (found?.User) {
|
|
8537
|
+
const tags = await iam.send(new IAM.ListUserTagsCommand({ UserName: user }));
|
|
8538
|
+
const tag = (key) => tags.Tags?.find((t) => t.Key === key)?.Value;
|
|
8539
|
+
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);
|
|
8540
|
+
await ctx.state.ready(entry, user);
|
|
8541
|
+
} else await ctx.change({
|
|
8542
|
+
construct: id,
|
|
8543
|
+
resource: `IAM user ${user}`,
|
|
8544
|
+
change: `create under ${IAM_USER_PATH}`
|
|
8545
|
+
}, async () => {
|
|
8546
|
+
await ctx.state.pending(entry);
|
|
8547
|
+
await iam.send(new IAM.CreateUserCommand({
|
|
8548
|
+
UserName: user,
|
|
8549
|
+
Path: IAM_USER_PATH,
|
|
8550
|
+
Tags: identityTags(ctx, id)
|
|
8551
|
+
}));
|
|
8552
|
+
await ctx.state.ready(entry, user);
|
|
8553
|
+
});
|
|
8554
|
+
const exists = Boolean(found?.User) || !ctx.dryRun;
|
|
8555
|
+
const current = exists ? await readOr(() => iam.send(new IAM.GetUserPolicyCommand({
|
|
8556
|
+
UserName: user,
|
|
8557
|
+
PolicyName: IAM_POLICY_NAME
|
|
8558
|
+
})), ["NoSuchEntity", "NoSuchEntityException"]) : void 0;
|
|
8559
|
+
const document = current?.PolicyDocument ? JSON.parse(decodePolicy(current.PolicyDocument)) : void 0;
|
|
8560
|
+
if (!document || canonicalPolicy(document) !== canonicalPolicy(policy)) await ctx.change({
|
|
8561
|
+
construct: id,
|
|
8562
|
+
resource: `IAM user ${user}`,
|
|
8563
|
+
change: `policy ${IAM_POLICY_NAME}: this bucket only`
|
|
8564
|
+
}, async () => {
|
|
8565
|
+
await iam.send(new IAM.PutUserPolicyCommand({
|
|
8566
|
+
UserName: user,
|
|
8567
|
+
PolicyName: IAM_POLICY_NAME,
|
|
8568
|
+
PolicyDocument: JSON.stringify(policy)
|
|
8569
|
+
}));
|
|
8570
|
+
});
|
|
8571
|
+
return {
|
|
8572
|
+
user,
|
|
8573
|
+
exists
|
|
8574
|
+
};
|
|
8575
|
+
}
|
|
8576
|
+
/** IAM returns a policy document URL-encoded; an emulator may not. */
|
|
8577
|
+
function decodePolicy(document) {
|
|
8578
|
+
try {
|
|
8579
|
+
return decodeURIComponent(document);
|
|
8580
|
+
} catch {
|
|
8581
|
+
return document;
|
|
8582
|
+
}
|
|
8583
|
+
}
|
|
8584
|
+
/**
|
|
8585
|
+
* The user's access key, in the stage's secrets as the bucket's URL.
|
|
8586
|
+
*
|
|
8587
|
+
* - First run: one key, written into the stage's secrets.
|
|
8588
|
+
* - `--rotate-keys`: a second key, written in place of the first; the first
|
|
8589
|
+
* stays active until the stage has deployed with the second, and the next
|
|
8590
|
+
* run after that deploy — or `--retire-old-keys` — deletes it.
|
|
8591
|
+
* - A key the secrets no longer hold (a store emptied by hand): replaced the
|
|
8592
|
+
* same way, since its secret cannot be read back.
|
|
8593
|
+
*/
|
|
8594
|
+
async function ensureAccessKey(ctx, clients, input) {
|
|
8595
|
+
const { IAM, iam } = clients;
|
|
8596
|
+
const { id, bucket, user } = input;
|
|
8597
|
+
const urlKey = (0, _geekmidas_manifest.provideKey)(id, "url");
|
|
8598
|
+
const entryKey = `iam-access-key:${id}`;
|
|
8599
|
+
const record = ctx.state.record(entryKey);
|
|
8600
|
+
const given = ctx.secrets[urlKey];
|
|
8601
|
+
let inUrl;
|
|
8602
|
+
if (given !== void 0) {
|
|
8603
|
+
let parsed;
|
|
8604
|
+
try {
|
|
8605
|
+
parsed = _geekmidas_storage_s3_url.parse(given);
|
|
8606
|
+
} catch {
|
|
8607
|
+
parsed = void 0;
|
|
8608
|
+
}
|
|
8609
|
+
if (parsed?.bucket !== bucket) throw new require_workspace.BucketKeySetElsewhere(urlKey, bucket, parsed?.bucket ?? given.replace(/\/\/[^@]*@/, "//"), ctx.stage);
|
|
8610
|
+
inUrl = parsed.accessKeyId;
|
|
8611
|
+
}
|
|
8612
|
+
const listed = input.exists ? (await iam.send(new IAM.ListAccessKeysCommand({ UserName: user }))).AccessKeyMetadata?.map((k) => k.AccessKeyId) ?? [] : [];
|
|
8613
|
+
let current = record?.status === "ready" ? record.id : void 0;
|
|
8614
|
+
let previous = typeof record?.data?.previous === "string" ? record.data.previous : void 0;
|
|
8615
|
+
const rotatedAt = typeof record?.data?.rotatedAt === "string" ? record.data.rotatedAt : void 0;
|
|
8616
|
+
const entry = (data = {}) => ({
|
|
8617
|
+
key: entryKey,
|
|
8618
|
+
type: "iam-access-key",
|
|
8619
|
+
data: {
|
|
8620
|
+
user,
|
|
8621
|
+
...data
|
|
8622
|
+
}
|
|
8623
|
+
});
|
|
8624
|
+
if (previous && current) {
|
|
8625
|
+
const deployed = rotatedAt !== void 0 && ctx.state.lastDeployedAt !== void 0 && ctx.state.lastDeployedAt > rotatedAt;
|
|
8626
|
+
if (ctx.retireOldKeys || deployed) {
|
|
8627
|
+
const old = previous;
|
|
8628
|
+
const keep = current;
|
|
8629
|
+
await ctx.change({
|
|
8630
|
+
construct: id,
|
|
8631
|
+
resource: `IAM user ${user}`,
|
|
8632
|
+
change: `delete the rotated-out access key ${old}`
|
|
8633
|
+
}, async () => {
|
|
8634
|
+
if (listed.includes(old)) await iam.send(new IAM.DeleteAccessKeyCommand({
|
|
8635
|
+
UserName: user,
|
|
8636
|
+
AccessKeyId: old
|
|
8637
|
+
}));
|
|
8638
|
+
await ctx.state.ready(entry(), keep);
|
|
8639
|
+
});
|
|
8640
|
+
previous = void 0;
|
|
8641
|
+
} 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)`);
|
|
8642
|
+
}
|
|
8643
|
+
if (!current && inUrl && listed.includes(inUrl)) {
|
|
8644
|
+
await ctx.state.ready(entry(), inUrl);
|
|
8645
|
+
current = inUrl;
|
|
8646
|
+
}
|
|
8647
|
+
if (current !== void 0 && listed.includes(current) && inUrl === current && !ctx.rotateKeys) return;
|
|
8648
|
+
if (ctx.rotateKeys && previous) throw new require_workspace.RotationInProgress(id, previous, ctx.stage);
|
|
8649
|
+
const outgoing = current && listed.includes(current) ? current : void 0;
|
|
8650
|
+
if (listed.length >= 2) throw new require_workspace.AccessKeyLimit(user, listed);
|
|
8651
|
+
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)";
|
|
8652
|
+
await ctx.change({
|
|
8653
|
+
construct: id,
|
|
8654
|
+
resource: `IAM user ${user}`,
|
|
8655
|
+
change: why
|
|
8656
|
+
}, async () => {
|
|
8657
|
+
await ctx.state.pending(entry(outgoing ? { previous: outgoing } : {}));
|
|
8658
|
+
const key = (await iam.send(new IAM.CreateAccessKeyCommand({ UserName: user }))).AccessKey;
|
|
8659
|
+
await ctx.writeSecrets({ [urlKey]: _geekmidas_storage_s3_url.build({
|
|
8660
|
+
bucket,
|
|
8661
|
+
region: clients.region,
|
|
8662
|
+
...clients.endpoint ? {
|
|
8663
|
+
endpoint: clients.endpoint,
|
|
8664
|
+
forcePathStyle: true
|
|
8665
|
+
} : {},
|
|
8666
|
+
accessKeyId: key.AccessKeyId,
|
|
8667
|
+
secretAccessKey: key.SecretAccessKey
|
|
8668
|
+
}) });
|
|
8669
|
+
await ctx.state.ready(entry(outgoing ? {
|
|
8670
|
+
previous: outgoing,
|
|
8671
|
+
rotatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8672
|
+
} : {}), key.AccessKeyId);
|
|
8673
|
+
});
|
|
8674
|
+
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}`);
|
|
8675
|
+
}
|
|
8676
|
+
/**
|
|
8677
|
+
* Each provisioned bucket answers `HeadBucket` to the key in the stage's
|
|
8678
|
+
* secrets — the one the apps will use. A bucket whose URL carries no key is
|
|
8679
|
+
* not one this provider wrote, and the deploy's own check covers a URL that
|
|
8680
|
+
* is missing.
|
|
8681
|
+
*/
|
|
8682
|
+
async function verifyS3(ctx) {
|
|
8683
|
+
const S3 = await import("@aws-sdk/client-s3");
|
|
8684
|
+
for (const { id } of bucketConstructs(ctx.manifest)) {
|
|
8685
|
+
const url = ctx.secrets[(0, _geekmidas_manifest.provideKey)(id, "url")];
|
|
8686
|
+
if (url === void 0) continue;
|
|
8687
|
+
const address = _geekmidas_storage_s3_url.parse(url);
|
|
8688
|
+
if (!address.accessKeyId || !address.secretAccessKey) continue;
|
|
8689
|
+
const client = new S3.S3Client({
|
|
8690
|
+
region: address.region ?? ctx.config.region ?? "us-east-1",
|
|
8691
|
+
credentials: {
|
|
8692
|
+
accessKeyId: address.accessKeyId,
|
|
8693
|
+
secretAccessKey: address.secretAccessKey
|
|
8694
|
+
},
|
|
8695
|
+
...address.endpoint ? { endpoint: address.endpoint } : {},
|
|
8696
|
+
...address.forcePathStyle ? { forcePathStyle: true } : {}
|
|
8697
|
+
});
|
|
8698
|
+
try {
|
|
8699
|
+
await client.send(new S3.HeadBucketCommand({ Bucket: address.bucket }));
|
|
8700
|
+
} catch (error) {
|
|
8701
|
+
const name = errorName(error);
|
|
8702
|
+
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));
|
|
8703
|
+
} finally {
|
|
8704
|
+
client.destroy();
|
|
8705
|
+
}
|
|
8706
|
+
}
|
|
8707
|
+
}
|
|
8708
|
+
//#endregion
|
|
8709
|
+
//#region src/providers/registry.ts
|
|
8710
|
+
/**
|
|
8711
|
+
* The providers gkm ships, by kind and name — registered the way a deploy
|
|
8712
|
+
* target's built-ins are (`target/builtins.ts`). A provider from a package is
|
|
8713
|
+
* one more entry here once there is a loading story for it.
|
|
8714
|
+
*/
|
|
8715
|
+
const BUILTIN_PROVIDERS = { objects: { s3: {
|
|
8716
|
+
kind: "objects",
|
|
8717
|
+
name: "s3",
|
|
8718
|
+
check: require_workspace.checkS3Config,
|
|
8719
|
+
provisioning: require_workspace.S3_PROVISIONING,
|
|
8720
|
+
credentials: awsProvisioningCredentials,
|
|
8721
|
+
runtimeKeys(manifest) {
|
|
8722
|
+
return bucketConstructs(manifest).flatMap((b) => [(0, _geekmidas_manifest.provideKey)(b.id, "url"), ...b.servers.map((s) => (0, _geekmidas_manifest.provideKey)(s.id, "url"))]);
|
|
8723
|
+
},
|
|
8724
|
+
ensure: ensureS3,
|
|
8725
|
+
verify: verifyS3
|
|
8726
|
+
} } };
|
|
8727
|
+
/** The provider `kind` names `name`, if gkm has one. */
|
|
8728
|
+
function builtinProvider(kind, name) {
|
|
8729
|
+
return Object.hasOwn(BUILTIN_PROVIDERS[kind], name) ? BUILTIN_PROVIDERS[kind][name] : void 0;
|
|
8730
|
+
}
|
|
8731
|
+
//#endregion
|
|
8732
|
+
//#region src/providers/index.ts
|
|
8733
|
+
/** The constructs of a kind: `objects` is every bucket. */
|
|
8734
|
+
function constructsOf(manifest, kind) {
|
|
8735
|
+
return Object.entries(manifest).filter(([, d]) => d.kind === kind).map(([id]) => id).sort();
|
|
8736
|
+
}
|
|
8737
|
+
/**
|
|
8738
|
+
* What every deploy runs: each provider's `verify()` — the cheap check that
|
|
8739
|
+
* what it created is still there and the stage's key reaches it. Nothing on
|
|
8740
|
+
* the local stage, and nothing for a kind on `external`.
|
|
8741
|
+
*
|
|
8742
|
+
* @throws {StageProviderDisabled} for a declared kind the stage has none of
|
|
8743
|
+
*/
|
|
8744
|
+
async function verifyStageProviders(input) {
|
|
8745
|
+
assertStageProvidersEnabled(input.workspace, input.manifest, input.stage);
|
|
8746
|
+
const verified = [];
|
|
8747
|
+
for (const kind of PROVIDER_KINDS) {
|
|
8748
|
+
if (constructsOf(input.manifest, kind).length === 0) continue;
|
|
8749
|
+
const choice = require_workspace.stageProvider(input.workspace, kind, input.stage);
|
|
8750
|
+
if (choice.mode !== "provider") continue;
|
|
8751
|
+
const provider = builtinProvider(kind, choice.name);
|
|
8752
|
+
await provider.verify({
|
|
8753
|
+
workspace: input.workspace,
|
|
8754
|
+
manifest: input.manifest,
|
|
8755
|
+
stage: input.stage,
|
|
8756
|
+
config: choice.config,
|
|
8757
|
+
secrets: input.secrets
|
|
8758
|
+
});
|
|
8759
|
+
verified.push(`${kind}: ${provider.name}`);
|
|
8760
|
+
}
|
|
8761
|
+
return verified;
|
|
8762
|
+
}
|
|
8763
|
+
/**
|
|
8764
|
+
* `ensure()` for every provider the stage configures — find or create, fix
|
|
8765
|
+
* drift, record in the stage's state, write the runtime keys into its
|
|
8766
|
+
* secrets. A provider without provisioning credentials leaves the stage as
|
|
8767
|
+
* `external` and says how to supply them.
|
|
8768
|
+
*/
|
|
8769
|
+
async function provisionStage(options) {
|
|
8770
|
+
const { workspace, stage } = options;
|
|
8771
|
+
const dryRun = options.dryRun === true;
|
|
8772
|
+
const env = options.env ?? process.env;
|
|
8773
|
+
const log = options.log ?? ((line) => console.log(line));
|
|
8774
|
+
const named = PROVIDER_KINDS.some((kind) => require_workspace.stageProvider(workspace, kind, stage).mode === "provider");
|
|
8775
|
+
let found = options.manifest;
|
|
8776
|
+
let runnables = options.runnables ?? {};
|
|
8777
|
+
if (!found && named) {
|
|
8778
|
+
runnables = {};
|
|
8779
|
+
found = await require_discover.discover({
|
|
8780
|
+
patterns: constructGlobs(workspace),
|
|
8781
|
+
cwd: workspace.root,
|
|
8782
|
+
runnables
|
|
8783
|
+
});
|
|
8784
|
+
}
|
|
8785
|
+
const manifest = found ?? {};
|
|
8786
|
+
const reports = [];
|
|
8787
|
+
const configured = [];
|
|
8788
|
+
for (const kind of PROVIDER_KINDS) {
|
|
8789
|
+
const choice = require_workspace.stageProvider(workspace, kind, stage);
|
|
8790
|
+
if (choice.mode !== "provider") {
|
|
8791
|
+
reports.push({
|
|
8792
|
+
kind,
|
|
8793
|
+
mode: choice.mode,
|
|
8794
|
+
actions: []
|
|
8795
|
+
});
|
|
8796
|
+
continue;
|
|
8797
|
+
}
|
|
8798
|
+
const provider = builtinProvider(kind, choice.name);
|
|
8799
|
+
const report = {
|
|
8800
|
+
kind,
|
|
8801
|
+
mode: "provider",
|
|
8802
|
+
provider: provider.name,
|
|
8803
|
+
actions: []
|
|
8804
|
+
};
|
|
8805
|
+
reports.push(report);
|
|
8806
|
+
if (constructsOf(manifest, kind).length === 0) {
|
|
8807
|
+
log(` ${kind}: ${provider.name} — nothing declared to provision`);
|
|
8808
|
+
continue;
|
|
8809
|
+
}
|
|
8810
|
+
const credential = await provider.credentials({
|
|
8811
|
+
stage,
|
|
8812
|
+
env,
|
|
8813
|
+
...options.profile ? { profile: options.profile } : {},
|
|
8814
|
+
...options.home ? { home: options.home } : {}
|
|
8815
|
+
});
|
|
8816
|
+
report.credentials = credential !== void 0;
|
|
8817
|
+
if (credential === void 0) {
|
|
8818
|
+
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)}.`);
|
|
8819
|
+
continue;
|
|
8820
|
+
}
|
|
8821
|
+
configured.push({
|
|
8822
|
+
kind,
|
|
8823
|
+
provider,
|
|
8824
|
+
config: choice.config,
|
|
8825
|
+
credential,
|
|
8826
|
+
report
|
|
8827
|
+
});
|
|
8828
|
+
}
|
|
8829
|
+
if (configured.length === 0) return reports;
|
|
8830
|
+
const identity = deployIdentity(workspace, stage);
|
|
8831
|
+
const secretsStore = await secretsStoreFor(workspace, stage, {
|
|
8832
|
+
...options.profile ? { profile: options.profile } : {},
|
|
8833
|
+
...options.home ? { home: options.home } : {}
|
|
8834
|
+
});
|
|
8835
|
+
let secrets = (await secretsStore.read(stage))?.custom ?? {};
|
|
8836
|
+
const stateStore = await createStateStore({
|
|
8837
|
+
config: workspace.state,
|
|
8838
|
+
workspaceRoot: workspace.root,
|
|
8839
|
+
workspaceName: workspace.name
|
|
8840
|
+
});
|
|
8841
|
+
const lock = dryRun ? void 0 : await stateStore.lock(stage, { operation: "provision" });
|
|
8842
|
+
try {
|
|
8843
|
+
const state = dryRun ? await readOnlyState(stateStore, stage) : journalState(await DeployJournal.open(stateStore, stage, () => createEmptyState(stage, "", "")));
|
|
8844
|
+
const writeSecrets = async (values) => {
|
|
8845
|
+
if (dryRun) return;
|
|
8846
|
+
const stored = await secretsStore.read(stage) ?? initStageSecrets(stage);
|
|
8847
|
+
const next = {
|
|
8848
|
+
...stored,
|
|
8849
|
+
custom: {
|
|
8850
|
+
...stored.custom,
|
|
8851
|
+
...values
|
|
8852
|
+
},
|
|
8853
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8854
|
+
};
|
|
8855
|
+
await secretsStore.write(stage, next);
|
|
8856
|
+
secrets = next.custom;
|
|
8857
|
+
};
|
|
8858
|
+
for (const entry of configured) {
|
|
8859
|
+
log(` ${entry.kind}: ${entry.provider.name}${dryRun ? " (dry run)" : ""}`);
|
|
8860
|
+
await entry.provider.ensure({
|
|
8861
|
+
workspace,
|
|
8862
|
+
manifest,
|
|
8863
|
+
runnables,
|
|
8864
|
+
stage,
|
|
8865
|
+
identity,
|
|
8866
|
+
config: entry.config,
|
|
8867
|
+
credential: entry.credential,
|
|
8868
|
+
get secrets() {
|
|
8869
|
+
return secrets;
|
|
8870
|
+
},
|
|
8871
|
+
state,
|
|
8872
|
+
dryRun,
|
|
8873
|
+
rotateKeys: options.rotateKeys === true,
|
|
8874
|
+
retireOldKeys: options.retireOldKeys === true,
|
|
8875
|
+
async change(action, apply) {
|
|
8876
|
+
if (!dryRun) await apply();
|
|
8877
|
+
entry.report.actions.push(action);
|
|
8878
|
+
log(` ${dryRun ? "would " : ""}${action.change} — ${action.resource}`);
|
|
8879
|
+
},
|
|
8880
|
+
writeSecrets,
|
|
8881
|
+
log
|
|
8882
|
+
});
|
|
8883
|
+
if (entry.report.actions.length === 0) log(" up to date");
|
|
8884
|
+
}
|
|
8885
|
+
} finally {
|
|
8886
|
+
await lock?.release();
|
|
8887
|
+
}
|
|
8888
|
+
return reports;
|
|
8889
|
+
}
|
|
8890
|
+
/** The journal as `ensure()` sees it. */
|
|
8891
|
+
function journalState(journal) {
|
|
8892
|
+
return {
|
|
8893
|
+
record: (key) => journal.record(key),
|
|
8894
|
+
pending: (entry) => journal.pending(entry),
|
|
8895
|
+
ready: (entry, id) => journal.ready(entry, id),
|
|
8896
|
+
lastDeployedAt: journal.existed ? journal.state.lastDeployedAt : void 0
|
|
8897
|
+
};
|
|
8898
|
+
}
|
|
8899
|
+
/** What a dry run reads, and the writes it does not make. */
|
|
8900
|
+
async function readOnlyState(store, stage) {
|
|
8901
|
+
const stored = await store.read(stage);
|
|
8902
|
+
return {
|
|
8903
|
+
record: (key) => stored?.resources[key],
|
|
8904
|
+
pending: async () => {},
|
|
8905
|
+
ready: async () => {},
|
|
8906
|
+
...stored ? { lastDeployedAt: stored.state.lastDeployedAt } : {}
|
|
8907
|
+
};
|
|
8908
|
+
}
|
|
8909
|
+
//#endregion
|
|
7437
8910
|
//#region src/secrets/stale.ts
|
|
7438
8911
|
/**
|
|
7439
8912
|
* Stored secrets an older gkm generated that now shadow a derived value.
|
|
@@ -7582,6 +9055,12 @@ function reportPlannedSeeds(planned, log) {
|
|
|
7582
9055
|
for (const { folder, seeds } of planned) log(`🌱 ${folder}: would run ${seeds.length} (${seeds.join(", ")})`);
|
|
7583
9056
|
}
|
|
7584
9057
|
//#endregion
|
|
9058
|
+
Object.defineProperty(exports, "AllowDevServicesTakesNoValue", {
|
|
9059
|
+
enumerable: true,
|
|
9060
|
+
get: function() {
|
|
9061
|
+
return AllowDevServicesTakesNoValue;
|
|
9062
|
+
}
|
|
9063
|
+
});
|
|
7585
9064
|
Object.defineProperty(exports, "CLUSTER_DATABASE", {
|
|
7586
9065
|
enumerable: true,
|
|
7587
9066
|
get: function() {
|
|
@@ -7636,6 +9115,12 @@ Object.defineProperty(exports, "DockerPushFailed", {
|
|
|
7636
9115
|
return DockerPushFailed;
|
|
7637
9116
|
}
|
|
7638
9117
|
});
|
|
9118
|
+
Object.defineProperty(exports, "DocumentStateStore", {
|
|
9119
|
+
enumerable: true,
|
|
9120
|
+
get: function() {
|
|
9121
|
+
return DocumentStateStore;
|
|
9122
|
+
}
|
|
9123
|
+
});
|
|
7639
9124
|
Object.defineProperty(exports, "EMULATOR_ACCESS_KEY_ID", {
|
|
7640
9125
|
enumerable: true,
|
|
7641
9126
|
get: function() {
|
|
@@ -7720,22 +9205,52 @@ Object.defineProperty(exports, "STORAGE_KEY_PAIR", {
|
|
|
7720
9205
|
return STORAGE_KEY_PAIR;
|
|
7721
9206
|
}
|
|
7722
9207
|
});
|
|
7723
|
-
Object.defineProperty(exports, "
|
|
9208
|
+
Object.defineProperty(exports, "StageProviderDisabled", {
|
|
7724
9209
|
enumerable: true,
|
|
7725
9210
|
get: function() {
|
|
7726
|
-
return
|
|
9211
|
+
return StageProviderDisabled;
|
|
7727
9212
|
}
|
|
7728
9213
|
});
|
|
7729
|
-
Object.defineProperty(exports, "
|
|
9214
|
+
Object.defineProperty(exports, "StageStateMissing", {
|
|
7730
9215
|
enumerable: true,
|
|
7731
9216
|
get: function() {
|
|
7732
|
-
return
|
|
9217
|
+
return StageStateMissing;
|
|
9218
|
+
}
|
|
9219
|
+
});
|
|
9220
|
+
Object.defineProperty(exports, "StateLocked", {
|
|
9221
|
+
enumerable: true,
|
|
9222
|
+
get: function() {
|
|
9223
|
+
return StateLocked;
|
|
7733
9224
|
}
|
|
7734
9225
|
});
|
|
7735
|
-
Object.defineProperty(exports, "
|
|
9226
|
+
Object.defineProperty(exports, "StateStoreBusy", {
|
|
7736
9227
|
enumerable: true,
|
|
7737
9228
|
get: function() {
|
|
7738
|
-
return
|
|
9229
|
+
return StateStoreBusy;
|
|
9230
|
+
}
|
|
9231
|
+
});
|
|
9232
|
+
Object.defineProperty(exports, "StateStoreWithoutLocking", {
|
|
9233
|
+
enumerable: true,
|
|
9234
|
+
get: function() {
|
|
9235
|
+
return StateStoreWithoutLocking;
|
|
9236
|
+
}
|
|
9237
|
+
});
|
|
9238
|
+
Object.defineProperty(exports, "StateVersionConflict", {
|
|
9239
|
+
enumerable: true,
|
|
9240
|
+
get: function() {
|
|
9241
|
+
return StateVersionConflict;
|
|
9242
|
+
}
|
|
9243
|
+
});
|
|
9244
|
+
Object.defineProperty(exports, "TELEMETRY_CONTAINER", {
|
|
9245
|
+
enumerable: true,
|
|
9246
|
+
get: function() {
|
|
9247
|
+
return TELEMETRY_CONTAINER;
|
|
9248
|
+
}
|
|
9249
|
+
});
|
|
9250
|
+
Object.defineProperty(exports, "TURBO_VERSION", {
|
|
9251
|
+
enumerable: true,
|
|
9252
|
+
get: function() {
|
|
9253
|
+
return TURBO_VERSION;
|
|
7739
9254
|
}
|
|
7740
9255
|
});
|
|
7741
9256
|
Object.defineProperty(exports, "WORKER_PORT", {
|
|
@@ -7792,6 +9307,12 @@ Object.defineProperty(exports, "applyPostgres", {
|
|
|
7792
9307
|
return applyPostgres;
|
|
7793
9308
|
}
|
|
7794
9309
|
});
|
|
9310
|
+
Object.defineProperty(exports, "assertDevServicesFlag", {
|
|
9311
|
+
enumerable: true,
|
|
9312
|
+
get: function() {
|
|
9313
|
+
return assertDevServicesFlag;
|
|
9314
|
+
}
|
|
9315
|
+
});
|
|
7795
9316
|
Object.defineProperty(exports, "assertExternalServices", {
|
|
7796
9317
|
enumerable: true,
|
|
7797
9318
|
get: function() {
|
|
@@ -7804,6 +9325,12 @@ Object.defineProperty(exports, "assertNoStaleSecrets", {
|
|
|
7804
9325
|
return assertNoStaleSecrets;
|
|
7805
9326
|
}
|
|
7806
9327
|
});
|
|
9328
|
+
Object.defineProperty(exports, "assertStageProvidersEnabled", {
|
|
9329
|
+
enumerable: true,
|
|
9330
|
+
get: function() {
|
|
9331
|
+
return assertStageProvidersEnabled;
|
|
9332
|
+
}
|
|
9333
|
+
});
|
|
7807
9334
|
Object.defineProperty(exports, "backendsOf", {
|
|
7808
9335
|
enumerable: true,
|
|
7809
9336
|
get: function() {
|
|
@@ -7846,6 +9373,12 @@ Object.defineProperty(exports, "constructGlobs", {
|
|
|
7846
9373
|
return constructGlobs;
|
|
7847
9374
|
}
|
|
7848
9375
|
});
|
|
9376
|
+
Object.defineProperty(exports, "contentVersion", {
|
|
9377
|
+
enumerable: true,
|
|
9378
|
+
get: function() {
|
|
9379
|
+
return contentVersion;
|
|
9380
|
+
}
|
|
9381
|
+
});
|
|
7849
9382
|
Object.defineProperty(exports, "createCredentialsPreload", {
|
|
7850
9383
|
enumerable: true,
|
|
7851
9384
|
get: function() {
|
|
@@ -7864,6 +9397,12 @@ Object.defineProperty(exports, "createEntryWrapper", {
|
|
|
7864
9397
|
return createEntryWrapper;
|
|
7865
9398
|
}
|
|
7866
9399
|
});
|
|
9400
|
+
Object.defineProperty(exports, "createStateStore", {
|
|
9401
|
+
enumerable: true,
|
|
9402
|
+
get: function() {
|
|
9403
|
+
return createStateStore;
|
|
9404
|
+
}
|
|
9405
|
+
});
|
|
7867
9406
|
Object.defineProperty(exports, "deployIdentity", {
|
|
7868
9407
|
enumerable: true,
|
|
7869
9408
|
get: function() {
|
|
@@ -8128,16 +9667,16 @@ Object.defineProperty(exports, "networkEnv", {
|
|
|
8128
9667
|
return networkEnv;
|
|
8129
9668
|
}
|
|
8130
9669
|
});
|
|
8131
|
-
Object.defineProperty(exports, "
|
|
9670
|
+
Object.defineProperty(exports, "newLockHolder", {
|
|
8132
9671
|
enumerable: true,
|
|
8133
9672
|
get: function() {
|
|
8134
|
-
return
|
|
9673
|
+
return newLockHolder;
|
|
8135
9674
|
}
|
|
8136
9675
|
});
|
|
8137
|
-
Object.defineProperty(exports, "
|
|
9676
|
+
Object.defineProperty(exports, "ownershipMarker", {
|
|
8138
9677
|
enumerable: true,
|
|
8139
9678
|
get: function() {
|
|
8140
|
-
return
|
|
9679
|
+
return ownershipMarker;
|
|
8141
9680
|
}
|
|
8142
9681
|
});
|
|
8143
9682
|
Object.defineProperty(exports, "pgClient", {
|
|
@@ -8212,6 +9751,18 @@ Object.defineProperty(exports, "providerOf", {
|
|
|
8212
9751
|
return providerOf;
|
|
8213
9752
|
}
|
|
8214
9753
|
});
|
|
9754
|
+
Object.defineProperty(exports, "provisionCommand", {
|
|
9755
|
+
enumerable: true,
|
|
9756
|
+
get: function() {
|
|
9757
|
+
return provisionCommand;
|
|
9758
|
+
}
|
|
9759
|
+
});
|
|
9760
|
+
Object.defineProperty(exports, "provisionStage", {
|
|
9761
|
+
enumerable: true,
|
|
9762
|
+
get: function() {
|
|
9763
|
+
return provisionStage;
|
|
9764
|
+
}
|
|
9765
|
+
});
|
|
8215
9766
|
Object.defineProperty(exports, "quoteIdentifier", {
|
|
8216
9767
|
enumerable: true,
|
|
8217
9768
|
get: function() {
|
|
@@ -8356,6 +9907,12 @@ Object.defineProperty(exports, "stackRedis", {
|
|
|
8356
9907
|
return stackRedis;
|
|
8357
9908
|
}
|
|
8358
9909
|
});
|
|
9910
|
+
Object.defineProperty(exports, "stageProviderNotes", {
|
|
9911
|
+
enumerable: true,
|
|
9912
|
+
get: function() {
|
|
9913
|
+
return stageProviderNotes;
|
|
9914
|
+
}
|
|
9915
|
+
});
|
|
8359
9916
|
Object.defineProperty(exports, "storageBackendFor", {
|
|
8360
9917
|
enumerable: true,
|
|
8361
9918
|
get: function() {
|
|
@@ -8416,6 +9973,12 @@ Object.defineProperty(exports, "validateImageRef", {
|
|
|
8416
9973
|
return validateImageRef;
|
|
8417
9974
|
}
|
|
8418
9975
|
});
|
|
9976
|
+
Object.defineProperty(exports, "verifyStageProviders", {
|
|
9977
|
+
enumerable: true,
|
|
9978
|
+
get: function() {
|
|
9979
|
+
return verifyStageProviders;
|
|
9980
|
+
}
|
|
9981
|
+
});
|
|
8419
9982
|
Object.defineProperty(exports, "withCustomSecret", {
|
|
8420
9983
|
enumerable: true,
|
|
8421
9984
|
get: function() {
|
|
@@ -8483,4 +10046,4 @@ Object.defineProperty(exports, "workspace_exports", {
|
|
|
8483
10046
|
}
|
|
8484
10047
|
});
|
|
8485
10048
|
|
|
8486
|
-
//# sourceMappingURL=seeds-
|
|
10049
|
+
//# sourceMappingURL=seeds-CsI0A_Ef.cjs.map
|