@geekmidas/cli 10.0.0-alpha.2 → 10.0.0-alpha.21
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/README.md +25 -48
- package/bin/adjacent-tsconfig.mjs +59 -0
- package/bin/gkm.mjs +7 -1
- package/dist/{HostingerProvider-Cft7lEYr.mjs → HostingerProvider-BY26bWwB.mjs} +20 -21
- package/dist/HostingerProvider-BY26bWwB.mjs.map +1 -0
- package/dist/{HostingerProvider-h3EbEp-Z.cjs → HostingerProvider-BZFGYYVA.cjs} +21 -22
- package/dist/HostingerProvider-BZFGYYVA.cjs.map +1 -0
- package/dist/{LocalStateProvider-Db9SEjyH.cjs → LocalStateProvider-7HR6SS2t.cjs} +11 -6
- package/dist/LocalStateProvider-7HR6SS2t.cjs.map +1 -0
- package/dist/{LocalStateProvider-Bn6tS-oL.mjs → LocalStateProvider-D7QyJoLM.mjs} +10 -4
- package/dist/LocalStateProvider-D7QyJoLM.mjs.map +1 -0
- package/dist/{Route53Provider-RdpUDJQk.mjs → Route53Provider-Cvj40A8I.mjs} +12 -12
- package/dist/Route53Provider-Cvj40A8I.mjs.map +1 -0
- package/dist/{Route53Provider-di2fMM5N.cjs → Route53Provider-DVILBsvV.cjs} +20 -21
- package/dist/Route53Provider-DVILBsvV.cjs.map +1 -0
- package/dist/{SSMStateProvider-BRt2py-A.mjs → SSMStateProvider-B0Hu1MKZ.mjs} +13 -4
- package/dist/SSMStateProvider-B0Hu1MKZ.mjs.map +1 -0
- package/dist/{SSMStateProvider-DyX8s9HI.cjs → SSMStateProvider-VrQ-yeqd.cjs} +17 -9
- package/dist/SSMStateProvider-VrQ-yeqd.cjs.map +1 -0
- package/dist/{backup-provisioner-BrsHwYG0.cjs → backup-provisioner-BmhIO_Yr.cjs} +21 -19
- package/dist/backup-provisioner-BmhIO_Yr.cjs.map +1 -0
- package/dist/{backup-provisioner-Bgj5Vi_L.mjs → backup-provisioner-DIVCbfee.mjs} +10 -7
- package/dist/backup-provisioner-DIVCbfee.mjs.map +1 -0
- package/dist/{bundler-BY1OmV1e.cjs → bundler-mjqGAvu1.cjs} +10 -28
- package/dist/bundler-mjqGAvu1.cjs.map +1 -0
- package/dist/{bundler-Be8Zg4Wd.mjs → bundler-mwH7WOb1.mjs} +9 -26
- package/dist/bundler-mwH7WOb1.mjs.map +1 -0
- package/dist/{config-BholeD-0.cjs → config-DSXI480r.cjs} +80 -148
- package/dist/config-DSXI480r.cjs.map +1 -0
- package/dist/{config-CzWnfcXM.mjs → config-DdKkoUE1.mjs} +33 -88
- package/dist/config-DdKkoUE1.mjs.map +1 -0
- package/dist/config.cjs +4 -5
- package/dist/config.d.cts +14 -17
- package/dist/config.d.cts.map +1 -1
- package/dist/config.d.mts +14 -17
- package/dist/config.d.mts.map +1 -1
- package/dist/config.mjs +3 -5
- package/dist/{credentials-DQmbRnr1.cjs → credentials-BfmmB4WD.cjs} +1508 -1243
- package/dist/credentials-BfmmB4WD.cjs.map +1 -0
- package/dist/{credentials-BZcp3CGx.mjs → credentials-CD8ftUS6.mjs} +1374 -1114
- package/dist/credentials-CD8ftUS6.mjs.map +1 -0
- package/dist/deploy/sniffer-envkit-patch.cjs +18 -10
- package/dist/deploy/sniffer-envkit-patch.cjs.map +1 -1
- package/dist/deploy/sniffer-envkit-patch.d.cts +0 -2
- package/dist/deploy/sniffer-envkit-patch.d.cts.map +1 -1
- package/dist/deploy/sniffer-envkit-patch.d.mts +0 -2
- package/dist/deploy/sniffer-envkit-patch.d.mts.map +1 -1
- package/dist/deploy/sniffer-envkit-patch.mjs +10 -2
- package/dist/deploy/sniffer-envkit-patch.mjs.map +1 -1
- package/dist/deploy/sniffer-hooks.cjs +13 -7
- package/dist/deploy/sniffer-hooks.cjs.map +1 -1
- package/dist/deploy/sniffer-hooks.d.cts +1 -2
- package/dist/deploy/sniffer-hooks.d.cts.map +1 -1
- package/dist/deploy/sniffer-hooks.d.mts +1 -2
- package/dist/deploy/sniffer-hooks.d.mts.map +1 -1
- package/dist/deploy/sniffer-hooks.mjs +9 -2
- package/dist/deploy/sniffer-hooks.mjs.map +1 -1
- package/dist/deploy/sniffer-loader.cjs +16 -7
- package/dist/deploy/sniffer-loader.cjs.map +1 -1
- package/dist/deploy/sniffer-loader.d.cts +1 -1
- package/dist/deploy/sniffer-loader.d.mts +1 -1
- package/dist/deploy/sniffer-loader.mjs +13 -2
- package/dist/deploy/sniffer-loader.mjs.map +1 -1
- package/dist/deploy/sniffer-routes-worker.cjs +11 -11
- package/dist/deploy/sniffer-routes-worker.cjs.map +1 -1
- package/dist/deploy/sniffer-routes-worker.d.cts +1 -1
- package/dist/deploy/sniffer-routes-worker.d.mts +1 -1
- package/dist/deploy/sniffer-routes-worker.mjs +2 -2
- package/dist/deploy/sniffer-routes-worker.mjs.map +1 -1
- package/dist/deploy/sniffer-worker.cjs +15 -6
- package/dist/deploy/sniffer-worker.cjs.map +1 -1
- package/dist/deploy/sniffer-worker.d.cts +1 -3
- package/dist/deploy/sniffer-worker.d.cts.map +1 -1
- package/dist/deploy/sniffer-worker.d.mts +1 -3
- package/dist/deploy/sniffer-worker.d.mts.map +1 -1
- package/dist/deploy/sniffer-worker.mjs +15 -4
- package/dist/deploy/sniffer-worker.mjs.map +1 -1
- package/dist/{discover-DTmFeloL.cjs → discover-BERGFg8v.cjs} +81 -64
- package/dist/discover-BERGFg8v.cjs.map +1 -0
- package/dist/{discover-CAlFgYec.mjs → discover-CK-n3w6b.mjs} +35 -20
- package/dist/discover-CK-n3w6b.mjs.map +1 -0
- package/dist/{emit-CbcRolsa.cjs → emit-BdoPFjZf.cjs} +53 -36
- package/dist/emit-BdoPFjZf.cjs.map +1 -0
- package/dist/{emit-CiZS_5Ub.mjs → emit-Bzq7JsMP.mjs} +24 -6
- package/dist/emit-Bzq7JsMP.mjs.map +1 -0
- package/dist/{generator-VE2JrFlY.mjs → generator-LfZU7NlU.mjs} +8 -14
- package/dist/generator-LfZU7NlU.mjs.map +1 -0
- package/dist/{generator-CNzBw7g5.cjs → generator-Wdfp0jCS.cjs} +38 -45
- package/dist/generator-Wdfp0jCS.cjs.map +1 -0
- package/dist/github-Bho-C6Zg.cjs +216 -0
- package/dist/github-Bho-C6Zg.cjs.map +1 -0
- package/dist/github-De-yMwVt.mjs +216 -0
- package/dist/github-De-yMwVt.mjs.map +1 -0
- package/dist/{index-CHB4tSzX.d.cts → index-B4H4XF7A.d.mts} +4 -13
- package/dist/index-B4H4XF7A.d.mts.map +1 -0
- package/dist/{index-BThUzEIp.d.mts → index-RCUizGxL.d.cts} +4 -13
- package/dist/index-RCUizGxL.d.cts.map +1 -0
- package/dist/index.cjs +5108 -3381
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +5025 -3315
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-c9bpieI5.cjs → openapi-B8bu_OXN.cjs} +228 -188
- package/dist/openapi-B8bu_OXN.cjs.map +1 -0
- package/dist/{openapi-CY9xFkTH.mjs → openapi-DfjbhrsM.mjs} +172 -143
- package/dist/openapi-DfjbhrsM.mjs.map +1 -0
- package/dist/openapi.cjs +5 -6
- package/dist/openapi.d.cts +47 -14
- package/dist/openapi.d.cts.map +1 -1
- package/dist/openapi.d.mts +47 -14
- package/dist/openapi.d.mts.map +1 -1
- package/dist/openapi.mjs +2 -6
- package/dist/reconcile/public.cjs +4 -4
- package/dist/reconcile/public.d.cts +8 -26
- package/dist/reconcile/public.d.cts.map +1 -1
- package/dist/reconcile/public.d.mts +8 -26
- package/dist/reconcile/public.d.mts.map +1 -1
- package/dist/reconcile/public.mjs +3 -4
- package/dist/{fullstack-secrets-D1o9-G5l.mjs → reconcile-gb1lz1Dk.mjs} +43 -17
- package/dist/reconcile-gb1lz1Dk.mjs.map +1 -0
- package/dist/{fullstack-secrets-C-GGohke.cjs → reconcile-hx_vBvux.cjs} +63 -38
- package/dist/reconcile-hx_vBvux.cjs.map +1 -0
- package/dist/rolldown-runtime-BhDjJH2R.mjs +15 -0
- package/dist/{chunk-CUT6urMc.cjs → rolldown-runtime-C0BPl7ul.cjs} +23 -10
- package/dist/ssm-BKN1Cg4z.mjs +60 -0
- package/dist/ssm-BKN1Cg4z.mjs.map +1 -0
- package/dist/ssm-FxeSAt35.cjs +60 -0
- package/dist/ssm-FxeSAt35.cjs.map +1 -0
- package/dist/{storage-BJmzYxt8.cjs → storage-DgypfSD6.cjs} +65 -77
- package/dist/storage-DgypfSD6.cjs.map +1 -0
- package/dist/{storage-Bi24WfoW.mjs → storage-TP0x118M.mjs} +22 -15
- package/dist/storage-TP0x118M.mjs.map +1 -0
- package/dist/transfer-CXoyQZbB.cjs +151 -0
- package/dist/transfer-CXoyQZbB.cjs.map +1 -0
- package/dist/transfer-Gsxfy6m-.mjs +122 -0
- package/dist/transfer-Gsxfy6m-.mjs.map +1 -0
- package/dist/types-7R81hu0i.d.mts +6302 -0
- package/dist/types-7R81hu0i.d.mts.map +1 -0
- package/dist/types-N7c2O2m7.d.cts +6303 -0
- package/dist/types-N7c2O2m7.d.cts.map +1 -0
- package/dist/workspace/index.cjs +3 -3
- package/dist/workspace/index.d.cts +3 -3
- package/dist/workspace/index.d.mts +3 -3
- package/dist/workspace/index.mjs +2 -3
- package/dist/workspace-Bu3xNCej.mjs +6563 -0
- package/dist/workspace-Bu3xNCej.mjs.map +1 -0
- package/dist/workspace-CRD9YinV.cjs +6741 -0
- package/dist/workspace-CRD9YinV.cjs.map +1 -0
- package/package.json +33 -43
- package/dist/CachedStateProvider-BLo7zv7S.cjs +0 -3
- package/dist/CachedStateProvider-BxoAm-B2.mjs +0 -3
- package/dist/CachedStateProvider-CqPdW7CB.cjs +0 -60
- package/dist/CachedStateProvider-CqPdW7CB.cjs.map +0 -1
- package/dist/CachedStateProvider-DTniehMP.mjs +0 -54
- package/dist/CachedStateProvider-DTniehMP.mjs.map +0 -1
- package/dist/HostingerProvider-Cft7lEYr.mjs.map +0 -1
- package/dist/HostingerProvider-h3EbEp-Z.cjs.map +0 -1
- package/dist/LocalStateProvider-Bn6tS-oL.mjs.map +0 -1
- package/dist/LocalStateProvider-Db9SEjyH.cjs.map +0 -1
- package/dist/Route53Provider-RdpUDJQk.mjs.map +0 -1
- package/dist/Route53Provider-di2fMM5N.cjs.map +0 -1
- package/dist/SSMStateProvider-BRt2py-A.mjs.map +0 -1
- package/dist/SSMStateProvider-DyX8s9HI.cjs.map +0 -1
- package/dist/backup-provisioner-Bgj5Vi_L.mjs.map +0 -1
- package/dist/backup-provisioner-BrsHwYG0.cjs.map +0 -1
- package/dist/bundler-BY1OmV1e.cjs.map +0 -1
- package/dist/bundler-Be8Zg4Wd.mjs.map +0 -1
- package/dist/chunk-Duj1WY3L.mjs +0 -7
- package/dist/config-BholeD-0.cjs.map +0 -1
- package/dist/config-CzWnfcXM.mjs.map +0 -1
- package/dist/credentials-B5zRHdAl.mjs +0 -141
- package/dist/credentials-B5zRHdAl.mjs.map +0 -1
- package/dist/credentials-BZcp3CGx.mjs.map +0 -1
- package/dist/credentials-BapzjyV5.cjs +0 -195
- package/dist/credentials-BapzjyV5.cjs.map +0 -1
- package/dist/credentials-DQmbRnr1.cjs.map +0 -1
- package/dist/discover-CAlFgYec.mjs.map +0 -1
- package/dist/discover-DTmFeloL.cjs.map +0 -1
- package/dist/dokploy-api-BS2xnJLZ.mjs +0 -3
- package/dist/dokploy-api-BsVH0y_s.cjs +0 -718
- package/dist/dokploy-api-BsVH0y_s.cjs.map +0 -1
- package/dist/dokploy-api-BuxDRo3l.cjs +0 -3
- package/dist/dokploy-api-aTFPz7_4.mjs +0 -706
- package/dist/dokploy-api-aTFPz7_4.mjs.map +0 -1
- package/dist/emit-CbcRolsa.cjs.map +0 -1
- package/dist/emit-CiZS_5Ub.mjs.map +0 -1
- package/dist/encryption-C6jaU0hE.cjs +0 -54
- package/dist/encryption-C6jaU0hE.cjs.map +0 -1
- package/dist/encryption-CHzzeR8a.cjs +0 -4
- package/dist/encryption-CQUncu7v.mjs +0 -3
- package/dist/encryption-D-abyhZx.mjs +0 -42
- package/dist/encryption-D-abyhZx.mjs.map +0 -1
- package/dist/fullstack-secrets-C-GGohke.cjs.map +0 -1
- package/dist/fullstack-secrets-D1o9-G5l.mjs.map +0 -1
- package/dist/generator-CNzBw7g5.cjs.map +0 -1
- package/dist/generator-VE2JrFlY.mjs.map +0 -1
- package/dist/index-BThUzEIp.d.mts.map +0 -1
- package/dist/index-CHB4tSzX.d.cts.map +0 -1
- package/dist/openapi-CY9xFkTH.mjs.map +0 -1
- package/dist/openapi-c9bpieI5.cjs.map +0 -1
- package/dist/openapi-react-query-Byltvm_7.mjs +0 -167
- package/dist/openapi-react-query-Byltvm_7.mjs.map +0 -1
- package/dist/openapi-react-query-CmJ1xDbn.cjs +0 -173
- package/dist/openapi-react-query-CmJ1xDbn.cjs.map +0 -1
- package/dist/openapi-react-query.cjs +0 -4
- package/dist/openapi-react-query.d.cts +0 -11
- package/dist/openapi-react-query.d.cts.map +0 -1
- package/dist/openapi-react-query.d.mts +0 -11
- package/dist/openapi-react-query.d.mts.map +0 -1
- package/dist/openapi-react-query.mjs +0 -4
- package/dist/reconcile-CFs8C_1a.mjs +0 -37
- package/dist/reconcile-CFs8C_1a.mjs.map +0 -1
- package/dist/reconcile-D0pimNiy.cjs +0 -37
- package/dist/reconcile-D0pimNiy.cjs.map +0 -1
- package/dist/setup-Bc73t0ve.mjs +0 -443
- package/dist/setup-Bc73t0ve.mjs.map +0 -1
- package/dist/setup-C48C9hto.cjs +0 -473
- package/dist/setup-C48C9hto.cjs.map +0 -1
- package/dist/setup-DCuudnCl.mjs +0 -11
- package/dist/setup-fG_ivuxO.cjs +0 -11
- package/dist/storage-BJmzYxt8.cjs.map +0 -1
- package/dist/storage-BM9hGpgv.cjs +0 -7
- package/dist/storage-Bi24WfoW.mjs.map +0 -1
- package/dist/storage-CYlGwWTj.mjs +0 -3
- package/dist/sync-CmEuNZsB.mjs +0 -76
- package/dist/sync-CmEuNZsB.mjs.map +0 -1
- package/dist/sync-D306yOWu.cjs +0 -4
- package/dist/sync-SizaChmC.cjs +0 -93
- package/dist/sync-SizaChmC.cjs.map +0 -1
- package/dist/sync-hOqAWHR6.mjs +0 -3
- package/dist/types-C-arnQ1r.d.mts +0 -2184
- package/dist/types-C-arnQ1r.d.mts.map +0 -1
- package/dist/types-abJQuk7m.d.cts +0 -2184
- package/dist/types-abJQuk7m.d.cts.map +0 -1
- package/dist/workspace-7FqprxuJ.mjs +0 -4323
- package/dist/workspace-7FqprxuJ.mjs.map +0 -1
- package/dist/workspace-BO_KWohI.cjs +0 -9
- package/dist/workspace-DK4WWJIn.mjs +0 -8
- package/dist/workspace-XvqO9jIv.cjs +0 -4472
- package/dist/workspace-XvqO9jIv.cjs.map +0 -1
|
@@ -1,64 +1,38 @@
|
|
|
1
|
-
const
|
|
2
|
-
const require_discover = require(
|
|
3
|
-
const require_config = require(
|
|
4
|
-
const require_workspace = require(
|
|
5
|
-
const require_generator = require(
|
|
6
|
-
const require_storage = require(
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
1
|
+
const require_rolldown_runtime = require("./rolldown-runtime-C0BPl7ul.cjs");
|
|
2
|
+
const require_discover = require("./discover-BERGFg8v.cjs");
|
|
3
|
+
const require_config = require("./config-DSXI480r.cjs");
|
|
4
|
+
const require_workspace = require("./workspace-CRD9YinV.cjs");
|
|
5
|
+
const require_generator = require("./generator-Wdfp0jCS.cjs");
|
|
6
|
+
const require_storage = require("./storage-DgypfSD6.cjs");
|
|
7
|
+
let node_fs = require("node:fs");
|
|
8
|
+
let node_fs_promises = require("node:fs/promises");
|
|
9
|
+
let node_path = require("node:path");
|
|
10
|
+
let node_child_process = require("node:child_process");
|
|
11
|
+
let _geekmidas_manifest = require("@geekmidas/manifest");
|
|
12
|
+
let node_net = require("node:net");
|
|
13
|
+
let dotenv = require("dotenv");
|
|
14
|
+
let yaml = require("yaml");
|
|
15
|
+
let node_crypto = require("node:crypto");
|
|
16
|
+
let _geekmidas_db_pg_roles = require("@geekmidas/db/pg/roles");
|
|
17
|
+
let _aws_sdk_client_s3 = require("@aws-sdk/client-s3");
|
|
18
|
+
let pg = require("pg");
|
|
19
|
+
let node_util = require("node:util");
|
|
20
|
+
let _geekmidas_cache_postgres = require("@geekmidas/cache/postgres");
|
|
22
21
|
//#region src/workspace/backends.ts
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
];
|
|
38
|
-
/**
|
|
39
|
-
* The cache backend a workspace selected, or the one its target implies.
|
|
40
|
-
*
|
|
41
|
-
* Defaulted here rather than by every caller, so "what happens when nobody
|
|
42
|
-
* said" is answered once and the answer is greppable — and now answered by
|
|
43
|
-
* *where it deploys*, because there is no single right answer for both a
|
|
44
|
-
* Lambda and a box running its own Postgres.
|
|
45
|
-
*
|
|
46
|
-
* `on` is the deploy target, not where this code happens to be running. A
|
|
47
|
-
* local `gkm dev` for an AWS app still defaults to Upstash, because a backend
|
|
48
|
-
* that differed between local and deployed would be worse than a slower one —
|
|
49
|
-
* which is the property the type's own documentation asks for.
|
|
50
|
-
*/
|
|
51
|
-
function cacheBackendOf(value, on = "aws") {
|
|
52
|
-
return isBackend(value, CACHE_BACKENDS) ? value : require_discover.DEFAULT_CACHE[on];
|
|
53
|
-
}
|
|
54
|
-
/** The email backend a workspace selected, defaulting to SES on every target. */
|
|
55
|
-
function emailBackendOf(value) {
|
|
56
|
-
return isBackend(value, EMAIL_BACKENDS) ? value : require_discover.DEFAULT_EMAIL;
|
|
57
|
-
}
|
|
58
|
-
/** The storage backend a workspace selected, or the one its target implies. */
|
|
59
|
-
function storageBackendOf(value, on = "aws") {
|
|
60
|
-
return isBackend(value, STORAGE_BACKENDS) ? value : require_discover.DEFAULT_STORAGE[on];
|
|
61
|
-
}
|
|
22
|
+
/**
|
|
23
|
+
* Which backend each declared kind resolves to — decided by where the project
|
|
24
|
+
* deploys, and by nothing else.
|
|
25
|
+
*
|
|
26
|
+
* These used to be read out of a `services:` block, where one key answered two
|
|
27
|
+
* questions: *does this exist* and *what backs it*. The first is the manifest's
|
|
28
|
+
* — a declared `Cache` is why there is a cache — and the second is the deploy
|
|
29
|
+
* target's, because the right cache for a Lambda is not the right cache for a
|
|
30
|
+
* box already running Postgres. With both answered elsewhere there was nothing
|
|
31
|
+
* left for the block to say, so it is gone and these read the target.
|
|
32
|
+
*
|
|
33
|
+
* Mail has no answer here at all: every provider speaks SMTP, so the provider
|
|
34
|
+
* is whichever one the stage's mail URL points at.
|
|
35
|
+
*/
|
|
62
36
|
/**
|
|
63
37
|
* Which family of defaults a workspace's deploy target belongs to.
|
|
64
38
|
*
|
|
@@ -77,17 +51,162 @@ function providerOf(workspace) {
|
|
|
77
51
|
return target === "dokploy" || target === "server" ? "server" : "aws";
|
|
78
52
|
}
|
|
79
53
|
/**
|
|
80
|
-
*
|
|
81
|
-
* a `services:` value can be.
|
|
54
|
+
* The cache backend a target implies.
|
|
82
55
|
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
56
|
+
* The same answer locally and deployed: a local `gkm dev` for an AWS app still
|
|
57
|
+
* uses the Upstash protocol, because a backend that differed between the two
|
|
58
|
+
* would be worse than a slower one.
|
|
86
59
|
*/
|
|
87
|
-
function
|
|
88
|
-
return
|
|
60
|
+
function cacheBackendFor(on) {
|
|
61
|
+
return require_discover.DEFAULT_CACHE[on];
|
|
62
|
+
}
|
|
63
|
+
/** Where a declared bucket lives on a target. */
|
|
64
|
+
function storageBackendFor(on) {
|
|
65
|
+
return require_discover.DEFAULT_STORAGE[on];
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Which broker carries declared queues and topics on a target.
|
|
69
|
+
*
|
|
70
|
+
* Only consulted when something declares one; a project with no `Topic` or
|
|
71
|
+
* `Queue` has no events at all, whatever the target.
|
|
72
|
+
*/
|
|
73
|
+
function eventsBackendFor(on) {
|
|
74
|
+
return require_discover.DEFAULT_EVENTS[on];
|
|
75
|
+
}
|
|
76
|
+
//#endregion
|
|
77
|
+
//#region src/reconcile/containers.ts
|
|
78
|
+
/**
|
|
79
|
+
* What gkm knows about each container it can derive.
|
|
80
|
+
*
|
|
81
|
+
* Everything provider-specific about the local target lives here: the image, the
|
|
82
|
+
* ports the image listens on, and whether its data is worth keeping. The plan
|
|
83
|
+
* says *which* containers exist and never says any of this, which is the split
|
|
84
|
+
* the design asks for — neutral facts from the declaration, provider detail
|
|
85
|
+
* from here and from config.
|
|
86
|
+
*
|
|
87
|
+
* Shared by allocation and by the compose writer so the two cannot disagree
|
|
88
|
+
* about how many ports a container needs.
|
|
89
|
+
*/
|
|
90
|
+
/**
|
|
91
|
+
* The Postgres image for a major version.
|
|
92
|
+
*
|
|
93
|
+
* One place that knows the tag shape, so a declared version reaches the
|
|
94
|
+
* container without anyone restating `-alpine` beside it.
|
|
95
|
+
*/
|
|
96
|
+
function postgresImage(version) {
|
|
97
|
+
return `postgres:${version}-alpine`;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The default image for each container.
|
|
101
|
+
*
|
|
102
|
+
* Pinned to a major rather than `latest` where the image publishes one, so a
|
|
103
|
+
* rebuild does not silently move a developer's Postgres a version. Overridable
|
|
104
|
+
* per project: a project needing `postgis/postgis` says so in config.
|
|
105
|
+
*/
|
|
106
|
+
const DEFAULT_IMAGES = {
|
|
107
|
+
postgres: postgresImage(_geekmidas_manifest.DEFAULT_POSTGRES_VERSION),
|
|
108
|
+
minio: "pgsty/minio:RELEASE.2026-08-04T00-00-00Z",
|
|
109
|
+
mailpit: "axllent/mailpit:latest",
|
|
110
|
+
redis: "redis:8-alpine",
|
|
111
|
+
"redis-http": "hiett/serverless-redis-http:latest",
|
|
112
|
+
rabbitmq: "rabbitmq:4-management-alpine",
|
|
113
|
+
localstack: "floci/floci:latest",
|
|
114
|
+
caddy: "caddy:2-alpine"
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* The ports each container listens on.
|
|
118
|
+
*
|
|
119
|
+
* The first is always the one an app connects to; the rest are consoles and
|
|
120
|
+
* management UIs, published because a bucket or an inbox you can look at is most
|
|
121
|
+
* of why these are real containers rather than stubs.
|
|
122
|
+
*/
|
|
123
|
+
const PORTS = {
|
|
124
|
+
postgres: [{
|
|
125
|
+
key: "postgres",
|
|
126
|
+
inside: 5432,
|
|
127
|
+
label: "postgres"
|
|
128
|
+
}],
|
|
129
|
+
minio: [{
|
|
130
|
+
key: "minio",
|
|
131
|
+
inside: 9e3,
|
|
132
|
+
label: "minio api"
|
|
133
|
+
}, {
|
|
134
|
+
key: "minio-console",
|
|
135
|
+
inside: 9001,
|
|
136
|
+
label: "minio console"
|
|
137
|
+
}],
|
|
138
|
+
mailpit: [{
|
|
139
|
+
key: "mailpit",
|
|
140
|
+
inside: 1025,
|
|
141
|
+
label: "smtp"
|
|
142
|
+
}, {
|
|
143
|
+
key: "mailpit-web",
|
|
144
|
+
inside: 8025,
|
|
145
|
+
label: "mailpit inbox"
|
|
146
|
+
}],
|
|
147
|
+
redis: [{
|
|
148
|
+
key: "redis",
|
|
149
|
+
inside: 6379,
|
|
150
|
+
label: "redis"
|
|
151
|
+
}],
|
|
152
|
+
"redis-http": [{
|
|
153
|
+
key: "redis-http",
|
|
154
|
+
inside: 80,
|
|
155
|
+
label: "cache"
|
|
156
|
+
}],
|
|
157
|
+
rabbitmq: [{
|
|
158
|
+
key: "rabbitmq",
|
|
159
|
+
inside: 5672,
|
|
160
|
+
label: "amqp"
|
|
161
|
+
}, {
|
|
162
|
+
key: "rabbitmq-management",
|
|
163
|
+
inside: 15672,
|
|
164
|
+
label: "rabbitmq console"
|
|
165
|
+
}],
|
|
166
|
+
localstack: [{
|
|
167
|
+
key: "localstack",
|
|
168
|
+
inside: 4566,
|
|
169
|
+
label: "localstack"
|
|
170
|
+
}],
|
|
171
|
+
caddy: [{
|
|
172
|
+
key: "caddy",
|
|
173
|
+
inside: 443,
|
|
174
|
+
label: "https edge"
|
|
175
|
+
}]
|
|
176
|
+
};
|
|
177
|
+
/** The ports a container needs published. Empty for one gkm does not know. */
|
|
178
|
+
function portsOf(container) {
|
|
179
|
+
return PORTS[container] ?? [];
|
|
180
|
+
}
|
|
181
|
+
/** The key a container's primary port is allocated under. */
|
|
182
|
+
function primaryPortKey(container) {
|
|
183
|
+
return portsOf(container)[0]?.key ?? container;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Every port key a set of containers needs, in a stable order.
|
|
187
|
+
*
|
|
188
|
+
* What allocation is handed: it assigns ports to names and knows nothing about
|
|
189
|
+
* what listens on them.
|
|
190
|
+
*/
|
|
191
|
+
function portKeys(containers) {
|
|
192
|
+
return [...containers].sort().flatMap((container) => portsOf(container).map((port) => port.key));
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* The named volume a container's data lives in.
|
|
196
|
+
*
|
|
197
|
+
* Mailpit deliberately has none: local mail is disposable, and an inbox that
|
|
198
|
+
* survives days of dev is noise rather than state.
|
|
199
|
+
*/
|
|
200
|
+
function volumeOf(container) {
|
|
201
|
+
return {
|
|
202
|
+
postgres: "postgres-data",
|
|
203
|
+
minio: "minio-data",
|
|
204
|
+
redis: "redis-data",
|
|
205
|
+
rabbitmq: "rabbitmq-data",
|
|
206
|
+
localstack: "localstack-data",
|
|
207
|
+
caddy: "caddy-data"
|
|
208
|
+
}[container];
|
|
89
209
|
}
|
|
90
|
-
|
|
91
210
|
//#endregion
|
|
92
211
|
//#region src/reconcile/caddyfile.ts
|
|
93
212
|
/**
|
|
@@ -164,7 +283,7 @@ function portOf(address) {
|
|
|
164
283
|
const { port, protocol } = new URL(address);
|
|
165
284
|
return port || (protocol === "https:" ? "443" : "80");
|
|
166
285
|
} catch {
|
|
167
|
-
return
|
|
286
|
+
return;
|
|
168
287
|
}
|
|
169
288
|
}
|
|
170
289
|
/**
|
|
@@ -247,1139 +366,1200 @@ const GENERATED = `# Generated by gkm from the construct manifest — do not edi
|
|
|
247
366
|
# root and point Node at it, so nothing has to be installed to use these URLs
|
|
248
367
|
# from code; a browser needs the root trusted once.
|
|
249
368
|
`;
|
|
250
|
-
|
|
251
|
-
//#endregion
|
|
252
|
-
//#region src/reconcile/clients.ts
|
|
253
|
-
/** The credential local containers are brought up with. */
|
|
254
|
-
const LOCAL_USER$2 = "geekmidas";
|
|
255
|
-
/**
|
|
256
|
-
* A Postgres client that opens one connection per database it is asked about.
|
|
257
|
-
*
|
|
258
|
-
* `CREATE DATABASE` cannot run from a connection to the database being created,
|
|
259
|
-
* and a schema must be created from inside its own database — so the database is
|
|
260
|
-
* part of each query rather than fixed when the client is built.
|
|
261
|
-
*/
|
|
262
|
-
function pgClient(port) {
|
|
263
|
-
return { async query(database, sql, values) {
|
|
264
|
-
const client = new pg.Client({
|
|
265
|
-
host: "localhost",
|
|
266
|
-
port,
|
|
267
|
-
user: LOCAL_USER$2,
|
|
268
|
-
password: LOCAL_USER$2,
|
|
269
|
-
database: database ?? LOCAL_USER$2
|
|
270
|
-
});
|
|
271
|
-
await client.connect();
|
|
272
|
-
try {
|
|
273
|
-
const result = await client.query(sql, values);
|
|
274
|
-
return result.rows;
|
|
275
|
-
} finally {
|
|
276
|
-
await client.end();
|
|
277
|
-
}
|
|
278
|
-
} };
|
|
279
|
-
}
|
|
280
|
-
/** A bucket client pointed at the local MinIO. */
|
|
281
|
-
function bucketClient(port) {
|
|
282
|
-
const s3 = new __aws_sdk_client_s3.S3Client({
|
|
283
|
-
region: "us-east-1",
|
|
284
|
-
endpoint: `http://localhost:${port}`,
|
|
285
|
-
forcePathStyle: true,
|
|
286
|
-
credentials: {
|
|
287
|
-
accessKeyId: LOCAL_USER$2,
|
|
288
|
-
secretAccessKey: LOCAL_USER$2
|
|
289
|
-
}
|
|
290
|
-
});
|
|
291
|
-
return {
|
|
292
|
-
async exists(bucket) {
|
|
293
|
-
try {
|
|
294
|
-
await s3.send(new __aws_sdk_client_s3.HeadBucketCommand({ Bucket: bucket }));
|
|
295
|
-
return true;
|
|
296
|
-
} catch {
|
|
297
|
-
return false;
|
|
298
|
-
}
|
|
299
|
-
},
|
|
300
|
-
async create(bucket) {
|
|
301
|
-
await s3.send(new __aws_sdk_client_s3.CreateBucketCommand({ Bucket: bucket }));
|
|
302
|
-
},
|
|
303
|
-
async policy(bucket) {
|
|
304
|
-
try {
|
|
305
|
-
const result = await s3.send(new __aws_sdk_client_s3.GetBucketPolicyCommand({ Bucket: bucket }));
|
|
306
|
-
return result.Policy;
|
|
307
|
-
} catch {
|
|
308
|
-
return void 0;
|
|
309
|
-
}
|
|
310
|
-
},
|
|
311
|
-
async setPolicy(bucket, policy) {
|
|
312
|
-
await s3.send(new __aws_sdk_client_s3.PutBucketPolicyCommand({
|
|
313
|
-
Bucket: bucket,
|
|
314
|
-
Policy: policy
|
|
315
|
-
}));
|
|
316
|
-
}
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
|
-
|
|
320
369
|
//#endregion
|
|
321
|
-
//#region src/reconcile/
|
|
370
|
+
//#region src/reconcile/plan.ts
|
|
322
371
|
/**
|
|
323
|
-
*
|
|
372
|
+
* The reconcile plan — what a manifest wants to exist, for one stage.
|
|
324
373
|
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
* the design asks for — neutral facts from the declaration, provider detail
|
|
329
|
-
* from here and from config.
|
|
374
|
+
* Pure. Turning the plan into running containers and provisioned resources is
|
|
375
|
+
* the applier's job; deciding *what* should exist is this, so the decisions can
|
|
376
|
+
* be asserted as data rather than by starting Docker.
|
|
330
377
|
*
|
|
331
|
-
*
|
|
332
|
-
*
|
|
378
|
+
* One plan serves every stage. `gkm dev` reconciles `development` and `gkm test`
|
|
379
|
+
* reconciles `test`; they differ only in what the resources are called, never in
|
|
380
|
+
* what infrastructure exists. Roles are cluster-scoped in Postgres while grants
|
|
381
|
+
* are per-object, so one container and one role pair serve both.
|
|
333
382
|
*/
|
|
334
383
|
/**
|
|
335
|
-
*
|
|
384
|
+
* Which container serves a kind.
|
|
336
385
|
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
386
|
+
* Derived kinds map to the same container as their parent — a schema tenant
|
|
387
|
+
* lives in its parent's Postgres — so the set is deduplicated rather than
|
|
388
|
+
* counted.
|
|
339
389
|
*/
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
390
|
+
const CONTAINERS = {
|
|
391
|
+
database: "postgres",
|
|
392
|
+
"database-reader": "postgres",
|
|
393
|
+
"database-schema": "postgres",
|
|
394
|
+
objects: "minio",
|
|
395
|
+
"file-server": "caddy",
|
|
396
|
+
email: "mailpit"
|
|
397
|
+
};
|
|
343
398
|
/**
|
|
344
|
-
* The
|
|
399
|
+
* The container a cache backend needs locally.
|
|
345
400
|
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
401
|
+
* Each one matches the protocol its deployed form speaks, which is the property
|
|
402
|
+
* worth having: a cache that behaves differently in the two places is worse than
|
|
403
|
+
* a slower one.
|
|
404
|
+
*
|
|
405
|
+
* `db` is deliberately absent, the same way `pgboss` is absent from
|
|
406
|
+
* `EVENT_CONTAINERS`: the cache is a table in a database the manifest already
|
|
407
|
+
* declares, so mapping it here would start a container for a project that needs
|
|
408
|
+
* none.
|
|
349
409
|
*/
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
mailpit: "axllent/mailpit:latest",
|
|
354
|
-
redis: "redis:8-alpine",
|
|
355
|
-
"redis-http": "hiett/serverless-redis-http:latest",
|
|
356
|
-
rabbitmq: "rabbitmq:4-management-alpine",
|
|
357
|
-
localstack: "floci/floci:latest",
|
|
358
|
-
caddy: "caddy:2-alpine"
|
|
410
|
+
const CACHE_CONTAINERS = {
|
|
411
|
+
upstash: "redis-http",
|
|
412
|
+
elasticache: "redis"
|
|
359
413
|
};
|
|
360
414
|
/**
|
|
361
|
-
*
|
|
415
|
+
* Kinds that resolve a value without anything running.
|
|
362
416
|
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
*/
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
}],
|
|
373
|
-
minio: [{
|
|
374
|
-
key: "minio",
|
|
375
|
-
inside: 9e3,
|
|
376
|
-
label: "minio api"
|
|
377
|
-
}, {
|
|
378
|
-
key: "minio-console",
|
|
379
|
-
inside: 9001,
|
|
380
|
-
label: "minio console"
|
|
381
|
-
}],
|
|
382
|
-
mailpit: [{
|
|
383
|
-
key: "mailpit",
|
|
384
|
-
inside: 1025,
|
|
385
|
-
label: "smtp"
|
|
386
|
-
}, {
|
|
387
|
-
key: "mailpit-web",
|
|
388
|
-
inside: 8025,
|
|
389
|
-
label: "mailpit inbox"
|
|
390
|
-
}],
|
|
391
|
-
redis: [{
|
|
392
|
-
key: "redis",
|
|
393
|
-
inside: 6379,
|
|
394
|
-
label: "redis"
|
|
395
|
-
}],
|
|
396
|
-
"redis-http": [{
|
|
397
|
-
key: "redis-http",
|
|
398
|
-
inside: 80,
|
|
399
|
-
label: "cache"
|
|
400
|
-
}],
|
|
401
|
-
rabbitmq: [{
|
|
402
|
-
key: "rabbitmq",
|
|
403
|
-
inside: 5672,
|
|
404
|
-
label: "amqp"
|
|
405
|
-
}, {
|
|
406
|
-
key: "rabbitmq-management",
|
|
407
|
-
inside: 15672,
|
|
408
|
-
label: "rabbitmq console"
|
|
409
|
-
}],
|
|
410
|
-
localstack: [{
|
|
411
|
-
key: "localstack",
|
|
412
|
-
inside: 4566,
|
|
413
|
-
label: "localstack"
|
|
414
|
-
}],
|
|
415
|
-
caddy: [{
|
|
416
|
-
key: "caddy",
|
|
417
|
-
inside: 443,
|
|
418
|
-
label: "https edge"
|
|
419
|
-
}]
|
|
417
|
+
* A secret is the case: it has no address, so there is nothing to start and
|
|
418
|
+
* nothing to connect to — but it still appears in the plan, because the target
|
|
419
|
+
* still has to resolve it.
|
|
420
|
+
*/
|
|
421
|
+
const CONTAINERLESS = {
|
|
422
|
+
secret: true,
|
|
423
|
+
credential: true,
|
|
424
|
+
"rest-api": true,
|
|
425
|
+
site: true
|
|
420
426
|
};
|
|
421
|
-
/** The ports a container needs published. Empty for one gkm does not know. */
|
|
422
|
-
function portsOf(container) {
|
|
423
|
-
return PORTS[container] ?? [];
|
|
424
|
-
}
|
|
425
|
-
/** The key a container's primary port is allocated under. */
|
|
426
|
-
function primaryPortKey(container) {
|
|
427
|
-
return portsOf(container)[0]?.key ?? container;
|
|
428
|
-
}
|
|
429
427
|
/**
|
|
430
|
-
*
|
|
428
|
+
* Containers that cannot run alone.
|
|
431
429
|
*
|
|
432
|
-
*
|
|
433
|
-
*
|
|
430
|
+
* The one case is the cache: `serverless-redis-http` is a proxy and needs the
|
|
431
|
+
* Redis it proxies. Kept here rather than in the compose definition so the plan
|
|
432
|
+
* -- which is what decides what must be running -- still names everything.
|
|
434
433
|
*/
|
|
435
|
-
|
|
436
|
-
return [...containers].sort().flatMap((container) => portsOf(container).map((port) => port.key));
|
|
437
|
-
}
|
|
434
|
+
const REQUIRES = { "redis-http": ["redis"] };
|
|
438
435
|
/**
|
|
439
|
-
* The
|
|
436
|
+
* The role each kind's one key plays, where it is not a URL.
|
|
440
437
|
*
|
|
441
|
-
*
|
|
442
|
-
*
|
|
438
|
+
* The twin of `ProvidesByKind` in `@geekmidas/manifest`: that is what a
|
|
439
|
+
* construct declares, this is what the local target resolves, and both derive
|
|
440
|
+
* the key from the same `provideKey` so they cannot disagree.
|
|
443
441
|
*/
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
442
|
+
const ROLES = {
|
|
443
|
+
queue: "publisherConnectionString",
|
|
444
|
+
topic: "publisherConnectionString",
|
|
445
|
+
credential: "credential"
|
|
446
|
+
};
|
|
447
|
+
/** The kinds whose container is the events backend's rather than their own. */
|
|
448
|
+
const EVENT_KINDS = {
|
|
449
|
+
queue: true,
|
|
450
|
+
topic: true
|
|
451
|
+
};
|
|
452
|
+
/**
|
|
453
|
+
* Which kinds provision something *inside* their container.
|
|
454
|
+
*
|
|
455
|
+
* A database creates a database, a bucket creates a bucket; mail creates
|
|
456
|
+
* nothing — Mailpit accepts whatever is sent to it and has no per-stage object
|
|
457
|
+
* to name. Both still appear in the plan, because both still resolve a URL; the
|
|
458
|
+
* flag is the difference between "must be running" and "must be created".
|
|
459
|
+
*/
|
|
460
|
+
const PROVISIONS = {
|
|
461
|
+
database: true,
|
|
462
|
+
"database-reader": true,
|
|
463
|
+
"database-schema": true,
|
|
464
|
+
objects: true,
|
|
465
|
+
cache: true,
|
|
466
|
+
"file-server": true
|
|
467
|
+
};
|
|
468
|
+
/**
|
|
469
|
+
* The container each events backend needs, where it needs one of its own.
|
|
470
|
+
*
|
|
471
|
+
* `pgboss` is deliberately absent rather than mapped to `postgres`: it is a
|
|
472
|
+
* schema tenant in a database the manifest already declares, so mapping it here
|
|
473
|
+
* would start a Postgres for a project that declared no database at all.
|
|
474
|
+
*
|
|
475
|
+
* This one comes from config rather than from a declaration because there are
|
|
476
|
+
* no `topic` or `queue` kinds yet. When they land the mapping moves to
|
|
477
|
+
* {@link CONTAINERS} keyed by kind, and this constant goes with them.
|
|
478
|
+
*/
|
|
479
|
+
const EVENT_CONTAINERS = {
|
|
480
|
+
sns: "localstack",
|
|
481
|
+
rabbitmq: "rabbitmq"
|
|
482
|
+
};
|
|
483
|
+
/** The backend a project gets when it declares queues or topics and says nothing. */
|
|
484
|
+
const DEFAULT_EVENTS = "pgboss";
|
|
485
|
+
/**
|
|
486
|
+
* How a resource of each kind separates its stage suffix.
|
|
487
|
+
*
|
|
488
|
+
* Postgres identifiers are snake_case and a hyphen would need quoting
|
|
489
|
+
* everywhere; buckets and queues are kebab and disallow underscores in some
|
|
490
|
+
* providers. The suffix is the same word either way.
|
|
491
|
+
*/
|
|
492
|
+
const SEPARATORS = {
|
|
493
|
+
database: "_",
|
|
494
|
+
"database-reader": "_",
|
|
495
|
+
"database-schema": "_",
|
|
496
|
+
objects: "-"
|
|
497
|
+
};
|
|
498
|
+
/**
|
|
499
|
+
* The stage-scoped name for a resource.
|
|
500
|
+
*
|
|
501
|
+
* The local stage carries no suffix, so `gkm dev` keeps using `orders` and a
|
|
502
|
+
* developer's psql history, saved connections, and muscle memory survive a
|
|
503
|
+
* change of stage name. Every other stage is suffixed.
|
|
504
|
+
*/
|
|
505
|
+
function resourceName(id, kind, stage, localStage) {
|
|
506
|
+
const base = id.toLowerCase();
|
|
507
|
+
if (stage === localStage) return base;
|
|
508
|
+
return `${base}${SEPARATORS[kind] ?? "-"}${stage}`;
|
|
454
509
|
}
|
|
455
|
-
|
|
456
|
-
//#endregion
|
|
457
|
-
//#region src/reconcile/compose.ts
|
|
458
|
-
/** The credential every derived container is brought up with locally. */
|
|
459
|
-
const LOCAL_USER$1 = "geekmidas";
|
|
460
510
|
/**
|
|
461
|
-
* The
|
|
511
|
+
* The container a kind needs, or `undefined` if it needs none.
|
|
462
512
|
*
|
|
463
|
-
*
|
|
464
|
-
*
|
|
465
|
-
*
|
|
513
|
+
* Queues and topics have no container of their own — they live in whichever
|
|
514
|
+
* broker the project selected, which is why the backend is an argument here
|
|
515
|
+
* rather than a second pass that adds containers after the fact.
|
|
466
516
|
*/
|
|
467
|
-
|
|
517
|
+
function containerFor(kind, events = DEFAULT_EVENTS, cache = require_discover.DEFAULT_CACHE.aws, derived = false, edge = true) {
|
|
518
|
+
if (EVENT_KINDS[kind]) return EVENT_CONTAINERS[events] ?? CONTAINERS.database;
|
|
519
|
+
if (kind === "file-server") return edge ? "caddy" : CONTAINERS.objects;
|
|
520
|
+
if (kind === "cache") {
|
|
521
|
+
if (derived) return CONTAINERS.database;
|
|
522
|
+
return CACHE_CONTAINERS[cache] ?? CONTAINERS.database;
|
|
523
|
+
}
|
|
524
|
+
return CONTAINERS[kind];
|
|
525
|
+
}
|
|
468
526
|
/**
|
|
469
|
-
* Build the
|
|
527
|
+
* Build the plan for a manifest and stage.
|
|
470
528
|
*
|
|
471
|
-
*
|
|
472
|
-
*
|
|
473
|
-
*
|
|
529
|
+
* `order` is the caller's provisioning order — parents before children — which
|
|
530
|
+
* `provisionOrder` produces from the manifest. Passing it in keeps this
|
|
531
|
+
* function free of the traversal and makes the ordering assertable on its own.
|
|
474
532
|
*/
|
|
475
|
-
function
|
|
476
|
-
const
|
|
477
|
-
const
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
const
|
|
484
|
-
if (
|
|
533
|
+
function planFor(manifest, stage, order, options = {}) {
|
|
534
|
+
const containers = /* @__PURE__ */ new Set();
|
|
535
|
+
const resources = [];
|
|
536
|
+
const events = options.events ?? "pgboss";
|
|
537
|
+
const cache = options.cache ?? require_discover.DEFAULT_CACHE.aws;
|
|
538
|
+
for (const id of order) {
|
|
539
|
+
const declaration = manifest[id];
|
|
540
|
+
if (!declaration) continue;
|
|
541
|
+
const container = containerFor(declaration.kind, events, cache, "of" in declaration && typeof declaration.of === "string", options.edge !== false);
|
|
542
|
+
if (!container && !CONTAINERLESS[declaration.kind]) continue;
|
|
543
|
+
if (container) {
|
|
544
|
+
containers.add(container);
|
|
545
|
+
for (const required of REQUIRES[container] ?? []) containers.add(required);
|
|
546
|
+
}
|
|
547
|
+
resources.push({
|
|
548
|
+
id,
|
|
549
|
+
kind: declaration.kind,
|
|
550
|
+
...container ? { container } : {},
|
|
551
|
+
name: resourceName(id, declaration.kind, stage, options.localStage),
|
|
552
|
+
envKey: (0, _geekmidas_manifest.providedKeyFor)(id, declaration.kind, ROLES[declaration.kind] ?? "url"),
|
|
553
|
+
provisions: PROVISIONS[declaration.kind] === true,
|
|
554
|
+
...declaration.kind === "rest-api" ? { callers: (0, _geekmidas_manifest.dependentsOf)(manifest, id) } : {},
|
|
555
|
+
...declaration.kind === "site" ? { publicEnv: (0, _geekmidas_manifest.publicEnvFor)(declaration, manifest) } : {},
|
|
556
|
+
...declaration.kind === "file-server" && declaration.open?.length ? { open: declaration.open } : {},
|
|
557
|
+
..."of" in declaration ? { of: declaration.of } : {},
|
|
558
|
+
..."schema" in declaration && declaration.schema ? { schema: declaration.schema } : {},
|
|
559
|
+
..."version" in declaration && declaration.version ? { version: declaration.version } : {},
|
|
560
|
+
..."roles" in declaration && declaration.roles === false ? { roles: false } : {},
|
|
561
|
+
..."table" in declaration && declaration.table ? { table: declaration.table } : {}
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
if (options.edge !== false && resources.some((r) => EDGE_KINDS[r.kind] === true)) containers.add("caddy");
|
|
565
|
+
for (const extra of options.extraContainers ?? []) containers.add(extra);
|
|
566
|
+
if (events === "pgboss" && resources.some((r) => EVENT_KINDS[r.kind]) && !resources.some((r) => r.kind === "database")) throw new PgBossNeedsDatabase(resources.filter((r) => EVENT_KINDS[r.kind]).map((r) => r.id));
|
|
567
|
+
if (cache === "db") {
|
|
568
|
+
const placeless = resources.filter((r) => r.kind === "cache" && !r.of);
|
|
569
|
+
if (placeless.length > 0) {
|
|
570
|
+
const databases = resources.filter((r) => r.kind === "database");
|
|
571
|
+
const [only] = databases;
|
|
572
|
+
if (!only) throw new CacheNeedsDatabase(resources.filter((r) => r.kind === "cache").map((r) => r.id));
|
|
573
|
+
if (databases.length > 1) throw new CacheIsAmbiguous(placeless.map((r) => r.id), databases.map((r) => r.id));
|
|
574
|
+
for (const resource of placeless) resource.of = only.id;
|
|
575
|
+
}
|
|
485
576
|
}
|
|
486
577
|
return {
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
578
|
+
stage,
|
|
579
|
+
events,
|
|
580
|
+
cache,
|
|
581
|
+
containers: [...containers],
|
|
582
|
+
resources
|
|
490
583
|
};
|
|
491
584
|
}
|
|
492
|
-
/**
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
container;
|
|
506
|
-
/** The containers that have a default, so the caller can see the set. */
|
|
507
|
-
known = Object.keys(DEFAULT_IMAGES).sort();
|
|
508
|
-
constructor(container) {
|
|
509
|
-
super("No image is known for this container; pin one in config");
|
|
510
|
-
this.name = "UnknownContainer";
|
|
511
|
-
this.container = container;
|
|
585
|
+
/**
|
|
586
|
+
* Queues or topics were declared with pg-boss and no database to hold them.
|
|
587
|
+
*
|
|
588
|
+
* Not a reconcile failure but a manifest one: the fix is a line of application
|
|
589
|
+
* code either way — declare a database, or select a backend that brings its own
|
|
590
|
+
* broker.
|
|
591
|
+
*/
|
|
592
|
+
var CacheNeedsDatabase = class extends Error {
|
|
593
|
+
ids;
|
|
594
|
+
constructor(ids) {
|
|
595
|
+
super(`A cache backed by the database needs a declared database, and none was declared, and on this deploy target that is where a cache lives. Declare one. Caches affected: ${ids.join(", ")}.`);
|
|
596
|
+
this.ids = ids;
|
|
597
|
+
this.name = "CacheNeedsDatabase";
|
|
512
598
|
}
|
|
513
599
|
};
|
|
514
|
-
/** One container's definition, published on the port it was assigned. */
|
|
515
|
-
function define(container, image, published) {
|
|
516
|
-
switch (container) {
|
|
517
|
-
case "postgres": return {
|
|
518
|
-
image,
|
|
519
|
-
restart: "unless-stopped",
|
|
520
|
-
ports: published,
|
|
521
|
-
environment: {
|
|
522
|
-
POSTGRES_USER: LOCAL_USER$1,
|
|
523
|
-
POSTGRES_PASSWORD: LOCAL_USER$1,
|
|
524
|
-
POSTGRES_DB: LOCAL_USER$1
|
|
525
|
-
},
|
|
526
|
-
volumes: ["postgres-data:/var/lib/postgresql"],
|
|
527
|
-
healthcheck: {
|
|
528
|
-
test: ["CMD-SHELL", `pg_isready -U ${LOCAL_USER$1}`],
|
|
529
|
-
interval: "5s",
|
|
530
|
-
timeout: "3s",
|
|
531
|
-
retries: 10,
|
|
532
|
-
start_period: "10s"
|
|
533
|
-
}
|
|
534
|
-
};
|
|
535
|
-
case "minio": return {
|
|
536
|
-
image,
|
|
537
|
-
restart: "unless-stopped",
|
|
538
|
-
ports: published,
|
|
539
|
-
environment: {
|
|
540
|
-
MINIO_ROOT_USER: LOCAL_USER$1,
|
|
541
|
-
MINIO_ROOT_PASSWORD: LOCAL_USER$1
|
|
542
|
-
},
|
|
543
|
-
command: "server --console-address \":9001\" /data",
|
|
544
|
-
volumes: ["minio-data:/data"],
|
|
545
|
-
healthcheck: {
|
|
546
|
-
test: [
|
|
547
|
-
"CMD",
|
|
548
|
-
"mc",
|
|
549
|
-
"ready",
|
|
550
|
-
"local"
|
|
551
|
-
],
|
|
552
|
-
interval: "10s",
|
|
553
|
-
timeout: "5s",
|
|
554
|
-
retries: 5,
|
|
555
|
-
start_period: "10s"
|
|
556
|
-
}
|
|
557
|
-
};
|
|
558
|
-
case "caddy": return {
|
|
559
|
-
image,
|
|
560
|
-
restart: "unless-stopped",
|
|
561
|
-
ports: published,
|
|
562
|
-
volumes: [
|
|
563
|
-
"./Caddyfile:/etc/caddy/Caddyfile:ro",
|
|
564
|
-
"./caddy-sites:/etc/caddy/sites:ro",
|
|
565
|
-
"caddy-data:/data"
|
|
566
|
-
],
|
|
567
|
-
extra_hosts: ["host.docker.internal:host-gateway"],
|
|
568
|
-
healthcheck: {
|
|
569
|
-
test: [
|
|
570
|
-
"CMD",
|
|
571
|
-
"caddy",
|
|
572
|
-
"version"
|
|
573
|
-
],
|
|
574
|
-
interval: "10s",
|
|
575
|
-
timeout: "5s",
|
|
576
|
-
retries: 5
|
|
577
|
-
}
|
|
578
|
-
};
|
|
579
|
-
case "mailpit": return {
|
|
580
|
-
image,
|
|
581
|
-
restart: "unless-stopped",
|
|
582
|
-
ports: published,
|
|
583
|
-
environment: {
|
|
584
|
-
MP_SMTP_AUTH_ACCEPT_ANY: "1",
|
|
585
|
-
MP_SMTP_AUTH_ALLOW_INSECURE: "1"
|
|
586
|
-
},
|
|
587
|
-
healthcheck: {
|
|
588
|
-
test: [
|
|
589
|
-
"CMD",
|
|
590
|
-
"wget",
|
|
591
|
-
"-q",
|
|
592
|
-
"--spider",
|
|
593
|
-
"http://localhost:8025"
|
|
594
|
-
],
|
|
595
|
-
interval: "10s",
|
|
596
|
-
timeout: "5s",
|
|
597
|
-
retries: 5
|
|
598
|
-
}
|
|
599
|
-
};
|
|
600
|
-
case "redis": return {
|
|
601
|
-
image,
|
|
602
|
-
restart: "unless-stopped",
|
|
603
|
-
ports: published,
|
|
604
|
-
volumes: ["redis-data:/data"],
|
|
605
|
-
healthcheck: {
|
|
606
|
-
test: [
|
|
607
|
-
"CMD",
|
|
608
|
-
"redis-cli",
|
|
609
|
-
"ping"
|
|
610
|
-
],
|
|
611
|
-
interval: "5s",
|
|
612
|
-
timeout: "3s",
|
|
613
|
-
retries: 5
|
|
614
|
-
}
|
|
615
|
-
};
|
|
616
|
-
case "redis-http": return {
|
|
617
|
-
image,
|
|
618
|
-
restart: "unless-stopped",
|
|
619
|
-
ports: published,
|
|
620
|
-
environment: {
|
|
621
|
-
SRH_MODE: "env",
|
|
622
|
-
SRH_TOKEN: LOCAL_TOKEN$1,
|
|
623
|
-
SRH_CONNECTION_STRING: "redis://redis:6379"
|
|
624
|
-
},
|
|
625
|
-
depends_on: ["redis"],
|
|
626
|
-
healthcheck: {
|
|
627
|
-
test: [
|
|
628
|
-
"CMD",
|
|
629
|
-
"wget",
|
|
630
|
-
"-q",
|
|
631
|
-
"-O",
|
|
632
|
-
"/dev/null",
|
|
633
|
-
"http://127.0.0.1:80/"
|
|
634
|
-
],
|
|
635
|
-
interval: "5s",
|
|
636
|
-
timeout: "3s",
|
|
637
|
-
retries: 10
|
|
638
|
-
}
|
|
639
|
-
};
|
|
640
|
-
case "rabbitmq": return {
|
|
641
|
-
image,
|
|
642
|
-
restart: "unless-stopped",
|
|
643
|
-
ports: published,
|
|
644
|
-
environment: {
|
|
645
|
-
RABBITMQ_DEFAULT_USER: LOCAL_USER$1,
|
|
646
|
-
RABBITMQ_DEFAULT_PASS: LOCAL_USER$1
|
|
647
|
-
},
|
|
648
|
-
volumes: ["rabbitmq-data:/var/lib/rabbitmq"],
|
|
649
|
-
healthcheck: {
|
|
650
|
-
test: [
|
|
651
|
-
"CMD",
|
|
652
|
-
"rabbitmq-diagnostics",
|
|
653
|
-
"-q",
|
|
654
|
-
"ping"
|
|
655
|
-
],
|
|
656
|
-
interval: "10s",
|
|
657
|
-
timeout: "5s",
|
|
658
|
-
retries: 5
|
|
659
|
-
}
|
|
660
|
-
};
|
|
661
|
-
case "localstack": return {
|
|
662
|
-
image,
|
|
663
|
-
restart: "unless-stopped",
|
|
664
|
-
ports: published,
|
|
665
|
-
environment: {
|
|
666
|
-
AWS_DEFAULT_REGION: "us-east-1",
|
|
667
|
-
AWS_ACCESS_KEY_ID: "LSIAQAAAAAAVNCBMPNSG",
|
|
668
|
-
AWS_SECRET_ACCESS_KEY: LOCAL_USER$1
|
|
669
|
-
},
|
|
670
|
-
volumes: ["localstack-data:/var/lib/localstack"],
|
|
671
|
-
healthcheck: {
|
|
672
|
-
test: [
|
|
673
|
-
"CMD",
|
|
674
|
-
"curl",
|
|
675
|
-
"-f",
|
|
676
|
-
"http://localhost:4566/_localstack/health"
|
|
677
|
-
],
|
|
678
|
-
interval: "10s",
|
|
679
|
-
timeout: "5s",
|
|
680
|
-
retries: 5,
|
|
681
|
-
start_period: "10s"
|
|
682
|
-
}
|
|
683
|
-
};
|
|
684
|
-
default: throw new UnknownContainer(container);
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
600
|
/**
|
|
688
|
-
*
|
|
601
|
+
* A `'db'` cache — a server target's default — in an app that declares more
|
|
602
|
+
* than one database.
|
|
689
603
|
*
|
|
690
|
-
*
|
|
691
|
-
*
|
|
692
|
-
*
|
|
604
|
+
* The target says *a* database and the graph offers several, so there is no
|
|
605
|
+
* answer to give — and the wrong kind of failure would be to pick one, since a
|
|
606
|
+
* cache silently landing in the wrong database is a bug that surfaces as
|
|
607
|
+
* missing rows much later. The fix is a stronger statement in application code:
|
|
608
|
+
* `orders.cache('Sessions')` names the database, which is a fact about the
|
|
609
|
+
* application rather than a deployment choice.
|
|
693
610
|
*/
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
var UnassignedPort = class extends Error {
|
|
703
|
-
/** The port key that went unassigned, e.g. `minio-console`. */
|
|
704
|
-
key;
|
|
705
|
-
constructor(key) {
|
|
706
|
-
super("No port was assigned for this container port");
|
|
707
|
-
this.name = "UnassignedPort";
|
|
708
|
-
this.key = key;
|
|
611
|
+
var CacheIsAmbiguous = class extends Error {
|
|
612
|
+
ids;
|
|
613
|
+
databases;
|
|
614
|
+
constructor(ids, databases) {
|
|
615
|
+
super(`A cache backed by the database needs to know which one, and this app declares ${databases.length}: ${databases.join(", ")}. Declare the cache from its database — e.g. ${databases[0]}.cache('Sessions') — rather than as a bare new Cache(). Caches affected: ${ids.join(", ")}.`);
|
|
616
|
+
this.ids = ids;
|
|
617
|
+
this.databases = databases;
|
|
618
|
+
this.name = "CacheIsAmbiguous";
|
|
709
619
|
}
|
|
710
620
|
};
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
const dockerCli = {
|
|
718
|
-
async publishedPort(composePath, service, inside) {
|
|
719
|
-
try {
|
|
720
|
-
const { stdout } = await run("docker", [
|
|
721
|
-
"compose",
|
|
722
|
-
"-f",
|
|
723
|
-
composePath,
|
|
724
|
-
"port",
|
|
725
|
-
service,
|
|
726
|
-
String(inside)
|
|
727
|
-
]);
|
|
728
|
-
const port = Number(stdout.trim().split(":").pop());
|
|
729
|
-
return Number.isFinite(port) && port > 0 ? port : void 0;
|
|
730
|
-
} catch {
|
|
731
|
-
return void 0;
|
|
732
|
-
}
|
|
733
|
-
},
|
|
734
|
-
async up(composePath, services) {
|
|
735
|
-
await run("docker", [
|
|
736
|
-
"compose",
|
|
737
|
-
"-f",
|
|
738
|
-
composePath,
|
|
739
|
-
"up",
|
|
740
|
-
"-d",
|
|
741
|
-
"--remove-orphans",
|
|
742
|
-
"--wait",
|
|
743
|
-
`--wait-timeout=${Math.floor(HEALTH_TIMEOUT_MS / 1e3)}`,
|
|
744
|
-
...services
|
|
745
|
-
]);
|
|
746
|
-
},
|
|
747
|
-
async copyOut(composePath, service, from, to) {
|
|
748
|
-
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(to), { recursive: true });
|
|
749
|
-
await run("docker", [
|
|
750
|
-
"compose",
|
|
751
|
-
"-f",
|
|
752
|
-
composePath,
|
|
753
|
-
"cp",
|
|
754
|
-
`${service}:${from}`,
|
|
755
|
-
to
|
|
756
|
-
]);
|
|
757
|
-
},
|
|
758
|
-
async reload(composePath, service) {
|
|
759
|
-
await run("docker", [
|
|
760
|
-
"compose",
|
|
761
|
-
"-f",
|
|
762
|
-
composePath,
|
|
763
|
-
"exec",
|
|
764
|
-
"-T",
|
|
765
|
-
service,
|
|
766
|
-
"caddy",
|
|
767
|
-
"reload",
|
|
768
|
-
"--config",
|
|
769
|
-
"/etc/caddy/Caddyfile"
|
|
770
|
-
]);
|
|
771
|
-
},
|
|
772
|
-
async healthy(composePath, services) {
|
|
773
|
-
if (services.length === 0) return true;
|
|
774
|
-
try {
|
|
775
|
-
const { stdout } = await run("docker", [
|
|
776
|
-
"compose",
|
|
777
|
-
"-f",
|
|
778
|
-
composePath,
|
|
779
|
-
"ps",
|
|
780
|
-
"--format",
|
|
781
|
-
"json",
|
|
782
|
-
...services
|
|
783
|
-
]);
|
|
784
|
-
const running = /* @__PURE__ */ new Set();
|
|
785
|
-
for (const line of stdout.trim().split("\n").filter(Boolean)) {
|
|
786
|
-
const entry = JSON.parse(line);
|
|
787
|
-
const ok = entry.State === "running" && (entry.Health === void 0 || entry.Health === "" || entry.Health === "healthy");
|
|
788
|
-
if (ok && entry.Service) running.add(entry.Service);
|
|
789
|
-
}
|
|
790
|
-
return services.every((service) => running.has(service));
|
|
791
|
-
} catch {
|
|
792
|
-
return false;
|
|
793
|
-
}
|
|
621
|
+
var PgBossNeedsDatabase = class extends Error {
|
|
622
|
+
ids;
|
|
623
|
+
constructor(ids) {
|
|
624
|
+
super(`pg-boss keeps its queues in a declared database, and none was declared. Declare one: on this deploy target, events are carried by pg-boss. Queues and topics affected: ${ids.join(", ")}.`);
|
|
625
|
+
this.ids = ids;
|
|
626
|
+
this.name = "PgBossNeedsDatabase";
|
|
794
627
|
}
|
|
795
628
|
};
|
|
796
|
-
|
|
797
629
|
//#endregion
|
|
798
|
-
//#region src/reconcile/
|
|
799
|
-
/** The stage whose resources carry no suffix. */
|
|
800
|
-
const DEFAULT_STAGE = "development";
|
|
630
|
+
//#region src/reconcile/env.ts
|
|
801
631
|
/**
|
|
802
|
-
*
|
|
632
|
+
* The URLs a reconciled stage injects.
|
|
803
633
|
*
|
|
804
|
-
*
|
|
805
|
-
*
|
|
806
|
-
*
|
|
634
|
+
* One `<NAME>_URL` per construct, and the protocol picks the driver. This is the
|
|
635
|
+
* seam between the target that writes a URL and the client that parses it, so
|
|
636
|
+
* every component of it is read off the resource — never inherited from ambient
|
|
637
|
+
* environment. A bucket can live in a different region than the function reading
|
|
638
|
+
* it, and `AWS_REGION` in a Lambda is the *function's* region, so a URL that
|
|
639
|
+
* omits `?region=` breaks cross-region silently at runtime.
|
|
640
|
+
*
|
|
641
|
+
* Locally that self-containment is what lets the port be whatever was free: the
|
|
642
|
+
* app reads `ORDERS_URL` and never sees a host or a port.
|
|
807
643
|
*/
|
|
808
|
-
const CONTAINERS = {
|
|
809
|
-
database: "postgres",
|
|
810
|
-
"database-reader": "postgres",
|
|
811
|
-
"database-schema": "postgres",
|
|
812
|
-
objects: "minio",
|
|
813
|
-
"file-server": "caddy",
|
|
814
|
-
email: "mailpit"
|
|
815
|
-
};
|
|
816
644
|
/**
|
|
817
|
-
* The
|
|
645
|
+
* The credential local containers are brought up with.
|
|
818
646
|
*
|
|
819
|
-
*
|
|
820
|
-
*
|
|
821
|
-
*
|
|
647
|
+
* The design's `roles: false` case — both URLs fall back to the cluster's master
|
|
648
|
+
* credential. A deliberate downgrade, not a default: it holds until the role DDL
|
|
649
|
+
* lands in `@geekmidas/db`, at which point the runtime role's URL replaces this
|
|
650
|
+
* and the owner URL stops being reachable from any manifest edge.
|
|
651
|
+
*/
|
|
652
|
+
const LOCAL_USER$2 = "geekmidas";
|
|
653
|
+
/** The host containers are published on. */
|
|
654
|
+
const LOCAL_HOST = "localhost";
|
|
655
|
+
/** The region local buckets claim, so the client has one to sign with. */
|
|
656
|
+
const LOCAL_REGION = "us-east-1";
|
|
657
|
+
/**
|
|
658
|
+
* The credentials the S3 client resolves for MinIO.
|
|
822
659
|
*
|
|
823
|
-
* `
|
|
824
|
-
*
|
|
825
|
-
*
|
|
826
|
-
*
|
|
660
|
+
* An `s3://` URL deliberately carries none: deployed, the SDK reads them from
|
|
661
|
+
* the execution role, so a URL that embedded a key would be one more thing to
|
|
662
|
+
* rotate and one more thing to leak into a log. Locally there is no role, and
|
|
663
|
+
* the same chain reads these — which is why they are injected beside the URL
|
|
664
|
+
* rather than written into it.
|
|
827
665
|
*/
|
|
828
|
-
const
|
|
829
|
-
|
|
830
|
-
|
|
666
|
+
const STORAGE_CREDENTIALS = {
|
|
667
|
+
AWS_ACCESS_KEY_ID: LOCAL_USER$2,
|
|
668
|
+
AWS_SECRET_ACCESS_KEY: LOCAL_USER$2,
|
|
669
|
+
AWS_REGION: LOCAL_REGION
|
|
831
670
|
};
|
|
671
|
+
/** The token the local cache proxy accepts. Matches the compose definition. */
|
|
672
|
+
const LOCAL_TOKEN$1 = "geekmidas";
|
|
673
|
+
/** The port Mailpit's inbox — its web UI and HTTP API — is published under. */
|
|
674
|
+
const MAILPIT_INBOX_PORT = "mailpit-web";
|
|
675
|
+
/** The schema pg-boss keeps its tables in, inside the declared database. */
|
|
676
|
+
const PGBOSS_SCHEMA = "pgboss";
|
|
677
|
+
/** The exchange every local topic and queue shares on RabbitMQ. */
|
|
678
|
+
const RABBITMQ_EXCHANGE = "geekmidas.events";
|
|
832
679
|
/**
|
|
833
|
-
*
|
|
680
|
+
* Every env key a reconciled plan resolves, and its value.
|
|
834
681
|
*
|
|
835
|
-
*
|
|
836
|
-
*
|
|
837
|
-
* still has to resolve it.
|
|
682
|
+
* Keys come from the declaration rather than being re-derived, so what the build
|
|
683
|
+
* publishes and what the app reads cannot drift.
|
|
838
684
|
*/
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
685
|
+
function envFor(plan, options) {
|
|
686
|
+
const env = {};
|
|
687
|
+
const resolved = {};
|
|
688
|
+
for (const resource of plan.resources) {
|
|
689
|
+
const url = urlFor(resource, plan, options.ports, options.project ?? "", options.addresses);
|
|
690
|
+
if (url) resolved[resource.id] = url;
|
|
691
|
+
}
|
|
692
|
+
for (const resource of plan.resources) {
|
|
693
|
+
const url = resolved[resource.id];
|
|
694
|
+
if (resource.kind === "rest-api") Object.assign(env, surfaceEnv(resource, url, resolved));
|
|
695
|
+
if (url) env[resource.envKey] = url;
|
|
696
|
+
if (isDatabase(resource.kind) && resource.roles !== false) {
|
|
697
|
+
const owner = ownerUrl(resource, plan, options.ports, options.project ?? "");
|
|
698
|
+
if (owner) env[(0, _geekmidas_manifest.provideKey)(resource.id, "ownerUrl")] = owner;
|
|
699
|
+
}
|
|
700
|
+
if (resource.kind === "email" && url) {
|
|
701
|
+
env[(0, _geekmidas_manifest.provideKey)(resource.id, "from")] = options.mailFrom ?? `noreply@${LOCAL_HOST}`;
|
|
702
|
+
const inbox = options.ports[MAILPIT_INBOX_PORT];
|
|
703
|
+
if (inbox !== void 0) env[(0, _geekmidas_manifest.provideKey)(resource.id, "inboxUrl")] = `http://${LOCAL_HOST}:${inbox}`;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
Object.assign(env, publicEnv(plan, env));
|
|
707
|
+
Object.assign(env, brokerEnv(plan, options.ports));
|
|
708
|
+
if (plan.resources.some((r) => r.kind === "objects" && env[r.envKey])) Object.assign(env, STORAGE_CREDENTIALS);
|
|
709
|
+
return env;
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* The values a site's bundler inlines, under the names it inlines them by.
|
|
713
|
+
*
|
|
714
|
+
* A rename and nothing more. `API_URL` was resolved once, by the same code that
|
|
715
|
+
* resolved it for the server; a site reads the same value under `VITE_API_URL`
|
|
716
|
+
* because that prefix is how its bundler is told to ship it. Nothing is derived
|
|
717
|
+
* twice, so a site and its API cannot come to disagree about where the API is.
|
|
718
|
+
*
|
|
719
|
+
* A source that resolved to nothing is skipped rather than written empty: an
|
|
720
|
+
* inlined empty string is a frontend that builds and then fails at runtime
|
|
721
|
+
* against `http:///`, where a missing variable fails at build with the name of
|
|
722
|
+
* the thing that is missing.
|
|
723
|
+
*/
|
|
724
|
+
function publicEnv(plan, resolved) {
|
|
725
|
+
const env = {};
|
|
726
|
+
for (const resource of plan.resources) for (const [key, source] of Object.entries(resource.publicEnv ?? {})) {
|
|
727
|
+
const value = resolved[source];
|
|
728
|
+
if (value) env[key] = value;
|
|
729
|
+
}
|
|
730
|
+
return env;
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* What a surface publishes beyond its own address: who may call it, and where
|
|
734
|
+
* a cookie set by it is readable.
|
|
735
|
+
*
|
|
736
|
+
* Both are read off the same list — the constructs that declared an edge to
|
|
737
|
+
* this surface — which is why neither appears in application code. A surface
|
|
738
|
+
* that listed its own callers would be edited every time something new called
|
|
739
|
+
* it, and the thing being edited is already recorded in the graph.
|
|
740
|
+
*
|
|
741
|
+
* An empty origin list is written, and a surface with no address writes nothing
|
|
742
|
+
* at all. The two cases look alike and are not: "nothing depends on this yet" is
|
|
743
|
+
* a real state a target should publish, while "this surface has no address here"
|
|
744
|
+
* means it is not running in this stage, and keys belong with the address they
|
|
745
|
+
* describe.
|
|
746
|
+
*/
|
|
747
|
+
function surfaceEnv(resource, url, resolved = {}) {
|
|
748
|
+
if (!url) return {};
|
|
749
|
+
const origins = [...new Set((resource.callers ?? []).map((caller) => resolved[caller]).filter((address) => Boolean(address)).map(originOf).filter((origin) => Boolean(origin)))].sort();
|
|
750
|
+
const domain = (0, _geekmidas_manifest.cookieDomain)([url, ...origins]);
|
|
751
|
+
return {
|
|
752
|
+
[(0, _geekmidas_manifest.provideKey)(resource.id, "trustedOrigins")]: origins.join(","),
|
|
753
|
+
...domain ? { [(0, _geekmidas_manifest.provideKey)(resource.id, "cookieDomain")]: domain } : {}
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
/** An address reduced to the origin a browser compares against. */
|
|
757
|
+
function originOf(address) {
|
|
758
|
+
try {
|
|
759
|
+
return new URL(address).origin;
|
|
760
|
+
} catch {
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
845
764
|
/**
|
|
846
|
-
*
|
|
765
|
+
* The runtime role of whatever a derived node hangs off.
|
|
847
766
|
*
|
|
848
|
-
*
|
|
849
|
-
*
|
|
850
|
-
*
|
|
767
|
+
* A reader's parent may itself be a schema tenant, so this follows `of` by one
|
|
768
|
+
* hop rather than assuming the cluster — the reader of a tenant reads that
|
|
769
|
+
* tenant's schema, not the database's.
|
|
851
770
|
*/
|
|
852
|
-
|
|
771
|
+
function parentRole(resource, plan) {
|
|
772
|
+
const parent = plan.resources.find((r) => r.id === resource.of);
|
|
773
|
+
return parent ? localRole(parent) : resource.name;
|
|
774
|
+
}
|
|
775
|
+
/** Whether a kind connects to Postgres and therefore has an owner role. */
|
|
776
|
+
function isDatabase(kind) {
|
|
777
|
+
return kind === "database" || kind === "database-schema";
|
|
778
|
+
}
|
|
853
779
|
/**
|
|
854
|
-
* The
|
|
780
|
+
* The URL a migrator connects with — the owner role, which can create, alter
|
|
781
|
+
* and drop.
|
|
855
782
|
*
|
|
856
|
-
*
|
|
857
|
-
*
|
|
858
|
-
*
|
|
783
|
+
* Never in `provides`, which is the whole point. A handler that could reach this
|
|
784
|
+
* could `DROP TABLE`, and the security property the role split exists for is
|
|
785
|
+
* that it cannot: the value is injected by `gkm exec` for the migrate step and
|
|
786
|
+
* by nothing else.
|
|
859
787
|
*/
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
}
|
|
788
|
+
function ownerUrl(resource, plan, ports, project) {
|
|
789
|
+
if (!resource.container) return void 0;
|
|
790
|
+
const port = ports[primaryPortKey(resource.container)];
|
|
791
|
+
if (port === void 0) return void 0;
|
|
792
|
+
const owner = (0, _geekmidas_db_pg_roles.ownerRole)(localRole(resource));
|
|
793
|
+
return postgres(port, rootDatabase(resource, plan), {
|
|
794
|
+
user: owner,
|
|
795
|
+
password: localRolePassword(project, plan, owner)
|
|
796
|
+
});
|
|
797
|
+
}
|
|
870
798
|
/**
|
|
871
|
-
*
|
|
799
|
+
* The shared broker keys, when anything is published or consumed.
|
|
872
800
|
*
|
|
873
|
-
* A
|
|
874
|
-
*
|
|
875
|
-
*
|
|
876
|
-
*
|
|
801
|
+
* A queue's own key is the producer's. These two are the *connection* itself,
|
|
802
|
+
* which locally is one broker for every queue and topic in the project: the
|
|
803
|
+
* generated pollers open a single connection and subscribe each worker by name
|
|
804
|
+
* on it. Deployed there is no such thing — a Lambda is handed its own event
|
|
805
|
+
* source — so this pair exists for the local target and says so.
|
|
877
806
|
*/
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
"
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
807
|
+
function brokerEnv(plan, ports) {
|
|
808
|
+
const carrier = plan.resources.find((r) => r.kind === "queue" || r.kind === "topic");
|
|
809
|
+
if (!carrier) return {};
|
|
810
|
+
const publisher = urlFor(carrier, plan, ports, "");
|
|
811
|
+
if (!publisher) return {};
|
|
812
|
+
return {
|
|
813
|
+
EVENT_PUBLISHER_CONNECTION_STRING: publisher,
|
|
814
|
+
EVENT_SUBSCRIBER_CONNECTION_STRING: plan.events === "sns" ? publisher.replace(/^sns:/, "sqs:") : publisher
|
|
815
|
+
};
|
|
816
|
+
}
|
|
886
817
|
/**
|
|
887
|
-
* The
|
|
818
|
+
* The URL for one planned resource.
|
|
888
819
|
*
|
|
889
|
-
*
|
|
890
|
-
|
|
891
|
-
|
|
820
|
+
* Every part of it comes from the resource and the port it was published on.
|
|
821
|
+
*/
|
|
822
|
+
function urlFor(resource, plan, ports, project, addresses = {}) {
|
|
823
|
+
if (resource.kind === "secret") return localSecret(project, plan, resource);
|
|
824
|
+
if (resource.kind === "credential") return void 0;
|
|
825
|
+
if (resource.kind === "rest-api" || resource.kind === "site") {
|
|
826
|
+
const address = addresses[resource.id];
|
|
827
|
+
const edge = plan.containers.includes("caddy") ? ports[primaryPortKey("caddy")] : void 0;
|
|
828
|
+
return address && edge !== void 0 ? `https://${hostFor(resource, project)}:${edge}` : address;
|
|
829
|
+
}
|
|
830
|
+
if (!resource.container) return void 0;
|
|
831
|
+
const port = ports[primaryPortKey(resource.container)];
|
|
832
|
+
if (port === void 0) return void 0;
|
|
833
|
+
switch (resource.kind) {
|
|
834
|
+
case "database":
|
|
835
|
+
case "database-schema":
|
|
836
|
+
case "database-reader": {
|
|
837
|
+
const database = rootDatabase(resource, plan);
|
|
838
|
+
if (resource.roles === false) {
|
|
839
|
+
const schema = schemaOf(resource, plan);
|
|
840
|
+
return schema ? `${postgres(port, database)}?search_path=${schema}` : postgres(port, database);
|
|
841
|
+
}
|
|
842
|
+
const role = resource.kind === "database-reader" ? (0, _geekmidas_db_pg_roles.readerRole)(parentRole(resource, plan)) : localRole(resource);
|
|
843
|
+
return postgres(port, database, {
|
|
844
|
+
user: role,
|
|
845
|
+
password: localRolePassword(project, plan, role)
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
case "email": return `smtp://${LOCAL_HOST}:${port}`;
|
|
849
|
+
case "objects": return `s3://${resource.name}?region=${LOCAL_REGION}&endpoint=http://${LOCAL_HOST}:${port}&forcePathStyle=true`;
|
|
850
|
+
case "file-server": {
|
|
851
|
+
const origin = plan.resources.find((r) => r.id === resource.of);
|
|
852
|
+
if (!origin) return void 0;
|
|
853
|
+
return plan.containers.includes("caddy") ? `https://${hostFor(resource, project)}:${port}` : `http://${LOCAL_HOST}:${port}/${origin.name}`;
|
|
854
|
+
}
|
|
855
|
+
case "cache":
|
|
856
|
+
if (resource.of) {
|
|
857
|
+
const parent = plan.resources.find((r) => r.id === resource.of);
|
|
858
|
+
const url = parent ? urlFor(parent, plan, ports, project, addresses) : void 0;
|
|
859
|
+
return url ? withTable(url, resource.table ?? (0, _geekmidas_manifest.cacheTable)(resource.id)) : void 0;
|
|
860
|
+
}
|
|
861
|
+
switch (plan.cache) {
|
|
862
|
+
case "elasticache": return `redis://${LOCAL_HOST}:${port}`;
|
|
863
|
+
default: return `http://:${LOCAL_TOKEN$1}@${LOCAL_HOST}:${port}`;
|
|
864
|
+
}
|
|
865
|
+
case "queue":
|
|
866
|
+
case "topic": return broker(plan, port);
|
|
867
|
+
default: return;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* The producer's connection string, composed from the backend the plan chose.
|
|
892
872
|
*
|
|
893
|
-
*
|
|
894
|
-
*
|
|
895
|
-
*
|
|
873
|
+
* The protocol is what picks the transport, so a producer never branches: the
|
|
874
|
+
* same `.publish()` reaches pg-boss here and SQS deployed because the string it
|
|
875
|
+
* was handed said so.
|
|
896
876
|
*/
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
877
|
+
function broker(plan, port) {
|
|
878
|
+
switch (plan.events) {
|
|
879
|
+
case "pgboss": {
|
|
880
|
+
const database = plan.resources.find((r) => r.kind === "database");
|
|
881
|
+
if (!database) throw new PgBossNeedsDatabase([]);
|
|
882
|
+
return `pgboss://${LOCAL_USER$2}:${LOCAL_USER$2}@${LOCAL_HOST}:${port}/${database.name}?schema=${PGBOSS_SCHEMA}`;
|
|
883
|
+
}
|
|
884
|
+
case "rabbitmq": return `rabbitmq://${LOCAL_USER$2}:${LOCAL_USER$2}@${LOCAL_HOST}:${port}?exchange=${RABBITMQ_EXCHANGE}`;
|
|
885
|
+
case "sns": throw new UnprovisionedEventsBackend("sns");
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
/** A local Postgres URL, master credential and all. */
|
|
889
|
+
function postgres(port, database, credential = {
|
|
890
|
+
user: LOCAL_USER$2,
|
|
891
|
+
password: LOCAL_USER$2
|
|
892
|
+
}) {
|
|
893
|
+
return `postgres://${credential.user}:${credential.password}@${LOCAL_HOST}:${port}/${database}`;
|
|
894
|
+
}
|
|
903
895
|
/**
|
|
904
|
-
*
|
|
896
|
+
* The role a handler connects as, for one database or tenant.
|
|
905
897
|
*
|
|
906
|
-
*
|
|
907
|
-
*
|
|
908
|
-
*
|
|
898
|
+
* The construct's id, lowercased and stage-scoped exactly as its database or
|
|
899
|
+
* schema is — so `Orders` in the `test` stage connects as `orders_test`, and two
|
|
900
|
+
* stages sharing one cluster cannot share a credential. Roles are cluster-scoped
|
|
901
|
+
* in Postgres, which is why the suffix is not optional here the way it is for a
|
|
902
|
+
* schema.
|
|
909
903
|
*/
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
"database-schema": "_",
|
|
914
|
-
objects: "-"
|
|
915
|
-
};
|
|
904
|
+
function localRole(resource) {
|
|
905
|
+
return resource.name;
|
|
906
|
+
}
|
|
916
907
|
/**
|
|
917
|
-
* The
|
|
908
|
+
* The password for a derived local role.
|
|
918
909
|
*
|
|
919
|
-
*
|
|
920
|
-
*
|
|
921
|
-
*
|
|
910
|
+
* Derived rather than random, for the same reason the local secret is: a
|
|
911
|
+
* password that changed on every `gkm dev` would lock a developer out of the
|
|
912
|
+
* data they had a moment ago. Seeded by the project so two checkouts do not
|
|
913
|
+
* share one, and by the stage so `test` and `development` do not.
|
|
914
|
+
*
|
|
915
|
+
* It is a *local* credential by construction — the cluster is on loopback and
|
|
916
|
+
* the seed is a checkout path — and no deployed target uses this function.
|
|
922
917
|
*/
|
|
923
|
-
function
|
|
924
|
-
|
|
925
|
-
if (stage === DEFAULT_STAGE) return base;
|
|
926
|
-
return `${base}${SEPARATORS[kind] ?? "-"}${stage}`;
|
|
918
|
+
function localRolePassword(project, plan, role) {
|
|
919
|
+
return (0, node_crypto.createHash)("sha256").update(`${project}:${plan.stage}:role:${role}`).digest("base64url").slice(0, 32);
|
|
927
920
|
}
|
|
928
921
|
/**
|
|
929
|
-
*
|
|
922
|
+
* A cache's address: its database's URL, carrying the table it reads.
|
|
930
923
|
*
|
|
931
|
-
*
|
|
932
|
-
*
|
|
933
|
-
*
|
|
924
|
+
* Appended rather than kept beside the URL as a second key, for the reason the
|
|
925
|
+
* design gives everywhere else — an address and what it opens are one fact, and
|
|
926
|
+
* two keys are one more thing to keep in step.
|
|
934
927
|
*/
|
|
935
|
-
function
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
if (derived) return CONTAINERS.database;
|
|
940
|
-
return CACHE_CONTAINERS[cache] ?? CONTAINERS.database;
|
|
941
|
-
}
|
|
942
|
-
return CONTAINERS[kind];
|
|
928
|
+
function withTable(url, table) {
|
|
929
|
+
const parsed = new URL(url);
|
|
930
|
+
parsed.searchParams.set("table", table);
|
|
931
|
+
return parsed.toString();
|
|
943
932
|
}
|
|
944
933
|
/**
|
|
945
|
-
*
|
|
934
|
+
* The physical database a derived resource lives in.
|
|
946
935
|
*
|
|
947
|
-
* `
|
|
948
|
-
*
|
|
949
|
-
*
|
|
936
|
+
* Walks `of` to the top rather than reading the immediate parent, because a
|
|
937
|
+
* reader's parent may itself be a schema tenant — two hops from the database
|
|
938
|
+
* that actually exists.
|
|
950
939
|
*/
|
|
951
|
-
function
|
|
952
|
-
const
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
...container ? { container } : {},
|
|
969
|
-
name: resourceName(id, declaration.kind, stage),
|
|
970
|
-
envKey: (0, __geekmidas_manifest.providedKeyFor)(id, declaration.kind, ROLES[declaration.kind] ?? "url"),
|
|
971
|
-
provisions: PROVISIONS[declaration.kind] === true,
|
|
972
|
-
...declaration.kind === "rest-api" ? { callers: (0, __geekmidas_manifest.dependentsOf)(manifest, id) } : {},
|
|
973
|
-
...declaration.kind === "site" ? { publicEnv: (0, __geekmidas_manifest.publicEnvFor)(declaration, manifest) } : {},
|
|
974
|
-
...declaration.kind === "file-server" && declaration.open?.length ? { open: declaration.open } : {},
|
|
975
|
-
..."of" in declaration ? { of: declaration.of } : {},
|
|
976
|
-
..."schema" in declaration && declaration.schema ? { schema: declaration.schema } : {},
|
|
977
|
-
..."version" in declaration && declaration.version ? { version: declaration.version } : {},
|
|
978
|
-
..."roles" in declaration && declaration.roles === false ? { roles: false } : {},
|
|
979
|
-
..."table" in declaration && declaration.table ? { table: declaration.table } : {}
|
|
980
|
-
});
|
|
981
|
-
}
|
|
982
|
-
if (options.edge !== false && resources.some((r) => EDGE_KINDS[r.kind] === true)) containers.add("caddy");
|
|
983
|
-
for (const extra of options.extraContainers ?? []) containers.add(extra);
|
|
984
|
-
if (events === "pgboss" && resources.some((r) => EVENT_KINDS[r.kind]) && !resources.some((r) => r.kind === "database")) throw new PgBossNeedsDatabase(resources.filter((r) => EVENT_KINDS[r.kind]).map((r) => r.id));
|
|
985
|
-
if (cache === "db") {
|
|
986
|
-
const placeless = resources.filter((r) => r.kind === "cache" && !r.of);
|
|
987
|
-
if (placeless.length > 0) {
|
|
988
|
-
const databases = resources.filter((r) => r.kind === "database");
|
|
989
|
-
const [only] = databases;
|
|
990
|
-
if (!only) throw new CacheNeedsDatabase(resources.filter((r) => r.kind === "cache").map((r) => r.id));
|
|
991
|
-
if (databases.length > 1) throw new CacheIsAmbiguous(placeless.map((r) => r.id), databases.map((r) => r.id));
|
|
992
|
-
for (const resource of placeless) resource.of = only.id;
|
|
993
|
-
}
|
|
940
|
+
function rootDatabase(resource, plan) {
|
|
941
|
+
const byId = new Map(plan.resources.map((r) => [r.id, r]));
|
|
942
|
+
let current = resource;
|
|
943
|
+
while (current.of) {
|
|
944
|
+
const parent = byId.get(current.of);
|
|
945
|
+
if (!parent) break;
|
|
946
|
+
current = parent;
|
|
947
|
+
}
|
|
948
|
+
return current.name;
|
|
949
|
+
}
|
|
950
|
+
/** The schema a derived resource puts on its search path, if it has one. */
|
|
951
|
+
function schemaOf(resource, plan) {
|
|
952
|
+
const byId = new Map(plan.resources.map((r) => [r.id, r]));
|
|
953
|
+
let current = resource;
|
|
954
|
+
while (current) {
|
|
955
|
+
if (current.kind === "database-schema") return current.schema;
|
|
956
|
+
current = current.of ? byId.get(current.of) : void 0;
|
|
994
957
|
}
|
|
995
|
-
return {
|
|
996
|
-
stage,
|
|
997
|
-
events,
|
|
998
|
-
cache,
|
|
999
|
-
containers: [...containers],
|
|
1000
|
-
resources
|
|
1001
|
-
};
|
|
1002
958
|
}
|
|
1003
959
|
/**
|
|
1004
|
-
*
|
|
960
|
+
* A backend whose local addresses cannot be derived yet.
|
|
1005
961
|
*
|
|
1006
|
-
*
|
|
1007
|
-
*
|
|
1008
|
-
*
|
|
962
|
+
* SNS and SQS are addressed by ARN, which means the topic and queue have to be
|
|
963
|
+
* created in LocalStack before a URL can name them. Until that provisioning
|
|
964
|
+
* lands, saying so is better than handing out a string that fails on first use.
|
|
1009
965
|
*/
|
|
1010
|
-
var
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
this.
|
|
966
|
+
var UnprovisionedEventsBackend = class extends Error {
|
|
967
|
+
backend;
|
|
968
|
+
constructor(backend) {
|
|
969
|
+
super(`The local target cannot derive addresses for '${backend}' yet — its topics and queues are named by ARN, which nothing creates locally. Use 'pgboss' (the default) or 'rabbitmq' for local development.`);
|
|
970
|
+
this.backend = backend;
|
|
971
|
+
this.name = "UnprovisionedEventsBackend";
|
|
1015
972
|
}
|
|
1016
973
|
};
|
|
1017
974
|
/**
|
|
1018
|
-
*
|
|
975
|
+
* The value a secret resolves to locally.
|
|
1019
976
|
*
|
|
1020
|
-
*
|
|
1021
|
-
*
|
|
1022
|
-
*
|
|
1023
|
-
*
|
|
1024
|
-
* `orders.cache('Sessions')` names the database, which is a fact about the
|
|
1025
|
-
* application rather than a deployment choice.
|
|
977
|
+
* A hash of what identifies it, so it is stable, distinct per project, stage
|
|
978
|
+
* and construct, and never written to disk as a literal. Deployed this is the
|
|
979
|
+
* one kind whose value does *not* come from here — a secret manager generates
|
|
980
|
+
* it once and the target reads it back.
|
|
1026
981
|
*/
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
this.ids = ids;
|
|
1031
|
-
this.databases = databases;
|
|
1032
|
-
this.name = "CacheIsAmbiguous";
|
|
1033
|
-
}
|
|
1034
|
-
};
|
|
1035
|
-
var PgBossNeedsDatabase = class extends Error {
|
|
1036
|
-
constructor(ids) {
|
|
1037
|
-
super(`pg-boss keeps its queues in a declared database, and none was declared. Declare one, or set services.events to 'rabbitmq' or 'sns'. Queues and topics affected: ${ids.join(", ")}.`);
|
|
1038
|
-
this.ids = ids;
|
|
1039
|
-
this.name = "PgBossNeedsDatabase";
|
|
1040
|
-
}
|
|
1041
|
-
};
|
|
1042
|
-
|
|
982
|
+
function localSecret(project, plan, resource) {
|
|
983
|
+
return (0, node_crypto.createHash)("sha256").update(`${project}:${plan.stage}:${resource.id}`).digest("base64url").slice(0, 43);
|
|
984
|
+
}
|
|
1043
985
|
//#endregion
|
|
1044
|
-
//#region src/reconcile/
|
|
986
|
+
//#region src/reconcile/apps.ts
|
|
1045
987
|
/**
|
|
1046
|
-
* The
|
|
988
|
+
* The apps, as services beside the containers their constructs derive.
|
|
1047
989
|
*
|
|
1048
|
-
*
|
|
1049
|
-
*
|
|
1050
|
-
*
|
|
1051
|
-
*
|
|
990
|
+
* `gkm docker` used to write these by hand: every backend got
|
|
991
|
+
* `DATABASE_URL=postgresql://…@postgres:5432/app` and `REDIS_URL=redis://redis`,
|
|
992
|
+
* which was right only for a project whose single database happened to be
|
|
993
|
+
* called `Database` — an `Orders` database publishes `ORDERS_URL`, a schema
|
|
994
|
+
* tenant has a URL of its own, and buckets and mail were not wired at all.
|
|
995
|
+
*
|
|
996
|
+
* Now an app's environment is exactly what reconcile hands `gkm dev`: every key
|
|
997
|
+
* the declared constructs provide, from the same derivation. The only
|
|
998
|
+
* difference is where the addresses point — at the containers by service name
|
|
999
|
+
* on the compose network, rather than at `localhost` on the ports published for
|
|
1000
|
+
* the host.
|
|
1001
|
+
*
|
|
1002
|
+
* Always from the *local* stage's plan, whichever stage is being reconciled:
|
|
1003
|
+
* `gkm test` rewrites the same file, and running the tests must not quietly
|
|
1004
|
+
* point these apps at the test databases.
|
|
1052
1005
|
*/
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
const LOCAL_HOST = "localhost";
|
|
1056
|
-
/** The region local buckets claim, so the client has one to sign with. */
|
|
1057
|
-
const LOCAL_REGION = "us-east-1";
|
|
1006
|
+
/** The profile app services carry, so `gkm dev` starts only the containers. */
|
|
1007
|
+
const APPS_PROFILE = "apps";
|
|
1058
1008
|
/**
|
|
1059
|
-
*
|
|
1009
|
+
* Where an app's image is built from.
|
|
1060
1010
|
*
|
|
1061
|
-
* An
|
|
1062
|
-
*
|
|
1063
|
-
* rotate and one more thing to leak into a log. Locally there is no role, and
|
|
1064
|
-
* the same chain reads these — which is why they are injected beside the URL
|
|
1065
|
-
* rather than written into it.
|
|
1011
|
+
* An app at the project root is a single-app project, whose `gkm docker`
|
|
1012
|
+
* writes `.gkm/docker/Dockerfile`; a workspace writes one per app.
|
|
1066
1013
|
*/
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
AWS_REGION: LOCAL_REGION
|
|
1071
|
-
};
|
|
1072
|
-
/** The token the local cache proxy accepts. Matches the compose definition. */
|
|
1073
|
-
const LOCAL_TOKEN = "geekmidas";
|
|
1074
|
-
/** The schema pg-boss keeps its tables in, inside the declared database. */
|
|
1075
|
-
const PGBOSS_SCHEMA = "pgboss";
|
|
1076
|
-
/** The exchange every local topic and queue shares on RabbitMQ. */
|
|
1077
|
-
const RABBITMQ_EXCHANGE = "geekmidas.events";
|
|
1014
|
+
function dockerfileOf(appName, path) {
|
|
1015
|
+
return path === "." ? ".gkm/docker/Dockerfile" : `.gkm/docker/Dockerfile.${appName}`;
|
|
1016
|
+
}
|
|
1078
1017
|
/**
|
|
1079
|
-
* Every
|
|
1018
|
+
* Every construct key, addressed across the compose network.
|
|
1080
1019
|
*
|
|
1081
|
-
*
|
|
1082
|
-
*
|
|
1020
|
+
* `envFor` is asked with a placeholder port for every port key, so each
|
|
1021
|
+
* `localhost:<placeholder>` in its answer names exactly one container port —
|
|
1022
|
+
* two containers listening on the same port inside cannot be confused — and is
|
|
1023
|
+
* rewritten to `<container>:<inside>`.
|
|
1083
1024
|
*/
|
|
1084
|
-
function
|
|
1085
|
-
const
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1025
|
+
function inNetworkEnv(workspace, manifest) {
|
|
1026
|
+
const plan = planFor(manifest, workspace.stages.local, (0, _geekmidas_manifest.provisionOrder)(manifest), {
|
|
1027
|
+
localStage: workspace.stages.local,
|
|
1028
|
+
...backendsOf(workspace)
|
|
1029
|
+
});
|
|
1030
|
+
const placeholders = {};
|
|
1031
|
+
const targets = /* @__PURE__ */ new Map();
|
|
1032
|
+
let next = 49152;
|
|
1033
|
+
for (const container of [...plan.containers].sort()) for (const port of portsOf(container)) {
|
|
1034
|
+
placeholders[port.key] = next;
|
|
1035
|
+
targets.set(next, `${container}:${port.inside}`);
|
|
1036
|
+
next += 1;
|
|
1090
1037
|
}
|
|
1091
|
-
for (const
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1038
|
+
for (const key of portKeys(plan.containers)) placeholders[key] ??= next++;
|
|
1039
|
+
const env = envFor(plan, {
|
|
1040
|
+
ports: placeholders,
|
|
1041
|
+
project: workspace.name,
|
|
1042
|
+
addresses: surfaceAddresses(workspace, manifest, (app, port) => `http://${app}:${port}`)
|
|
1043
|
+
});
|
|
1044
|
+
const rewrite = (value) => value.replace(/(?:localhost|127\.0\.0\.1):(\d+)/g, (match, port) => {
|
|
1045
|
+
return targets.get(Number(port)) ?? match;
|
|
1046
|
+
});
|
|
1047
|
+
return Object.fromEntries(Object.entries(env).map(([key, value]) => [key, rewrite(value)]));
|
|
1048
|
+
}
|
|
1049
|
+
/** One compose service per app the workspace runs, behind the apps profile. */
|
|
1050
|
+
function appServices(workspace, manifest, containers) {
|
|
1051
|
+
const env = inNetworkEnv(workspace, manifest);
|
|
1052
|
+
const services = {};
|
|
1053
|
+
for (const [name, app] of Object.entries(workspace.apps)) {
|
|
1054
|
+
if (app.type === "mobile" || !app.port) continue;
|
|
1055
|
+
const key = require_config.appKey(name);
|
|
1056
|
+
const health = app.type === "web" ? "/" : "/health";
|
|
1057
|
+
services[key] = {
|
|
1058
|
+
image: `${key}:\${TAG:-latest}`,
|
|
1059
|
+
build: {
|
|
1060
|
+
context: ".",
|
|
1061
|
+
dockerfile: dockerfileOf(name, app.path)
|
|
1062
|
+
},
|
|
1063
|
+
profiles: [APPS_PROFILE],
|
|
1064
|
+
ports: [`${app.port}:${app.port}`],
|
|
1065
|
+
environment: {
|
|
1066
|
+
NODE_ENV: "production",
|
|
1067
|
+
PORT: String(app.port),
|
|
1068
|
+
...env
|
|
1069
|
+
},
|
|
1070
|
+
...containers.some((c) => c !== "caddy") ? { depends_on: containers.filter((c) => c !== "caddy") } : {},
|
|
1071
|
+
healthcheck: {
|
|
1072
|
+
test: [
|
|
1073
|
+
"CMD",
|
|
1074
|
+
"wget",
|
|
1075
|
+
"-q",
|
|
1076
|
+
"--spider",
|
|
1077
|
+
`http://localhost:${app.port}${health}`
|
|
1078
|
+
],
|
|
1079
|
+
interval: "10s",
|
|
1080
|
+
timeout: "5s",
|
|
1081
|
+
retries: 5
|
|
1082
|
+
}
|
|
1083
|
+
};
|
|
1100
1084
|
}
|
|
1101
|
-
|
|
1102
|
-
Object.assign(env, brokerEnv(plan, options.ports));
|
|
1103
|
-
if (plan.resources.some((r) => r.kind === "objects" && env[r.envKey])) Object.assign(env, STORAGE_CREDENTIALS);
|
|
1104
|
-
return env;
|
|
1085
|
+
return services;
|
|
1105
1086
|
}
|
|
1087
|
+
//#endregion
|
|
1088
|
+
//#region src/reconcile/clients.ts
|
|
1106
1089
|
/**
|
|
1107
|
-
* The
|
|
1090
|
+
* The real Postgres and MinIO clients the applier runs against.
|
|
1108
1091
|
*
|
|
1109
|
-
*
|
|
1110
|
-
*
|
|
1111
|
-
*
|
|
1112
|
-
* twice, so a site and its API cannot come to disagree about where the API is.
|
|
1092
|
+
* Kept apart from `provision.ts` so the rules there — what gets created, in what
|
|
1093
|
+
* order, and never dropping anything — are asserted against fakes, while the
|
|
1094
|
+
* drivers live here where there is nothing to assert but wiring.
|
|
1113
1095
|
*
|
|
1114
|
-
*
|
|
1115
|
-
*
|
|
1116
|
-
* against `http:///`, where a missing variable fails at build with the name of
|
|
1117
|
-
* the thing that is missing.
|
|
1096
|
+
* Both are constructed from the same local addresses the env writer composes,
|
|
1097
|
+
* so the applier cannot end up talking to a different container than the app.
|
|
1118
1098
|
*/
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
for (const resource of plan.resources) for (const [key, source] of Object.entries(resource.publicEnv ?? {})) {
|
|
1122
|
-
const value = resolved[source];
|
|
1123
|
-
if (value) env[key] = value;
|
|
1124
|
-
}
|
|
1125
|
-
return env;
|
|
1126
|
-
}
|
|
1099
|
+
/** The credential local containers are brought up with. */
|
|
1100
|
+
const LOCAL_USER$1 = "geekmidas";
|
|
1127
1101
|
/**
|
|
1128
|
-
*
|
|
1129
|
-
* a cookie set by it is readable.
|
|
1130
|
-
*
|
|
1131
|
-
* Both are read off the same list — the constructs that declared an edge to
|
|
1132
|
-
* this surface — which is why neither appears in application code. A surface
|
|
1133
|
-
* that listed its own callers would be edited every time something new called
|
|
1134
|
-
* it, and the thing being edited is already recorded in the graph.
|
|
1102
|
+
* A Postgres client that opens one connection per database it is asked about.
|
|
1135
1103
|
*
|
|
1136
|
-
*
|
|
1137
|
-
*
|
|
1138
|
-
*
|
|
1139
|
-
* means it is not running in this stage, and keys belong with the address they
|
|
1140
|
-
* describe.
|
|
1104
|
+
* `CREATE DATABASE` cannot run from a connection to the database being created,
|
|
1105
|
+
* and a schema must be created from inside its own database — so the database is
|
|
1106
|
+
* part of each query rather than fixed when the client is built.
|
|
1141
1107
|
*/
|
|
1142
|
-
function
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1108
|
+
function pgClient(port) {
|
|
1109
|
+
return { async query(database, sql, values) {
|
|
1110
|
+
const client = new pg.Client({
|
|
1111
|
+
host: "localhost",
|
|
1112
|
+
port,
|
|
1113
|
+
user: LOCAL_USER$1,
|
|
1114
|
+
password: LOCAL_USER$1,
|
|
1115
|
+
database: database ?? LOCAL_USER$1
|
|
1116
|
+
});
|
|
1117
|
+
await client.connect();
|
|
1118
|
+
try {
|
|
1119
|
+
return (await client.query(sql, values)).rows;
|
|
1120
|
+
} finally {
|
|
1121
|
+
await client.end();
|
|
1122
|
+
}
|
|
1123
|
+
} };
|
|
1124
|
+
}
|
|
1125
|
+
/** A bucket client pointed at the local MinIO. */
|
|
1126
|
+
function bucketClient(port) {
|
|
1127
|
+
const s3 = new _aws_sdk_client_s3.S3Client({
|
|
1128
|
+
region: "us-east-1",
|
|
1129
|
+
endpoint: `http://localhost:${port}`,
|
|
1130
|
+
forcePathStyle: true,
|
|
1131
|
+
credentials: {
|
|
1132
|
+
accessKeyId: LOCAL_USER$1,
|
|
1133
|
+
secretAccessKey: LOCAL_USER$1
|
|
1134
|
+
}
|
|
1135
|
+
});
|
|
1146
1136
|
return {
|
|
1147
|
-
|
|
1148
|
-
|
|
1137
|
+
async exists(bucket) {
|
|
1138
|
+
try {
|
|
1139
|
+
await s3.send(new _aws_sdk_client_s3.HeadBucketCommand({ Bucket: bucket }));
|
|
1140
|
+
return true;
|
|
1141
|
+
} catch {
|
|
1142
|
+
return false;
|
|
1143
|
+
}
|
|
1144
|
+
},
|
|
1145
|
+
async create(bucket) {
|
|
1146
|
+
await s3.send(new _aws_sdk_client_s3.CreateBucketCommand({ Bucket: bucket }));
|
|
1147
|
+
},
|
|
1148
|
+
async policy(bucket) {
|
|
1149
|
+
try {
|
|
1150
|
+
return (await s3.send(new _aws_sdk_client_s3.GetBucketPolicyCommand({ Bucket: bucket }))).Policy;
|
|
1151
|
+
} catch {
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
},
|
|
1155
|
+
async setPolicy(bucket, policy) {
|
|
1156
|
+
await s3.send(new _aws_sdk_client_s3.PutBucketPolicyCommand({
|
|
1157
|
+
Bucket: bucket,
|
|
1158
|
+
Policy: policy
|
|
1159
|
+
}));
|
|
1160
|
+
}
|
|
1149
1161
|
};
|
|
1150
1162
|
}
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
try {
|
|
1154
|
-
return new URL(address).origin;
|
|
1155
|
-
} catch {
|
|
1156
|
-
return void 0;
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1163
|
+
//#endregion
|
|
1164
|
+
//#region src/reconcile/compose.ts
|
|
1159
1165
|
/**
|
|
1160
|
-
* The
|
|
1166
|
+
* The compose document a plan implies.
|
|
1161
1167
|
*
|
|
1162
|
-
*
|
|
1163
|
-
*
|
|
1164
|
-
*
|
|
1165
|
-
|
|
1166
|
-
function parentRole(resource, plan) {
|
|
1167
|
-
const parent = plan.resources.find((r) => r.id === resource.of);
|
|
1168
|
-
return parent ? localRole(parent) : resource.name;
|
|
1169
|
-
}
|
|
1170
|
-
/** Whether a kind connects to Postgres and therefore has an owner role. */
|
|
1171
|
-
function isDatabase(kind) {
|
|
1172
|
-
return kind === "database" || kind === "database-schema";
|
|
1173
|
-
}
|
|
1174
|
-
/**
|
|
1175
|
-
* The URL a migrator connects with — the owner role, which can create, alter
|
|
1176
|
-
* and drop.
|
|
1168
|
+
* The third declaration of the same containers disappears here: today a
|
|
1169
|
+
* `services: ['postgres','minio']` block is kept in step with the code by hand,
|
|
1170
|
+
* which is the triplication this design exists to remove, in the one place you
|
|
1171
|
+
* touch every day. A `database` implies Postgres; the list is derived.
|
|
1177
1172
|
*
|
|
1178
|
-
*
|
|
1179
|
-
*
|
|
1180
|
-
*
|
|
1181
|
-
*
|
|
1173
|
+
* Returns an object rather than a string for the same reason `plan` is pure —
|
|
1174
|
+
* what should exist is asserted as data, and turning it into YAML is one line at
|
|
1175
|
+
* the edge.
|
|
1176
|
+
*
|
|
1177
|
+
* What stays config is unchanged: images and versions are pinned explicitly, and
|
|
1178
|
+
* a container no construct implies is still declared by hand. Neutral facts come
|
|
1179
|
+
* from the declaration, provider detail from config.
|
|
1182
1180
|
*/
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
const port = ports[primaryPortKey(resource.container)];
|
|
1186
|
-
if (port === void 0) return void 0;
|
|
1187
|
-
const owner = (0, __geekmidas_db_pg_roles.ownerRole)(localRole(resource));
|
|
1188
|
-
return postgres(port, rootDatabase(resource, plan), {
|
|
1189
|
-
user: owner,
|
|
1190
|
-
password: localRolePassword(project, plan, owner)
|
|
1191
|
-
});
|
|
1192
|
-
}
|
|
1181
|
+
/** The credential every derived container is brought up with locally. */
|
|
1182
|
+
const LOCAL_USER = "geekmidas";
|
|
1193
1183
|
/**
|
|
1194
|
-
* The
|
|
1184
|
+
* The token the local cache accepts.
|
|
1195
1185
|
*
|
|
1196
|
-
*
|
|
1197
|
-
*
|
|
1198
|
-
*
|
|
1199
|
-
* on it. Deployed there is no such thing — a Lambda is handed its own event
|
|
1200
|
-
* source — so this pair exists for the local target and says so.
|
|
1186
|
+
* Fixed rather than generated: it is only ever reachable on a port this project
|
|
1187
|
+
* published, and the URL that carries it is derived, so nothing is made safer
|
|
1188
|
+
* by making it unguessable and a stable one keeps `redis-cli` sessions working.
|
|
1201
1189
|
*/
|
|
1202
|
-
|
|
1203
|
-
const carrier = plan.resources.find((r) => r.kind === "queue" || r.kind === "topic");
|
|
1204
|
-
if (!carrier) return {};
|
|
1205
|
-
const publisher = urlFor(carrier, plan, ports, "");
|
|
1206
|
-
if (!publisher) return {};
|
|
1207
|
-
return {
|
|
1208
|
-
EVENT_PUBLISHER_CONNECTION_STRING: publisher,
|
|
1209
|
-
EVENT_SUBSCRIBER_CONNECTION_STRING: plan.events === "sns" ? publisher.replace(/^sns:/, "sqs:") : publisher
|
|
1210
|
-
};
|
|
1211
|
-
}
|
|
1190
|
+
const LOCAL_TOKEN = "geekmidas";
|
|
1212
1191
|
/**
|
|
1213
|
-
*
|
|
1192
|
+
* Build the compose document for a plan.
|
|
1214
1193
|
*
|
|
1215
|
-
* Every
|
|
1194
|
+
* Every container is published on its assigned port, never on the image's
|
|
1195
|
+
* default: the point of allocation is that two projects can run at once, and
|
|
1196
|
+
* one container publishing 5432 puts that back.
|
|
1216
1197
|
*/
|
|
1217
|
-
function
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
const
|
|
1222
|
-
const
|
|
1223
|
-
|
|
1198
|
+
function composeFor(plan, options) {
|
|
1199
|
+
const services = {};
|
|
1200
|
+
const volumes = {};
|
|
1201
|
+
for (const container of [...plan.containers].sort()) {
|
|
1202
|
+
const declared = container === "postgres" ? plan.resources.find((r) => r.version)?.version : void 0;
|
|
1203
|
+
const image = declared ? postgresImage(declared) : DEFAULT_IMAGES[container];
|
|
1204
|
+
if (!image) throw new UnknownContainer(container);
|
|
1205
|
+
services[container] = define(container, image, publish(container, options.ports));
|
|
1206
|
+
const volume = volumeOf(container);
|
|
1207
|
+
if (volume) volumes[volume] = {};
|
|
1224
1208
|
}
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1209
|
+
return {
|
|
1210
|
+
name: options.project,
|
|
1211
|
+
services: {
|
|
1212
|
+
...services,
|
|
1213
|
+
...options.apps
|
|
1214
|
+
},
|
|
1215
|
+
...Object.keys(volumes).length ? { volumes } : {}
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
/** Render a compose document as the file it becomes. */
|
|
1219
|
+
function toYaml(file) {
|
|
1220
|
+
return `# Generated by gkm from the construct manifest — do not edit.
|
|
1221
|
+
# Containers are derived from what the app declares. Image pins and anything
|
|
1222
|
+
# no construct implies go in your own docker-compose.yml, which is merged over
|
|
1223
|
+
# this file and wins:
|
|
1224
|
+
#
|
|
1225
|
+
# docker compose -f docker-compose.constructs.yml -f docker-compose.yml …
|
|
1226
|
+
#
|
|
1227
|
+
# "gkm dev" and "gkm test" start the containers. The apps sit behind the
|
|
1228
|
+
# "apps" profile: run "gkm setup", then
|
|
1229
|
+
# docker compose -f docker-compose.constructs.yml --profile apps up --build
|
|
1230
|
+
${(0, yaml.stringify)(file, { lineWidth: 0 })}`;
|
|
1231
|
+
}
|
|
1232
|
+
/** A container with no image, from config or from the defaults. */
|
|
1233
|
+
var UnknownContainer = class extends Error {
|
|
1234
|
+
/** The container that could not be resolved. */
|
|
1235
|
+
container;
|
|
1236
|
+
/** The containers that have a default, so the caller can see the set. */
|
|
1237
|
+
known = Object.keys(DEFAULT_IMAGES).sort();
|
|
1238
|
+
constructor(container) {
|
|
1239
|
+
super("No image is known for this container; pin one in config");
|
|
1240
|
+
this.name = "UnknownContainer";
|
|
1241
|
+
this.container = container;
|
|
1242
|
+
}
|
|
1243
|
+
};
|
|
1244
|
+
/** One container's definition, published on the port it was assigned. */
|
|
1245
|
+
function define(container, image, published) {
|
|
1246
|
+
switch (container) {
|
|
1247
|
+
case "postgres": return {
|
|
1248
|
+
image,
|
|
1249
|
+
restart: "unless-stopped",
|
|
1250
|
+
ports: published,
|
|
1251
|
+
environment: {
|
|
1252
|
+
POSTGRES_USER: LOCAL_USER,
|
|
1253
|
+
POSTGRES_PASSWORD: LOCAL_USER,
|
|
1254
|
+
POSTGRES_DB: LOCAL_USER
|
|
1255
|
+
},
|
|
1256
|
+
volumes: ["postgres-data:/var/lib/postgresql"],
|
|
1257
|
+
healthcheck: {
|
|
1258
|
+
test: ["CMD-SHELL", `pg_isready -U ${LOCAL_USER}`],
|
|
1259
|
+
interval: "5s",
|
|
1260
|
+
timeout: "3s",
|
|
1261
|
+
retries: 10,
|
|
1262
|
+
start_period: "10s"
|
|
1236
1263
|
}
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1264
|
+
};
|
|
1265
|
+
case "minio": return {
|
|
1266
|
+
image,
|
|
1267
|
+
restart: "unless-stopped",
|
|
1268
|
+
ports: published,
|
|
1269
|
+
environment: {
|
|
1270
|
+
MINIO_ROOT_USER: LOCAL_USER,
|
|
1271
|
+
MINIO_ROOT_PASSWORD: LOCAL_USER
|
|
1272
|
+
},
|
|
1273
|
+
command: "server --console-address \":9001\" /data",
|
|
1274
|
+
volumes: ["minio-data:/data"],
|
|
1275
|
+
healthcheck: {
|
|
1276
|
+
test: [
|
|
1277
|
+
"CMD",
|
|
1278
|
+
"mc",
|
|
1279
|
+
"ready",
|
|
1280
|
+
"local"
|
|
1281
|
+
],
|
|
1282
|
+
interval: "10s",
|
|
1283
|
+
timeout: "5s",
|
|
1284
|
+
retries: 5,
|
|
1285
|
+
start_period: "10s"
|
|
1255
1286
|
}
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1287
|
+
};
|
|
1288
|
+
case "caddy": return {
|
|
1289
|
+
image,
|
|
1290
|
+
restart: "unless-stopped",
|
|
1291
|
+
ports: published,
|
|
1292
|
+
volumes: [
|
|
1293
|
+
"./.gkm/Caddyfile:/etc/caddy/Caddyfile:ro",
|
|
1294
|
+
"./.gkm/caddy-sites:/etc/caddy/sites:ro",
|
|
1295
|
+
"caddy-data:/data"
|
|
1296
|
+
],
|
|
1297
|
+
extra_hosts: ["host.docker.internal:host-gateway"],
|
|
1298
|
+
healthcheck: {
|
|
1299
|
+
test: [
|
|
1300
|
+
"CMD",
|
|
1301
|
+
"caddy",
|
|
1302
|
+
"version"
|
|
1303
|
+
],
|
|
1304
|
+
interval: "10s",
|
|
1305
|
+
timeout: "5s",
|
|
1306
|
+
retries: 5
|
|
1259
1307
|
}
|
|
1260
|
-
|
|
1261
|
-
case "
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1308
|
+
};
|
|
1309
|
+
case "mailpit": return {
|
|
1310
|
+
image,
|
|
1311
|
+
restart: "unless-stopped",
|
|
1312
|
+
ports: published,
|
|
1313
|
+
environment: {
|
|
1314
|
+
MP_SMTP_AUTH_ACCEPT_ANY: "1",
|
|
1315
|
+
MP_SMTP_AUTH_ALLOW_INSECURE: "1"
|
|
1316
|
+
},
|
|
1317
|
+
healthcheck: {
|
|
1318
|
+
test: [
|
|
1319
|
+
"CMD",
|
|
1320
|
+
"wget",
|
|
1321
|
+
"-q",
|
|
1322
|
+
"--spider",
|
|
1323
|
+
"http://localhost:8025"
|
|
1324
|
+
],
|
|
1325
|
+
interval: "10s",
|
|
1326
|
+
timeout: "5s",
|
|
1327
|
+
retries: 5
|
|
1328
|
+
}
|
|
1329
|
+
};
|
|
1330
|
+
case "redis": return {
|
|
1331
|
+
image,
|
|
1332
|
+
restart: "unless-stopped",
|
|
1333
|
+
ports: published,
|
|
1334
|
+
volumes: ["redis-data:/data"],
|
|
1335
|
+
healthcheck: {
|
|
1336
|
+
test: [
|
|
1337
|
+
"CMD",
|
|
1338
|
+
"redis-cli",
|
|
1339
|
+
"ping"
|
|
1340
|
+
],
|
|
1341
|
+
interval: "5s",
|
|
1342
|
+
timeout: "3s",
|
|
1343
|
+
retries: 5
|
|
1344
|
+
}
|
|
1345
|
+
};
|
|
1346
|
+
case "redis-http": return {
|
|
1347
|
+
image,
|
|
1348
|
+
restart: "unless-stopped",
|
|
1349
|
+
ports: published,
|
|
1350
|
+
environment: {
|
|
1351
|
+
SRH_MODE: "env",
|
|
1352
|
+
SRH_TOKEN: LOCAL_TOKEN,
|
|
1353
|
+
SRH_CONNECTION_STRING: "redis://redis:6379"
|
|
1354
|
+
},
|
|
1355
|
+
depends_on: ["redis"],
|
|
1356
|
+
healthcheck: {
|
|
1357
|
+
test: [
|
|
1358
|
+
"CMD",
|
|
1359
|
+
"wget",
|
|
1360
|
+
"-q",
|
|
1361
|
+
"-O",
|
|
1362
|
+
"/dev/null",
|
|
1363
|
+
"http://127.0.0.1:80/"
|
|
1364
|
+
],
|
|
1365
|
+
interval: "5s",
|
|
1366
|
+
timeout: "3s",
|
|
1367
|
+
retries: 10
|
|
1368
|
+
}
|
|
1369
|
+
};
|
|
1370
|
+
case "rabbitmq": return {
|
|
1371
|
+
image,
|
|
1372
|
+
restart: "unless-stopped",
|
|
1373
|
+
ports: published,
|
|
1374
|
+
environment: {
|
|
1375
|
+
RABBITMQ_DEFAULT_USER: LOCAL_USER,
|
|
1376
|
+
RABBITMQ_DEFAULT_PASS: LOCAL_USER
|
|
1377
|
+
},
|
|
1378
|
+
volumes: ["rabbitmq-data:/var/lib/rabbitmq"],
|
|
1379
|
+
healthcheck: {
|
|
1380
|
+
test: [
|
|
1381
|
+
"CMD",
|
|
1382
|
+
"rabbitmq-diagnostics",
|
|
1383
|
+
"-q",
|
|
1384
|
+
"ping"
|
|
1385
|
+
],
|
|
1386
|
+
interval: "10s",
|
|
1387
|
+
timeout: "5s",
|
|
1388
|
+
retries: 5
|
|
1389
|
+
}
|
|
1390
|
+
};
|
|
1391
|
+
case "localstack": return {
|
|
1392
|
+
image,
|
|
1393
|
+
restart: "unless-stopped",
|
|
1394
|
+
ports: published,
|
|
1395
|
+
environment: {
|
|
1396
|
+
AWS_DEFAULT_REGION: "us-east-1",
|
|
1397
|
+
AWS_ACCESS_KEY_ID: "LSIAQAAAAAAVNCBMPNSG",
|
|
1398
|
+
AWS_SECRET_ACCESS_KEY: LOCAL_USER
|
|
1399
|
+
},
|
|
1400
|
+
volumes: ["localstack-data:/var/lib/localstack"],
|
|
1401
|
+
healthcheck: {
|
|
1402
|
+
test: [
|
|
1403
|
+
"CMD",
|
|
1404
|
+
"curl",
|
|
1405
|
+
"-f",
|
|
1406
|
+
"http://localhost:4566/_localstack/health"
|
|
1407
|
+
],
|
|
1408
|
+
interval: "10s",
|
|
1409
|
+
timeout: "5s",
|
|
1410
|
+
retries: 5,
|
|
1411
|
+
start_period: "10s"
|
|
1412
|
+
}
|
|
1413
|
+
};
|
|
1414
|
+
default: throw new UnknownContainer(container);
|
|
1281
1415
|
}
|
|
1282
1416
|
}
|
|
1283
|
-
/** A local Postgres URL, master credential and all. */
|
|
1284
|
-
function postgres(port, database, credential = {
|
|
1285
|
-
user: LOCAL_USER,
|
|
1286
|
-
password: LOCAL_USER
|
|
1287
|
-
}) {
|
|
1288
|
-
return `postgres://${credential.user}:${credential.password}@${LOCAL_HOST}:${port}/${database}`;
|
|
1289
|
-
}
|
|
1290
1417
|
/**
|
|
1291
|
-
* The
|
|
1418
|
+
* The published port mappings for a container.
|
|
1292
1419
|
*
|
|
1293
|
-
*
|
|
1294
|
-
*
|
|
1295
|
-
*
|
|
1296
|
-
* in Postgres, which is why the suffix is not optional here the way it is for a
|
|
1297
|
-
* schema.
|
|
1420
|
+
* Every port is looked up by its own key rather than being derived as an offset
|
|
1421
|
+
* from the container's first, so MinIO's console cannot land on whatever was
|
|
1422
|
+
* allocated to the next container.
|
|
1298
1423
|
*/
|
|
1299
|
-
function
|
|
1300
|
-
return
|
|
1424
|
+
function publish(container, ports) {
|
|
1425
|
+
return portsOf(container).map((port) => {
|
|
1426
|
+
const assigned = ports[port.key];
|
|
1427
|
+
if (assigned === void 0) throw new UnassignedPort(port.key);
|
|
1428
|
+
return `${assigned}:${port.inside}`;
|
|
1429
|
+
});
|
|
1301
1430
|
}
|
|
1431
|
+
/** A planned port that allocation never assigned. */
|
|
1432
|
+
var UnassignedPort = class extends Error {
|
|
1433
|
+
/** The port key that went unassigned, e.g. `minio-console`. */
|
|
1434
|
+
key;
|
|
1435
|
+
constructor(key) {
|
|
1436
|
+
super("No port was assigned for this container port");
|
|
1437
|
+
this.name = "UnassignedPort";
|
|
1438
|
+
this.key = key;
|
|
1439
|
+
}
|
|
1440
|
+
};
|
|
1441
|
+
//#endregion
|
|
1442
|
+
//#region src/reconcile/docker.ts
|
|
1302
1443
|
/**
|
|
1303
|
-
* The
|
|
1304
|
-
*
|
|
1305
|
-
* Derived rather than random, for the same reason the local secret is: a
|
|
1306
|
-
* password that changed on every `gkm dev` would lock a developer out of the
|
|
1307
|
-
* data they had a moment ago. Seeded by the project so two checkouts do not
|
|
1308
|
-
* share one, and by the stage so `test` and `development` do not.
|
|
1444
|
+
* The real Docker, behind the small interface reconcile needs.
|
|
1309
1445
|
*
|
|
1310
|
-
*
|
|
1311
|
-
*
|
|
1312
|
-
|
|
1313
|
-
function localRolePassword(project, plan, role) {
|
|
1314
|
-
return (0, node_crypto.createHash)("sha256").update(`${project}:${plan.stage}:role:${role}`).digest("base64url").slice(0, 32);
|
|
1315
|
-
}
|
|
1316
|
-
/**
|
|
1317
|
-
* A cache's address: its database's URL, carrying the table it reads.
|
|
1446
|
+
* Shells out to `docker compose` rather than talking to the daemon socket: it is
|
|
1447
|
+
* what a developer runs by hand when something is wrong, so the two cannot
|
|
1448
|
+
* disagree, and it keeps Docker out of this package's dependencies.
|
|
1318
1449
|
*
|
|
1319
|
-
*
|
|
1320
|
-
*
|
|
1321
|
-
* two keys are one more thing to keep in step.
|
|
1450
|
+
* Every call is scoped to the generated compose file. Nothing here can touch a
|
|
1451
|
+
* container this project did not generate.
|
|
1322
1452
|
*/
|
|
1323
|
-
|
|
1324
|
-
const parsed = new URL(url);
|
|
1325
|
-
parsed.searchParams.set("table", table);
|
|
1326
|
-
return parsed.toString();
|
|
1327
|
-
}
|
|
1453
|
+
const run = (0, node_util.promisify)(node_child_process.execFile);
|
|
1328
1454
|
/**
|
|
1329
|
-
* The
|
|
1455
|
+
* The generated file, then the project's own if it has one.
|
|
1330
1456
|
*
|
|
1331
|
-
*
|
|
1332
|
-
*
|
|
1333
|
-
* that actually exists.
|
|
1457
|
+
* Merged in that order so the project's wins — an `image:` there pins a
|
|
1458
|
+
* container this file derived, and a service there is added beside them.
|
|
1334
1459
|
*/
|
|
1335
|
-
function
|
|
1336
|
-
const
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
}
|
|
1343
|
-
return current.name;
|
|
1344
|
-
}
|
|
1345
|
-
/** The schema a derived resource puts on its search path, if it has one. */
|
|
1346
|
-
function schemaOf(resource, plan) {
|
|
1347
|
-
const byId = new Map(plan.resources.map((r) => [r.id, r]));
|
|
1348
|
-
let current = resource;
|
|
1349
|
-
while (current) {
|
|
1350
|
-
if (current.kind === "database-schema") return current.schema;
|
|
1351
|
-
current = current.of ? byId.get(current.of) : void 0;
|
|
1352
|
-
}
|
|
1353
|
-
return void 0;
|
|
1460
|
+
function composeFiles(composePath) {
|
|
1461
|
+
const project = (0, node_path.join)((0, node_path.dirname)(composePath), "docker-compose.yml");
|
|
1462
|
+
return [
|
|
1463
|
+
"-f",
|
|
1464
|
+
composePath,
|
|
1465
|
+
...(0, node_fs.existsSync)(project) ? ["-f", project] : []
|
|
1466
|
+
];
|
|
1354
1467
|
}
|
|
1355
|
-
/**
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1468
|
+
/** How long to wait for containers to pass their health checks. */
|
|
1469
|
+
const HEALTH_TIMEOUT_MS = 12e4;
|
|
1470
|
+
const dockerCli = {
|
|
1471
|
+
async publishedPort(composePath, service, inside) {
|
|
1472
|
+
try {
|
|
1473
|
+
const { stdout } = await run("docker", [
|
|
1474
|
+
"compose",
|
|
1475
|
+
...composeFiles(composePath),
|
|
1476
|
+
"port",
|
|
1477
|
+
service,
|
|
1478
|
+
String(inside)
|
|
1479
|
+
]);
|
|
1480
|
+
const port = Number(stdout.trim().split(":").pop());
|
|
1481
|
+
return Number.isFinite(port) && port > 0 ? port : void 0;
|
|
1482
|
+
} catch {
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
},
|
|
1486
|
+
async up(composePath, services) {
|
|
1487
|
+
await run("docker", [
|
|
1488
|
+
"compose",
|
|
1489
|
+
...composeFiles(composePath),
|
|
1490
|
+
"up",
|
|
1491
|
+
"-d",
|
|
1492
|
+
"--remove-orphans",
|
|
1493
|
+
"--wait",
|
|
1494
|
+
`--wait-timeout=${Math.floor(HEALTH_TIMEOUT_MS / 1e3)}`,
|
|
1495
|
+
...services
|
|
1496
|
+
]);
|
|
1497
|
+
},
|
|
1498
|
+
async copyOut(composePath, service, from, to) {
|
|
1499
|
+
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(to), { recursive: true });
|
|
1500
|
+
await run("docker", [
|
|
1501
|
+
"compose",
|
|
1502
|
+
...composeFiles(composePath),
|
|
1503
|
+
"cp",
|
|
1504
|
+
`${service}:${from}`,
|
|
1505
|
+
to
|
|
1506
|
+
]);
|
|
1507
|
+
},
|
|
1508
|
+
async reload(composePath, service) {
|
|
1509
|
+
await run("docker", [
|
|
1510
|
+
"compose",
|
|
1511
|
+
...composeFiles(composePath),
|
|
1512
|
+
"exec",
|
|
1513
|
+
"-T",
|
|
1514
|
+
service,
|
|
1515
|
+
"caddy",
|
|
1516
|
+
"reload",
|
|
1517
|
+
"--config",
|
|
1518
|
+
"/etc/caddy/Caddyfile"
|
|
1519
|
+
]);
|
|
1520
|
+
},
|
|
1521
|
+
async healthy(composePath, services) {
|
|
1522
|
+
if (services.length === 0) return true;
|
|
1523
|
+
try {
|
|
1524
|
+
const { stdout } = await run("docker", [
|
|
1525
|
+
"compose",
|
|
1526
|
+
...composeFiles(composePath),
|
|
1527
|
+
"ps",
|
|
1528
|
+
"--format",
|
|
1529
|
+
"json",
|
|
1530
|
+
...services
|
|
1531
|
+
]);
|
|
1532
|
+
const running = /* @__PURE__ */ new Set();
|
|
1533
|
+
for (const line of stdout.trim().split("\n").filter(Boolean)) {
|
|
1534
|
+
const entry = JSON.parse(line);
|
|
1535
|
+
if (entry.State === "running" && (entry.Health === void 0 || entry.Health === "" || entry.Health === "healthy") && entry.Service) running.add(entry.Service);
|
|
1536
|
+
}
|
|
1537
|
+
return services.every((service) => running.has(service));
|
|
1538
|
+
} catch {
|
|
1539
|
+
return false;
|
|
1540
|
+
}
|
|
1367
1541
|
}
|
|
1368
1542
|
};
|
|
1543
|
+
//#endregion
|
|
1544
|
+
//#region src/reconcile/ports.ts
|
|
1369
1545
|
/**
|
|
1370
|
-
*
|
|
1546
|
+
* Port allocation — why two projects can run at once.
|
|
1371
1547
|
*
|
|
1372
|
-
*
|
|
1373
|
-
*
|
|
1374
|
-
*
|
|
1375
|
-
*
|
|
1548
|
+
* Publishing `5432:5432` means the second project on your machine cannot start.
|
|
1549
|
+
* Nothing solves that except giving each project its own ports, and what makes
|
|
1550
|
+
* it *possible* is the single-`<NAME>_URL` rule: an app reads `ORDERS_URL` and
|
|
1551
|
+
* never sees a host or a port, so reconcile is free to bind whatever is
|
|
1552
|
+
* available and compose the URL from what it bound. An app reading `DB_HOST` and
|
|
1553
|
+
* `DB_PORT` separately could not be moved this way.
|
|
1554
|
+
*
|
|
1555
|
+
* Assignments are **persisted, not recomputed**. Recomputing means adding a
|
|
1556
|
+
* construct renumbers its neighbours, and every running container, saved
|
|
1557
|
+
* connection string, and `psql` history goes stale at once.
|
|
1558
|
+
*
|
|
1559
|
+
* Pure, with the liveness probe injected — a collision with another project's
|
|
1560
|
+
* stack is a fact about the machine, and keeping it a parameter is what lets the
|
|
1561
|
+
* rules be asserted without binding a socket.
|
|
1376
1562
|
*/
|
|
1377
|
-
function localSecret(project, plan, resource) {
|
|
1378
|
-
return (0, node_crypto.createHash)("sha256").update(`${project}:${plan.stage}:${resource.id}`).digest("base64url").slice(0, 43);
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
//#endregion
|
|
1382
|
-
//#region src/reconcile/ports.ts
|
|
1383
1563
|
/**
|
|
1384
1564
|
* The window ports are drawn from.
|
|
1385
1565
|
*
|
|
@@ -1478,17 +1658,33 @@ async function nextFree(from, taken, probe) {
|
|
|
1478
1658
|
* rather than as a connection failure ten minutes later.
|
|
1479
1659
|
*/
|
|
1480
1660
|
function isPortFree(port) {
|
|
1481
|
-
return new Promise((resolve
|
|
1661
|
+
return new Promise((resolve) => {
|
|
1482
1662
|
const server = (0, node_net.createServer)();
|
|
1483
|
-
server.once("error", () => resolve
|
|
1484
|
-
server.once("listening", () => server.close(() => resolve
|
|
1663
|
+
server.once("error", () => resolve(false));
|
|
1664
|
+
server.once("listening", () => server.close(() => resolve(true)));
|
|
1485
1665
|
server.listen(port, "0.0.0.0");
|
|
1486
1666
|
});
|
|
1487
1667
|
}
|
|
1488
|
-
|
|
1489
1668
|
//#endregion
|
|
1490
1669
|
//#region src/reconcile/provision.ts
|
|
1491
1670
|
/**
|
|
1671
|
+
* Creating what the URLs name.
|
|
1672
|
+
*
|
|
1673
|
+
* Convergent and idempotent: every statement asks whether the thing exists
|
|
1674
|
+
* first, so reconciling repeatedly is free and a half-applied run recovers by
|
|
1675
|
+
* being run again. That property is what lets `gkm dev` do this on every start.
|
|
1676
|
+
*
|
|
1677
|
+
* The line the design draws is at data, not at side effects — this creates
|
|
1678
|
+
* databases, schemas, and buckets, and it never seeds, resets, or drops. A
|
|
1679
|
+
* developer who wants their data gone asks for it explicitly.
|
|
1680
|
+
*
|
|
1681
|
+
* Statement generation is separated from application for the reason the design
|
|
1682
|
+
* gives: the same DDL has to run in-process locally and from a provisioner in a
|
|
1683
|
+
* VPC, and one implementation is what stops "works locally, fails deployed".
|
|
1684
|
+
* When the role DDL lands in `@geekmidas/db` this delegates to it rather than
|
|
1685
|
+
* growing its own.
|
|
1686
|
+
*/
|
|
1687
|
+
/**
|
|
1492
1688
|
* The Postgres work a plan implies.
|
|
1493
1689
|
*
|
|
1494
1690
|
* Databases before the schemas inside them, which `provisionOrder` already
|
|
@@ -1539,9 +1735,9 @@ function postgresStatements(plan, project = "") {
|
|
|
1539
1735
|
function cacheTableDdl(cache, home, plan) {
|
|
1540
1736
|
const database = rootDatabase(home, plan);
|
|
1541
1737
|
const schema = home.roles === false ? void 0 : home.schema;
|
|
1542
|
-
const name = cache.table ?? (0,
|
|
1738
|
+
const name = cache.table ?? (0, _geekmidas_manifest.cacheTable)(cache.id);
|
|
1543
1739
|
const table = schema ? `${schema}.${name}` : name;
|
|
1544
|
-
const created = (0,
|
|
1740
|
+
const created = (0, _geekmidas_cache_postgres.cacheTableStatements)({ table }).map((statement) => ({
|
|
1545
1741
|
id: cache.id,
|
|
1546
1742
|
describe: statement.describe,
|
|
1547
1743
|
database,
|
|
@@ -1550,7 +1746,7 @@ function cacheTableDdl(cache, home, plan) {
|
|
|
1550
1746
|
}));
|
|
1551
1747
|
if (!schema) return created;
|
|
1552
1748
|
const runtime = localRole(home);
|
|
1553
|
-
const owner = (0,
|
|
1749
|
+
const owner = (0, _geekmidas_db_pg_roles.ownerRole)(runtime);
|
|
1554
1750
|
const qualified = table.split(".").map((part) => quoteIdentifier(part)).join(".");
|
|
1555
1751
|
return [
|
|
1556
1752
|
...created,
|
|
@@ -1578,9 +1774,9 @@ function cacheTableDdl(cache, home, plan) {
|
|
|
1578
1774
|
function rolesFor(resource, schema, plan, project) {
|
|
1579
1775
|
if (resource.roles === false) return [];
|
|
1580
1776
|
const runtime = localRole(resource);
|
|
1581
|
-
const owner = (0,
|
|
1582
|
-
const reader = plan.resources.some((r) => r.kind === "database-reader" && r.of === resource.id) ? (0,
|
|
1583
|
-
return (0,
|
|
1777
|
+
const owner = (0, _geekmidas_db_pg_roles.ownerRole)(runtime);
|
|
1778
|
+
const reader = plan.resources.some((r) => r.kind === "database-reader" && r.of === resource.id) ? (0, _geekmidas_db_pg_roles.readerRole)(runtime) : void 0;
|
|
1779
|
+
return (0, _geekmidas_db_pg_roles.roleStatements)({
|
|
1584
1780
|
runtime,
|
|
1585
1781
|
owner,
|
|
1586
1782
|
...reader ? { reader } : {},
|
|
@@ -1630,8 +1826,7 @@ async function applyPostgres(client, statements) {
|
|
|
1630
1826
|
});
|
|
1631
1827
|
continue;
|
|
1632
1828
|
}
|
|
1633
|
-
|
|
1634
|
-
if (rows.length > 0) {
|
|
1829
|
+
if ((await client.query(statement.database, statement.exists.sql, statement.exists.values)).length > 0) {
|
|
1635
1830
|
applied.push({
|
|
1636
1831
|
id: statement.id,
|
|
1637
1832
|
describe: statement.describe,
|
|
@@ -1756,9 +1951,20 @@ async function applyBuckets(client, buckets) {
|
|
|
1756
1951
|
}
|
|
1757
1952
|
return applied;
|
|
1758
1953
|
}
|
|
1759
|
-
|
|
1760
1954
|
//#endregion
|
|
1761
1955
|
//#region src/reconcile/state.ts
|
|
1956
|
+
/**
|
|
1957
|
+
* What reconcile records, and the hash that lets it do nothing.
|
|
1958
|
+
*
|
|
1959
|
+
* Reconciling on every `gkm dev` is only acceptable if the converged case is
|
|
1960
|
+
* free, so the desired plan is hashed: matching hash plus healthy containers
|
|
1961
|
+
* means there is nothing to do. Without that, the automatic reconcile the design
|
|
1962
|
+
* asks for would tax every start.
|
|
1963
|
+
*
|
|
1964
|
+
* Almost nothing needs recording — roles are queried, migrations have their own
|
|
1965
|
+
* table, containers are remembered by Docker. Only ports and this hash have no
|
|
1966
|
+
* other owner.
|
|
1967
|
+
*/
|
|
1762
1968
|
const STATE_DIR = ".gkm/reconcile";
|
|
1763
1969
|
/**
|
|
1764
1970
|
* The hash of everything a reconcile would act on.
|
|
@@ -1787,7 +1993,7 @@ async function loadState(root, stage) {
|
|
|
1787
1993
|
const state = JSON.parse(raw);
|
|
1788
1994
|
return state.stage === stage ? state : void 0;
|
|
1789
1995
|
} catch {
|
|
1790
|
-
return
|
|
1996
|
+
return;
|
|
1791
1997
|
}
|
|
1792
1998
|
}
|
|
1793
1999
|
/** Record what was applied, so the next run can skip it. */
|
|
@@ -1799,11 +2005,36 @@ async function saveState(root, state) {
|
|
|
1799
2005
|
function statePath(root, stage) {
|
|
1800
2006
|
return (0, node_path.join)(root, STATE_DIR, `${stage}.json`);
|
|
1801
2007
|
}
|
|
1802
|
-
|
|
1803
2008
|
//#endregion
|
|
1804
2009
|
//#region src/reconcile/index.ts
|
|
1805
|
-
/**
|
|
1806
|
-
|
|
2010
|
+
/**
|
|
2011
|
+
* Reconcile — the one convergent function `gkm dev`, `gkm test`, and `gkm setup`
|
|
2012
|
+
* all call.
|
|
2013
|
+
*
|
|
2014
|
+
* Three phases, and only this one has side effects: declaring is pure (`gkm
|
|
2015
|
+
* build` and the dev watcher), running reads `<NAME>_URL` and provisions
|
|
2016
|
+
* nothing. Reconcile computes the desired state, compares, and applies the
|
|
2017
|
+
* difference — which is what makes "when does it run" answerable with
|
|
2018
|
+
* *whenever, repeatedly*.
|
|
2019
|
+
*
|
|
2020
|
+
* It is safe to run automatically because its blast radius is entirely local:
|
|
2021
|
+
* this project's containers, this project's `.gkm/`. Nothing it does can reach a
|
|
2022
|
+
* cloud. The line is drawn at data, not at side effects — it allocates ports,
|
|
2023
|
+
* writes compose, starts containers, and creates roles; it never seeds, resets,
|
|
2024
|
+
* or drops.
|
|
2025
|
+
*
|
|
2026
|
+
* Docker is injected rather than imported so the rules above can be asserted
|
|
2027
|
+
* without a daemon. The default implementation is the real one.
|
|
2028
|
+
*/
|
|
2029
|
+
/**
|
|
2030
|
+
* Where the generated compose file is written, relative to the project root.
|
|
2031
|
+
*
|
|
2032
|
+
* At the root, beside the project's own `docker-compose.yml`, because the two
|
|
2033
|
+
* are one stack: this file is everything the constructs imply and is never
|
|
2034
|
+
* edited, that one is anything they cannot express — an image pin, an extra
|
|
2035
|
+
* service — and is merged over it, so it wins.
|
|
2036
|
+
*/
|
|
2037
|
+
const COMPOSE_PATH = "docker-compose.constructs.yml";
|
|
1807
2038
|
/**
|
|
1808
2039
|
* The local edge's config, beside the compose file that mounts it.
|
|
1809
2040
|
*
|
|
@@ -1840,7 +2071,8 @@ const CADDY_ROOT_IN_CONTAINER = "/data/caddy/pki/authorities/local/root.crt";
|
|
|
1840
2071
|
*/
|
|
1841
2072
|
async function reconcile(options) {
|
|
1842
2073
|
const { root, project, manifest, stage, start = true, provision = true, docker = dockerCli, probe = isPortFree, sql = pgClient, buckets = bucketClient } = options;
|
|
1843
|
-
const plan = planFor(manifest, stage, (0,
|
|
2074
|
+
const plan = planFor(manifest, stage, (0, _geekmidas_manifest.provisionOrder)(manifest), {
|
|
2075
|
+
localStage: options.localStage,
|
|
1844
2076
|
events: options.events,
|
|
1845
2077
|
cache: options.cache,
|
|
1846
2078
|
extraContainers: options.extraContainers,
|
|
@@ -1855,7 +2087,7 @@ async function reconcile(options) {
|
|
|
1855
2087
|
const compose = composeFor(plan, {
|
|
1856
2088
|
project,
|
|
1857
2089
|
ports,
|
|
1858
|
-
...options.
|
|
2090
|
+
...options.apps ? { apps: options.apps(plan.containers) } : {}
|
|
1859
2091
|
});
|
|
1860
2092
|
const caddyfile = toCaddyfile(sitesFor(plan, project, options.addresses));
|
|
1861
2093
|
const hash = planHash(plan, compose, { caddyfile });
|
|
@@ -1878,13 +2110,16 @@ async function reconcile(options) {
|
|
|
1878
2110
|
hash,
|
|
1879
2111
|
changed: false
|
|
1880
2112
|
};
|
|
1881
|
-
|
|
1882
|
-
const converged = recorded?.hash === hash && (!start || await docker.healthy(composePath, plan.containers));
|
|
1883
|
-
if (converged) return result;
|
|
2113
|
+
if ((await loadState(root, stage))?.hash === hash && (0, node_fs.existsSync)(composePath) && (!start || await docker.healthy(composePath, plan.containers))) return result;
|
|
1884
2114
|
await write(composePath, toYaml(compose));
|
|
1885
2115
|
if (plan.containers.includes("caddy")) {
|
|
1886
2116
|
await write((0, node_path.join)(root, CADDYFILE_PATH), caddyfileRoot());
|
|
1887
2117
|
await write((0, node_path.join)(root, caddySitesPath(stage)), caddyfile);
|
|
2118
|
+
await pruneCaddySites(root, [
|
|
2119
|
+
stage,
|
|
2120
|
+
options.localStage,
|
|
2121
|
+
require_workspace.TEST_STAGE
|
|
2122
|
+
]);
|
|
1888
2123
|
}
|
|
1889
2124
|
if (start && plan.containers.length > 0) await docker.up(composePath, plan.containers);
|
|
1890
2125
|
if (start && plan.containers.includes("caddy")) await docker.reload(composePath, "caddy").catch(() => {});
|
|
@@ -1950,6 +2185,20 @@ function addressesFor(containers, ports) {
|
|
|
1950
2185
|
return addresses;
|
|
1951
2186
|
}
|
|
1952
2187
|
/** Write a file only when its content would change, so mtimes stay meaningful. */
|
|
2188
|
+
/**
|
|
2189
|
+
* Remove the routes of stages this project no longer reconciles.
|
|
2190
|
+
*
|
|
2191
|
+
* Only the local stage and `test` are ever written, so any other file is left
|
|
2192
|
+
* over from a stage that was renamed — `development` becoming `dev`. Caddy
|
|
2193
|
+
* imports every file, and the leftover declares the same hosts as its
|
|
2194
|
+
* replacement: it refuses the whole config as ambiguous, and every URL behind
|
|
2195
|
+
* the edge goes with it.
|
|
2196
|
+
*/
|
|
2197
|
+
async function pruneCaddySites(root, stages) {
|
|
2198
|
+
const dir = (0, node_path.dirname)((0, node_path.join)(root, caddySitesPath(require_workspace.TEST_STAGE)));
|
|
2199
|
+
const keep = new Set(stages.map((stage) => stage && `${stage}.caddy`));
|
|
2200
|
+
for (const file of await (0, node_fs_promises.readdir)(dir)) if (file.endsWith(".caddy") && !keep.has(file)) await (0, node_fs_promises.rm)((0, node_path.join)(dir, file));
|
|
2201
|
+
}
|
|
1953
2202
|
async function write(path, content) {
|
|
1954
2203
|
try {
|
|
1955
2204
|
if (await (0, node_fs_promises.readFile)(path, "utf-8") === content) return;
|
|
@@ -1957,10 +2206,28 @@ async function write(path, content) {
|
|
|
1957
2206
|
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(path), { recursive: true });
|
|
1958
2207
|
await (0, node_fs_promises.writeFile)(path, content);
|
|
1959
2208
|
}
|
|
1960
|
-
|
|
1961
2209
|
//#endregion
|
|
1962
2210
|
//#region src/reconcile/workspace.ts
|
|
1963
2211
|
/**
|
|
2212
|
+
* Workspace config → reconcile.
|
|
2213
|
+
*
|
|
2214
|
+
* Nothing here is read from a `services:` block — there is none. Which
|
|
2215
|
+
* containers exist comes from the manifest; which backend a cache or a broker
|
|
2216
|
+
* resolves to comes from the deploy target; and an image pin is the project's
|
|
2217
|
+
* own `docker-compose.yml`, merged over the generated file.
|
|
2218
|
+
*
|
|
2219
|
+
* Shared by `gkm setup`, `gkm dev`, and `gkm test` so all three converge on the
|
|
2220
|
+
* same state, differing only in the stage they pass.
|
|
2221
|
+
*/
|
|
2222
|
+
var workspace_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
|
|
2223
|
+
backendsOf: () => backendsOf,
|
|
2224
|
+
constructGlobs: () => constructGlobs,
|
|
2225
|
+
derivedContainers: () => derivedContainers,
|
|
2226
|
+
reconcileWorkspace: () => reconcileWorkspace,
|
|
2227
|
+
surfaceAddresses: () => surfaceAddresses,
|
|
2228
|
+
usesConstructs: () => usesConstructs
|
|
2229
|
+
});
|
|
2230
|
+
/**
|
|
1964
2231
|
* Every constructs glob in the workspace, resolved against its app.
|
|
1965
2232
|
*
|
|
1966
2233
|
* Absolute because discovery globs from a single cwd while apps live in
|
|
@@ -1973,16 +2240,13 @@ function constructGlobs(workspace) {
|
|
|
1973
2240
|
function usesConstructs(workspace) {
|
|
1974
2241
|
return constructGlobs(workspace).length > 0;
|
|
1975
2242
|
}
|
|
1976
|
-
/**
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
*/
|
|
1984
|
-
function imagePins(workspace) {
|
|
1985
|
-
return { ...workspace.services.images };
|
|
2243
|
+
/** The backends a workspace's target implies, as the plan takes them. */
|
|
2244
|
+
function backendsOf(workspace) {
|
|
2245
|
+
const on = providerOf(workspace);
|
|
2246
|
+
return {
|
|
2247
|
+
events: eventsBackendFor(on),
|
|
2248
|
+
cache: cacheBackendFor(on)
|
|
2249
|
+
};
|
|
1986
2250
|
}
|
|
1987
2251
|
/**
|
|
1988
2252
|
* The containers a workspace's declarations imply, without reconciling.
|
|
@@ -1999,9 +2263,9 @@ async function derivedContainers(workspace, stage, manifest) {
|
|
|
1999
2263
|
patterns: constructGlobs(workspace),
|
|
2000
2264
|
cwd: workspace.root
|
|
2001
2265
|
});
|
|
2002
|
-
return planFor(found, stage, (0,
|
|
2003
|
-
|
|
2004
|
-
|
|
2266
|
+
return planFor(found, stage, (0, _geekmidas_manifest.provisionOrder)(found), {
|
|
2267
|
+
localStage: workspace.stages.local,
|
|
2268
|
+
...backendsOf(workspace)
|
|
2005
2269
|
}).containers;
|
|
2006
2270
|
}
|
|
2007
2271
|
/**
|
|
@@ -2020,11 +2284,11 @@ async function reconcileWorkspace(workspace, options) {
|
|
|
2020
2284
|
project: workspace.name,
|
|
2021
2285
|
manifest,
|
|
2022
2286
|
stage: options.stage,
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
images: imagePins(workspace),
|
|
2287
|
+
localStage: workspace.stages.local,
|
|
2288
|
+
...backendsOf(workspace),
|
|
2026
2289
|
saved: await loadPortState(workspace.root),
|
|
2027
2290
|
addresses: surfaceAddresses(workspace, manifest),
|
|
2291
|
+
apps: (containers) => appServices(workspace, manifest, containers),
|
|
2028
2292
|
...options.start === void 0 ? {} : { start: options.start }
|
|
2029
2293
|
});
|
|
2030
2294
|
await savePortState(workspace.root, { ...result.ports });
|
|
@@ -2046,26 +2310,20 @@ async function reconcileWorkspace(workspace, options) {
|
|
|
2046
2310
|
* an app serving both its own API and an auth server publishes one address
|
|
2047
2311
|
* twice — which is exactly what it does at runtime.
|
|
2048
2312
|
*/
|
|
2049
|
-
function surfaceAddresses(workspace, manifest) {
|
|
2313
|
+
function surfaceAddresses(workspace, manifest, at = (_app, port) => localAddress(port)) {
|
|
2050
2314
|
const addresses = {};
|
|
2051
2315
|
for (const [id, declaration] of Object.entries(manifest)) {
|
|
2052
2316
|
if (declaration.kind !== "rest-api" && declaration.kind !== "site") continue;
|
|
2053
|
-
const host =
|
|
2054
|
-
const
|
|
2055
|
-
if (
|
|
2317
|
+
const host = require_config.appKey(id);
|
|
2318
|
+
const port = workspace.apps[host]?.port;
|
|
2319
|
+
if (port) addresses[id] = at(host, port);
|
|
2056
2320
|
}
|
|
2057
2321
|
return addresses;
|
|
2058
2322
|
}
|
|
2059
|
-
/** The app key serving a surface, following a mount to its host. */
|
|
2060
|
-
function appKeyOfHost(manifest, id) {
|
|
2061
|
-
const host = require_config.hostOf(manifest, id);
|
|
2062
|
-
return host ? require_config.appKey(host) : void 0;
|
|
2063
|
-
}
|
|
2064
2323
|
/** Where a local process answers, given the port the workspace gave it. */
|
|
2065
2324
|
function localAddress(port) {
|
|
2066
2325
|
return `http://localhost:${port}`;
|
|
2067
2326
|
}
|
|
2068
|
-
|
|
2069
2327
|
//#endregion
|
|
2070
2328
|
//#region src/credentials/index.ts
|
|
2071
2329
|
const logger = console;
|
|
@@ -2098,15 +2356,15 @@ function loadEnvFiles(envConfig, cwd = process.cwd()) {
|
|
|
2098
2356
|
* @internal Exported for testing
|
|
2099
2357
|
*/
|
|
2100
2358
|
async function isPortAvailable(port) {
|
|
2101
|
-
return new Promise((resolve
|
|
2359
|
+
return new Promise((resolve) => {
|
|
2102
2360
|
const server = (0, node_net.createServer)();
|
|
2103
2361
|
server.once("error", (err) => {
|
|
2104
|
-
if (err.code === "EADDRINUSE") resolve
|
|
2105
|
-
else resolve
|
|
2362
|
+
if (err.code === "EADDRINUSE") resolve(false);
|
|
2363
|
+
else resolve(false);
|
|
2106
2364
|
});
|
|
2107
2365
|
server.once("listening", () => {
|
|
2108
2366
|
server.close();
|
|
2109
|
-
resolve
|
|
2367
|
+
resolve(true);
|
|
2110
2368
|
});
|
|
2111
2369
|
server.listen(port);
|
|
2112
2370
|
});
|
|
@@ -2175,11 +2433,10 @@ async function savePortState(workspaceRoot, ports) {
|
|
|
2175
2433
|
*/
|
|
2176
2434
|
function getContainerHostPort(workspaceRoot, service, containerPort) {
|
|
2177
2435
|
try {
|
|
2178
|
-
const
|
|
2436
|
+
const match = (0, node_child_process.execSync)(`docker compose port ${service} ${containerPort}`, {
|
|
2179
2437
|
cwd: workspaceRoot,
|
|
2180
2438
|
stdio: "pipe"
|
|
2181
|
-
}).toString().trim();
|
|
2182
|
-
const match = result.match(/:(\d+)$/);
|
|
2439
|
+
}).toString().trim().match(/:(\d+)$/);
|
|
2183
2440
|
return match ? Number(match[1]) : null;
|
|
2184
2441
|
} catch {
|
|
2185
2442
|
return null;
|
|
@@ -2192,8 +2449,7 @@ function getContainerHostPort(workspaceRoot, service, containerPort) {
|
|
|
2192
2449
|
* @internal Exported for testing
|
|
2193
2450
|
*/
|
|
2194
2451
|
async function resolveServicePorts(workspaceRoot) {
|
|
2195
|
-
const
|
|
2196
|
-
const mappings = parseComposePortMappings(composePath);
|
|
2452
|
+
const mappings = parseComposePortMappings((0, node_path.join)(workspaceRoot, "docker-compose.yml"));
|
|
2197
2453
|
if (mappings.length === 0) return {
|
|
2198
2454
|
dockerEnv: {},
|
|
2199
2455
|
ports: {},
|
|
@@ -2359,14 +2615,13 @@ async function startWorkspaceServices(workspace, portEnv, secretsEnv) {
|
|
|
2359
2615
|
* For workspace app: maps {APP}_DATABASE_URL → DATABASE_URL.
|
|
2360
2616
|
* @internal Exported for testing
|
|
2361
2617
|
*/
|
|
2362
|
-
async function loadSecretsForApp(secretsRoot,
|
|
2618
|
+
async function loadSecretsForApp(secretsRoot, stage, appName) {
|
|
2363
2619
|
let secrets = {};
|
|
2364
|
-
|
|
2620
|
+
if (require_storage.secretsExist(stage, secretsRoot)) {
|
|
2365
2621
|
const stageSecrets = await require_storage.readStageSecrets(stage, secretsRoot);
|
|
2366
2622
|
if (stageSecrets) {
|
|
2367
2623
|
logger.log(`🔐 Loading secrets from stage: ${stage}`);
|
|
2368
2624
|
secrets = require_storage.toEmbeddableSecrets(stageSecrets);
|
|
2369
|
-
break;
|
|
2370
2625
|
}
|
|
2371
2626
|
}
|
|
2372
2627
|
if (Object.keys(secrets).length === 0) return {};
|
|
@@ -2428,13 +2683,12 @@ ${generateCredentialsInjection(secretsJsonPath)}`;
|
|
|
2428
2683
|
* @internal Exported for testing
|
|
2429
2684
|
*/
|
|
2430
2685
|
async function createEntryWrapper(wrapperPath, entryPath, secretsJsonPath) {
|
|
2431
|
-
const credentialsInjection = secretsJsonPath ? `${generateCredentialsInjection(secretsJsonPath)}
|
|
2432
|
-
` : "";
|
|
2433
2686
|
const content = `#!/usr/bin/env node
|
|
2434
2687
|
/**
|
|
2435
2688
|
* Entry wrapper generated by 'gkm dev --entry'
|
|
2436
2689
|
*/
|
|
2437
|
-
${
|
|
2690
|
+
${secretsJsonPath ? `${generateCredentialsInjection(secretsJsonPath)}
|
|
2691
|
+
` : ""}// Import and run the user's entry file (dynamic import ensures secrets load first)
|
|
2438
2692
|
await import('${entryPath}');
|
|
2439
2693
|
`;
|
|
2440
2694
|
await (0, node_fs_promises.writeFile)(wrapperPath, content);
|
|
@@ -2447,7 +2701,7 @@ await import('${entryPath}');
|
|
|
2447
2701
|
* @param options.resolveDockerPorts - How to resolve Docker ports:
|
|
2448
2702
|
* - `'full'` (default): probe running containers, saved state, then find available ports. Used by dev/test.
|
|
2449
2703
|
* - `'readonly'`: check running containers and saved state only, never probe for new ports. Used by exec.
|
|
2450
|
-
* @param options.
|
|
2704
|
+
* @param options.stage - The stage whose secrets to load. Default: the project's `stages.local`.
|
|
2451
2705
|
* @param options.startDocker - Start Docker Compose services after port resolution. Default: false.
|
|
2452
2706
|
* @param options.secretsFileName - Custom secrets JSON filename. Default: 'dev-secrets-{appName}.json' or 'dev-secrets.json'.
|
|
2453
2707
|
* @internal Exported for testing
|
|
@@ -2459,31 +2713,34 @@ async function prepareEntryCredentials(options) {
|
|
|
2459
2713
|
let secretsRoot = cwd;
|
|
2460
2714
|
let appName;
|
|
2461
2715
|
let appInfo;
|
|
2716
|
+
let workspace;
|
|
2462
2717
|
try {
|
|
2463
2718
|
appInfo = await require_config.loadWorkspaceAppInfo(cwd);
|
|
2719
|
+
workspace = appInfo.workspace;
|
|
2464
2720
|
workspaceAppPort = appInfo.app.port;
|
|
2465
2721
|
secretsRoot = appInfo.workspaceRoot;
|
|
2466
2722
|
appName = appInfo.appName;
|
|
2467
2723
|
} catch {
|
|
2468
|
-
|
|
2724
|
+
workspace = await require_config.loadWorkspaceConfig(cwd).then((loaded) => loaded.workspace).catch(() => void 0);
|
|
2725
|
+
secretsRoot = workspace?.root ?? findSecretsRoot(cwd);
|
|
2469
2726
|
appName = require_config.getAppNameFromCwd(cwd) ?? void 0;
|
|
2470
2727
|
}
|
|
2471
2728
|
const resolvedPort = options.explicitPort ?? workspaceAppPort ?? 3e3;
|
|
2472
|
-
const
|
|
2729
|
+
const stage = options.stage ?? workspace?.stages.local;
|
|
2730
|
+
const credentials = stage ? await loadSecretsForApp(secretsRoot, stage, appName) : {};
|
|
2473
2731
|
credentials.PORT = String(resolvedPort);
|
|
2474
2732
|
const declaredKeys = [];
|
|
2475
|
-
if (
|
|
2476
|
-
const { reconcileWorkspace
|
|
2477
|
-
const reconciled = await reconcileWorkspace
|
|
2478
|
-
stage: options.reconcileStage ??
|
|
2733
|
+
if (workspace && usesConstructs(workspace)) {
|
|
2734
|
+
const { reconcileWorkspace } = await Promise.resolve().then(() => workspace_exports);
|
|
2735
|
+
const reconciled = await reconcileWorkspace(workspace, {
|
|
2736
|
+
stage: options.reconcileStage ?? workspace.stages.local,
|
|
2479
2737
|
start: options.startDocker ?? false
|
|
2480
2738
|
});
|
|
2481
2739
|
Object.assign(credentials, reconciled.env);
|
|
2482
2740
|
declaredKeys.push(...Object.keys(reconciled.env));
|
|
2483
2741
|
if (Object.keys(reconciled.env).length > 0) logger.log(`🔌 Resolved ${Object.keys(reconciled.env).length} declared URL(s)`);
|
|
2484
2742
|
} else {
|
|
2485
|
-
const
|
|
2486
|
-
const mappings = parseComposePortMappings(composePath);
|
|
2743
|
+
const mappings = parseComposePortMappings((0, node_path.join)(secretsRoot, "docker-compose.yml"));
|
|
2487
2744
|
if (mappings.length > 0) {
|
|
2488
2745
|
let resolvedPorts;
|
|
2489
2746
|
if (portMode === "full") resolvedPorts = await resolveServicePorts(secretsRoot);
|
|
@@ -2504,8 +2761,10 @@ async function prepareEntryCredentials(options) {
|
|
|
2504
2761
|
mappings
|
|
2505
2762
|
};
|
|
2506
2763
|
}
|
|
2507
|
-
if (options.startDocker)
|
|
2508
|
-
|
|
2764
|
+
if (options.startDocker) {
|
|
2765
|
+
if (appInfo) await startWorkspaceServices(appInfo.workspace, resolvedPorts.dockerEnv, credentials);
|
|
2766
|
+
else await startComposeServices(secretsRoot, resolvedPorts.dockerEnv, credentials);
|
|
2767
|
+
}
|
|
2509
2768
|
if (Object.keys(resolvedPorts.ports).length > 0) {
|
|
2510
2769
|
const rewritten = rewriteUrlsWithPorts(credentials, resolvedPorts);
|
|
2511
2770
|
Object.assign(credentials, rewritten);
|
|
@@ -2543,138 +2802,144 @@ async function prepareEntryCredentials(options) {
|
|
|
2543
2802
|
declaredKeys
|
|
2544
2803
|
};
|
|
2545
2804
|
}
|
|
2546
|
-
|
|
2547
2805
|
//#endregion
|
|
2548
|
-
Object.defineProperty(exports,
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2806
|
+
Object.defineProperty(exports, "COMPOSE_PATH", {
|
|
2807
|
+
enumerable: true,
|
|
2808
|
+
get: function() {
|
|
2809
|
+
return COMPOSE_PATH;
|
|
2810
|
+
}
|
|
2553
2811
|
});
|
|
2554
|
-
Object.defineProperty(exports,
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2812
|
+
Object.defineProperty(exports, "LOCAL_CA_PATH", {
|
|
2813
|
+
enumerable: true,
|
|
2814
|
+
get: function() {
|
|
2815
|
+
return LOCAL_CA_PATH;
|
|
2816
|
+
}
|
|
2817
|
+
});
|
|
2818
|
+
Object.defineProperty(exports, "applyPostgres", {
|
|
2819
|
+
enumerable: true,
|
|
2820
|
+
get: function() {
|
|
2821
|
+
return applyPostgres;
|
|
2822
|
+
}
|
|
2559
2823
|
});
|
|
2560
|
-
Object.defineProperty(exports,
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2824
|
+
Object.defineProperty(exports, "cacheBackendFor", {
|
|
2825
|
+
enumerable: true,
|
|
2826
|
+
get: function() {
|
|
2827
|
+
return cacheBackendFor;
|
|
2828
|
+
}
|
|
2565
2829
|
});
|
|
2566
|
-
Object.defineProperty(exports,
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2830
|
+
Object.defineProperty(exports, "constructGlobs", {
|
|
2831
|
+
enumerable: true,
|
|
2832
|
+
get: function() {
|
|
2833
|
+
return constructGlobs;
|
|
2834
|
+
}
|
|
2571
2835
|
});
|
|
2572
|
-
Object.defineProperty(exports,
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2836
|
+
Object.defineProperty(exports, "createCredentialsPreload", {
|
|
2837
|
+
enumerable: true,
|
|
2838
|
+
get: function() {
|
|
2839
|
+
return createCredentialsPreload;
|
|
2840
|
+
}
|
|
2577
2841
|
});
|
|
2578
|
-
Object.defineProperty(exports,
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2842
|
+
Object.defineProperty(exports, "createEntryWrapper", {
|
|
2843
|
+
enumerable: true,
|
|
2844
|
+
get: function() {
|
|
2845
|
+
return createEntryWrapper;
|
|
2846
|
+
}
|
|
2583
2847
|
});
|
|
2584
|
-
Object.defineProperty(exports,
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2848
|
+
Object.defineProperty(exports, "derivedContainers", {
|
|
2849
|
+
enumerable: true,
|
|
2850
|
+
get: function() {
|
|
2851
|
+
return derivedContainers;
|
|
2852
|
+
}
|
|
2589
2853
|
});
|
|
2590
|
-
Object.defineProperty(exports,
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2854
|
+
Object.defineProperty(exports, "eventsBackendFor", {
|
|
2855
|
+
enumerable: true,
|
|
2856
|
+
get: function() {
|
|
2857
|
+
return eventsBackendFor;
|
|
2858
|
+
}
|
|
2595
2859
|
});
|
|
2596
|
-
Object.defineProperty(exports,
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2860
|
+
Object.defineProperty(exports, "findAvailablePort", {
|
|
2861
|
+
enumerable: true,
|
|
2862
|
+
get: function() {
|
|
2863
|
+
return findAvailablePort;
|
|
2864
|
+
}
|
|
2601
2865
|
});
|
|
2602
|
-
Object.defineProperty(exports,
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2866
|
+
Object.defineProperty(exports, "isPortAvailable", {
|
|
2867
|
+
enumerable: true,
|
|
2868
|
+
get: function() {
|
|
2869
|
+
return isPortAvailable;
|
|
2870
|
+
}
|
|
2607
2871
|
});
|
|
2608
|
-
Object.defineProperty(exports,
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2872
|
+
Object.defineProperty(exports, "loadEnvFiles", {
|
|
2873
|
+
enumerable: true,
|
|
2874
|
+
get: function() {
|
|
2875
|
+
return loadEnvFiles;
|
|
2876
|
+
}
|
|
2613
2877
|
});
|
|
2614
|
-
Object.defineProperty(exports,
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2878
|
+
Object.defineProperty(exports, "loadSecretsForApp", {
|
|
2879
|
+
enumerable: true,
|
|
2880
|
+
get: function() {
|
|
2881
|
+
return loadSecretsForApp;
|
|
2882
|
+
}
|
|
2619
2883
|
});
|
|
2620
|
-
Object.defineProperty(exports,
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2884
|
+
Object.defineProperty(exports, "prepareEntryCredentials", {
|
|
2885
|
+
enumerable: true,
|
|
2886
|
+
get: function() {
|
|
2887
|
+
return prepareEntryCredentials;
|
|
2888
|
+
}
|
|
2625
2889
|
});
|
|
2626
|
-
Object.defineProperty(exports,
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2890
|
+
Object.defineProperty(exports, "providerOf", {
|
|
2891
|
+
enumerable: true,
|
|
2892
|
+
get: function() {
|
|
2893
|
+
return providerOf;
|
|
2894
|
+
}
|
|
2631
2895
|
});
|
|
2632
|
-
Object.defineProperty(exports,
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2896
|
+
Object.defineProperty(exports, "reconcileWorkspace", {
|
|
2897
|
+
enumerable: true,
|
|
2898
|
+
get: function() {
|
|
2899
|
+
return reconcileWorkspace;
|
|
2900
|
+
}
|
|
2637
2901
|
});
|
|
2638
|
-
Object.defineProperty(exports,
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2902
|
+
Object.defineProperty(exports, "resolveServicePorts", {
|
|
2903
|
+
enumerable: true,
|
|
2904
|
+
get: function() {
|
|
2905
|
+
return resolveServicePorts;
|
|
2906
|
+
}
|
|
2643
2907
|
});
|
|
2644
|
-
Object.defineProperty(exports,
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2908
|
+
Object.defineProperty(exports, "resourceName", {
|
|
2909
|
+
enumerable: true,
|
|
2910
|
+
get: function() {
|
|
2911
|
+
return resourceName;
|
|
2912
|
+
}
|
|
2649
2913
|
});
|
|
2650
|
-
Object.defineProperty(exports,
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2914
|
+
Object.defineProperty(exports, "rewriteUrlsWithPorts", {
|
|
2915
|
+
enumerable: true,
|
|
2916
|
+
get: function() {
|
|
2917
|
+
return rewriteUrlsWithPorts;
|
|
2918
|
+
}
|
|
2655
2919
|
});
|
|
2656
|
-
Object.defineProperty(exports,
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2920
|
+
Object.defineProperty(exports, "startWorkspaceServices", {
|
|
2921
|
+
enumerable: true,
|
|
2922
|
+
get: function() {
|
|
2923
|
+
return startWorkspaceServices;
|
|
2924
|
+
}
|
|
2661
2925
|
});
|
|
2662
|
-
Object.defineProperty(exports,
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2926
|
+
Object.defineProperty(exports, "storageBackendFor", {
|
|
2927
|
+
enumerable: true,
|
|
2928
|
+
get: function() {
|
|
2929
|
+
return storageBackendFor;
|
|
2930
|
+
}
|
|
2667
2931
|
});
|
|
2668
|
-
Object.defineProperty(exports,
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2932
|
+
Object.defineProperty(exports, "usesConstructs", {
|
|
2933
|
+
enumerable: true,
|
|
2934
|
+
get: function() {
|
|
2935
|
+
return usesConstructs;
|
|
2936
|
+
}
|
|
2673
2937
|
});
|
|
2674
|
-
Object.defineProperty(exports,
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2938
|
+
Object.defineProperty(exports, "workspace_exports", {
|
|
2939
|
+
enumerable: true,
|
|
2940
|
+
get: function() {
|
|
2941
|
+
return workspace_exports;
|
|
2942
|
+
}
|
|
2679
2943
|
});
|
|
2680
|
-
|
|
2944
|
+
|
|
2945
|
+
//# sourceMappingURL=credentials-BfmmB4WD.cjs.map
|