@caelo-cms/provisioning 0.11.10 → 0.11.12
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/adapter.d.ts +6 -0
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adopt-existing.d.ts +6 -2
- package/dist/adopt-existing.d.ts.map +1 -1
- package/dist/adopt-existing.js +57 -0
- package/dist/adopt-existing.js.map +1 -1
- package/dist/agent/command.d.ts +5 -2
- package/dist/agent/command.d.ts.map +1 -1
- package/dist/agent/command.js +1 -1
- package/dist/agent/command.js.map +1 -1
- package/dist/agent-db.js +196 -23
- package/dist/cli-routing.js +1 -1
- package/dist/cli-routing.js.map +1 -1
- package/dist/cli.js +59 -7
- package/dist/cli.js.map +1 -1
- package/dist/cloudsql-tls.d.ts +30 -0
- package/dist/cloudsql-tls.d.ts.map +1 -1
- package/dist/cloudsql-tls.js +7 -1
- package/dist/cloudsql-tls.js.map +1 -1
- package/dist/compose.d.ts +14 -0
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +54 -1
- package/dist/compose.js.map +1 -1
- package/dist/converge-db-trust.d.ts +44 -0
- package/dist/converge-db-trust.d.ts.map +1 -0
- package/dist/converge-db-trust.js +158 -0
- package/dist/converge-db-trust.js.map +1 -0
- package/dist/converge-job-gcp.d.ts +12 -2
- package/dist/converge-job-gcp.d.ts.map +1 -1
- package/dist/converge-job-gcp.js +42 -3
- package/dist/converge-job-gcp.js.map +1 -1
- package/dist/converge-job-preflight.d.ts +34 -2
- package/dist/converge-job-preflight.d.ts.map +1 -1
- package/dist/converge-job-preflight.js +76 -1
- package/dist/converge-job-preflight.js.map +1 -1
- package/dist/converge-job-setup.d.ts.map +1 -1
- package/dist/converge-job-setup.js +4 -1
- package/dist/converge-job-setup.js.map +1 -1
- package/dist/converge-job.d.ts +44 -3
- package/dist/converge-job.d.ts.map +1 -1
- package/dist/converge-job.js +105 -11
- package/dist/converge-job.js.map +1 -1
- package/dist/gcp-names.d.ts +13 -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/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.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 +22 -1
- package/dist/infra-contract.js.map +1 -1
- package/dist/install-job.d.ts +10 -2
- package/dist/install-job.d.ts.map +1 -1
- package/dist/install-job.js +10 -1
- package/dist/install-job.js.map +1 -1
- package/dist/install-state.d.ts +7 -0
- package/dist/install-state.d.ts.map +1 -1
- package/dist/install-state.js.map +1 -1
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +3 -0
- package/dist/lifecycle.js.map +1 -1
- package/dist/mcp-auth-proxy-setup.d.ts +3 -1
- package/dist/mcp-auth-proxy-setup.d.ts.map +1 -1
- package/dist/mcp-auth-proxy-setup.js +9 -4
- package/dist/mcp-auth-proxy-setup.js.map +1 -1
- package/dist/migration-runner.d.ts.map +1 -1
- package/dist/migration-runner.js +8 -3
- package/dist/migration-runner.js.map +1 -1
- package/dist/migration-targets.d.ts +11 -0
- package/dist/migration-targets.d.ts.map +1 -0
- package/dist/migration-targets.js +12 -0
- package/dist/migration-targets.js.map +1 -0
- package/dist/operator-access.d.ts +10 -0
- package/dist/operator-access.d.ts.map +1 -1
- package/dist/operator-access.js +5 -0
- package/dist/operator-access.js.map +1 -1
- package/dist/pool-budget-stack.d.ts +13 -1
- package/dist/pool-budget-stack.d.ts.map +1 -1
- package/dist/pool-budget-stack.js +11 -1
- package/dist/pool-budget-stack.js.map +1 -1
- package/dist/public-role-isolation.d.ts +7 -3
- package/dist/public-role-isolation.d.ts.map +1 -1
- package/dist/public-role-isolation.js +25 -13
- package/dist/public-role-isolation.js.map +1 -1
- 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/runtime-secrets.d.ts +2 -0
- package/dist/runtime-secrets.d.ts.map +1 -1
- package/dist/runtime-secrets.js +19 -15
- package/dist/runtime-secrets.js.map +1 -1
- package/dist/scheduler-setup.d.ts +75 -0
- package/dist/scheduler-setup.d.ts.map +1 -0
- package/dist/scheduler-setup.js +331 -0
- package/dist/scheduler-setup.js.map +1 -0
- package/dist/self-hosted-roles.d.ts +16 -9
- package/dist/self-hosted-roles.d.ts.map +1 -1
- package/dist/self-hosted-roles.js +49 -15
- package/dist/self-hosted-roles.js.map +1 -1
- package/dist/self-hosted-scheduler-interval.d.ts +21 -0
- package/dist/self-hosted-scheduler-interval.d.ts.map +1 -0
- package/dist/self-hosted-scheduler-interval.js +68 -0
- package/dist/self-hosted-scheduler-interval.js.map +1 -0
- package/dist/self-hosted-upgrade.d.ts +7 -4
- package/dist/self-hosted-upgrade.d.ts.map +1 -1
- package/dist/self-hosted-upgrade.js +9 -5
- package/dist/self-hosted-upgrade.js.map +1 -1
- package/dist/stack-contract.d.ts +67 -6
- package/dist/stack-contract.d.ts.map +1 -1
- package/dist/stack-contract.js +87 -6
- package/dist/stack-contract.js.map +1 -1
- package/dist/staging-database.d.ts +22 -0
- package/dist/staging-database.d.ts.map +1 -0
- package/dist/staging-database.js +117 -0
- package/dist/staging-database.js.map +1 -0
- package/dist/upgrade/converge.d.ts +5 -0
- package/dist/upgrade/converge.d.ts.map +1 -1
- package/dist/upgrade/converge.js +110 -10
- package/dist/upgrade/converge.js.map +1 -1
- package/dist/upgrade/upgrade.d.ts +6 -0
- package/dist/upgrade/upgrade.d.ts.map +1 -1
- package/dist/upgrade/upgrade.js +3 -0
- package/dist/upgrade/upgrade.js.map +1 -1
- package/dist/upgrader-gcp.d.ts +5 -4
- package/dist/upgrader-gcp.d.ts.map +1 -1
- package/dist/upgrader-gcp.js +5 -4
- package/dist/upgrader-gcp.js.map +1 -1
- package/dist/upgrader-iam-preflight.d.ts +8 -6
- package/dist/upgrader-iam-preflight.d.ts.map +1 -1
- package/dist/upgrader-iam-preflight.js +5 -1
- package/dist/upgrader-iam-preflight.js.map +1 -1
- package/dist/wizard.d.ts +7 -0
- package/dist/wizard.d.ts.map +1 -1
- package/dist/wizard.js +11 -0
- package/dist/wizard.js.map +1 -1
- package/dist/wizards/gcp-cost.d.ts +31 -0
- package/dist/wizards/gcp-cost.d.ts.map +1 -1
- package/dist/wizards/gcp-cost.js +82 -12
- package/dist/wizards/gcp-cost.js.map +1 -1
- package/dist/wizards/gcp.d.ts.map +1 -1
- package/dist/wizards/gcp.js +87 -11
- package/dist/wizards/gcp.js.map +1 -1
- package/package.json +2 -2
- package/src/adapter.ts +6 -0
- package/src/admin-memory.test.ts +1 -1
- package/src/adopt-existing.test.ts +46 -0
- package/src/adopt-existing.ts +58 -2
- package/src/agent/command.ts +6 -3
- package/src/caddy-redirects.test.ts +1 -1
- package/src/cli-routing.test.ts +1 -1
- package/src/cli-routing.ts +1 -1
- package/src/cli.ts +63 -8
- package/src/cloudsql-tls.test.ts +30 -0
- package/src/cloudsql-tls.ts +10 -3
- package/src/compose.ts +68 -0
- package/src/converge-db-trust.test.ts +237 -0
- package/src/converge-db-trust.ts +212 -0
- package/src/converge-job-gcp.ts +42 -2
- package/src/converge-job-preflight.test.ts +106 -0
- package/src/converge-job-preflight.ts +97 -3
- package/src/converge-job-setup.ts +4 -1
- package/src/converge-job.test.ts +213 -0
- package/src/converge-job.ts +175 -19
- package/src/database-tls-builders.test.ts +3 -2
- package/src/deployed-release.test.ts +3 -0
- package/src/gateway-env-isolation.test.ts +40 -7
- package/src/gcp-names.ts +25 -0
- package/src/index.test.ts +3 -3
- package/src/index.ts +6 -0
- package/src/infra-contract.test.ts +5 -3
- package/src/infra-contract.ts +22 -1
- package/src/install-job.ts +10 -1
- package/src/install-state.ts +7 -0
- package/src/lifecycle.ts +3 -0
- package/src/mcp-auth-proxy-setup.test.ts +33 -1
- package/src/mcp-auth-proxy-setup.ts +11 -4
- package/src/migration-runner.ts +14 -9
- package/src/migration-targets.ts +12 -0
- package/src/operator-access.test.ts +15 -0
- package/src/operator-access.ts +14 -0
- package/src/pool-budget-stack.ts +15 -0
- package/src/pool-budget-stacks.test.ts +38 -13
- package/src/public-role-isolation.test.ts +18 -8
- package/src/public-role-isolation.ts +31 -17
- package/src/release-contract.ts +3 -0
- package/src/runtime-secrets.test.ts +18 -8
- package/src/runtime-secrets.ts +37 -18
- package/src/scheduler-setup.test.ts +201 -0
- package/src/scheduler-setup.ts +375 -0
- package/src/self-hosted-backup.test.ts +1 -0
- package/src/self-hosted-compose.test.ts +72 -1
- package/src/self-hosted-roles.integration.test.ts +2 -0
- package/src/self-hosted-roles.test.ts +49 -19
- package/src/self-hosted-roles.ts +60 -20
- package/src/self-hosted-scheduler-interval.test.ts +66 -0
- package/src/self-hosted-scheduler-interval.ts +79 -0
- package/src/self-hosted-start.test.ts +1 -0
- package/src/self-hosted-upgrade.test.ts +2 -1
- package/src/self-hosted-upgrade.ts +14 -8
- package/src/stack-contract.test.ts +31 -3
- package/src/stack-contract.ts +113 -6
- package/src/stack-converge-secrets.test.ts +2 -0
- package/src/stack-converge.test.ts +6 -4
- package/src/staging-database.test.ts +114 -0
- package/src/staging-database.ts +148 -0
- package/src/upgrade/converge.ts +130 -9
- package/src/upgrade/upgrade.test.ts +85 -2
- package/src/upgrade/upgrade.ts +9 -0
- package/src/upgrader-gcp.ts +5 -4
- package/src/upgrader-iam-preflight.ts +15 -10
- package/src/wizard.ts +19 -0
- package/src/wizards/gcp-cost.test.ts +54 -1
- package/src/wizards/gcp-cost.ts +102 -12
- package/src/wizards/gcp.ts +109 -8
- package/stacks/aws/Pulumi.yaml +1 -1
- package/stacks/aws/index.ts +41 -1
- package/stacks/azure/index.ts +51 -3
- package/stacks/gcp/Pulumi.yaml +1 -1
- package/stacks/gcp/README.md +1 -0
- package/stacks/gcp/index.ts +50 -1
- package/stacks/gcp-firebase/Pulumi.yaml +1 -1
- package/stacks/gcp-firebase/index.ts +50 -1
- package/stacks/self-hosted/Pulumi.yaml +3 -0
- package/stacks/self-hosted/README.md +4 -0
- package/stacks/self-hosted/index.ts +29 -4
package/src/stack-contract.ts
CHANGED
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
mcpIapServiceAccountEmail,
|
|
50
50
|
operatorAccessJobResource,
|
|
51
51
|
runServiceAccountEmail,
|
|
52
|
+
schedulerServiceAccountEmail,
|
|
52
53
|
staticPublisherServiceAccountId,
|
|
53
54
|
upgraderJobResource,
|
|
54
55
|
} from "./gcp-names.js";
|
|
@@ -161,6 +162,16 @@ export interface DatabaseUrls<V = string> {
|
|
|
161
162
|
* state the admin publishes into cms_public.
|
|
162
163
|
*/
|
|
163
164
|
readonly public: V;
|
|
165
|
+
/**
|
|
166
|
+
* #1329 — admin_role on cms_public_staging: the admin's third pool, for
|
|
167
|
+
* the staging database's plugin schemas and its half of the projection.
|
|
168
|
+
*/
|
|
169
|
+
readonly publicStagingAdmin: V;
|
|
170
|
+
/**
|
|
171
|
+
* #1329 — public_staging_role on cms_public_staging, the staging
|
|
172
|
+
* gateway's only pool (served from P3b-2 on). It reaches no live data.
|
|
173
|
+
*/
|
|
174
|
+
readonly publicStaging: V;
|
|
164
175
|
}
|
|
165
176
|
|
|
166
177
|
/** Port Cloud SQL Postgres listens on (private IP). */
|
|
@@ -191,6 +202,8 @@ export function databaseUrls(host: string): DatabaseUrls {
|
|
|
191
202
|
admin: url("admin_role", "cms_admin"),
|
|
192
203
|
publicAdmin: url("admin_role", "cms_public"),
|
|
193
204
|
public: url("public_role", "cms_public"),
|
|
205
|
+
publicStagingAdmin: url("admin_role", "cms_public_staging"),
|
|
206
|
+
publicStaging: url("public_staging_role", "cms_public_staging"),
|
|
194
207
|
};
|
|
195
208
|
}
|
|
196
209
|
|
|
@@ -238,7 +251,7 @@ export function cloudSqlPrivateDnsName(
|
|
|
238
251
|
|
|
239
252
|
/** The database URL vars each service carries (the env contract below sets them). */
|
|
240
253
|
export const DATABASE_URL_VARS = {
|
|
241
|
-
admin: ["ADMIN_DATABASE_URL", "PUBLIC_ADMIN_DATABASE_URL"],
|
|
254
|
+
admin: ["ADMIN_DATABASE_URL", "PUBLIC_ADMIN_DATABASE_URL", "PUBLIC_STAGING_ADMIN_DATABASE_URL"],
|
|
242
255
|
gateway: ["PUBLIC_DATABASE_URL"],
|
|
243
256
|
// #1328 — no database login at all. On gcp-firebase the service still
|
|
244
257
|
// carries the gateway's URL until its roll removes it with the rest of
|
|
@@ -328,6 +341,54 @@ export const CAPTURE_JOB_ENV_VAR = "CAELO_CAPTURE_JOB";
|
|
|
328
341
|
export const CAPTURE_BUCKET_ENV_VAR = "CAELO_CAPTURE_BUCKET";
|
|
329
342
|
export const MEDIA_BUCKET_ENV_VAR = "CAELO_MEDIA_BUCKET";
|
|
330
343
|
|
|
344
|
+
/**
|
|
345
|
+
* #1140 — the admin env var naming the service account of the scheduler
|
|
346
|
+
* tick (scheduler-setup.ts): the one IAP identity the admin's
|
|
347
|
+
* `/api/internal/scheduler/tick` accepts. `upgrade` leaves it untouched when
|
|
348
|
+
* it could not set the tick up.
|
|
349
|
+
*/
|
|
350
|
+
export const SCHEDULER_SERVICE_ACCOUNT_ENV_VAR = "CAELO_SCHEDULER_SERVICE_ACCOUNT";
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* #1140 — how often the one scheduler job calls the admin's tick, in
|
|
354
|
+
* minutes (the `schedulerIntervalMinutes` install knob). Each task's own
|
|
355
|
+
* cadence lives in the admin's due-table; this only bounds how soon a due
|
|
356
|
+
* task runs. 1 is Cloud Scheduler's floor; above 15 Cloud Run has stopped
|
|
357
|
+
* the idle admin, so every tick would pay a cold start without saving
|
|
358
|
+
* anything (maintainer decision on #1140, 2026-10-26).
|
|
359
|
+
*/
|
|
360
|
+
export const SCHEDULER_INTERVAL_MINUTES = { default: 5, min: 1, max: 15 } as const;
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Parse the `schedulerIntervalMinutes` knob. Undefined (not configured) is
|
|
364
|
+
* the default; anything else must be an integer in the bounds — an
|
|
365
|
+
* out-of-range or non-numeric value throws (CLAUDE.md §2: no silent
|
|
366
|
+
* substitute).
|
|
367
|
+
*
|
|
368
|
+
* @param raw - the configured value (CLI flag, Pulumi config, install.json).
|
|
369
|
+
* @param source - names where the value came from, for the error.
|
|
370
|
+
*/
|
|
371
|
+
export function parseSchedulerIntervalMinutes(
|
|
372
|
+
raw: string | number | undefined,
|
|
373
|
+
source = "schedulerIntervalMinutes",
|
|
374
|
+
): number {
|
|
375
|
+
if (raw === undefined || raw === "") return SCHEDULER_INTERVAL_MINUTES.default;
|
|
376
|
+
const n = typeof raw === "number" ? raw : /^\s*\d+\s*$/.test(raw) ? Number(raw) : Number.NaN;
|
|
377
|
+
const { min, max } = SCHEDULER_INTERVAL_MINUTES;
|
|
378
|
+
if (!Number.isInteger(n) || n < min || n > max) {
|
|
379
|
+
throw new Error(
|
|
380
|
+
`${source} ${JSON.stringify(raw)} is not a whole number of minutes between ${min} and ${max} (default ${SCHEDULER_INTERVAL_MINUTES.default})`,
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
return n;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** The Cloud Scheduler / cron expression of a tick every `minutes` minutes (UTC). */
|
|
387
|
+
export function schedulerCron(minutes: number): string {
|
|
388
|
+
const m = parseSchedulerIntervalMinutes(minutes);
|
|
389
|
+
return m === 1 ? "* * * * *" : `*/${m} * * * *`;
|
|
390
|
+
}
|
|
391
|
+
|
|
331
392
|
/** Where the admin image ships the static-generator CLI. */
|
|
332
393
|
const GENERATOR_CLI = "/app/apps/static-generator/src/cli.ts";
|
|
333
394
|
|
|
@@ -397,6 +458,8 @@ export function adminMediaVolumeTemplate(volume: MediaVolumeContract) {
|
|
|
397
458
|
export type RuntimeSecret =
|
|
398
459
|
| "postgres-password"
|
|
399
460
|
| "public-role-password"
|
|
461
|
+
/** #1329 — public_staging_role's password, the staging gateway's login. */
|
|
462
|
+
| "public-staging-role-password"
|
|
400
463
|
| "secret-kek"
|
|
401
464
|
| "internal-secret"
|
|
402
465
|
| "tool-approval-secret"
|
|
@@ -418,7 +481,7 @@ export type RuntimeSecret =
|
|
|
418
481
|
| "mcp-oauth-seal-key";
|
|
419
482
|
|
|
420
483
|
/** The Postgres roles Caelo's services log in as, with their passwords. */
|
|
421
|
-
export type DatabaseRole = "admin_role" | "public_role";
|
|
484
|
+
export type DatabaseRole = "admin_role" | "public_role" | "public_staging_role";
|
|
422
485
|
|
|
423
486
|
/**
|
|
424
487
|
* Each database role's password secret — one per role (#613), so the
|
|
@@ -430,6 +493,8 @@ export type DatabaseRole = "admin_role" | "public_role";
|
|
|
430
493
|
export const DATABASE_ROLE_SECRET: Readonly<Record<DatabaseRole, RuntimeSecret>> = {
|
|
431
494
|
admin_role: "postgres-password",
|
|
432
495
|
public_role: "public-role-password",
|
|
496
|
+
// #1329 — its own, so neither gateway can ever log in as the other.
|
|
497
|
+
public_staging_role: "public-staging-role-password",
|
|
433
498
|
};
|
|
434
499
|
|
|
435
500
|
/**
|
|
@@ -445,6 +510,9 @@ export const CLI_GENERATED_SECRETS = [
|
|
|
445
510
|
// #613 — the gateway's own role password. Generated (never seeded from
|
|
446
511
|
// `postgres-password`), so the gateway never learns admin_role's.
|
|
447
512
|
"public-role-password",
|
|
513
|
+
// #1329 — the staging gateway's role password, never shared with the
|
|
514
|
+
// live gateway's (a test asserts the two public logins share no secret).
|
|
515
|
+
"public-staging-role-password",
|
|
448
516
|
// #650 — the staging access gate's key: signs "Open staging" links and
|
|
449
517
|
// session cookies, derives the crawler credentials. Read by the admin
|
|
450
518
|
// (issues) and the gateway (verifies).
|
|
@@ -457,6 +525,19 @@ export const CLI_GENERATED_SECRETS = [
|
|
|
457
525
|
"mcp-oauth-seal-key",
|
|
458
526
|
] as const satisfies readonly RuntimeSecret[];
|
|
459
527
|
|
|
528
|
+
/**
|
|
529
|
+
* CLI-generated secrets no running service reads yet, each with what reads
|
|
530
|
+
* it until then. Everything else in {@link CLI_GENERATED_SECRETS} must be
|
|
531
|
+
* read by a service (stack-contract.test.ts), so an orphaned secret cannot
|
|
532
|
+
* ship by accident.
|
|
533
|
+
*/
|
|
534
|
+
export const SECRETS_SET_UP_AHEAD_OF_THEIR_READER: Readonly<
|
|
535
|
+
Partial<Record<(typeof CLI_GENERATED_SECRETS)[number], string>>
|
|
536
|
+
> = {
|
|
537
|
+
"public-staging-role-password":
|
|
538
|
+
"#1329 — public_staging_role's password. The stacks (gcp.sql.User) and the converge (staging-database.ts) set the role's password from it; the staging gateway service (#1330) is its first runtime reader.",
|
|
539
|
+
};
|
|
540
|
+
|
|
460
541
|
/** A CLI-generated secret's first value: random hex, random base64, or a fixed placeholder. */
|
|
461
542
|
export type GeneratedSecretFormat =
|
|
462
543
|
| { readonly kind: "hex32" }
|
|
@@ -473,6 +554,7 @@ export const GENERATED_SECRET_FORMAT: Readonly<
|
|
|
473
554
|
"internal-secret": { kind: "hex32" },
|
|
474
555
|
"tool-approval-secret": { kind: "hex32" },
|
|
475
556
|
"public-role-password": { kind: "hex32" },
|
|
557
|
+
"public-staging-role-password": { kind: "hex32" },
|
|
476
558
|
"staging-gate-secret": { kind: "hex32" },
|
|
477
559
|
// The gateway's seal key contract (#806): base64 of exactly 32 bytes.
|
|
478
560
|
"mcp-oauth-seal-key": { kind: "base64-32" },
|
|
@@ -510,6 +592,8 @@ export const SERVICE_SECRET_ENV: Readonly<
|
|
|
510
592
|
admin: {
|
|
511
593
|
ADMIN_DATABASE_PASSWORD: "postgres-password",
|
|
512
594
|
PUBLIC_ADMIN_DATABASE_PASSWORD: "postgres-password",
|
|
595
|
+
// #1329 — admin_role on the staging database.
|
|
596
|
+
PUBLIC_STAGING_ADMIN_DATABASE_PASSWORD: "postgres-password",
|
|
513
597
|
// P18 — encrypts AI provider keys and other at-rest secrets in cms_admin.
|
|
514
598
|
CAELO_SECRET_KEK: "secret-kek",
|
|
515
599
|
// P15.1 — HMAC key of the /api/internal/* bearer tokens (internal-jwt.ts).
|
|
@@ -747,6 +831,15 @@ function commonEnv(inputs: ServiceEnvInputs): CloudRunEnvVar[] {
|
|
|
747
831
|
*/
|
|
748
832
|
export const TRUSTED_PROXY_HOPS_ENV = "CAELO_TRUSTED_PROXY_HOPS";
|
|
749
833
|
|
|
834
|
+
/**
|
|
835
|
+
* #1329 — the env var naming which environment a gateway serves, `live` or
|
|
836
|
+
* `staging` (apps/api-gateway database-env.ts): its database and login
|
|
837
|
+
* follow from it, and it refuses anything of the other environment's. The
|
|
838
|
+
* gateway refuses to boot without it. Every adapter sets `live` on the
|
|
839
|
+
* gateway it runs today; the staging gateway (P3b-2) gets `staging`.
|
|
840
|
+
*/
|
|
841
|
+
export const GATEWAY_ENVIRONMENT_ENV = "CAELO_GATEWAY_ENVIRONMENT";
|
|
842
|
+
|
|
750
843
|
/**
|
|
751
844
|
* {@link TRUSTED_PROXY_HOPS_ENV} per provider — a property of each
|
|
752
845
|
* adapter's topology (CLAUDE.md §11.B), so it is declared here once and every
|
|
@@ -785,6 +878,9 @@ export const GATEWAY_TRUSTED_PROXY_HOPS: Readonly<Record<SupportedProvider, numb
|
|
|
785
878
|
export function gatewayEnvContract<V>(inputs: RuntimeEnvInputs<V>): CloudRunEnvVar<V | string>[] {
|
|
786
879
|
return [
|
|
787
880
|
...commonEnv(inputs),
|
|
881
|
+
// #1329 — this service is the live site's gateway: it may reach
|
|
882
|
+
// cms_public as public_role and refuses anything of the staging data.
|
|
883
|
+
{ name: GATEWAY_ENVIRONMENT_ENV, value: "live" },
|
|
788
884
|
{ name: TRUSTED_PROXY_HOPS_ENV, value: String(GATEWAY_TRUSTED_PROXY_HOPS[inputs.provider]) },
|
|
789
885
|
{ name: "PUBLIC_DATABASE_URL", value: inputs.databaseUrls.public },
|
|
790
886
|
...secretEnv("gateway", inputs.env),
|
|
@@ -900,12 +996,14 @@ export const DEFAULT_MCP_AUTH_PROXY_MAX_INSTANCES = 10;
|
|
|
900
996
|
* anyone choosing it — billed for every idle minute an instance stayed up.
|
|
901
997
|
*
|
|
902
998
|
* The gateway and the MCP-Auth-Proxy only do work inside requests. The
|
|
903
|
-
* admin
|
|
904
|
-
*
|
|
999
|
+
* admin's periodic work and plugin workers run on the scheduler tick
|
|
1000
|
+
* (#1140), but the quality audit and the site-import crawl still run on
|
|
1001
|
+
* in-process timers that need CPU between requests (#1359, #1360), and the
|
|
1002
|
+
* plugin runtime heartbeat would read an idling instance as down.
|
|
905
1003
|
*/
|
|
906
1004
|
export const SERVICE_CPU_IDLE: Readonly<Record<CloudRunSlug, boolean>> = {
|
|
907
|
-
// FIXME(issue #
|
|
908
|
-
//
|
|
1005
|
+
// FIXME(issue #1361): true once nothing in the admin depends on CPU
|
|
1006
|
+
// between requests (the category-A jobs, the heartbeat at request start).
|
|
909
1007
|
admin: false,
|
|
910
1008
|
gateway: true,
|
|
911
1009
|
"mcp-auth-proxy": true,
|
|
@@ -1180,6 +1278,9 @@ export function adminEnvContract<V>(inputs: AdminEnvInputs<V>): CloudRunEnvVar<V
|
|
|
1180
1278
|
// admin_role on cms_public: cross-DB reads, plugin DDL, migrations.
|
|
1181
1279
|
// Not public_role (write-limited).
|
|
1182
1280
|
{ name: "PUBLIC_ADMIN_DATABASE_URL", value: inputs.databaseUrls.publicAdmin },
|
|
1281
|
+
// #1329 — admin_role on cms_public_staging: the staging database's
|
|
1282
|
+
// plugin schemas and its half of the projection.
|
|
1283
|
+
{ name: "PUBLIC_STAGING_ADMIN_DATABASE_URL", value: inputs.databaseUrls.publicStagingAdmin },
|
|
1183
1284
|
// #551 — the admin seeds site_defaults.site_base_url from it (canonical,
|
|
1184
1285
|
// og:url, sitemap) when it is not configured yet.
|
|
1185
1286
|
{ name: "CAELO_SITE_URL", value: publicSiteUrl(domain) },
|
|
@@ -1211,6 +1312,12 @@ export function adminEnvContract<V>(inputs: AdminEnvInputs<V>): CloudRunEnvVar<V
|
|
|
1211
1312
|
{ name: CAPTURE_JOB_ENV_VAR, value: captureJobResource(projectId, inputs.region, env) },
|
|
1212
1313
|
{ name: CAPTURE_BUCKET_ENV_VAR, value: gcpBucketName(projectId, env, "capture") },
|
|
1213
1314
|
{ name: MEDIA_BUCKET_ENV_VAR, value: gcpBucketName(projectId, env, "media") },
|
|
1315
|
+
// #1140 — the only IAP identity the admin's scheduler tick endpoint
|
|
1316
|
+
// accepts (scheduler-setup.ts).
|
|
1317
|
+
{
|
|
1318
|
+
name: SCHEDULER_SERVICE_ACCOUNT_ENV_VAR,
|
|
1319
|
+
value: schedulerServiceAccountEmail(projectId, env),
|
|
1320
|
+
},
|
|
1214
1321
|
// #914 — where Settings → Google sign-in writes the install's Google
|
|
1215
1322
|
// sign-in client (add-version only, see the run SA's secretVersionAdder
|
|
1216
1323
|
// invariant).
|
|
@@ -360,6 +360,8 @@ describe("upgrading a v0.10.35 install: the gateway leaves admin_role (#613)", (
|
|
|
360
360
|
expect(plan.ok && plan.services.gateway.changes.map((c) => c.name).sort()).toEqual([
|
|
361
361
|
"ADMIN_DATABASE_PASSWORD",
|
|
362
362
|
"ADMIN_DATABASE_URL",
|
|
363
|
+
// #1329 — the gateway says it is the live site's.
|
|
364
|
+
"CAELO_GATEWAY_ENVIRONMENT",
|
|
363
365
|
// No remote-MCP env: the broker is the MCP-Auth-Proxy's (#1328).
|
|
364
366
|
// #650 — the staging gate key arrives in the same roll.
|
|
365
367
|
"CAELO_STAGING_GATE_SECRET",
|
|
@@ -335,14 +335,16 @@ describe("planContractEnv", () => {
|
|
|
335
335
|
// #650 — the same roll converges the protected staging domain's host
|
|
336
336
|
// and gate key onto an install that predates it.
|
|
337
337
|
expect(plan.services.admin.flags).toEqual([
|
|
338
|
-
|
|
339
|
-
"--update-
|
|
338
|
+
// #1329 — and the staging database's pool onto one that predates it.
|
|
339
|
+
"--update-env-vars=PUBLIC_STAGING_ADMIN_DATABASE_URL=postgresql://admin_role@db1.p-label.europe-west1.sql-psa.goog:5432/cms_public_staging?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem,CAELO_SITE_URL=https://acme.com,MEDIA_ROOT_DIR=/app/apps/admin/data/media,CAELO_MCP_IAP_SERVICE_ACCOUNT=caelo-mcp@acme.iam.gserviceaccount.com,CAELO_OPERATOR_ACCESS_JOB=projects/acme/locations/europe-west1/jobs/caelo-production-operator-access-sync,CAELO_UPGRADER_JOB=projects/acme/locations/europe-west1/jobs/caelo-production-upgrader,CAELO_CONVERGE_JOB=projects/acme/locations/europe-west1/jobs/caelo-production-converge,CAELO_AUDIT_JOB=projects/acme/locations/europe-west1/jobs/caelo-production-audit,CAELO_AUDIT_RESULTS_BUCKET=acme-caelo-production-audit-results,CAELO_CAPTURE_JOB=projects/acme/locations/europe-west1/jobs/caelo-production-capture,CAELO_CAPTURE_BUCKET=acme-caelo-production-capture,CAELO_MEDIA_BUCKET=acme-caelo-production-media,CAELO_SCHEDULER_SERVICE_ACCOUNT=caelo-production-scheduler@acme.iam.gserviceaccount.com,CAELO_OPERATOR_OAUTH_CLIENT_STORE=projects/acme/secrets/caelo-production-operator-oauth-client,CAELO_GATEWAY_SERVICE=caelo-production-data-gateway,CAELO_STAGING_HOST=staging.acme.com",
|
|
340
|
+
"--update-secrets=PUBLIC_STAGING_ADMIN_DATABASE_PASSWORD=caelo-production-postgres-password:latest,CAELO_STAGING_GATE_SECRET=caelo-production-staging-gate-secret:latest",
|
|
340
341
|
]);
|
|
341
342
|
// #1328 — the Firebase rewrite follows the data gateway (above), which
|
|
342
343
|
// gets no remote-MCP env.
|
|
343
344
|
expect(plan.services.gateway.flags).toEqual([
|
|
344
|
-
// The gateway learns which forwarded address is the visitor (#845)
|
|
345
|
-
|
|
345
|
+
// The gateway learns which forwarded address is the visitor (#845),
|
|
346
|
+
// and (#1329) that it is the live site's.
|
|
347
|
+
"--update-env-vars=CAELO_GATEWAY_ENVIRONMENT=live,CAELO_TRUSTED_PROXY_HOPS=1",
|
|
346
348
|
"--update-secrets=CAELO_STAGING_GATE_SECRET=caelo-production-staging-gate-secret:latest",
|
|
347
349
|
]);
|
|
348
350
|
// The old gateway service becomes the proxy in one revision: every
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "bun:test";
|
|
4
|
+
import type { GcloudResult } from "./gcloud.js";
|
|
5
|
+
import type { GcloudRunner } from "./gcloud-retry.js";
|
|
6
|
+
import type { HttpFetch } from "./runtime-secrets.js";
|
|
7
|
+
import { ensureStagingDatabase } from "./staging-database.js";
|
|
8
|
+
|
|
9
|
+
const ok = (stdout = ""): GcloudResult => ({ ok: true, stdout, stderr: "", exitCode: 0 });
|
|
10
|
+
const fail = (stderr: string): GcloudResult => ({ ok: false, stdout: "", stderr, exitCode: 1 });
|
|
11
|
+
const noSleep = async () => {};
|
|
12
|
+
const install = { projectId: "acme", env: "production", credentials: "operator" } as const;
|
|
13
|
+
|
|
14
|
+
/** gcloud with an instance `pg-1`, the databases and users given, and a staging password secret. */
|
|
15
|
+
function fakeGcloud(state: { databases: string[]; users: string[] }) {
|
|
16
|
+
const calls: string[] = [];
|
|
17
|
+
const run: GcloudRunner = async (args) => {
|
|
18
|
+
const line = args.join(" ");
|
|
19
|
+
calls.push(line);
|
|
20
|
+
if (line.startsWith("sql instances list")) return ok("pg-1\n");
|
|
21
|
+
if (line.startsWith("sql databases list")) return ok(`${state.databases.join("\n")}\n`);
|
|
22
|
+
if (line.startsWith("sql users list")) return ok(`${state.users.join("\n")}\n`);
|
|
23
|
+
if (line.startsWith("sql databases create")) return ok();
|
|
24
|
+
if (line.startsWith("secrets versions access latest")) return ok("s3cret-staging");
|
|
25
|
+
if (line.startsWith("auth print-access-token")) return ok("tok\n");
|
|
26
|
+
return fail(`unexpected: ${line}`);
|
|
27
|
+
};
|
|
28
|
+
return { run, calls };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The Cloud SQL Admin API, finishing every operation at once. */
|
|
32
|
+
function fakeHttp() {
|
|
33
|
+
const requests: { method: string; url: string; body?: string }[] = [];
|
|
34
|
+
const http: HttpFetch = async (url, init) => {
|
|
35
|
+
requests.push({ method: init.method, url, ...(init.body ? { body: init.body } : {}) });
|
|
36
|
+
return { ok: true, status: 200, text: async () => JSON.stringify({ status: "DONE" }) };
|
|
37
|
+
};
|
|
38
|
+
return { http, requests };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe("ensureStagingDatabase (#1329)", () => {
|
|
42
|
+
it("creates the database and the login on an install from before them", async () => {
|
|
43
|
+
const { run, calls } = fakeGcloud({
|
|
44
|
+
databases: ["cms_admin", "cms_public"],
|
|
45
|
+
users: ["admin_role", "public_role"],
|
|
46
|
+
});
|
|
47
|
+
const { http, requests } = fakeHttp();
|
|
48
|
+
const outcomes = await ensureStagingDatabase(install, { run, http, sleep: noSleep });
|
|
49
|
+
expect(outcomes.map((o) => [o.id, o.status])).toEqual([
|
|
50
|
+
["database cms_public_staging", "applied"],
|
|
51
|
+
["database role public_staging_role", "applied"],
|
|
52
|
+
]);
|
|
53
|
+
expect(calls).toContain(
|
|
54
|
+
"sql databases create cms_public_staging --instance=pg-1 --project=acme --quiet",
|
|
55
|
+
);
|
|
56
|
+
// The password goes in the request body, read from its own secret.
|
|
57
|
+
expect(calls).toContain(
|
|
58
|
+
"secrets versions access latest --secret=caelo-production-public-staging-role-password --project=acme",
|
|
59
|
+
);
|
|
60
|
+
expect(requests).toEqual([
|
|
61
|
+
{
|
|
62
|
+
method: "POST",
|
|
63
|
+
url: "https://sqladmin.googleapis.com/v1/projects/acme/instances/pg-1/users",
|
|
64
|
+
body: JSON.stringify({ name: "public_staging_role", password: "s3cret-staging" }),
|
|
65
|
+
},
|
|
66
|
+
]);
|
|
67
|
+
expect(calls.some((c) => c.includes("s3cret-staging"))).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("leaves an install that has both alone, re-applying the password", async () => {
|
|
71
|
+
const { run, calls } = fakeGcloud({
|
|
72
|
+
databases: ["cms_admin", "cms_public", "cms_public_staging"],
|
|
73
|
+
users: ["admin_role", "public_role", "public_staging_role"],
|
|
74
|
+
});
|
|
75
|
+
const { http, requests } = fakeHttp();
|
|
76
|
+
const outcomes = await ensureStagingDatabase(install, { run, http, sleep: noSleep });
|
|
77
|
+
expect(outcomes.map((o) => o.status)).toEqual(["present", "present"]);
|
|
78
|
+
expect(calls.some((c) => c.startsWith("sql databases create"))).toBe(false);
|
|
79
|
+
expect(requests.map((r) => [r.method, r.url])).toEqual([
|
|
80
|
+
[
|
|
81
|
+
"PUT",
|
|
82
|
+
"https://sqladmin.googleapis.com/v1/projects/acme/instances/pg-1/users?name=public_staging_role",
|
|
83
|
+
],
|
|
84
|
+
]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("reports a refused create with the permission it needs", async () => {
|
|
88
|
+
const calls: string[] = [];
|
|
89
|
+
const run: GcloudRunner = async (args) => {
|
|
90
|
+
const line = args.join(" ");
|
|
91
|
+
calls.push(line);
|
|
92
|
+
if (line.startsWith("sql instances list")) return ok("pg-1\n");
|
|
93
|
+
if (line.startsWith("sql databases list")) return ok("cms_admin\n");
|
|
94
|
+
if (line.startsWith("sql databases create")) return fail("PERMISSION_DENIED: nope");
|
|
95
|
+
return ok("public_staging_role\n");
|
|
96
|
+
};
|
|
97
|
+
const { http } = fakeHttp();
|
|
98
|
+
const [db] = await ensureStagingDatabase(install, { run, http, sleep: noSleep });
|
|
99
|
+
expect(db?.status).toBe("failed");
|
|
100
|
+
expect(db?.error).toContain("cloudsql.databases.create");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("fails without an instance and creates nothing", async () => {
|
|
104
|
+
const calls: string[] = [];
|
|
105
|
+
const run: GcloudRunner = async (args) => {
|
|
106
|
+
calls.push(args.join(" "));
|
|
107
|
+
return ok("");
|
|
108
|
+
};
|
|
109
|
+
const outcomes = await ensureStagingDatabase(install, { run, sleep: noSleep });
|
|
110
|
+
expect(outcomes).toHaveLength(1);
|
|
111
|
+
expect(outcomes[0]?.status).toBe("failed");
|
|
112
|
+
expect(calls).toHaveLength(1);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The staging database on an existing GCP install (issue #1329, gateway
|
|
5
|
+
* hardening P3b-1, epic #763 E10).
|
|
6
|
+
*
|
|
7
|
+
* A new install gets `cms_public_staging` and its gateway login
|
|
8
|
+
* `public_staging_role` from the stack (`gcp.sql.Database` / `gcp.sql.User`).
|
|
9
|
+
* An install from before has neither, and an update must bring them
|
|
10
|
+
* without a terminal (CLAUDE.md §11.C): the converge — `cms-provision
|
|
11
|
+
* upgrade` or the in-cloud converge job behind Settings → Updates — runs
|
|
12
|
+
* {@link ensureStagingDatabase} before the migrations, which need the
|
|
13
|
+
* database (migrate.ts `public-staging`, cms_admin migration 0530).
|
|
14
|
+
*
|
|
15
|
+
* Both go through the Cloud SQL Admin API, the way the stack creates them,
|
|
16
|
+
* so the next `pulumi up` adopts the same objects. The database is created
|
|
17
|
+
* with gcloud (no secret involved); the user through the Admin API with the
|
|
18
|
+
* password in the request body (runtime-secrets.ts `ensureDatabaseRolePassword`),
|
|
19
|
+
* read from its CLI-generated secret `public-staging-role-password`, never
|
|
20
|
+
* on argv. The Admin API makes the new user a member of cloudsqlsuperuser;
|
|
21
|
+
* the revoke that follows (public-role-isolation.ts) and the role audit
|
|
22
|
+
* take care of it, as for public_role. Idempotent: an install that has both
|
|
23
|
+
* only gets the password re-applied.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { findSqlInstance } from "./gateway-credentials.js";
|
|
27
|
+
import { gcloud as defaultGcloud } from "./gcloud.js";
|
|
28
|
+
import { type GcloudRunner, realSleep, runWithRetry, type Sleep } from "./gcloud-retry.js";
|
|
29
|
+
import {
|
|
30
|
+
type EnsureOutcome,
|
|
31
|
+
ensureDatabaseRolePassword,
|
|
32
|
+
type HttpFetch,
|
|
33
|
+
} from "./runtime-secrets.js";
|
|
34
|
+
import { type GcloudCredentials, lookupFailure } from "./upgrade/gcp-lookup.js";
|
|
35
|
+
|
|
36
|
+
/** The staging site's database (#1329). */
|
|
37
|
+
export const STAGING_DATABASE = "cms_public_staging";
|
|
38
|
+
|
|
39
|
+
/** The staging gateway's login (#1329). */
|
|
40
|
+
export const STAGING_DATABASE_ROLE = "public_staging_role";
|
|
41
|
+
|
|
42
|
+
/** The names gcloud lists, one per line. */
|
|
43
|
+
function names(stdout: string): string[] {
|
|
44
|
+
return stdout
|
|
45
|
+
.split("\n")
|
|
46
|
+
.map((l) => l.trim())
|
|
47
|
+
.filter(Boolean);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Create `cms_public_staging` and `public_staging_role` where the install
|
|
52
|
+
* lacks them, and set the role's password to its secret. Reports instead of
|
|
53
|
+
* throwing; a `failed` outcome names the next step.
|
|
54
|
+
*/
|
|
55
|
+
export async function ensureStagingDatabase(
|
|
56
|
+
install: {
|
|
57
|
+
readonly projectId: string;
|
|
58
|
+
readonly env: string;
|
|
59
|
+
readonly credentials: GcloudCredentials;
|
|
60
|
+
},
|
|
61
|
+
deps: { readonly run?: GcloudRunner; readonly sleep?: Sleep; readonly http?: HttpFetch } = {},
|
|
62
|
+
): Promise<EnsureOutcome[]> {
|
|
63
|
+
const run = deps.run ?? defaultGcloud;
|
|
64
|
+
const sleep = deps.sleep ?? realSleep;
|
|
65
|
+
const lookup = await findSqlInstance(install, { run });
|
|
66
|
+
if (lookup.status !== "found") {
|
|
67
|
+
return [
|
|
68
|
+
{
|
|
69
|
+
id: `database ${STAGING_DATABASE}`,
|
|
70
|
+
status: "failed",
|
|
71
|
+
error: lookupFailure(lookup, "Cloud SQL instance", install.credentials),
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
}
|
|
75
|
+
const instance = lookup.name;
|
|
76
|
+
const where = [`--instance=${instance}`, `--project=${install.projectId}`];
|
|
77
|
+
return [
|
|
78
|
+
await ensureDatabase(run, sleep, instance, where),
|
|
79
|
+
await ensureRole(run, deps, install, instance, where),
|
|
80
|
+
];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function ensureDatabase(
|
|
84
|
+
run: GcloudRunner,
|
|
85
|
+
sleep: Sleep,
|
|
86
|
+
instance: string,
|
|
87
|
+
where: readonly string[],
|
|
88
|
+
): Promise<EnsureOutcome> {
|
|
89
|
+
const id = `database ${STAGING_DATABASE}`;
|
|
90
|
+
const listed = await run(["sql", "databases", "list", ...where, "--format=value(name)"]);
|
|
91
|
+
if (!listed.ok) {
|
|
92
|
+
return {
|
|
93
|
+
id,
|
|
94
|
+
status: "failed",
|
|
95
|
+
error: `gcloud sql databases list on ${instance}: ${listed.stderr.trim()}`,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
if (names(listed.stdout).includes(STAGING_DATABASE)) return { id, status: "present" };
|
|
99
|
+
const created = await runWithRetry(run, sleep, [
|
|
100
|
+
"sql",
|
|
101
|
+
"databases",
|
|
102
|
+
"create",
|
|
103
|
+
STAGING_DATABASE,
|
|
104
|
+
...where,
|
|
105
|
+
"--quiet",
|
|
106
|
+
]);
|
|
107
|
+
if (!created.ok) {
|
|
108
|
+
return {
|
|
109
|
+
id,
|
|
110
|
+
status: "failed",
|
|
111
|
+
error: `gcloud sql databases create ${STAGING_DATABASE} on ${instance}: ${created.stderr.trim()}. The account running this needs cloudsql.databases.create on the project; re-run once it has.`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return { id, status: "applied", detail: `created on ${instance}` };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function ensureRole(
|
|
118
|
+
run: GcloudRunner,
|
|
119
|
+
deps: { readonly sleep?: Sleep; readonly http?: HttpFetch },
|
|
120
|
+
install: { readonly projectId: string; readonly env: string },
|
|
121
|
+
instance: string,
|
|
122
|
+
where: readonly string[],
|
|
123
|
+
): Promise<EnsureOutcome> {
|
|
124
|
+
const id = `database role ${STAGING_DATABASE_ROLE}`;
|
|
125
|
+
const listed = await run(["sql", "users", "list", ...where, "--format=value(name)"]);
|
|
126
|
+
if (!listed.ok) {
|
|
127
|
+
return {
|
|
128
|
+
id,
|
|
129
|
+
status: "failed",
|
|
130
|
+
error: `gcloud sql users list on ${instance}: ${listed.stderr.trim()}`,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const exists = names(listed.stdout).includes(STAGING_DATABASE_ROLE);
|
|
134
|
+
const outcome = await ensureDatabaseRolePassword(
|
|
135
|
+
{ projectId: install.projectId, env: install.env, sqlInstance: instance },
|
|
136
|
+
STAGING_DATABASE_ROLE,
|
|
137
|
+
{
|
|
138
|
+
run,
|
|
139
|
+
...(deps.sleep ? { sleep: deps.sleep } : {}),
|
|
140
|
+
...(deps.http ? { http: deps.http } : {}),
|
|
141
|
+
create: !exists,
|
|
142
|
+
},
|
|
143
|
+
);
|
|
144
|
+
if (outcome.status === "failed") return { ...outcome, id };
|
|
145
|
+
return exists
|
|
146
|
+
? { id, status: "present", detail: "password re-applied" }
|
|
147
|
+
: { id, status: "applied", detail: `created on ${instance}` };
|
|
148
|
+
}
|