@caelo-cms/provisioning 0.10.49 → 0.10.51
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/caddy/Caddyfile.production +4 -0
- package/caddy/Caddyfile.staging +4 -0
- package/dist/adapter.d.ts +4 -6
- package/dist/adapter.d.ts.map +1 -1
- package/dist/admin-internal.d.ts +41 -0
- package/dist/admin-internal.d.ts.map +1 -0
- package/dist/admin-internal.js +169 -0
- package/dist/admin-internal.js.map +1 -0
- package/dist/agent-db.js +93 -20
- package/dist/aws-task-roles.d.ts +27 -0
- package/dist/aws-task-roles.d.ts.map +1 -0
- package/dist/aws-task-roles.js +35 -0
- package/dist/aws-task-roles.js.map +1 -0
- package/dist/caddy-redirects.d.ts +15 -0
- package/dist/caddy-redirects.d.ts.map +1 -0
- package/dist/caddy-redirects.js +65 -0
- package/dist/caddy-redirects.js.map +1 -0
- package/dist/caddy.d.ts +22 -2
- package/dist/caddy.d.ts.map +1 -1
- package/dist/caddy.js +66 -17
- package/dist/caddy.js.map +1 -1
- package/dist/cli-routing.d.ts.map +1 -1
- package/dist/cli-routing.js +7 -0
- package/dist/cli-routing.js.map +1 -1
- package/dist/cli.js +282 -255
- package/dist/cli.js.map +1 -1
- package/dist/compose.d.ts +54 -2
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +88 -34
- package/dist/compose.js.map +1 -1
- package/dist/deployed-release.d.ts +3 -2
- package/dist/deployed-release.d.ts.map +1 -1
- package/dist/deployed-release.js +12 -5
- package/dist/deployed-release.js.map +1 -1
- package/dist/gateway-credentials.d.ts.map +1 -1
- package/dist/gateway-credentials.js +6 -3
- package/dist/gateway-credentials.js.map +1 -1
- package/dist/gcloud.d.ts +2 -2
- package/dist/gcloud.d.ts.map +1 -1
- package/dist/gcloud.js +3 -3
- package/dist/gcloud.js.map +1 -1
- package/dist/gcp-names.d.ts +12 -0
- package/dist/gcp-names.d.ts.map +1 -1
- package/dist/gcp-names.js +21 -0
- package/dist/gcp-names.js.map +1 -1
- package/dist/gcp-url-map.d.ts +27 -0
- package/dist/gcp-url-map.d.ts.map +1 -0
- package/dist/gcp-url-map.js +62 -0
- package/dist/gcp-url-map.js.map +1 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/infra-contract.d.ts +20 -1
- package/dist/infra-contract.d.ts.map +1 -1
- package/dist/infra-contract.js +44 -1
- package/dist/infra-contract.js.map +1 -1
- package/dist/owner-reset.d.ts +37 -0
- package/dist/owner-reset.d.ts.map +1 -0
- package/dist/owner-reset.js +80 -0
- package/dist/owner-reset.js.map +1 -0
- package/dist/public-role-isolation.d.ts +19 -0
- package/dist/public-role-isolation.d.ts.map +1 -0
- package/dist/public-role-isolation.js +69 -0
- package/dist/public-role-isolation.js.map +1 -0
- package/dist/release-contract.d.ts.map +1 -1
- package/dist/release-contract.js +3 -0
- package/dist/release-contract.js.map +1 -1
- package/dist/release-manifest.d.ts.map +1 -1
- package/dist/release-manifest.js +2 -1
- package/dist/release-manifest.js.map +1 -1
- package/dist/self-hosted-backup.d.ts +86 -0
- package/dist/self-hosted-backup.d.ts.map +1 -0
- package/dist/self-hosted-backup.js +350 -0
- package/dist/self-hosted-backup.js.map +1 -0
- package/dist/self-hosted-release.d.ts +12 -3
- package/dist/self-hosted-release.d.ts.map +1 -1
- package/dist/self-hosted-release.js +6 -4
- package/dist/self-hosted-release.js.map +1 -1
- package/dist/self-hosted-start.d.ts +50 -0
- package/dist/self-hosted-start.d.ts.map +1 -0
- package/dist/self-hosted-start.js +59 -0
- package/dist/self-hosted-start.js.map +1 -0
- package/dist/self-hosted-upgrade.d.ts +30 -0
- package/dist/self-hosted-upgrade.d.ts.map +1 -1
- package/dist/self-hosted-upgrade.js +29 -7
- package/dist/self-hosted-upgrade.js.map +1 -1
- package/dist/stack-contract.d.ts +9 -10
- package/dist/stack-contract.d.ts.map +1 -1
- package/dist/stack-contract.js +21 -10
- package/dist/stack-contract.js.map +1 -1
- package/dist/staging-admin.d.ts +12 -20
- package/dist/staging-admin.d.ts.map +1 -1
- package/dist/staging-admin.js +7 -159
- package/dist/staging-admin.js.map +1 -1
- package/dist/tar-entries.d.ts +14 -0
- package/dist/tar-entries.d.ts.map +1 -0
- package/dist/tar-entries.js +104 -0
- package/dist/tar-entries.js.map +1 -0
- package/dist/upgrade/converge.d.ts +2 -2
- package/dist/upgrade/converge.d.ts.map +1 -1
- package/dist/upgrade/converge.js +44 -2
- package/dist/upgrade/converge.js.map +1 -1
- package/dist/upgrader-assess.d.ts.map +1 -1
- package/dist/upgrader-assess.js +2 -1
- package/dist/upgrader-assess.js.map +1 -1
- package/dist/url-map-redirects-access.d.ts +15 -0
- package/dist/url-map-redirects-access.d.ts.map +1 -0
- package/dist/url-map-redirects-access.js +81 -0
- package/dist/url-map-redirects-access.js.map +1 -0
- package/dist/wizard.d.ts.map +1 -1
- package/dist/wizard.js +3 -1
- package/dist/wizard.js.map +1 -1
- package/dist/wizards/gcp-cost.js +1 -1
- package/dist/wizards/gcp.d.ts.map +1 -1
- package/dist/wizards/gcp.js +45 -0
- package/dist/wizards/gcp.js.map +1 -1
- package/package.json +2 -2
- package/src/adapter.ts +4 -6
- package/src/admin-internal.ts +212 -0
- package/src/admin-memory.test.ts +2 -2
- package/src/aws-task-roles.ts +58 -0
- package/src/caddy-redirects.docker.test.ts +255 -0
- package/src/caddy-redirects.test.ts +115 -0
- package/src/caddy-redirects.ts +68 -0
- package/src/caddy-validate.test.ts +119 -0
- package/src/caddy.test.ts +108 -0
- package/src/caddy.ts +89 -20
- package/src/checkpoint-secrets.test.ts +0 -1
- package/src/cli-routing.test.ts +12 -0
- package/src/cli-routing.ts +7 -0
- package/src/cli.ts +340 -288
- package/src/compose.ts +108 -36
- package/src/deployed-release.test.ts +22 -4
- package/src/deployed-release.ts +14 -7
- package/src/gateway-credentials.ts +6 -3
- package/src/gateway-env-isolation.test.ts +212 -0
- package/src/gcloud.ts +5 -3
- package/src/gcp-names.ts +23 -0
- package/src/gcp-url-map.test.ts +150 -0
- package/src/gcp-url-map.ts +81 -0
- package/src/index.test.ts +24 -152
- package/src/index.ts +0 -9
- package/src/infra-contract.test.ts +40 -0
- package/src/infra-contract.ts +47 -1
- package/src/owner-reset.test.ts +165 -0
- package/src/owner-reset.ts +106 -0
- package/src/public-role-isolation.test.ts +89 -0
- package/src/public-role-isolation.ts +83 -0
- package/src/release-contract.ts +3 -0
- package/src/release-manifest.test.ts +12 -0
- package/src/release-manifest.ts +2 -1
- package/src/runtime-secrets.test.ts +25 -8
- package/src/self-hosted-backup.test.ts +344 -0
- package/src/self-hosted-backup.ts +480 -0
- package/src/self-hosted-compose.test.ts +88 -4
- package/src/self-hosted-release.ts +22 -7
- package/src/self-hosted-start.test.ts +68 -0
- package/src/self-hosted-start.ts +123 -0
- package/src/self-hosted-upgrade.ts +35 -8
- package/src/stack-contract.test.ts +11 -20
- package/src/stack-contract.ts +25 -16
- package/src/stack-converge-secrets.test.ts +45 -10
- package/src/stack-converge.test.ts +19 -10
- package/src/staging-admin.ts +18 -181
- package/src/tar-entries.test.ts +52 -0
- package/src/tar-entries.ts +116 -0
- package/src/upgrade/converge.ts +51 -4
- package/src/upgrade/upgrade.test.ts +78 -1
- package/src/upgrader-assess.test.ts +22 -0
- package/src/upgrader-assess.ts +2 -1
- package/src/url-map-redirects-access.test.ts +88 -0
- package/src/url-map-redirects-access.ts +89 -0
- package/src/wizard.ts +3 -1
- package/src/wizards/gcp-cost.ts +1 -1
- package/src/wizards/gcp.ts +48 -0
- package/stacks/aws/README.md +1 -1
- package/stacks/aws/index.ts +89 -45
- package/stacks/azure/README.md +1 -1
- package/stacks/azure/index.ts +21 -14
- package/stacks/gcp/README.md +1 -1
- package/stacks/gcp/index.ts +26 -22
- package/stacks/gcp-firebase/index.ts +7 -3
- package/stacks/self-hosted/Pulumi.yaml +2 -2
- package/stacks/self-hosted/README.md +3 -3
- package/stacks/self-hosted/index.ts +45 -22
- package/dist/redirects-emit.d.ts +0 -52
- package/dist/redirects-emit.d.ts.map +0 -1
- package/dist/redirects-emit.js +0 -111
- package/dist/redirects-emit.js.map +0 -1
- package/src/redirects-emit.ts +0 -175
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* #587 — `cms-provision upgrade` gives older `gcp` installs the admin's
|
|
5
|
+
* URL-map redirects role: create or update the custom role, undelete a
|
|
6
|
+
* soft-deleted one, bind it to the admin's run SA.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, expect, it } from "bun:test";
|
|
10
|
+
import type { GcloudResult } from "./gcloud.js";
|
|
11
|
+
import { ensureUrlMapRedirectsAccess } from "./url-map-redirects-access.js";
|
|
12
|
+
|
|
13
|
+
const ok = (stdout = ""): GcloudResult => ({ ok: true, stdout, stderr: "", exitCode: 0 });
|
|
14
|
+
const fail = (stderr: string): GcloudResult => ({ ok: false, stdout: "", stderr, exitCode: 1 });
|
|
15
|
+
|
|
16
|
+
/** The gcloud verb of a call (`projects add-iam-policy-binding` has it second). */
|
|
17
|
+
const verb = (c: string[]) => (c[0] === "projects" ? c[1] : c[2]);
|
|
18
|
+
|
|
19
|
+
function runner(answer: (args: string[]) => GcloudResult) {
|
|
20
|
+
const calls: string[][] = [];
|
|
21
|
+
return {
|
|
22
|
+
calls,
|
|
23
|
+
run: async (args: string[]) => {
|
|
24
|
+
calls.push(args);
|
|
25
|
+
return answer(args);
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const PERMS =
|
|
31
|
+
"--permissions=compute.urlMaps.list,compute.urlMaps.get,compute.urlMaps.update,compute.backendBuckets.use,compute.backendServices.use,compute.globalOperations.get";
|
|
32
|
+
const BIND = [
|
|
33
|
+
"projects",
|
|
34
|
+
"add-iam-policy-binding",
|
|
35
|
+
"p1",
|
|
36
|
+
"--member=serviceAccount:caelo-production-run-sa@p1.iam.gserviceaccount.com",
|
|
37
|
+
"--role=projects/p1/roles/caelo_production_urlMapRedirects",
|
|
38
|
+
"--condition=None",
|
|
39
|
+
"--quiet",
|
|
40
|
+
"--format=none",
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
describe("ensureUrlMapRedirectsAccess", () => {
|
|
44
|
+
it("creates the role when missing, then binds it", async () => {
|
|
45
|
+
const r = runner((a) => (a[2] === "describe" ? fail("NOT_FOUND") : ok()));
|
|
46
|
+
const out = await ensureUrlMapRedirectsAccess({
|
|
47
|
+
projectId: "p1",
|
|
48
|
+
env: "production",
|
|
49
|
+
run: r.run,
|
|
50
|
+
});
|
|
51
|
+
expect(out).toEqual({ ok: true, role: "projects/p1/roles/caelo_production_urlMapRedirects" });
|
|
52
|
+
expect(r.calls[1]?.slice(0, 5)).toEqual([
|
|
53
|
+
"iam",
|
|
54
|
+
"roles",
|
|
55
|
+
"create",
|
|
56
|
+
"caelo_production_urlMapRedirects",
|
|
57
|
+
"--project=p1",
|
|
58
|
+
]);
|
|
59
|
+
expect(r.calls[1]).toContain(PERMS);
|
|
60
|
+
expect(r.calls[2]).toEqual(BIND);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("updates an existing role to the current permissions (no drift)", async () => {
|
|
64
|
+
const r = runner((a) => (a[2] === "describe" ? ok('{"name":"x"}') : ok()));
|
|
65
|
+
await ensureUrlMapRedirectsAccess({ projectId: "p1", env: "production", run: r.run });
|
|
66
|
+
expect(r.calls.map(verb)).toEqual(["describe", "update", "add-iam-policy-binding"]);
|
|
67
|
+
expect(r.calls[1]).toContain(PERMS);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("undeletes a soft-deleted role first", async () => {
|
|
71
|
+
const r = runner((a) => (a[2] === "describe" ? ok('{"deleted":true}') : ok()));
|
|
72
|
+
await ensureUrlMapRedirectsAccess({ projectId: "p1", env: "production", run: r.run });
|
|
73
|
+
expect(r.calls.map(verb)).toEqual(["describe", "undelete", "update", "add-iam-policy-binding"]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("reports a failure instead of throwing", async () => {
|
|
77
|
+
const r = runner((a) =>
|
|
78
|
+
a[2] === "describe" ? fail("NOT_FOUND") : fail("PERMISSION_DENIED: iam.roles.create"),
|
|
79
|
+
);
|
|
80
|
+
expect(
|
|
81
|
+
await ensureUrlMapRedirectsAccess({ projectId: "p1", env: "production", run: r.run }),
|
|
82
|
+
).toEqual({
|
|
83
|
+
ok: false,
|
|
84
|
+
error:
|
|
85
|
+
"create projects/p1/roles/caelo_production_urlMapRedirects: PERMISSION_DENIED: iam.roles.create",
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The admin's URL-map redirects role (#587) on `gcp` installs. The installer
|
|
5
|
+
* (wizards/gcp.ts) and `cms-provision upgrade` both call
|
|
6
|
+
* {@link ensureUrlMapRedirectsAccess}, so new and older installs get the same
|
|
7
|
+
* custom role + binding. The stack does not declare it: a role id exists once
|
|
8
|
+
* per project, so a stack-created role would collide with one upgrade made.
|
|
9
|
+
*
|
|
10
|
+
* 1. project custom role {@link urlMapRedirectsRoleId} with exactly
|
|
11
|
+
* {@link URL_MAP_REDIRECTS_PERMISSIONS} (created, or updated to that
|
|
12
|
+
* permission set; undeleted first if it was soft-deleted)
|
|
13
|
+
* 2. that role for the admin's run SA on the project
|
|
14
|
+
*
|
|
15
|
+
* Both steps are idempotent. Without them Confirm-publish fails with a 403
|
|
16
|
+
* when it writes the redirects into the URL map.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { gcloud as defaultGcloud } from "./gcloud.js";
|
|
20
|
+
import { type GcloudRunner, realSleep, runWithRetry, type Sleep } from "./gcloud-retry.js";
|
|
21
|
+
import {
|
|
22
|
+
runServiceAccountEmail,
|
|
23
|
+
URL_MAP_REDIRECTS_PERMISSIONS,
|
|
24
|
+
urlMapRedirectsRoleId,
|
|
25
|
+
} from "./gcp-names.js";
|
|
26
|
+
|
|
27
|
+
/** Ensure the custom role and the admin's binding; never throws for gcloud. */
|
|
28
|
+
export async function ensureUrlMapRedirectsAccess(opts: {
|
|
29
|
+
readonly projectId: string;
|
|
30
|
+
readonly env: string;
|
|
31
|
+
readonly run?: GcloudRunner;
|
|
32
|
+
readonly sleep?: Sleep;
|
|
33
|
+
}): Promise<{ ok: true; role: string } | { ok: false; error: string }> {
|
|
34
|
+
const run = opts.run ?? defaultGcloud;
|
|
35
|
+
const sleep = opts.sleep ?? realSleep;
|
|
36
|
+
const project = `--project=${opts.projectId}`;
|
|
37
|
+
const roleId = urlMapRedirectsRoleId(opts.env);
|
|
38
|
+
const role = `projects/${opts.projectId}/roles/${roleId}`;
|
|
39
|
+
const permissions = `--permissions=${URL_MAP_REDIRECTS_PERMISSIONS.join(",")}`;
|
|
40
|
+
|
|
41
|
+
const describe = await run(["iam", "roles", "describe", roleId, project, "--format=json"]);
|
|
42
|
+
if (describe.ok) {
|
|
43
|
+
const live = JSON.parse(describe.stdout) as { deleted?: boolean };
|
|
44
|
+
if (live.deleted) {
|
|
45
|
+
const undelete = await run(["iam", "roles", "undelete", roleId, project, "--quiet"]);
|
|
46
|
+
if (!undelete.ok) return { ok: false, error: `undelete ${role}: ${undelete.stderr.trim()}` };
|
|
47
|
+
}
|
|
48
|
+
const update = await run([
|
|
49
|
+
"iam",
|
|
50
|
+
"roles",
|
|
51
|
+
"update",
|
|
52
|
+
roleId,
|
|
53
|
+
project,
|
|
54
|
+
permissions,
|
|
55
|
+
"--quiet",
|
|
56
|
+
"--format=none",
|
|
57
|
+
]);
|
|
58
|
+
if (!update.ok) return { ok: false, error: `update ${role}: ${update.stderr.trim()}` };
|
|
59
|
+
} else {
|
|
60
|
+
const create = await run([
|
|
61
|
+
"iam",
|
|
62
|
+
"roles",
|
|
63
|
+
"create",
|
|
64
|
+
roleId,
|
|
65
|
+
project,
|
|
66
|
+
"--title=Caelo admin: URL-map redirects",
|
|
67
|
+
"--description=Lets the Caelo admin write the redirects table into the site's URL map (#587).",
|
|
68
|
+
permissions,
|
|
69
|
+
"--stage=GA",
|
|
70
|
+
"--quiet",
|
|
71
|
+
"--format=none",
|
|
72
|
+
]);
|
|
73
|
+
if (!create.ok) return { ok: false, error: `create ${role}: ${create.stderr.trim()}` };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// A just-created role takes a few seconds to become bindable.
|
|
77
|
+
const bind = await runWithRetry(run, sleep, [
|
|
78
|
+
"projects",
|
|
79
|
+
"add-iam-policy-binding",
|
|
80
|
+
opts.projectId,
|
|
81
|
+
`--member=serviceAccount:${runServiceAccountEmail(opts.projectId, opts.env)}`,
|
|
82
|
+
`--role=${role}`,
|
|
83
|
+
"--condition=None",
|
|
84
|
+
"--quiet",
|
|
85
|
+
"--format=none",
|
|
86
|
+
]);
|
|
87
|
+
if (!bind.ok) return { ok: false, error: `grant ${role}: ${bind.stderr.trim()}` };
|
|
88
|
+
return { ok: true, role };
|
|
89
|
+
}
|
package/src/wizard.ts
CHANGED
|
@@ -222,8 +222,10 @@ export async function runWizard(opts: WizardOptions = {}): Promise<void> {
|
|
|
222
222
|
});
|
|
223
223
|
break;
|
|
224
224
|
case "self-hosted":
|
|
225
|
+
// The self-hosted install runs on the Docker host itself, from the
|
|
226
|
+
// directory that should hold `.caelo/` — not from this wizard.
|
|
225
227
|
cancel(
|
|
226
|
-
`
|
|
228
|
+
`Run this on the Docker host, in the directory that should hold the install:\n ${selfCommand("--provider", "self-hosted", "--domain", domain, "--owner-email", ownerEmail)}`,
|
|
227
229
|
);
|
|
228
230
|
process.exit(0);
|
|
229
231
|
break;
|
package/src/wizards/gcp-cost.ts
CHANGED
|
@@ -104,7 +104,7 @@ export function estimateGcpCost(inputs: CostEstimateInputs): {
|
|
|
104
104
|
name: "Quality checks (Lighthouse on the admin)",
|
|
105
105
|
monthlyUsd: 1,
|
|
106
106
|
notes:
|
|
107
|
-
"~
|
|
107
|
+
"~2-4 min of admin CPU per audited Stage (up to 5 pages x 5 runs); ~$1/mo at a few Stages a day",
|
|
108
108
|
},
|
|
109
109
|
{
|
|
110
110
|
name: "Cloud Run gateway",
|
package/src/wizards/gcp.ts
CHANGED
|
@@ -99,6 +99,7 @@ import {
|
|
|
99
99
|
PROVISIONER_ACCOUNT_ID,
|
|
100
100
|
provisionerServiceAccountEmail,
|
|
101
101
|
} from "../provisioner-identity.js";
|
|
102
|
+
import { revokePublicRoleMemberships } from "../public-role-isolation.js";
|
|
102
103
|
import { decideRegion, detectCliRegion, installableRegions, suggestRegion } from "../regions.js";
|
|
103
104
|
import type { RunMode } from "../run-mode.js";
|
|
104
105
|
import { selfCommand } from "../self-command.js";
|
|
@@ -120,6 +121,7 @@ import {
|
|
|
120
121
|
verifyCosignAll,
|
|
121
122
|
} from "../upgrade/release-images.js";
|
|
122
123
|
import { ensureUpgraderJob, resolveUpgraderTarget } from "../upgrader-setup.js";
|
|
124
|
+
import { ensureUrlMapRedirectsAccess } from "../url-map-redirects-access.js";
|
|
123
125
|
import { estimateGcpCost } from "./gcp-cost.js";
|
|
124
126
|
import { promptProjectId } from "./gcp-prompts.js";
|
|
125
127
|
import { GCP_STACK_KNOB_DEFAULTS, pulumiUpGcp, readStackOutput } from "./gcp-pulumi.js";
|
|
@@ -430,6 +432,17 @@ export async function runGcpConverge(opts: GcpConvergeOpts): Promise<void> {
|
|
|
430
432
|
// P21 ship 3 — shared with `cms-provision upgrade`. Aborts the
|
|
431
433
|
// wizard with a clear error if migrations fail (vs. silently
|
|
432
434
|
// continuing with a half-migrated DB).
|
|
435
|
+
// === 10.9. public_role out of cloudsqlsuperuser (#633) ===
|
|
436
|
+
// The stack creates public_role through the Cloud SQL Admin API, which
|
|
437
|
+
// makes it a member of cloudsqlsuperuser. Revoked before migrations: the
|
|
438
|
+
// migration job's role audit verifies it and fails the install otherwise.
|
|
439
|
+
await stepPublicRoleIsolation(projectId);
|
|
440
|
+
|
|
441
|
+
// === 10.95. The admin may write redirects into the URL map (#587) ===
|
|
442
|
+
// Confirm-publish writes the redirects table into the load balancer's URL
|
|
443
|
+
// map; the custom role is the CLI's, not the stack's (url-map-redirects-access.ts).
|
|
444
|
+
if (provider === "gcp") await stepUrlMapRedirectsAccess(projectId);
|
|
445
|
+
|
|
433
446
|
phase("Applying database migrations");
|
|
434
447
|
{
|
|
435
448
|
const { runMigrationsViaCloudRunJob } = await import("../migration-runner.js");
|
|
@@ -886,6 +899,41 @@ async function stepGrantBaseRoles(
|
|
|
886
899
|
markStepDone(installId, stepName, { granted });
|
|
887
900
|
}
|
|
888
901
|
|
|
902
|
+
/**
|
|
903
|
+
* Revoke the database roles the Cloud SQL Admin API gave `public_role`
|
|
904
|
+
* (cloudsqlsuperuser, #633). Idempotent, so it runs on every wizard pass;
|
|
905
|
+
* the migration job's role audit verifies the result.
|
|
906
|
+
*/
|
|
907
|
+
async function stepPublicRoleIsolation(projectId: string): Promise<void> {
|
|
908
|
+
const s = spinner();
|
|
909
|
+
s.start("Removing public_role from the Cloud SQL superuser group...");
|
|
910
|
+
const r = await revokePublicRoleMemberships({ projectId, env: GCP_STACK_ENV });
|
|
911
|
+
if (r.status === "failed") {
|
|
912
|
+
s.stop(red(`Failed: ${r.error ?? ""}`));
|
|
913
|
+
log.error("Re-run the wizard once the cause is fixed (this step is idempotent).");
|
|
914
|
+
cancel("Aborted.");
|
|
915
|
+
process.exit(1);
|
|
916
|
+
}
|
|
917
|
+
s.stop(green("public_role holds no database roles"));
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* The admin's custom role for writing the redirects table into the URL map
|
|
922
|
+
* (#587). Idempotent, so it runs on every wizard pass.
|
|
923
|
+
*/
|
|
924
|
+
async function stepUrlMapRedirectsAccess(projectId: string): Promise<void> {
|
|
925
|
+
const s = spinner();
|
|
926
|
+
s.start("Letting the admin serve redirects from the load balancer...");
|
|
927
|
+
const r = await ensureUrlMapRedirectsAccess({ projectId, env: GCP_STACK_ENV });
|
|
928
|
+
if (!r.ok) {
|
|
929
|
+
s.stop(red(`Failed: ${r.error}`));
|
|
930
|
+
log.error("Re-run the wizard once the cause is fixed (this step is idempotent).");
|
|
931
|
+
cancel("Aborted.");
|
|
932
|
+
process.exit(1);
|
|
933
|
+
}
|
|
934
|
+
s.stop(green(`Redirects role ready (${r.role})`));
|
|
935
|
+
}
|
|
936
|
+
|
|
889
937
|
/**
|
|
890
938
|
* Give `gateway_role` (created by migration 0248) its password from the
|
|
891
939
|
* `gateway-role-password` secret (#613). public_role already has its own:
|
package/stacks/aws/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Pulumi stack provisioning Caelo on AWS managed services. Implements the shared `
|
|
|
10
10
|
| Blob storage | Two S3 buckets (`caelo-<env>-media` + `caelo-<env>-static`) |
|
|
11
11
|
| CDN | CloudFront distribution with two origins (S3 static + S3 media); static files are served as files, no edge compute on the request path |
|
|
12
12
|
| Container runtime | ECS Fargate cluster + four services (admin / gateway / orchestrator / runner) |
|
|
13
|
-
| Secret store | AWS Secrets Manager (postgres-password, public-role-password, gateway-role-password, csrf-secret, cookie-secret, anthropic-api-key, resend-api-key). The gateway gets `
|
|
13
|
+
| Secret store | AWS Secrets Manager (postgres-password, public-role-password, gateway-role-password, csrf-secret, cookie-secret, anthropic-api-key, resend-api-key). The gateway gets `publicDatabaseUrlOut` only, never a cms_admin URL of any role (#613, #635). |
|
|
14
14
|
| DNS + cert | ACM cert (us-east-1, DNS-validated, supports `*.<domain>`) |
|
|
15
15
|
| Edge-log sink | CloudFront access logs delivered to the CloudWatch log group `/caelo/<env>/cloudfront` in us-east-1 (queryable by P12A analytics plugin) |
|
|
16
16
|
|
package/stacks/aws/index.ts
CHANGED
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
* - Bootstrap script runs as a one-shot ECS task post-create.
|
|
12
12
|
* 3. S3 buckets: caelo-media + caelo-static (per env).
|
|
13
13
|
* 4. CloudFront distribution with two origins (S3 static + ALB-fronted ECS).
|
|
14
|
-
* Static files are served as files
|
|
15
|
-
*
|
|
14
|
+
* Static files are served as files (CLAUDE.md §11.B Tier 1); the only
|
|
15
|
+
* edge code is the redirects table's CloudFront Function, a KeyValueStore
|
|
16
|
+
* lookup that runs before the origin (#587).
|
|
16
17
|
* 5. ECS Fargate cluster + four services (admin, gateway, orchestrator, runner).
|
|
17
18
|
* 6. Secrets Manager entries (postgres-password, anthropic-api-key, ...).
|
|
18
19
|
* 7. Route 53 hosted zone + ACM certs per (domain, locale).
|
|
@@ -26,10 +27,17 @@
|
|
|
26
27
|
* is to wire those env vars to AWS-native resources.
|
|
27
28
|
*/
|
|
28
29
|
|
|
30
|
+
import { cloudFrontRedirectFunctionCode } from "@caelo-cms/shared";
|
|
29
31
|
import * as aws from "@pulumi/aws";
|
|
30
32
|
import * as awsx from "@pulumi/awsx";
|
|
31
33
|
import * as pulumi from "@pulumi/pulumi";
|
|
32
34
|
import type { CloudAdapterOutputs, DnsRecord } from "../../dist/adapter.js";
|
|
35
|
+
import {
|
|
36
|
+
AWS_SERVICE_SECRETS,
|
|
37
|
+
type AwsSecret,
|
|
38
|
+
type AwsTaskService,
|
|
39
|
+
secretsReadPolicy,
|
|
40
|
+
} from "../../dist/aws-task-roles.js";
|
|
33
41
|
import { generateBootstrapToken } from "../../dist/bootstrap-token.js";
|
|
34
42
|
import { ADMIN_MEMORY_DEFAULT, memoryQuantityMiB } from "../../dist/stack-contract.js";
|
|
35
43
|
|
|
@@ -87,9 +95,11 @@ function randomHex(bytes: number): string {
|
|
|
87
95
|
}
|
|
88
96
|
|
|
89
97
|
const postgresPassword = pulumi.secret(randomHex(32));
|
|
90
|
-
// #613 — one password per database role, so the gateway (public_role
|
|
91
|
-
//
|
|
92
|
-
//
|
|
98
|
+
// #613 — one password per database role, so the gateway (public_role only,
|
|
99
|
+
// #635) never holds admin_role's. bootstrap.sh creates the roles with these
|
|
100
|
+
// (PUBLIC_ROLE_PASSWORD / GATEWAY_ROLE_PASSWORD). gateway_role is no longer
|
|
101
|
+
// any service's login; its password stays until the contract release that
|
|
102
|
+
// drops the role (#635).
|
|
93
103
|
const publicRolePassword = pulumi.secret(randomHex(32));
|
|
94
104
|
const gatewayRolePassword = pulumi.secret(randomHex(32));
|
|
95
105
|
const csrfSecret = pulumi.secret(randomHex(32));
|
|
@@ -161,13 +171,6 @@ const publicDatabaseUrl = pulumi
|
|
|
161
171
|
([host, port, pw]) =>
|
|
162
172
|
`postgresql://public_role:${pw}@${host}:${port}/cms_public?sslmode=require`,
|
|
163
173
|
);
|
|
164
|
-
// The gateway's cms_admin login (#613): grants from migration 0248 only.
|
|
165
|
-
const gatewayDatabaseUrl = pulumi
|
|
166
|
-
.all([db.address, db.port, gatewayRolePassword])
|
|
167
|
-
.apply(
|
|
168
|
-
([host, port, pw]) =>
|
|
169
|
-
`postgresql://gateway_role:${pw}@${host}:${port}/cms_admin?sslmode=require`,
|
|
170
|
-
);
|
|
171
174
|
|
|
172
175
|
// === 4. S3 buckets ===
|
|
173
176
|
const mediaBucket = new aws.s3.BucketV2(`${namePrefix}-media`, {
|
|
@@ -225,6 +228,27 @@ const cert = new aws.acm.Certificate(
|
|
|
225
228
|
{ provider: usEast1Provider },
|
|
226
229
|
);
|
|
227
230
|
|
|
231
|
+
// === 6b. Redirects at the edge (#587) ===
|
|
232
|
+
// The redirects table: a CloudFront Function on viewer-request answers each
|
|
233
|
+
// redirected path from a KeyValueStore before the S3 origin is asked
|
|
234
|
+
// (redirect-emit-cloudfront.ts in @caelo-cms/shared). The deploy writes
|
|
235
|
+
// the store, so redirects change without republishing the function.
|
|
236
|
+
// FIXME(issue #749): the S3 static publisher is still a stub (#5); until
|
|
237
|
+
// it writes the store, the function finds no keys and every request passes
|
|
238
|
+
// through to the origin.
|
|
239
|
+
const redirectKvs = new aws.cloudfront.KeyValueStore(`${namePrefix}-redirects`, {
|
|
240
|
+
name: `${namePrefix}-redirects`,
|
|
241
|
+
comment: `Caelo ${env} redirects table (#587)`,
|
|
242
|
+
});
|
|
243
|
+
const redirectFunction = new aws.cloudfront.Function(`${namePrefix}-redirects`, {
|
|
244
|
+
name: `${namePrefix}-redirects`,
|
|
245
|
+
runtime: "cloudfront-js-2.0",
|
|
246
|
+
comment: `Caelo ${env}: answer the redirects table at the edge (#587)`,
|
|
247
|
+
publish: true,
|
|
248
|
+
code: redirectKvs.arn.apply((arn) => cloudFrontRedirectFunctionCode(kvsIdFromArn(arn))),
|
|
249
|
+
keyValueStoreAssociations: [redirectKvs.arn],
|
|
250
|
+
});
|
|
251
|
+
|
|
228
252
|
// === 7. CloudFront distribution ===
|
|
229
253
|
const distribution = new aws.cloudfront.Distribution(`${namePrefix}-cdn`, {
|
|
230
254
|
enabled: true,
|
|
@@ -252,6 +276,7 @@ const distribution = new aws.cloudfront.Distribution(`${namePrefix}-cdn`, {
|
|
|
252
276
|
minTtl: 0,
|
|
253
277
|
defaultTtl: 60,
|
|
254
278
|
maxTtl: 86400,
|
|
279
|
+
functionAssociations: [{ eventType: "viewer-request", functionArn: redirectFunction.arn }],
|
|
255
280
|
},
|
|
256
281
|
orderedCacheBehaviors: [
|
|
257
282
|
{
|
|
@@ -325,38 +350,48 @@ const cluster = new aws.ecs.Cluster(`${namePrefix}-ecs`, {
|
|
|
325
350
|
settings: [{ name: "containerInsights", value: "enabled" }],
|
|
326
351
|
});
|
|
327
352
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
353
|
+
// One task role per service (#635): the gateway's may read only the
|
|
354
|
+
// public_role password, never a secret that unlocks a cms_admin login
|
|
355
|
+
// (src/aws-task-roles.ts AWS_SERVICE_SECRETS, asserted by
|
|
356
|
+
// gateway-env-isolation.test.ts). The admin keeps the logical names the
|
|
357
|
+
// former shared role had, so upgrading replaces nothing on its side.
|
|
358
|
+
const secretArns: Record<AwsSecret, pulumi.Output<string>> = {
|
|
359
|
+
"postgres-password": pgSecret.arn,
|
|
360
|
+
"public-role-password": publicRoleSecret.arn,
|
|
361
|
+
"gateway-role-password": gatewayRoleSecret.arn,
|
|
362
|
+
"csrf-secret": csrfSecretRes.arn,
|
|
363
|
+
"cookie-secret": cookieSecretRes.arn,
|
|
364
|
+
"anthropic-api-key": anthropicSecretRes.arn,
|
|
365
|
+
"resend-api-key": resendSecretRes.arn,
|
|
366
|
+
};
|
|
340
367
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
.
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
368
|
+
function taskRole(service: AwsTaskService): aws.iam.Role {
|
|
369
|
+
const prefix = service === "admin" ? namePrefix : `${namePrefix}-${service}`;
|
|
370
|
+
const role = new aws.iam.Role(`${prefix}-task-role`, {
|
|
371
|
+
assumeRolePolicy: JSON.stringify({
|
|
372
|
+
Version: "2012-10-17",
|
|
373
|
+
Statement: [
|
|
374
|
+
{
|
|
375
|
+
Effect: "Allow",
|
|
376
|
+
Principal: { Service: "ecs-tasks.amazonaws.com" },
|
|
377
|
+
Action: "sts:AssumeRole",
|
|
378
|
+
},
|
|
379
|
+
],
|
|
380
|
+
}),
|
|
381
|
+
});
|
|
382
|
+
new aws.iam.RolePolicy(`${prefix}-task-secrets`, {
|
|
383
|
+
role: role.id,
|
|
384
|
+
policy: pulumi
|
|
385
|
+
.all(AWS_SERVICE_SECRETS[service].map((name) => secretArns[name]))
|
|
386
|
+
.apply(secretsReadPolicy),
|
|
387
|
+
});
|
|
388
|
+
return role;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const taskRoles: Record<AwsTaskService, aws.iam.Role> = {
|
|
392
|
+
admin: taskRole("admin"),
|
|
393
|
+
gateway: taskRole("gateway"),
|
|
394
|
+
};
|
|
360
395
|
|
|
361
396
|
const execRole = new aws.iam.Role(`${namePrefix}-exec-role`, {
|
|
362
397
|
assumeRolePolicy: JSON.stringify({
|
|
@@ -397,7 +432,6 @@ const dnsRecordsRequired: DnsRecord[] = [
|
|
|
397
432
|
const out: CloudAdapterOutputs = {
|
|
398
433
|
adminDatabaseUrl: adminDatabaseUrl as unknown as string,
|
|
399
434
|
publicDatabaseUrl: publicDatabaseUrl as unknown as string,
|
|
400
|
-
gatewayDatabaseUrl: gatewayDatabaseUrl as unknown as string,
|
|
401
435
|
mediaStorageUrl: pulumi.interpolate`s3://${mediaBucket.bucket}` as unknown as string,
|
|
402
436
|
mediaCdnBaseUrl:
|
|
403
437
|
pulumi.interpolate`https://${distribution.domainName}/media` as unknown as string,
|
|
@@ -412,7 +446,6 @@ const out: CloudAdapterOutputs = {
|
|
|
412
446
|
|
|
413
447
|
export const adminDatabaseUrlOut = out.adminDatabaseUrl;
|
|
414
448
|
export const publicDatabaseUrlOut = out.publicDatabaseUrl;
|
|
415
|
-
export const gatewayDatabaseUrlOut = out.gatewayDatabaseUrl;
|
|
416
449
|
export const mediaStorageUrlOut = out.mediaStorageUrl;
|
|
417
450
|
export const mediaCdnBaseUrlOut = out.mediaCdnBaseUrl;
|
|
418
451
|
export const bootstrapUrlOut = out.bootstrapUrl;
|
|
@@ -421,11 +454,22 @@ export const edgeLogSinkUrlOut = out.edgeLogSinkUrl;
|
|
|
421
454
|
export const providerOut = out.provider;
|
|
422
455
|
export const environmentOut = out.environment;
|
|
423
456
|
export const cloudfrontDomainOut = distribution.domainName;
|
|
457
|
+
export const redirectKvsArnOut = redirectKvs.arn;
|
|
424
458
|
export const ecsClusterArnOut = cluster.arn;
|
|
425
459
|
export const rdsAddressOut = db.address;
|
|
460
|
+
// The task roles the admin-side and gateway task definitions run as.
|
|
461
|
+
export const adminTaskRoleArnOut = taskRoles.admin.arn;
|
|
462
|
+
export const gatewayTaskRoleArnOut = taskRoles.gateway.arn;
|
|
426
463
|
|
|
427
464
|
// Note: the bootstrap token (and other secrets like postgres-password,
|
|
428
465
|
// CSRF/cookie secrets, Anthropic key) are stored in Pulumi's encrypted
|
|
429
466
|
// state via `pulumi.secret(...)`. Operators retrieve via
|
|
430
467
|
// `pulumi stack output --show-secrets`.
|
|
431
468
|
export const bootstrapTokenExpiresAtOut = tokenInfo.expiresAt;
|
|
469
|
+
|
|
470
|
+
/** A KeyValueStore's id: the last segment of its ARN. */
|
|
471
|
+
function kvsIdFromArn(arn: string): string {
|
|
472
|
+
const id = arn.slice(arn.lastIndexOf("/") + 1);
|
|
473
|
+
if (!id || id === arn) throw new Error(`not a CloudFront KeyValueStore ARN: ${arn}`);
|
|
474
|
+
return id;
|
|
475
|
+
}
|
package/stacks/azure/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Pulumi stack provisioning Caelo on Azure managed services. Implements the shared
|
|
|
10
10
|
| Blob storage | Storage Account with a private `media` container and static-website hosting (`$web`) |
|
|
11
11
|
| CDN / Edge | Azure Front Door Standard: the primary domain serves the static website as files, with a Front Door managed certificate; no edge compute on the request path |
|
|
12
12
|
| Container runtime | Four Container Apps (admin / gateway / orchestrator / runner) in one managed environment |
|
|
13
|
-
| Secret store | Key Vault (postgres-password, public-role-password, gateway-role-password, csrf-secret, cookie-secret, anthropic-api-key, resend-api-key). The gateway container gets `
|
|
13
|
+
| Secret store | Key Vault (postgres-password, public-role-password, gateway-role-password, csrf-secret, cookie-secret, anthropic-api-key, resend-api-key). The gateway container gets `PUBLIC_DATABASE_URL` only, never a cms_admin credential of any role (#613, #635). |
|
|
14
14
|
| Edge-log sink | Log Analytics workspace receives Container Apps logs (queryable by P12A analytics plugin via Azure Monitor) |
|
|
15
15
|
|
|
16
16
|
The Caelo runtime never knows it's on Azure — every Container App consumes plain `DATABASE_URL` / `MEDIA_STORAGE_URL` / `SECRETS_PROVIDER` env vars.
|
package/stacks/azure/index.ts
CHANGED
|
@@ -81,9 +81,11 @@ function randomHex(bytes: number): string {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
const postgresPassword = pulumi.secret(randomHex(32));
|
|
84
|
-
// #613 — one password per database role, so the gateway (public_role
|
|
85
|
-
//
|
|
86
|
-
//
|
|
84
|
+
// #613 — one password per database role, so the gateway (public_role only,
|
|
85
|
+
// #635) never holds admin_role's. bootstrap.sh creates the roles with these
|
|
86
|
+
// (PUBLIC_ROLE_PASSWORD / GATEWAY_ROLE_PASSWORD). gateway_role is no longer
|
|
87
|
+
// any service's login; its password stays until the contract release that
|
|
88
|
+
// drops the role (#635).
|
|
87
89
|
const publicRolePassword = pulumi.secret(randomHex(32));
|
|
88
90
|
const gatewayRolePassword = pulumi.secret(randomHex(32));
|
|
89
91
|
const csrfSecret = pulumi.secret(randomHex(32));
|
|
@@ -162,14 +164,11 @@ const adminDatabaseUrl = pulumi
|
|
|
162
164
|
const publicAdminDatabaseUrl = pulumi
|
|
163
165
|
.all([pgServer.fullyQualifiedDomainName, postgresPassword])
|
|
164
166
|
.apply(([host, pw]) => `postgresql://caelo_admin:${pw}@${host}:5432/cms_public?sslmode=require`);
|
|
165
|
-
// The gateway's
|
|
166
|
-
//
|
|
167
|
+
// The gateway's only login (#613, #635): public_role on cms_public. It has
|
|
168
|
+
// no cms_admin login at all (CLAUDE.md §2).
|
|
167
169
|
const publicDatabaseUrl = pulumi
|
|
168
170
|
.all([pgServer.fullyQualifiedDomainName, publicRolePassword])
|
|
169
171
|
.apply(([host, pw]) => `postgresql://public_role:${pw}@${host}:5432/cms_public?sslmode=require`);
|
|
170
|
-
const gatewayDatabaseUrl = pulumi
|
|
171
|
-
.all([pgServer.fullyQualifiedDomainName, gatewayRolePassword])
|
|
172
|
-
.apply(([host, pw]) => `postgresql://gateway_role:${pw}@${host}:5432/cms_admin?sslmode=require`);
|
|
173
172
|
|
|
174
173
|
// === 4. Storage account + containers ===
|
|
175
174
|
const storage = new azure.storage.StorageAccount(`${namePrefix}-st`, {
|
|
@@ -269,6 +268,18 @@ const siteDomain = new azure.cdn.AFDCustomDomain(`${namePrefix}-fd-domain`, {
|
|
|
269
268
|
tlsSettings: { certificateType: "ManagedCertificate", minimumTlsVersion: "TLS12" },
|
|
270
269
|
});
|
|
271
270
|
|
|
271
|
+
// #587 — the redirects table as rules-engine rules on the static route
|
|
272
|
+
// (redirect-emit-front-door.ts in @caelo-cms/shared): Front Door answers
|
|
273
|
+
// them at the edge before the Blob origin. The deploy owns the rules; the
|
|
274
|
+
// stack only creates the rule set and attaches it.
|
|
275
|
+
// FIXME(issue #750): the Blob static publisher is still a stub (#6); until
|
|
276
|
+
// it writes the rules, the set stays empty and no redirect is served.
|
|
277
|
+
const redirectRuleSet = new azure.cdn.RuleSet(`${namePrefix}-fd-redirects`, {
|
|
278
|
+
resourceGroupName: rg.name,
|
|
279
|
+
profileName: frontDoor.name,
|
|
280
|
+
ruleSetName: "caeloRedirects",
|
|
281
|
+
});
|
|
282
|
+
|
|
272
283
|
new azure.cdn.Route(
|
|
273
284
|
`${namePrefix}-fd-route`,
|
|
274
285
|
{
|
|
@@ -278,6 +289,7 @@ new azure.cdn.Route(
|
|
|
278
289
|
routeName: "static",
|
|
279
290
|
originGroup: { id: staticOriginGroup.id },
|
|
280
291
|
customDomains: [{ id: siteDomain.id }],
|
|
292
|
+
ruleSets: [{ id: redirectRuleSet.id }],
|
|
281
293
|
patternsToMatch: ["/*"],
|
|
282
294
|
// Without a cache configuration Front Door does not cache the route at
|
|
283
295
|
// all; static files are edge-cached per their Cache-Control (§11.B
|
|
@@ -406,10 +418,7 @@ const adminApp = containerApp({
|
|
|
406
418
|
});
|
|
407
419
|
const _gatewayApp = containerApp({
|
|
408
420
|
serviceName: "gateway",
|
|
409
|
-
databaseEnv: [
|
|
410
|
-
{ name: "GATEWAY_DATABASE_URL", value: gatewayDatabaseUrl },
|
|
411
|
-
{ name: "PUBLIC_DATABASE_URL", value: publicDatabaseUrl },
|
|
412
|
-
],
|
|
421
|
+
databaseEnv: [{ name: "PUBLIC_DATABASE_URL", value: publicDatabaseUrl }],
|
|
413
422
|
});
|
|
414
423
|
const _orchestratorApp = containerApp({
|
|
415
424
|
serviceName: "orchestrator",
|
|
@@ -433,7 +442,6 @@ const dnsRecordsRequired = pulumi
|
|
|
433
442
|
const out: CloudAdapterOutputs = {
|
|
434
443
|
adminDatabaseUrl: adminDatabaseUrl as unknown as string,
|
|
435
444
|
publicDatabaseUrl: publicDatabaseUrl as unknown as string,
|
|
436
|
-
gatewayDatabaseUrl: gatewayDatabaseUrl as unknown as string,
|
|
437
445
|
mediaStorageUrl:
|
|
438
446
|
pulumi.interpolate`https://${storage.name}.blob.core.windows.net/media` as unknown as string,
|
|
439
447
|
mediaCdnBaseUrl: pulumi.interpolate`https://${domain}/media` as unknown as string,
|
|
@@ -447,7 +455,6 @@ const out: CloudAdapterOutputs = {
|
|
|
447
455
|
|
|
448
456
|
export const adminDatabaseUrlOut = out.adminDatabaseUrl;
|
|
449
457
|
export const publicDatabaseUrlOut = out.publicDatabaseUrl;
|
|
450
|
-
export const gatewayDatabaseUrlOut = out.gatewayDatabaseUrl;
|
|
451
458
|
export const mediaStorageUrlOut = out.mediaStorageUrl;
|
|
452
459
|
export const mediaCdnBaseUrlOut = out.mediaCdnBaseUrl;
|
|
453
460
|
export const bootstrapUrlOut = out.bootstrapUrl;
|
package/stacks/gcp/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Pulumi stack provisioning Caelo on GCP managed services. Implements the shared `
|
|
|
10
10
|
| Blob storage | Two GCS buckets (`<project>-caelo-<env>-media`, `<project>-caelo-<env>-static`) with uniform bucket-level access |
|
|
11
11
|
| CDN | Global HTTPS load balancer + Cloud CDN backend bucket: `<domain>` serves the static bucket as files, `/api/*` goes to the gateway, `admin.<domain>` to the IAP-gated admin |
|
|
12
12
|
| Container runtime | Four Cloud Run services (admin / gateway / orchestrator / runner) |
|
|
13
|
-
| Secret store | Secret Manager: postgres-password (admin_role) + secret-kek (Pulumi); internal-secret, tool-approval-secret, public-role-password and gateway-role-password (created by the CLI before `pulumi up`, see `src/runtime-secrets.ts`). Cloud Run reads each as a `secretKeyRef`; database URLs carry no password. The gateway's service account reads only the
|
|
13
|
+
| Secret store | Secret Manager: postgres-password (admin_role) + secret-kek (Pulumi); internal-secret, tool-approval-secret, public-role-password and gateway-role-password (created by the CLI before `pulumi up`, see `src/runtime-secrets.ts`). Cloud Run reads each as a `secretKeyRef`; database URLs carry no password. The gateway's service account reads only `public-role-password` (#613, #635: the gateway has no cms_admin login); `gateway_role`, created by migration 0248, is no service's login any more. |
|
|
14
14
|
| Edge-log sink | Cloud Logging project sink → BigQuery dataset `<env>_edge_logs` (queryable by P12A analytics plugin) |
|
|
15
15
|
|
|
16
16
|
The Caelo runtime never knows it's on GCP — every Cloud Run service consumes plain `DATABASE_URL` / `MEDIA_STORAGE_URL` / `SECRETS_PROVIDER` env vars.
|