@caelo-cms/provisioning 0.10.52 → 0.10.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/command.js +1 -0
- package/dist/agent/command.js.map +1 -1
- package/dist/agent-db.js +9 -0
- package/dist/aws-task-roles.d.ts +1 -1
- package/dist/aws-task-roles.d.ts.map +1 -1
- package/dist/aws-task-roles.js +0 -1
- package/dist/aws-task-roles.js.map +1 -1
- package/dist/cli-routing.d.ts +20 -0
- package/dist/cli-routing.d.ts.map +1 -1
- package/dist/cli-routing.js +106 -0
- package/dist/cli-routing.js.map +1 -1
- package/dist/cli.js +5 -6
- package/dist/cli.js.map +1 -1
- package/dist/compose.d.ts +4 -3
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +3 -2
- package/dist/compose.js.map +1 -1
- package/dist/gateway-credentials.d.ts +3 -5
- package/dist/gateway-credentials.d.ts.map +1 -1
- package/dist/gateway-credentials.js +8 -18
- package/dist/gateway-credentials.js.map +1 -1
- package/dist/infra-contract.d.ts +1 -1
- package/dist/infra-contract.d.ts.map +1 -1
- package/dist/infra-contract.js +8 -1
- package/dist/infra-contract.js.map +1 -1
- package/dist/runtime-secrets.d.ts +1 -3
- package/dist/runtime-secrets.d.ts.map +1 -1
- package/dist/runtime-secrets.js +2 -6
- package/dist/runtime-secrets.js.map +1 -1
- package/dist/self-hosted-roles.d.ts +9 -8
- package/dist/self-hosted-roles.d.ts.map +1 -1
- package/dist/self-hosted-roles.js +9 -9
- package/dist/self-hosted-roles.js.map +1 -1
- package/dist/self-hosted-upgrade.d.ts +7 -1
- package/dist/self-hosted-upgrade.d.ts.map +1 -1
- package/dist/self-hosted-upgrade.js +17 -4
- package/dist/self-hosted-upgrade.js.map +1 -1
- package/dist/stack-contract.d.ts +52 -3
- package/dist/stack-contract.d.ts.map +1 -1
- package/dist/stack-contract.js +36 -36
- package/dist/stack-contract.js.map +1 -1
- package/dist/upgrade/converge.d.ts.map +1 -1
- package/dist/upgrade/converge.js +5 -13
- package/dist/upgrade/converge.js.map +1 -1
- package/dist/wizards/gcp.d.ts.map +1 -1
- package/dist/wizards/gcp.js +1 -30
- package/dist/wizards/gcp.js.map +1 -1
- package/package.json +2 -2
- package/src/admin-memory.test.ts +1 -1
- package/src/agent/command.ts +1 -0
- package/src/aws-task-roles.ts +0 -2
- package/src/caddy-redirects.test.ts +1 -1
- package/src/checkpoint-secrets.test.ts +0 -2
- package/src/cli-routing.test.ts +27 -1
- package/src/cli-routing.ts +131 -0
- package/src/cli.ts +5 -8
- package/src/compose.ts +13 -5
- package/src/gateway-credentials.test.ts +0 -29
- package/src/gateway-credentials.ts +11 -25
- package/src/gateway-env-isolation.test.ts +7 -2
- package/src/index.test.ts +7 -6
- package/src/infra-contract.test.ts +4 -3
- package/src/infra-contract.ts +8 -1
- package/src/runtime-secrets.test.ts +9 -81
- package/src/runtime-secrets.ts +2 -6
- package/src/self-hosted-compose.test.ts +1 -1
- package/src/self-hosted-roles.integration.test.ts +23 -38
- package/src/self-hosted-roles.test.ts +11 -7
- package/src/self-hosted-roles.ts +17 -16
- package/src/self-hosted-upgrade.test.ts +28 -1
- package/src/self-hosted-upgrade.ts +29 -4
- package/src/stack-contract.test.ts +15 -11
- package/src/stack-contract.ts +40 -5
- package/src/stack-converge-secrets.test.ts +2 -0
- package/src/stack-converge.test.ts +2 -0
- package/src/upgrade/converge.ts +5 -19
- package/src/wizards/gcp.ts +1 -31
- package/stacks/aws/README.md +1 -1
- package/stacks/aws/index.ts +1 -6
- package/stacks/azure/README.md +1 -1
- package/stacks/azure/index.ts +9 -6
- package/stacks/gcp/README.md +1 -1
- package/stacks/gcp/index.ts +1 -4
- package/stacks/gcp-firebase/index.ts +1 -4
- package/stacks/self-hosted/index.ts +4 -11
package/src/self-hosted-roles.ts
CHANGED
|
@@ -7,13 +7,14 @@
|
|
|
7
7
|
* bootstrap.sh creates the application roles, but only on the Postgres
|
|
8
8
|
* container's FIRST start (docker-entrypoint-initdb.d). An install created
|
|
9
9
|
* before the per-role passwords has a data volume whose roles have other
|
|
10
|
-
* passwords
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* gateway
|
|
10
|
+
* passwords, so emitting Compose URLs with the new passwords would lock the
|
|
11
|
+
* admin and the gateway out on the next restart. `cms-provision up`
|
|
12
|
+
* therefore runs {@link roleConvergenceScript} through the running container
|
|
13
|
+
* as the Postgres superuser BEFORE it writes the new Compose file: it
|
|
14
|
+
* creates missing roles and databases and sets each role's password to the
|
|
15
|
+
* configured one. It also drops the former `gateway_role` (#635: the
|
|
16
|
+
* gateway has no cms_admin login), with whatever it still holds. Every
|
|
17
|
+
* statement is idempotent.
|
|
17
18
|
*
|
|
18
19
|
* Passwords travel on psql's stdin, never on argv.
|
|
19
20
|
*/
|
|
@@ -34,7 +35,7 @@ function literal(s: string): string {
|
|
|
34
35
|
export const AGENT_DB_ROLE = "caelo_agent";
|
|
35
36
|
|
|
36
37
|
/** The application roles bootstrap.sh and the convergence create. */
|
|
37
|
-
const APP_ROLES = ["admin_role", "public_role"
|
|
38
|
+
const APP_ROLES = ["admin_role", "public_role"] as const;
|
|
38
39
|
type AppRole = (typeof APP_ROLES)[number];
|
|
39
40
|
|
|
40
41
|
/** How a role's password appears in the SQL: a literal, or a psql variable. */
|
|
@@ -69,12 +70,14 @@ function convergenceSql(password: PasswordSql, agent: boolean): string {
|
|
|
69
70
|
"REVOKE ALL ON DATABASE cms_public FROM PUBLIC;",
|
|
70
71
|
"GRANT CONNECT ON DATABASE cms_admin TO admin_role;",
|
|
71
72
|
"GRANT CONNECT ON DATABASE cms_public TO public_role, admin_role;",
|
|
73
|
+
// #635 — the gateway's former cms_admin login goes, with whatever it
|
|
74
|
+
// still holds in either database (the superuser may drop what
|
|
75
|
+
// migration 0285 could only revoke).
|
|
76
|
+
"\\connect cms_public",
|
|
77
|
+
"DO $$ BEGIN IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'gateway_role') THEN DROP OWNED BY gateway_role; END IF; END $$;",
|
|
72
78
|
"\\connect cms_admin",
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
"DO $$ BEGIN IF EXISTS (SELECT 1 FROM pg_proc WHERE proname = 'caelo_grant_gateway_role') THEN PERFORM caelo_grant_gateway_role(); END IF; END $$;",
|
|
76
|
-
// Migration 0251's staging-gate grants (issue #650), same convergence.
|
|
77
|
-
"DO $$ BEGIN IF EXISTS (SELECT 1 FROM pg_proc WHERE proname = 'caelo_grant_gateway_staging_access') THEN PERFORM caelo_grant_gateway_staging_access(); END IF; END $$;",
|
|
79
|
+
"DO $$ BEGIN IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'gateway_role') THEN DROP OWNED BY gateway_role; END IF; END $$;",
|
|
80
|
+
"DO $$ BEGIN IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'gateway_role') THEN DROP ROLE gateway_role; END IF; END $$;",
|
|
78
81
|
// Migrations 0267 + 0270 (#696): the upgrader role's grants, applied
|
|
79
82
|
// now that the role exists.
|
|
80
83
|
"DO $$ BEGIN IF EXISTS (SELECT 1 FROM pg_proc WHERE proname = 'caelo_grant_platform_updater') THEN PERFORM caelo_grant_platform_updater(); END IF; END $$;",
|
|
@@ -84,7 +87,7 @@ function convergenceSql(password: PasswordSql, agent: boolean): string {
|
|
|
84
87
|
|
|
85
88
|
/**
|
|
86
89
|
* The psql script (run as the superuser, connected to any database) that
|
|
87
|
-
* brings roles
|
|
90
|
+
* brings roles and databases to the configured state.
|
|
88
91
|
* Mirrors bootstrap.sh, plus the password reset bootstrap.sh never does.
|
|
89
92
|
* With `agentPassword`, also the host agent's login (#696 Phase 4c).
|
|
90
93
|
*/
|
|
@@ -92,7 +95,6 @@ export function roleConvergenceScript(passwords: RolePasswords, agentPassword?:
|
|
|
92
95
|
const byRole: Record<AppRole, string> = {
|
|
93
96
|
admin_role: passwords.admin,
|
|
94
97
|
public_role: passwords.public,
|
|
95
|
-
gateway_role: passwords.gateway,
|
|
96
98
|
};
|
|
97
99
|
return convergenceSql(
|
|
98
100
|
(role) => literal(role === AGENT_DB_ROLE ? (agentPassword ?? "") : byRole[role]),
|
|
@@ -104,7 +106,6 @@ export function roleConvergenceScript(passwords: RolePasswords, agentPassword?:
|
|
|
104
106
|
const PASSWORD_ENV: Record<AppRole | typeof AGENT_DB_ROLE, string> = {
|
|
105
107
|
admin_role: "ADMIN_ROLE_PASSWORD",
|
|
106
108
|
public_role: "PUBLIC_ROLE_PASSWORD",
|
|
107
|
-
gateway_role: "GATEWAY_ROLE_PASSWORD",
|
|
108
109
|
[AGENT_DB_ROLE]: "AGENT_ROLE_PASSWORD",
|
|
109
110
|
};
|
|
110
111
|
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
readRelease,
|
|
20
20
|
recordRelease,
|
|
21
21
|
} from "./self-hosted-release.js";
|
|
22
|
+
import type { ConvergeOutcome } from "./self-hosted-roles.js";
|
|
22
23
|
import {
|
|
23
24
|
composePath,
|
|
24
25
|
legacyComposePath,
|
|
@@ -121,11 +122,15 @@ beforeEach(() => {
|
|
|
121
122
|
});
|
|
122
123
|
afterEach(() => rmSync(dir, { recursive: true, force: true }));
|
|
123
124
|
|
|
124
|
-
function deps(runner: HostRunner) {
|
|
125
|
+
function deps(runner: HostRunner & { calls: string[] }, roles: ConvergeOutcome = { ok: true }) {
|
|
125
126
|
return {
|
|
126
127
|
caeloDir: dir,
|
|
127
128
|
runner,
|
|
128
129
|
renderCompose: render,
|
|
130
|
+
convergeRoles: async () => {
|
|
131
|
+
runner.calls.push("converge roles");
|
|
132
|
+
return roles;
|
|
133
|
+
},
|
|
129
134
|
log,
|
|
130
135
|
fetchImpl: registry,
|
|
131
136
|
sleep: async () => {},
|
|
@@ -151,6 +156,7 @@ describe("upgradeSelfHosted", () => {
|
|
|
151
156
|
"pull",
|
|
152
157
|
"migrate admin",
|
|
153
158
|
"migrate public",
|
|
159
|
+
"converge roles",
|
|
154
160
|
"up caelo-admin",
|
|
155
161
|
"health admin",
|
|
156
162
|
"up caelo-gateway",
|
|
@@ -269,6 +275,27 @@ describe("upgradeSelfHosted", () => {
|
|
|
269
275
|
expect(readFileSync(composePath(dir), "utf8")).toBe(render(OLD));
|
|
270
276
|
});
|
|
271
277
|
|
|
278
|
+
it("converges the database roles on every upgrade, after the migrations (#635: drops gateway_role)", async () => {
|
|
279
|
+
const runner = fakeRunner();
|
|
280
|
+
expect((await upgradeSelfHosted({ version: "0.11.0" }, deps(runner))).ok).toBe(true);
|
|
281
|
+
const s = steps(runner.calls);
|
|
282
|
+
expect(s.indexOf("converge roles")).toBeGreaterThan(s.indexOf("migrate public"));
|
|
283
|
+
expect(s.indexOf("converge roles")).toBeLessThan(s.indexOf("up caelo-admin"));
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("restores the Compose file and restarts nothing when the role convergence fails", async () => {
|
|
287
|
+
const runner = fakeRunner();
|
|
288
|
+
const r = await upgradeSelfHosted(
|
|
289
|
+
{ version: "0.11.0" },
|
|
290
|
+
deps(runner, { ok: false, reachable: true, error: "role is busy" }),
|
|
291
|
+
);
|
|
292
|
+
expect(r).toMatchObject({ ok: false, stage: "roles", previousRunning: true });
|
|
293
|
+
if (!r.ok) expect(r.error).toContain("role is busy");
|
|
294
|
+
expect(steps(runner.calls).some((x) => x.startsWith("up "))).toBe(false);
|
|
295
|
+
expect(readFileSync(composePath(dir), "utf8")).toBe(render(OLD));
|
|
296
|
+
expect(readRelease(dir)?.version).toBe("0.10.0");
|
|
297
|
+
});
|
|
298
|
+
|
|
272
299
|
it("rolls back to the previous images when the gateway fails its health check", async () => {
|
|
273
300
|
let compose = "";
|
|
274
301
|
const runner = fakeRunner([
|
|
@@ -13,9 +13,15 @@
|
|
|
13
13
|
* the same `migrate.ts` entrypoint the GCP migrate job runs) while the
|
|
14
14
|
* old containers keep serving — migrations are expand-only, so the
|
|
15
15
|
* running release works on the new schema;
|
|
16
|
-
* 5.
|
|
16
|
+
* 5. converge the database roles (the same script `cms-provision up`
|
|
17
|
+
* runs): bootstrap.sh only runs on the data volume's first start, so a
|
|
18
|
+
* role a release retires — #635's gateway_role — is dropped here, on
|
|
19
|
+
* every existing install. After the migrations, because the runner
|
|
20
|
+
* refuses an install that skipped the release the old role's last user
|
|
21
|
+
* was retired in (run-order.ts): past it, nothing running uses it;
|
|
22
|
+
* 6. recreate the admin, health-check it (`/_caelo/health`, which proves
|
|
17
23
|
* the DB pool and roles); then the gateway (`/healthz`);
|
|
18
|
-
*
|
|
24
|
+
* 7. record the release in `.caelo/release.json`.
|
|
19
25
|
* Any failure after step 3 puts the previous Compose file back; once a
|
|
20
26
|
* container was recreated the previous images are started again and
|
|
21
27
|
* health-checked (rollback). The database is never restored automatically:
|
|
@@ -52,6 +58,7 @@ import {
|
|
|
52
58
|
releaseImages,
|
|
53
59
|
resolveVerifiedRelease,
|
|
54
60
|
} from "./self-hosted-release.js";
|
|
61
|
+
import type { ConvergeOutcome } from "./self-hosted-roles.js";
|
|
55
62
|
import { fetchLatestReleaseTag, RELEASE_SERVICES, releaseTag } from "./upgrade/release-images.js";
|
|
56
63
|
|
|
57
64
|
/** The Compose service names of the two Caelo services. */
|
|
@@ -78,6 +85,11 @@ export interface SelfHostedUpgradeDeps {
|
|
|
78
85
|
/** Renders the Compose file for `images` from the install's config (secrets included). */
|
|
79
86
|
readonly renderCompose: (images: ReleaseImages) => string;
|
|
80
87
|
readonly log: UpgradeLog;
|
|
88
|
+
/**
|
|
89
|
+
* Converges the database roles as the Postgres superuser
|
|
90
|
+
* (`convergeSelfHostedRoles`); drops roles a release retired (#635).
|
|
91
|
+
*/
|
|
92
|
+
readonly convergeRoles: () => Promise<ConvergeOutcome>;
|
|
81
93
|
readonly fetchImpl?: typeof fetch;
|
|
82
94
|
readonly sleep?: (ms: number) => Promise<void>;
|
|
83
95
|
readonly now?: () => Date;
|
|
@@ -116,6 +128,7 @@ type UpgradeStage =
|
|
|
116
128
|
| "backup"
|
|
117
129
|
| "pull"
|
|
118
130
|
| "migrate"
|
|
131
|
+
| "roles"
|
|
119
132
|
| "roll-admin"
|
|
120
133
|
| "roll-gateway";
|
|
121
134
|
|
|
@@ -321,7 +334,19 @@ export async function upgradeSelfHosted(
|
|
|
321
334
|
);
|
|
322
335
|
}
|
|
323
336
|
|
|
324
|
-
// 5.
|
|
337
|
+
// 5. Database roles: drops what this release retired (#635 gateway_role).
|
|
338
|
+
log.step("Converging the database roles");
|
|
339
|
+
const roles = await deps.convergeRoles();
|
|
340
|
+
if (!roles.ok) {
|
|
341
|
+
restoreCompose(file, previousCompose);
|
|
342
|
+
return fail(
|
|
343
|
+
"roles",
|
|
344
|
+
`Could not converge the database roles: ${roles.error}\nThe previous release keeps running; the migrations are applied (expand-only). Fix the cause and re-run the upgrade. Database backup: ${backup.dir}`,
|
|
345
|
+
true,
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// 6. Roll admin, then gateway, each health-checked.
|
|
325
350
|
for (const service of RELEASE_SERVICES) {
|
|
326
351
|
const stage = service === "admin" ? "roll-admin" : "roll-gateway";
|
|
327
352
|
log.phase?.("roll");
|
|
@@ -353,7 +378,7 @@ export async function upgradeSelfHosted(
|
|
|
353
378
|
log.info(`${service} healthy ✓`);
|
|
354
379
|
}
|
|
355
380
|
|
|
356
|
-
//
|
|
381
|
+
// 7. Record.
|
|
357
382
|
log.phase?.("record");
|
|
358
383
|
recordRelease(
|
|
359
384
|
caeloDir,
|
|
@@ -158,12 +158,21 @@ describe("gatewayEnvContract", () => {
|
|
|
158
158
|
CAELO_PROVIDER: "gcp",
|
|
159
159
|
CAELO_ENV: "production",
|
|
160
160
|
MEDIA_STORAGE_URL: "gs://acme-caelo-production-media",
|
|
161
|
+
// #845 — gcp: the LB appends <client>,<LB IP>.
|
|
162
|
+
CAELO_TRUSTED_PROXY_HOPS: "2",
|
|
161
163
|
PUBLIC_DATABASE_URL: "postgresql://public_role@10.20.0.3:5432/cms_public?sslmode=require",
|
|
162
164
|
PUBLIC_DATABASE_PASSWORD: "secret:caelo-production-public-role-password:latest",
|
|
163
165
|
CAELO_STAGING_GATE_SECRET: "secret:caelo-production-staging-gate-secret:latest",
|
|
164
166
|
});
|
|
165
167
|
});
|
|
166
168
|
|
|
169
|
+
it("#845 — tells the gateway its provider's proxy hop count", () => {
|
|
170
|
+
// Firebase Hosting / the domain mapping: Cloud Run's front end only.
|
|
171
|
+
const fb = asRecord(gatewayEnvContract({ ...base, provider: "gcp-firebase" }));
|
|
172
|
+
expect(fb.CAELO_TRUSTED_PROXY_HOPS).toBe("1");
|
|
173
|
+
expect(asRecord(gateway).CAELO_TRUSTED_PROXY_HOPS).toBe("2");
|
|
174
|
+
});
|
|
175
|
+
|
|
167
176
|
it("#650 — reads the staging host from the database, not its env", () => {
|
|
168
177
|
const fb = asRecord(gatewayEnvContract({ ...base, provider: "gcp-firebase" }));
|
|
169
178
|
expect(fb.CAELO_STAGING_HOST).toBeUndefined();
|
|
@@ -223,14 +232,13 @@ describe("database role secrets (#613)", () => {
|
|
|
223
232
|
expect(DATABASE_ROLE_SECRET).toEqual({
|
|
224
233
|
admin_role: "postgres-password",
|
|
225
234
|
public_role: "public-role-password",
|
|
226
|
-
gateway_role: "gateway-role-password",
|
|
227
235
|
});
|
|
228
236
|
});
|
|
229
237
|
|
|
230
|
-
it("the CLI generates the gateway's role
|
|
231
|
-
expect(CLI_GENERATED_SECRETS).
|
|
232
|
-
|
|
233
|
-
);
|
|
238
|
+
it("the CLI generates the gateway's role password (never seeded from admin_role's)", () => {
|
|
239
|
+
expect(CLI_GENERATED_SECRETS).toContain("public-role-password");
|
|
240
|
+
// #635 — the former gateway_role is gone; nothing generates its password.
|
|
241
|
+
expect(CLI_GENERATED_SECRETS as readonly string[]).not.toContain("gateway-role-password");
|
|
234
242
|
});
|
|
235
243
|
|
|
236
244
|
it("retires the gateway SA's read access to admin_role's password", () => {
|
|
@@ -287,11 +295,7 @@ describe("runtime secrets", () => {
|
|
|
287
295
|
});
|
|
288
296
|
|
|
289
297
|
it("every CLI-generated secret is read by some service", () => {
|
|
290
|
-
|
|
291
|
-
// the legacy gateway_role's password set until the contract release
|
|
292
|
-
// drops the role (the previous release's gateway logs in with it while
|
|
293
|
-
// an upgrade rolls).
|
|
294
|
-
for (const s of CLI_GENERATED_SECRETS.filter((x) => x !== "gateway-role-password")) {
|
|
298
|
+
for (const s of CLI_GENERATED_SECRETS) {
|
|
295
299
|
expect([...serviceSecrets("admin"), ...serviceSecrets("gateway")]).toContain(s);
|
|
296
300
|
}
|
|
297
301
|
});
|
|
@@ -302,7 +306,7 @@ describe("self-hosted compose", () => {
|
|
|
302
306
|
const yaml = generateDockerCompose({
|
|
303
307
|
domain: "acme.com",
|
|
304
308
|
postgresPassword: "p",
|
|
305
|
-
rolePasswords: { admin: "a", public: "b"
|
|
309
|
+
rolePasswords: { admin: "a", public: "b" },
|
|
306
310
|
caeloSecretKek: "k",
|
|
307
311
|
diskSize: "10G",
|
|
308
312
|
ports: { http: 80, https: 443 },
|
package/src/stack-contract.ts
CHANGED
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
* Pure module — the stacks import it from `dist/`.
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
|
+
import type { SupportedProvider } from "./adapter.js";
|
|
37
38
|
import {
|
|
38
39
|
gatewayServiceAccountEmail,
|
|
39
40
|
gcpBucketName,
|
|
@@ -237,14 +238,13 @@ export function adminMediaVolumeTemplate(volume: MediaVolumeContract) {
|
|
|
237
238
|
export type RuntimeSecret =
|
|
238
239
|
| "postgres-password"
|
|
239
240
|
| "public-role-password"
|
|
240
|
-
| "gateway-role-password"
|
|
241
241
|
| "secret-kek"
|
|
242
242
|
| "internal-secret"
|
|
243
243
|
| "tool-approval-secret"
|
|
244
244
|
| "staging-gate-secret";
|
|
245
245
|
|
|
246
246
|
/** The Postgres roles Caelo's services log in as, with their passwords. */
|
|
247
|
-
export type DatabaseRole = "admin_role" | "public_role"
|
|
247
|
+
export type DatabaseRole = "admin_role" | "public_role";
|
|
248
248
|
|
|
249
249
|
/**
|
|
250
250
|
* Each database role's password secret — one per role (#613), so the
|
|
@@ -256,7 +256,6 @@ export type DatabaseRole = "admin_role" | "public_role" | "gateway_role";
|
|
|
256
256
|
export const DATABASE_ROLE_SECRET: Readonly<Record<DatabaseRole, RuntimeSecret>> = {
|
|
257
257
|
admin_role: "postgres-password",
|
|
258
258
|
public_role: "public-role-password",
|
|
259
|
-
gateway_role: "gateway-role-password",
|
|
260
259
|
};
|
|
261
260
|
|
|
262
261
|
/**
|
|
@@ -269,10 +268,9 @@ export const DATABASE_ROLE_SECRET: Readonly<Record<DatabaseRole, RuntimeSecret>>
|
|
|
269
268
|
export const CLI_GENERATED_SECRETS = [
|
|
270
269
|
"internal-secret",
|
|
271
270
|
"tool-approval-secret",
|
|
272
|
-
// #613 — the gateway's own role
|
|
271
|
+
// #613 — the gateway's own role password. Generated (never seeded from
|
|
273
272
|
// `postgres-password`), so the gateway never learns admin_role's.
|
|
274
273
|
"public-role-password",
|
|
275
|
-
"gateway-role-password",
|
|
276
274
|
// #650 — the staging access gate's key: signs "Open staging" links and
|
|
277
275
|
// session cookies, derives the crawler credentials. Read by the admin
|
|
278
276
|
// (issues) and the gateway (verifies).
|
|
@@ -358,6 +356,42 @@ function commonEnv(inputs: ServiceEnvInputs): CloudRunEnvVar[] {
|
|
|
358
356
|
];
|
|
359
357
|
}
|
|
360
358
|
|
|
359
|
+
/**
|
|
360
|
+
* The env var telling the gateway which `X-Forwarded-For` entry is the
|
|
361
|
+
* visitor (apps/api-gateway client-ip.ts): the number of entries the proxies
|
|
362
|
+
* in front of it append, counting the client's own.
|
|
363
|
+
*/
|
|
364
|
+
export const TRUSTED_PROXY_HOPS_ENV = "CAELO_TRUSTED_PROXY_HOPS";
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* {@link TRUSTED_PROXY_HOPS_ENV} per provider — a property of each
|
|
368
|
+
* adapter's topology (CLAUDE.md §11.B), so it is declared here once and every
|
|
369
|
+
* adapter sets it on its gateway. The gateway refuses to boot without it.
|
|
370
|
+
* A change to an adapter's path to the gateway must update its entry, or
|
|
371
|
+
* rate limits pool every visitor (too high) or trust a forgeable entry (too
|
|
372
|
+
* low).
|
|
373
|
+
*/
|
|
374
|
+
export const GATEWAY_TRUSTED_PROXY_HOPS: Readonly<Record<SupportedProvider, number>> = {
|
|
375
|
+
// External Application LB appends `<client>,<LB IP>`; the gateway's
|
|
376
|
+
// ingress is internal-and-LB only, so nothing reaches it around the LB.
|
|
377
|
+
gcp: 2,
|
|
378
|
+
// Cloud Run's front end appends its peer. Behind the Firebase Hosting
|
|
379
|
+
// rewrite that peer is the Hosting CDN edge, and the entry Hosting adds
|
|
380
|
+
// before it cannot be told apart from a forged one (the service is
|
|
381
|
+
// invokable on *.run.app too) — so the edge address is the most that can
|
|
382
|
+
// be trusted. Staging (Cloud Run domain mapping) gets the visitor.
|
|
383
|
+
"gcp-firebase": 1,
|
|
384
|
+
// Caddy without `trusted_proxies` replaces the header with its peer; the
|
|
385
|
+
// gateway is reachable only on the Compose network.
|
|
386
|
+
"self-hosted": 1,
|
|
387
|
+
// CloudFront appends the viewer, the ALB appends CloudFront. (No ECS task
|
|
388
|
+
// definition consumes it yet — see stacks/aws.)
|
|
389
|
+
aws: 2,
|
|
390
|
+
// Container Apps ingress appends its peer; the gateway's ingress is
|
|
391
|
+
// internal to the environment (no Front Door route to it yet).
|
|
392
|
+
azure: 1,
|
|
393
|
+
};
|
|
394
|
+
|
|
361
395
|
/**
|
|
362
396
|
* The env of the gateway service. Order is stable so the stacks see no
|
|
363
397
|
* spurious diffs.
|
|
@@ -365,6 +399,7 @@ function commonEnv(inputs: ServiceEnvInputs): CloudRunEnvVar[] {
|
|
|
365
399
|
export function gatewayEnvContract<V>(inputs: RuntimeEnvInputs<V>): CloudRunEnvVar<V | string>[] {
|
|
366
400
|
return [
|
|
367
401
|
...commonEnv(inputs),
|
|
402
|
+
{ name: TRUSTED_PROXY_HOPS_ENV, value: String(GATEWAY_TRUSTED_PROXY_HOPS[inputs.provider]) },
|
|
368
403
|
{ name: "PUBLIC_DATABASE_URL", value: inputs.databaseUrls.public },
|
|
369
404
|
...secretEnv("gateway", inputs.env),
|
|
370
405
|
];
|
|
@@ -344,6 +344,8 @@ describe("upgrading a v0.10.35 install: the gateway leaves admin_role (#613)", (
|
|
|
344
344
|
"ADMIN_DATABASE_URL",
|
|
345
345
|
// #650 — the staging gate key arrives in the same roll.
|
|
346
346
|
"CAELO_STAGING_GATE_SECRET",
|
|
347
|
+
// #845 — and which forwarded address is the visitor.
|
|
348
|
+
"CAELO_TRUSTED_PROXY_HOPS",
|
|
347
349
|
"PUBLIC_DATABASE_PASSWORD",
|
|
348
350
|
]);
|
|
349
351
|
});
|
|
@@ -316,6 +316,8 @@ describe("planContractEnv", () => {
|
|
|
316
316
|
"--update-secrets=CAELO_STAGING_GATE_SECRET=caelo-production-staging-gate-secret:latest",
|
|
317
317
|
]);
|
|
318
318
|
expect(plan.services.gateway.flags).toEqual([
|
|
319
|
+
// The gateway learns which forwarded address is the visitor (#845).
|
|
320
|
+
"--update-env-vars=CAELO_TRUSTED_PROXY_HOPS=1",
|
|
319
321
|
"--update-secrets=CAELO_STAGING_GATE_SECRET=caelo-production-staging-gate-secret:latest",
|
|
320
322
|
]);
|
|
321
323
|
});
|
package/src/upgrade/converge.ts
CHANGED
|
@@ -24,11 +24,7 @@
|
|
|
24
24
|
* {@link AppRollHooks} the app-roll calls in between, and a part after it.
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
import {
|
|
28
|
-
ensureGatewayRolePassword,
|
|
29
|
-
findSqlInstance,
|
|
30
|
-
planPublicRoleSwitch,
|
|
31
|
-
} from "../gateway-credentials.js";
|
|
27
|
+
import { findSqlInstance, planPublicRoleSwitch } from "../gateway-credentials.js";
|
|
32
28
|
import type { GcloudRunner, Sleep } from "../gcloud-retry.js";
|
|
33
29
|
import { upgraderServiceAccountEmail } from "../gcp-names.js";
|
|
34
30
|
import type { InstallMetadata } from "../install-state.js";
|
|
@@ -291,10 +287,9 @@ export function startConverge(
|
|
|
291
287
|
}
|
|
292
288
|
|
|
293
289
|
async function databaseLogins(): Promise<boolean> {
|
|
294
|
-
// #613 —
|
|
295
|
-
//
|
|
296
|
-
|
|
297
|
-
const s = r.step("Ensuring the gateway's database login (gateway_role)...");
|
|
290
|
+
// #613 — public_role moves onto its own password right before the
|
|
291
|
+
// gateway's roll. The gateway has no other database login (#635).
|
|
292
|
+
const s = r.step("Preparing the gateway's database login (public_role)...");
|
|
298
293
|
const sqlInstance = await findSqlInstance(install, { run });
|
|
299
294
|
if (!sqlInstance) {
|
|
300
295
|
s.failed("Could not find the Cloud SQL instance. Aborting upgrade.");
|
|
@@ -303,16 +298,7 @@ export function startConverge(
|
|
|
303
298
|
}
|
|
304
299
|
const roleTarget = { projectId: install.projectId, env: install.env, sqlInstance };
|
|
305
300
|
const roleDeps = { ...runDeps, ...(deps.http ? { http: deps.http } : {}) };
|
|
306
|
-
|
|
307
|
-
if (gatewayRole.status === "failed") {
|
|
308
|
-
s.failed("gateway_role could not be given its password. Aborting upgrade.");
|
|
309
|
-
r.error(` ${gatewayRole.error ?? ""}`);
|
|
310
|
-
r.warn(
|
|
311
|
-
"Migrations ran (0248 creates gateway_role); no traffic was shifted. Fix the above and re-run upgrade.",
|
|
312
|
-
);
|
|
313
|
-
return false;
|
|
314
|
-
}
|
|
315
|
-
s.done("gateway_role ready");
|
|
301
|
+
s.done("public_role ready to switch");
|
|
316
302
|
publicRole = planPublicRoleSwitch(roleTarget, service("gateway").liveEnv, roleDeps);
|
|
317
303
|
return true;
|
|
318
304
|
}
|
package/src/wizards/gcp.ts
CHANGED
|
@@ -45,7 +45,6 @@ import {
|
|
|
45
45
|
import { formatDnsRecordGroups, groupDnsRecords, type InstallDnsRecord } from "../dns/groups.js";
|
|
46
46
|
import { pickDnsAdapter } from "../dns/index.js";
|
|
47
47
|
import type { DnsRecord } from "../dns/types.js";
|
|
48
|
-
import { ensureGatewayRolePassword, findSqlInstance } from "../gateway-credentials.js";
|
|
49
48
|
import {
|
|
50
49
|
activeAccount,
|
|
51
50
|
type BillingAccount,
|
|
@@ -453,13 +452,7 @@ export async function runGcpConverge(opts: GcpConvergeOpts): Promise<void> {
|
|
|
453
452
|
}
|
|
454
453
|
}
|
|
455
454
|
|
|
456
|
-
|
|
457
|
-
// Migration 0248 created gateway_role without a password; it gets the
|
|
458
|
-
// gateway-role-password secret's value here. The gateway (deployed by
|
|
459
|
-
// Pulumi before migrations) retries its database on every request, so it
|
|
460
|
-
// starts serving /api/* as soon as this lands — no restart.
|
|
461
|
-
phase("Finishing: database logins, operator access, welcome page");
|
|
462
|
-
await stepGatewayRolePassword(projectId);
|
|
455
|
+
phase("Finishing: operator access, welcome page");
|
|
463
456
|
|
|
464
457
|
// === 11.4. The upgrader job: Settings → Updates → Install update (#696) ===
|
|
465
458
|
// Before operator access, which keeps the upgrader's caelo-mcp grant.
|
|
@@ -934,29 +927,6 @@ async function stepUrlMapRedirectsAccess(projectId: string): Promise<void> {
|
|
|
934
927
|
s.stop(green(`Redirects role ready (${r.role})`));
|
|
935
928
|
}
|
|
936
929
|
|
|
937
|
-
/**
|
|
938
|
-
* Give `gateway_role` (created by migration 0248) its password from the
|
|
939
|
-
* `gateway-role-password` secret (#613). public_role already has its own:
|
|
940
|
-
* the stack set it from `public-role-password`. Idempotent, so it runs on
|
|
941
|
-
* every wizard pass.
|
|
942
|
-
*/
|
|
943
|
-
async function stepGatewayRolePassword(projectId: string): Promise<void> {
|
|
944
|
-
const s = spinner();
|
|
945
|
-
s.start("Setting the gateway's database login (gateway_role)...");
|
|
946
|
-
const install = { projectId, env: GCP_STACK_ENV };
|
|
947
|
-
const sqlInstance = await findSqlInstance(install);
|
|
948
|
-
const r = sqlInstance
|
|
949
|
-
? await ensureGatewayRolePassword({ ...install, sqlInstance })
|
|
950
|
-
: { status: "failed" as const, error: "the Cloud SQL instance was not found" };
|
|
951
|
-
if (r.status === "failed") {
|
|
952
|
-
s.stop(red(`Failed: ${r.error ?? ""}`));
|
|
953
|
-
log.error("Re-run the wizard once the cause is fixed (this step is idempotent).");
|
|
954
|
-
cancel("Aborted.");
|
|
955
|
-
process.exit(1);
|
|
956
|
-
}
|
|
957
|
-
s.stop(green("gateway_role ready"));
|
|
958
|
-
}
|
|
959
|
-
|
|
960
930
|
/**
|
|
961
931
|
* Set up the upgrader job (upgrader-setup.ts, #696): what the admin's
|
|
962
932
|
* "Install update" starts. After migrations, because the job's database
|
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,
|
|
13
|
+
| Secret store | AWS Secrets Manager (postgres-password, public-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
|
@@ -97,11 +97,8 @@ function randomHex(bytes: number): string {
|
|
|
97
97
|
const postgresPassword = pulumi.secret(randomHex(32));
|
|
98
98
|
// #613 — one password per database role, so the gateway (public_role only,
|
|
99
99
|
// #635) never holds admin_role's. bootstrap.sh creates the roles with these
|
|
100
|
-
// (
|
|
101
|
-
// any service's login; its password stays until the contract release that
|
|
102
|
-
// drops the role (#635).
|
|
100
|
+
// (ADMIN_ROLE_PASSWORD / PUBLIC_ROLE_PASSWORD).
|
|
103
101
|
const publicRolePassword = pulumi.secret(randomHex(32));
|
|
104
|
-
const gatewayRolePassword = pulumi.secret(randomHex(32));
|
|
105
102
|
const csrfSecret = pulumi.secret(randomHex(32));
|
|
106
103
|
const cookieSecret = pulumi.secret(randomHex(32));
|
|
107
104
|
const anthropicApiKey = pulumi.secret(process.env.ANTHROPIC_API_KEY ?? "");
|
|
@@ -122,7 +119,6 @@ function secret(name: string, value: pulumi.Output<string>): aws.secretsmanager.
|
|
|
122
119
|
|
|
123
120
|
const pgSecret = secret("postgres-password", postgresPassword);
|
|
124
121
|
const publicRoleSecret = secret("public-role-password", publicRolePassword);
|
|
125
|
-
const gatewayRoleSecret = secret("gateway-role-password", gatewayRolePassword);
|
|
126
122
|
const csrfSecretRes = secret("csrf-secret", csrfSecret);
|
|
127
123
|
const cookieSecretRes = secret("cookie-secret", cookieSecret);
|
|
128
124
|
const anthropicSecretRes = secret("anthropic-api-key", anthropicApiKey);
|
|
@@ -365,7 +361,6 @@ const cluster = new aws.ecs.Cluster(`${namePrefix}-ecs`, {
|
|
|
365
361
|
const secretArns: Record<AwsSecret, pulumi.Output<string>> = {
|
|
366
362
|
"postgres-password": pgSecret.arn,
|
|
367
363
|
"public-role-password": publicRoleSecret.arn,
|
|
368
|
-
"gateway-role-password": gatewayRoleSecret.arn,
|
|
369
364
|
"csrf-secret": csrfSecretRes.arn,
|
|
370
365
|
"cookie-secret": cookieSecretRes.arn,
|
|
371
366
|
"anthropic-api-key": anthropicSecretRes.arn,
|
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,
|
|
13
|
+
| Secret store | Key Vault (postgres-password, public-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
|
@@ -39,7 +39,12 @@ import * as pulumi from "@pulumi/pulumi";
|
|
|
39
39
|
import type { CloudAdapterOutputs, DnsRecord } from "../../dist/adapter.js";
|
|
40
40
|
import { azureFrontDoorDnsRecords } from "../../dist/azure-front-door.js";
|
|
41
41
|
import { generateBootstrapToken } from "../../dist/bootstrap-token.js";
|
|
42
|
-
import {
|
|
42
|
+
import {
|
|
43
|
+
ADMIN_MEMORY_DEFAULT,
|
|
44
|
+
azureContainerAppResources,
|
|
45
|
+
GATEWAY_TRUSTED_PROXY_HOPS,
|
|
46
|
+
TRUSTED_PROXY_HOPS_ENV,
|
|
47
|
+
} from "../../dist/stack-contract.js";
|
|
43
48
|
|
|
44
49
|
const cfg = new pulumi.Config();
|
|
45
50
|
const domain = cfg.require("domain");
|
|
@@ -83,11 +88,8 @@ function randomHex(bytes: number): string {
|
|
|
83
88
|
const postgresPassword = pulumi.secret(randomHex(32));
|
|
84
89
|
// #613 — one password per database role, so the gateway (public_role only,
|
|
85
90
|
// #635) never holds admin_role's. bootstrap.sh creates the roles with these
|
|
86
|
-
// (
|
|
87
|
-
// any service's login; its password stays until the contract release that
|
|
88
|
-
// drops the role (#635).
|
|
91
|
+
// (ADMIN_ROLE_PASSWORD / PUBLIC_ROLE_PASSWORD).
|
|
89
92
|
const publicRolePassword = pulumi.secret(randomHex(32));
|
|
90
|
-
const gatewayRolePassword = pulumi.secret(randomHex(32));
|
|
91
93
|
const csrfSecret = pulumi.secret(randomHex(32));
|
|
92
94
|
const cookieSecret = pulumi.secret(randomHex(32));
|
|
93
95
|
const anthropicApiKey = pulumi.secret(process.env.ANTHROPIC_API_KEY ?? "");
|
|
@@ -117,7 +119,6 @@ function keyVaultSecret(shortName: string, value: pulumi.Output<string>): azure.
|
|
|
117
119
|
|
|
118
120
|
const _pgSecret = keyVaultSecret("pg-password", postgresPassword);
|
|
119
121
|
keyVaultSecret("public-role-password", publicRolePassword);
|
|
120
|
-
keyVaultSecret("gateway-role-password", gatewayRolePassword);
|
|
121
122
|
keyVaultSecret("csrf-secret", csrfSecret);
|
|
122
123
|
keyVaultSecret("cookie-secret", cookieSecret);
|
|
123
124
|
keyVaultSecret("anthropic-api-key", anthropicApiKey);
|
|
@@ -419,6 +420,8 @@ const adminApp = containerApp({
|
|
|
419
420
|
const _gatewayApp = containerApp({
|
|
420
421
|
serviceName: "gateway",
|
|
421
422
|
databaseEnv: [{ name: "PUBLIC_DATABASE_URL", value: publicDatabaseUrl }],
|
|
423
|
+
// Which X-Forwarded-For entry is the visitor (api-gateway client-ip.ts).
|
|
424
|
+
extraEnv: [{ name: TRUSTED_PROXY_HOPS_ENV, value: String(GATEWAY_TRUSTED_PROXY_HOPS.azure) }],
|
|
422
425
|
});
|
|
423
426
|
const _orchestratorApp = containerApp({
|
|
424
427
|
serviceName: "orchestrator",
|
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
|
|
13
|
+
| Secret store | Secret Manager: postgres-password (admin_role) + secret-kek (Pulumi); internal-secret, tool-approval-secret and public-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). |
|
|
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.
|
package/stacks/gcp/index.ts
CHANGED
|
@@ -251,14 +251,11 @@ const runtimeSecretIds: Record<RuntimeSecret, pulumi.Input<string>> = {
|
|
|
251
251
|
"tool-approval-secret": gcpSecretId(env, "tool-approval-secret"),
|
|
252
252
|
"staging-gate-secret": gcpSecretId(env, "staging-gate-secret"),
|
|
253
253
|
"public-role-password": gcpSecretId(env, "public-role-password"),
|
|
254
|
-
"gateway-role-password": gcpSecretId(env, "gateway-role-password"),
|
|
255
254
|
};
|
|
256
255
|
|
|
257
256
|
// #613 — public_role's own password (CLI-generated, stack-contract.ts
|
|
258
257
|
// DATABASE_ROLE_SECRET), so the gateway, which reads it, never holds
|
|
259
|
-
// admin_role's.
|
|
260
|
-
// it in SQL (an API-created user would join cloudsqlsuperuser) and the CLI
|
|
261
|
-
// sets its password after migrations.
|
|
258
|
+
// admin_role's. The gateway has no other database login (#635).
|
|
262
259
|
const publicRolePassword = pulumi.secret(
|
|
263
260
|
gcp.secretmanager.getSecretVersionOutput(
|
|
264
261
|
{ project, secret: gcpSecretId(env, "public-role-password") },
|
|
@@ -224,14 +224,11 @@ const runtimeSecretIds: Record<RuntimeSecret, pulumi.Input<string>> = {
|
|
|
224
224
|
"tool-approval-secret": gcpSecretId(env, "tool-approval-secret"),
|
|
225
225
|
"staging-gate-secret": gcpSecretId(env, "staging-gate-secret"),
|
|
226
226
|
"public-role-password": gcpSecretId(env, "public-role-password"),
|
|
227
|
-
"gateway-role-password": gcpSecretId(env, "gateway-role-password"),
|
|
228
227
|
};
|
|
229
228
|
|
|
230
229
|
// #613 — public_role's own password (CLI-generated, stack-contract.ts
|
|
231
230
|
// DATABASE_ROLE_SECRET), so the gateway, which reads it, never holds
|
|
232
|
-
// admin_role's.
|
|
233
|
-
// it in SQL (an API-created user would join cloudsqlsuperuser) and the CLI
|
|
234
|
-
// sets its password after migrations.
|
|
231
|
+
// admin_role's. The gateway has no other database login (#635).
|
|
235
232
|
const publicRolePassword = pulumi.secret(
|
|
236
233
|
gcp.secretmanager.getSecretVersionOutput(
|
|
237
234
|
{ project, secret: gcpSecretId(env, "public-role-password") },
|