@caelo-cms/provisioning 0.11.10 → 0.11.11
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 +25 -8
- 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 +5 -2
- package/dist/converge-job-gcp.d.ts.map +1 -1
- package/dist/converge-job-gcp.js +9 -2
- package/dist/converge-job-gcp.js.map +1 -1
- package/dist/converge-job-preflight.d.ts.map +1 -1
- package/dist/converge-job-preflight.js +8 -0
- package/dist/converge-job-preflight.js.map +1 -1
- package/dist/converge-job.d.ts +17 -0
- package/dist/converge-job.d.ts.map +1 -1
- package/dist/converge-job.js +12 -0
- 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 +4 -2
- package/dist/install-job.d.ts.map +1 -1
- package/dist/install-job.js +4 -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/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/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 +81 -11
- 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 +9 -1
- package/src/converge-job-preflight.test.ts +5 -0
- package/src/converge-job-preflight.ts +8 -0
- package/src/converge-job.test.ts +70 -0
- package/src/converge-job.ts +32 -0
- 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 +4 -1
- package/src/install-state.ts +7 -0
- package/src/lifecycle.ts +3 -0
- 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/wizard.ts +19 -0
- package/src/wizards/gcp-cost.test.ts +54 -1
- package/src/wizards/gcp-cost.ts +101 -11
- 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
|
@@ -20,8 +20,8 @@ describe("infrastructure contract steps", () => {
|
|
|
20
20
|
);
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
-
it("is at
|
|
24
|
-
expect(INFRA_CONTRACT_VERSION).toBe(
|
|
23
|
+
it("is at 18: public_role revoke (3), URL-map redirects role (4), no gateway cms_admin login (5), ops-bucket list (6), gateway proxy hops (7), quality-audit job (8), converge job (9), remote MCP (10), Google sign-in (11), project-level sign-in client (12), edge security headers (13), capture job (14), database verify-full TLS (15), MCP-Auth-Proxy (16), staging database (17), scheduler tick (18)", () => {
|
|
24
|
+
expect(INFRA_CONTRACT_VERSION).toBe(18);
|
|
25
25
|
expect(INFRA_CONTRACT_STEPS[3]).toContain("#633");
|
|
26
26
|
expect(INFRA_CONTRACT_STEPS[4]).toContain("#587");
|
|
27
27
|
expect(INFRA_CONTRACT_STEPS[5]).toContain("#635");
|
|
@@ -36,6 +36,8 @@ describe("infrastructure contract steps", () => {
|
|
|
36
36
|
expect(INFRA_CONTRACT_STEPS[14]).toContain("#1217");
|
|
37
37
|
expect(INFRA_CONTRACT_STEPS[15]).toContain("#1338");
|
|
38
38
|
expect(INFRA_CONTRACT_STEPS[16]).toContain("#1328");
|
|
39
|
+
expect(INFRA_CONTRACT_STEPS[17]).toContain("#1329");
|
|
40
|
+
expect(INFRA_CONTRACT_STEPS[18]).toContain("#1140");
|
|
39
41
|
});
|
|
40
42
|
|
|
41
43
|
it("lists the pending steps oldest first", () => {
|
|
@@ -46,6 +48,6 @@ describe("infrastructure contract steps", () => {
|
|
|
46
48
|
});
|
|
47
49
|
|
|
48
50
|
it("names a version a newer release added by number", () => {
|
|
49
|
-
expect(infraStepsText(
|
|
51
|
+
expect(infraStepsText(18, 19)).toBe("infrastructure step 19 (see that release's notes)");
|
|
50
52
|
});
|
|
51
53
|
});
|
package/src/infra-contract.ts
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* whose app-roll references infrastructure an install does not have yet fails
|
|
23
23
|
* at roll time, after migrations ran.
|
|
24
24
|
*/
|
|
25
|
-
export const INFRA_CONTRACT_VERSION =
|
|
25
|
+
export const INFRA_CONTRACT_VERSION = 18;
|
|
26
26
|
|
|
27
27
|
// History (why each bump happened — an install below a number needs the
|
|
28
28
|
// converge that introduced it):
|
|
@@ -131,6 +131,25 @@ export const INFRA_CONTRACT_VERSION = 16;
|
|
|
131
131
|
// services, accounts, IAM and load-balancer resources — none of which
|
|
132
132
|
// the in-cloud upgrader may create, and the app-roll rolls three
|
|
133
133
|
// services from now on.
|
|
134
|
+
// 17 — #1329: staging's own database. The Cloud SQL database
|
|
135
|
+
// `cms_public_staging`, the Admin API user `public_staging_role` (its
|
|
136
|
+
// password the new CLI-generated secret `public-staging-role-password`)
|
|
137
|
+
// and its membership revoke; the admin's PUBLIC_STAGING_ADMIN_DATABASE_URL
|
|
138
|
+
// and the gateway's CAELO_GATEWAY_ENVIRONMENT. The in-cloud upgrader
|
|
139
|
+
// may create no database, user or secret, and the new admin and
|
|
140
|
+
// gateway refuse to start without them. Additive only (converge
|
|
141
|
+
// release 1 of the gateway hardening): nothing is served from the
|
|
142
|
+
// staging database yet, so rolling back stays harmless.
|
|
143
|
+
// 18 — #1140: the scheduler tick (scheduler-setup.ts). Its service account
|
|
144
|
+
// (token creator on itself, workflows.invoker, logWriter, IAP access to
|
|
145
|
+
// the admin), the Cloud Workflows workflow that signs its IAP JWT and
|
|
146
|
+
// calls the admin's /api/internal/scheduler/tick, the one Cloud
|
|
147
|
+
// Scheduler job that starts it every schedulerIntervalMinutes, the
|
|
148
|
+
// operator-access job's CAELO_OPERATOR_ACCESS_IAP_SERVICE_ACCOUNTS (so
|
|
149
|
+
// its sync keeps the account on IAP) and CAELO_SCHEDULER_SERVICE_ACCOUNT
|
|
150
|
+
// on the admin. The new admin runs its periodic work and plugin workers
|
|
151
|
+
// only on that tick, so an in-cloud upgrader must not roll an install
|
|
152
|
+
// onto it before the tick exists.
|
|
134
153
|
|
|
135
154
|
/**
|
|
136
155
|
* What each contract version adds, in operator terms — the upgrader's
|
|
@@ -154,6 +173,8 @@ export const INFRA_CONTRACT_STEPS: Readonly<Record<number, string>> = {
|
|
|
154
173
|
14: "the capture job that takes server-side screenshots outside the admin, its service account, bucket and grants (#1217)",
|
|
155
174
|
15: "verified TLS to the database: Cloud SQL's shared server CA, encrypted connections only, and a private DNS name the services dial (#1338)",
|
|
156
175
|
16: "the MCP sign-in service as its own Cloud Run service with its own account (no database access), the visitor gateway as its own service on Firebase installs, and on Google Cloud load-balancer installs the sign-in service's backend and Cloud Armor rule (#1328)",
|
|
176
|
+
17: "the staging site's own database and its gateway login, kept apart from the live data (#1329)",
|
|
177
|
+
18: "the scheduler tick that runs the admin's periodic work and plugin workers every few minutes: its service account, workflow and Cloud Scheduler job, and its IAP access (#1140)",
|
|
157
178
|
};
|
|
158
179
|
|
|
159
180
|
/**
|
package/src/install-job.ts
CHANGED
|
@@ -65,13 +65,16 @@ export const OPS_IMAGE_REPOSITORY =
|
|
|
65
65
|
* Roles the provisioner needs on top of {@link PROVISIONER_ROLE_LIST} when it
|
|
66
66
|
* runs the install job instead of the operator: the operator-access sync job
|
|
67
67
|
* creates a project custom role and a Cloud Scheduler job (operator-access.ts),
|
|
68
|
-
*
|
|
68
|
+
* the scheduler tick deploys a Cloud Workflows workflow (scheduler-setup.ts,
|
|
69
|
+
* #1140; the converge job gets the same roles, converge-job-setup.ts), and
|
|
70
|
+
* the job's own output has to reach Cloud Logging so the terminal can
|
|
69
71
|
* tail it. Granted only to installs that use the job. Not an escalation: the
|
|
70
72
|
* provisioner already holds `resourcemanager.projectIamAdmin`.
|
|
71
73
|
*/
|
|
72
74
|
export const INSTALL_JOB_ROLES = [
|
|
73
75
|
"roles/iam.roleAdmin",
|
|
74
76
|
"roles/cloudscheduler.admin",
|
|
77
|
+
"roles/workflows.editor",
|
|
75
78
|
"roles/logging.logWriter",
|
|
76
79
|
] as const;
|
|
77
80
|
|
package/src/install-state.ts
CHANGED
|
@@ -115,6 +115,13 @@ export interface InstallMetadata {
|
|
|
115
115
|
* run while it is lower than the release's (upgrade/converge.ts).
|
|
116
116
|
*/
|
|
117
117
|
infraContractVersion?: number;
|
|
118
|
+
/**
|
|
119
|
+
* #1140 — the `schedulerIntervalMinutes` knob: how often the one Cloud
|
|
120
|
+
* Scheduler job calls the admin's tick (1–15 minutes; `--scheduler-
|
|
121
|
+
* interval-minutes` on the wizard). Absent: the default (5,
|
|
122
|
+
* stack-contract.ts SCHEDULER_INTERVAL_MINUTES).
|
|
123
|
+
*/
|
|
124
|
+
schedulerIntervalMinutes?: number;
|
|
118
125
|
}
|
|
119
126
|
|
|
120
127
|
/** The install's staging host: its recorded setting, else the default. */
|
package/src/lifecycle.ts
CHANGED
|
@@ -641,6 +641,9 @@ async function upgradeLockedInstall(
|
|
|
641
641
|
domain: meta.domain,
|
|
642
642
|
stagingHost,
|
|
643
643
|
ownerEmail: meta.ownerEmail,
|
|
644
|
+
...(meta.schedulerIntervalMinutes !== undefined
|
|
645
|
+
? { schedulerIntervalMinutes: meta.schedulerIntervalMinutes }
|
|
646
|
+
: {}),
|
|
644
647
|
targetTag,
|
|
645
648
|
infraContractVersion: INFRA_CONTRACT_VERSION,
|
|
646
649
|
skipVerify: opts.skipVerify === true,
|
package/src/migration-runner.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { green, red } from "kleur/colors";
|
|
|
24
24
|
import { gcloud as defaultGcloud } from "./gcloud.js";
|
|
25
25
|
import type { GcloudRunner } from "./gcloud-retry.js";
|
|
26
26
|
import { GCP_STACK_ENV, gcpSecretId, runServiceAccountEmail } from "./gcp-names.js";
|
|
27
|
+
import { MIGRATION_TARGETS } from "./migration-targets.js";
|
|
27
28
|
import { databaseUrls, SERVICE_SECRET_ENV } from "./stack-contract.js";
|
|
28
29
|
import { liveContainerEnv, liveDatabaseHost } from "./stack-converge.js";
|
|
29
30
|
|
|
@@ -58,15 +59,19 @@ const NEEDS_UPGRADE =
|
|
|
58
59
|
*/
|
|
59
60
|
export function migrationJobEnvArgs(databaseHost: string, env = GCP_STACK_ENV): string[] {
|
|
60
61
|
const urls = databaseUrls(databaseHost);
|
|
61
|
-
const passwords = (
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
)
|
|
62
|
+
const passwords = (
|
|
63
|
+
[
|
|
64
|
+
"ADMIN_DATABASE_PASSWORD",
|
|
65
|
+
"PUBLIC_ADMIN_DATABASE_PASSWORD",
|
|
66
|
+
"PUBLIC_STAGING_ADMIN_DATABASE_PASSWORD",
|
|
67
|
+
] as const
|
|
68
|
+
).map((name) => {
|
|
69
|
+
const secret = SERVICE_SECRET_ENV.admin[name];
|
|
70
|
+
if (!secret) throw new Error(`the admin env contract has no ${name}`);
|
|
71
|
+
return `${name}=${gcpSecretId(env, secret)}:latest`;
|
|
72
|
+
});
|
|
68
73
|
return [
|
|
69
|
-
`--set-env-vars=ADMIN_DATABASE_URL=${urls.admin},PUBLIC_ADMIN_DATABASE_URL=${urls.publicAdmin}`,
|
|
74
|
+
`--set-env-vars=ADMIN_DATABASE_URL=${urls.admin},PUBLIC_ADMIN_DATABASE_URL=${urls.publicAdmin},PUBLIC_STAGING_ADMIN_DATABASE_URL=${urls.publicStagingAdmin}`,
|
|
70
75
|
`--set-secrets=${passwords.join(",")}`,
|
|
71
76
|
];
|
|
72
77
|
}
|
|
@@ -300,7 +305,7 @@ export async function runMigrationsViaCloudRunJob(
|
|
|
300
305
|
sAdmin.stop(green(`Admin config resolved (${migrationImage.slice(-19)})`));
|
|
301
306
|
}
|
|
302
307
|
|
|
303
|
-
for (const target of
|
|
308
|
+
for (const target of MIGRATION_TARGETS) {
|
|
304
309
|
const s = spinner();
|
|
305
310
|
s.start(`Applying ${target} migrations via one-shot Cloud Run Job...`);
|
|
306
311
|
// Delete-then-create so each invocation runs with the freshest
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The targets of `packages/migrations/src/migrate.ts`, in the order every
|
|
5
|
+
* runner applies them — the GCP migration job (migration-runner.ts), the
|
|
6
|
+
* self-hosted install and upgrade (self-hosted-start.ts,
|
|
7
|
+
* self-hosted-upgrade.ts) and the self-hosted stack: cms_admin first (its
|
|
8
|
+
* role audit covers the whole cluster), then the two public databases,
|
|
9
|
+
* which run the same cms_public files (#1329: cms_public_staging is the
|
|
10
|
+
* staging site's twin, with its own gateway login).
|
|
11
|
+
*/
|
|
12
|
+
export const MIGRATION_TARGETS = ["admin", "public", "public-staging"] as const;
|
|
@@ -205,6 +205,21 @@ describe("ensureOperatorAccessSync", () => {
|
|
|
205
205
|
);
|
|
206
206
|
});
|
|
207
207
|
|
|
208
|
+
it("#1140 — tells the job which service accounts keep IAP access (the scheduler tick)", async () => {
|
|
209
|
+
const { run, calls } = freshInstall();
|
|
210
|
+
await ensureOperatorAccessSync(
|
|
211
|
+
{ ...target, iapServiceAccounts: ["caelo-production-scheduler@p.iam.gserviceaccount.com"] },
|
|
212
|
+
{ run, sleep: async () => {} },
|
|
213
|
+
);
|
|
214
|
+
const deploy = calls.find((c) => c.slice(0, 3).join(" ") === "run jobs deploy") ?? [];
|
|
215
|
+
const env = deploy.find((a) => a.startsWith("--set-env-vars=")) ?? "";
|
|
216
|
+
expect(env).toContain(
|
|
217
|
+
"|CAELO_OPERATOR_ACCESS_IAP_SERVICE_ACCOUNTS=caelo-production-scheduler@p.iam.gserviceaccount.com",
|
|
218
|
+
);
|
|
219
|
+
// The static members stay the install's allowlist (converge-job-gcp.ts compares them).
|
|
220
|
+
expect(env).toContain("|CAELO_OPERATOR_ACCESS_STATIC_MEMBERS=user:owner@x.com|");
|
|
221
|
+
});
|
|
222
|
+
|
|
208
223
|
it("#914 — lets the job read exactly the Google sign-in client's secret, nothing else", async () => {
|
|
209
224
|
const { run, lines } = freshInstall();
|
|
210
225
|
await ensureOperatorAccessSync(target, { run, sleep: async () => {} });
|
package/src/operator-access.ts
CHANGED
|
@@ -218,11 +218,22 @@ export interface OperatorAccessSyncTarget {
|
|
|
218
218
|
* The job reconciles that binding, so anyone missing here loses it.
|
|
219
219
|
*/
|
|
220
220
|
readonly mcpSigners?: readonly string[];
|
|
221
|
+
/**
|
|
222
|
+
* #1140 — service accounts (besides caelo-mcp) that keep
|
|
223
|
+
* `roles/iap.httpsResourceAccessor` on the admin: the scheduler tick's
|
|
224
|
+
* account (scheduler-setup.ts) calls the admin through IAP. The job
|
|
225
|
+
* reconciles that binding to exactly its members, so anyone missing here
|
|
226
|
+
* loses it on the next run. Emails, not `serviceAccount:` members.
|
|
227
|
+
*/
|
|
228
|
+
readonly iapServiceAccounts?: readonly string[];
|
|
221
229
|
}
|
|
222
230
|
|
|
223
231
|
/** Env var of the sync job listing {@link OperatorAccessSyncTarget.mcpSigners}. */
|
|
224
232
|
export const MCP_SIGNERS_ENV = "CAELO_OPERATOR_ACCESS_MCP_SIGNERS";
|
|
225
233
|
|
|
234
|
+
/** #1140 — env var of the sync job listing {@link OperatorAccessSyncTarget.iapServiceAccounts}. */
|
|
235
|
+
export const IAP_SERVICE_ACCOUNTS_ENV = "CAELO_OPERATOR_ACCESS_IAP_SERVICE_ACCOUNTS";
|
|
236
|
+
|
|
226
237
|
/** #914 — env var of the sync job naming the Google sign-in client's secret. */
|
|
227
238
|
export const OPERATOR_OAUTH_CLIENT_SECRET_ENV = "CAELO_OPERATOR_OAUTH_CLIENT_SECRET";
|
|
228
239
|
|
|
@@ -424,6 +435,9 @@ export async function ensureOperatorAccessSync(
|
|
|
424
435
|
...(target.mcpSigners && target.mcpSigners.length > 0
|
|
425
436
|
? [`${MCP_SIGNERS_ENV}=${target.mcpSigners.join(",")}`]
|
|
426
437
|
: []),
|
|
438
|
+
...(target.iapServiceAccounts && target.iapServiceAccounts.length > 0
|
|
439
|
+
? [`${IAP_SERVICE_ACCOUNTS_ENV}=${target.iapServiceAccounts.join(",")}`]
|
|
440
|
+
: []),
|
|
427
441
|
];
|
|
428
442
|
const deploy = await run([
|
|
429
443
|
"run",
|
package/src/pool-budget-stack.ts
CHANGED
|
@@ -14,11 +14,14 @@
|
|
|
14
14
|
|
|
15
15
|
import {
|
|
16
16
|
computePoolBudget,
|
|
17
|
+
DEFAULT_STAGING_GATEWAY_MAX_INSTANCES,
|
|
17
18
|
type PoolBudget,
|
|
18
19
|
type PoolBudgetInput,
|
|
19
20
|
poolBudgetEnv,
|
|
20
21
|
} from "@caelo-cms/shared";
|
|
21
22
|
|
|
23
|
+
export { DEFAULT_STAGING_GATEWAY_MAX_INSTANCES };
|
|
24
|
+
|
|
22
25
|
/** The adapters that provision a Postgres server. */
|
|
23
26
|
export type BudgetProvider = "gcp" | "gcp-firebase" | "aws" | "azure" | "self-hosted";
|
|
24
27
|
|
|
@@ -84,12 +87,24 @@ export const DEFAULT_MAX_CONNECTIONS: Readonly<Record<BudgetProvider, number>> =
|
|
|
84
87
|
*/
|
|
85
88
|
export const DEFAULT_GATEWAY_MAX_INSTANCES = 10;
|
|
86
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Admin instances a GCP or AWS stack lets start (`adminMaxInstances` knob).
|
|
92
|
+
* Five before #1329; the staging database added the admin's
|
|
93
|
+
* cms_public_staging pool and the staging gateway to the budget, and on the
|
|
94
|
+
* default server (100 connections) five instances no longer get the
|
|
95
|
+
* minimum cms_admin pool. Raising `max_connections` instead was ruled out
|
|
96
|
+
* for db-f1-micro (docs/dev/connection-budget.md, "#1329").
|
|
97
|
+
*/
|
|
98
|
+
export const DEFAULT_ADMIN_MAX_INSTANCES = 4;
|
|
99
|
+
|
|
87
100
|
/** The server and scale-out a stack sizes its pools for. */
|
|
88
101
|
export interface StackScale {
|
|
89
102
|
readonly maxConnections: number;
|
|
90
103
|
/** Every instance that opens the admin's pools (admin-side services included). */
|
|
91
104
|
readonly adminMaxInstances: number;
|
|
92
105
|
readonly gatewayMaxInstances: number;
|
|
106
|
+
/** #1329 — the staging gateway's scale-out ({@link DEFAULT_STAGING_GATEWAY_MAX_INSTANCES}). */
|
|
107
|
+
readonly stagingGatewayMaxInstances: number;
|
|
93
108
|
}
|
|
94
109
|
|
|
95
110
|
/** A stack's budget plus the env each service sets from it. */
|
|
@@ -12,8 +12,10 @@ import { resolve } from "node:path";
|
|
|
12
12
|
import { COMPOSE_POOL_BUDGET } from "./compose.js";
|
|
13
13
|
import {
|
|
14
14
|
type BudgetProvider,
|
|
15
|
+
DEFAULT_ADMIN_MAX_INSTANCES,
|
|
15
16
|
DEFAULT_GATEWAY_MAX_INSTANCES,
|
|
16
17
|
DEFAULT_MAX_CONNECTIONS,
|
|
18
|
+
DEFAULT_STAGING_GATEWAY_MAX_INSTANCES,
|
|
17
19
|
stackPoolBudget,
|
|
18
20
|
} from "./pool-budget-stack.js";
|
|
19
21
|
|
|
@@ -21,22 +23,39 @@ const stackSource = (provider: string): string =>
|
|
|
21
23
|
readFileSync(resolve(import.meta.dir, "..", "stacks", provider, "index.ts"), "utf8");
|
|
22
24
|
|
|
23
25
|
/** Each stack's default scale-out, as its index.ts reads it. */
|
|
24
|
-
const DEFAULT_SCALE: Record<BudgetProvider, { admin: number; gateway: number }> = {
|
|
25
|
-
gcp: {
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
const DEFAULT_SCALE: Record<BudgetProvider, { admin: number; gateway: number; staging: number }> = {
|
|
27
|
+
gcp: {
|
|
28
|
+
admin: DEFAULT_ADMIN_MAX_INSTANCES,
|
|
29
|
+
gateway: DEFAULT_GATEWAY_MAX_INSTANCES,
|
|
30
|
+
staging: DEFAULT_STAGING_GATEWAY_MAX_INSTANCES,
|
|
31
|
+
},
|
|
32
|
+
"gcp-firebase": {
|
|
33
|
+
admin: DEFAULT_ADMIN_MAX_INSTANCES,
|
|
34
|
+
gateway: DEFAULT_GATEWAY_MAX_INSTANCES,
|
|
35
|
+
staging: DEFAULT_STAGING_GATEWAY_MAX_INSTANCES,
|
|
36
|
+
},
|
|
37
|
+
aws: {
|
|
38
|
+
admin: DEFAULT_ADMIN_MAX_INSTANCES,
|
|
39
|
+
gateway: DEFAULT_GATEWAY_MAX_INSTANCES,
|
|
40
|
+
staging: DEFAULT_STAGING_GATEWAY_MAX_INSTANCES,
|
|
41
|
+
},
|
|
28
42
|
// admin + orchestrator + runner apps, 10 replicas each.
|
|
29
|
-
azure: { admin: 30, gateway: 10 },
|
|
30
|
-
"self-hosted": { admin: 1, gateway: 1 },
|
|
43
|
+
azure: { admin: 30, gateway: 10, staging: DEFAULT_STAGING_GATEWAY_MAX_INSTANCES },
|
|
44
|
+
"self-hosted": { admin: 1, gateway: 1, staging: 1 },
|
|
31
45
|
};
|
|
32
46
|
|
|
33
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* [cms_admin pool per admin instance, connections left unassigned]. #1329
|
|
49
|
+
* added the staging gateway and the admin's cms_public_staging pool; the
|
|
50
|
+
* GCP and AWS stacks start four admin instances instead of five so the
|
|
51
|
+
* cms_admin pool stays at five (docs/dev/connection-budget.md).
|
|
52
|
+
*/
|
|
34
53
|
const EXPECTED: Record<BudgetProvider, [number, number]> = {
|
|
35
|
-
gcp: [5,
|
|
36
|
-
"gcp-firebase": [5,
|
|
37
|
-
aws: [5,
|
|
38
|
-
azure: [
|
|
39
|
-
"self-hosted": [
|
|
54
|
+
gcp: [5, 0],
|
|
55
|
+
"gcp-firebase": [5, 0],
|
|
56
|
+
aws: [5, 2],
|
|
57
|
+
azure: [5, 9],
|
|
58
|
+
"self-hosted": [72, 0],
|
|
40
59
|
};
|
|
41
60
|
|
|
42
61
|
describe("stack pool budgets at the default config", () => {
|
|
@@ -46,7 +65,10 @@ describe("stack pool budgets at the default config", () => {
|
|
|
46
65
|
maxConnections: DEFAULT_MAX_CONNECTIONS[provider],
|
|
47
66
|
adminMaxInstances: DEFAULT_SCALE[provider].admin,
|
|
48
67
|
gatewayMaxInstances: DEFAULT_SCALE[provider].gateway,
|
|
68
|
+
stagingGatewayMaxInstances: DEFAULT_SCALE[provider].staging,
|
|
49
69
|
});
|
|
70
|
+
expect(budget.reserves.map((r) => r.name)).toContain("staging-gateway");
|
|
71
|
+
expect(budget.reserves.map((r) => r.name)).toContain("admin-public-staging");
|
|
50
72
|
expect([budget.adminPoolMax, budget.unassigned]).toEqual(EXPECTED[provider]);
|
|
51
73
|
expect(env.admin).toEqual([
|
|
52
74
|
{ name: "CAELO_DB_POOL_MAX", value: String(EXPECTED[provider][0]) },
|
|
@@ -60,8 +82,9 @@ describe("stack pool budgets at the default config", () => {
|
|
|
60
82
|
expect(() =>
|
|
61
83
|
stackPoolBudget("gcp", {
|
|
62
84
|
maxConnections: 100,
|
|
63
|
-
adminMaxInstances:
|
|
85
|
+
adminMaxInstances: 4,
|
|
64
86
|
gatewayMaxInstances: 100,
|
|
87
|
+
stagingGatewayMaxInstances: 1,
|
|
65
88
|
}),
|
|
66
89
|
).toThrow(/CONNECTION LIMIT, #1338/);
|
|
67
90
|
});
|
|
@@ -75,6 +98,8 @@ describe("stacks set the budget on their services", () => {
|
|
|
75
98
|
expect(src).toContain("...poolBudget.env.admin");
|
|
76
99
|
expect(src).toContain("...poolBudget.env.gateway");
|
|
77
100
|
expect(src).toContain("maxInstances: gatewayMaxInstances");
|
|
101
|
+
expect(src).toContain("stagingGatewayMaxInstances: DEFAULT_STAGING_GATEWAY_MAX_INSTANCES");
|
|
102
|
+
expect(src).toContain('cfg.get("adminMaxInstances") ?? String(DEFAULT_ADMIN_MAX_INSTANCES)');
|
|
78
103
|
expect(src).not.toContain("maxInstances: 100");
|
|
79
104
|
});
|
|
80
105
|
}
|
|
@@ -25,7 +25,11 @@ function fake(answers: (line: string) => GcloudResult) {
|
|
|
25
25
|
describe("revokePublicRoleMembershipsArgs (#633)", () => {
|
|
26
26
|
it("revokes every database role of the built-in public_role user", () => {
|
|
27
27
|
expect(
|
|
28
|
-
revokePublicRoleMembershipsArgs({
|
|
28
|
+
revokePublicRoleMembershipsArgs({
|
|
29
|
+
projectId: "acme",
|
|
30
|
+
sqlInstance: "caelo-production-pg-1",
|
|
31
|
+
role: "public_role",
|
|
32
|
+
}),
|
|
29
33
|
).toEqual([
|
|
30
34
|
"sql",
|
|
31
35
|
"users",
|
|
@@ -40,7 +44,12 @@ describe("revokePublicRoleMembershipsArgs (#633)", () => {
|
|
|
40
44
|
});
|
|
41
45
|
|
|
42
46
|
it("grants nothing in exchange (no --database-roles)", () => {
|
|
43
|
-
const args = revokePublicRoleMembershipsArgs({
|
|
47
|
+
const args = revokePublicRoleMembershipsArgs({
|
|
48
|
+
projectId: "p",
|
|
49
|
+
sqlInstance: "i",
|
|
50
|
+
role: "public_staging_role",
|
|
51
|
+
});
|
|
52
|
+
expect(args[3]).toBe("public_staging_role");
|
|
44
53
|
expect(args.some((a) => a.startsWith("--database-roles"))).toBe(false);
|
|
45
54
|
});
|
|
46
55
|
});
|
|
@@ -48,17 +57,18 @@ describe("revokePublicRoleMembershipsArgs (#633)", () => {
|
|
|
48
57
|
describe("revokePublicRoleMemberships", () => {
|
|
49
58
|
const install = { projectId: "acme", env: "production", credentials: "operator" } as const;
|
|
50
59
|
|
|
51
|
-
it("finds the instance, then revokes", async () => {
|
|
60
|
+
it("finds the instance, then revokes for both gateway logins (#1329)", async () => {
|
|
52
61
|
const { run, calls } = fake((l) => (l.startsWith("sql instances list") ? ok("pg-1\n") : ok()));
|
|
53
62
|
const r = await revokePublicRoleMemberships(install, { run, sleep: noSleep });
|
|
54
63
|
expect(r).toEqual({
|
|
55
|
-
id: "
|
|
64
|
+
id: "gateway logins' database role memberships",
|
|
56
65
|
status: "applied",
|
|
57
|
-
detail: "no database roles
|
|
66
|
+
detail: "public_role and public_staging_role hold no database roles on pg-1",
|
|
58
67
|
});
|
|
59
|
-
expect(calls
|
|
68
|
+
expect(calls.slice(1)).toEqual([
|
|
60
69
|
"sql users assign-roles public_role --instance=pg-1 --type=BUILT_IN --revoke-existing-roles --project=acme --quiet",
|
|
61
|
-
|
|
70
|
+
"sql users assign-roles public_staging_role --instance=pg-1 --type=BUILT_IN --revoke-existing-roles --project=acme --quiet",
|
|
71
|
+
]);
|
|
62
72
|
});
|
|
63
73
|
|
|
64
74
|
it("is idempotent: a second run issues the same revoke and succeeds", async () => {
|
|
@@ -66,7 +76,7 @@ describe("revokePublicRoleMemberships", () => {
|
|
|
66
76
|
await revokePublicRoleMemberships(install, { run, sleep: noSleep });
|
|
67
77
|
const again = await revokePublicRoleMemberships(install, { run, sleep: noSleep });
|
|
68
78
|
expect(again.status).toBe("applied");
|
|
69
|
-
expect(calls
|
|
79
|
+
expect(calls.slice(4)).toEqual(calls.slice(1, 3));
|
|
70
80
|
});
|
|
71
81
|
|
|
72
82
|
it("fails without an instance and never calls the revoke", async () => {
|
|
@@ -28,6 +28,9 @@
|
|
|
28
28
|
* `public_role` is still a member of any role. This step therefore runs
|
|
29
29
|
* BEFORE migrations, on every install pass and every `upgrade`. Idempotent:
|
|
30
30
|
* revoking from a role that has none changes nothing.
|
|
31
|
+
*
|
|
32
|
+
* Since #1329 the staging gateway's login `public_staging_role` is an
|
|
33
|
+
* Admin API user too and gets the same revoke.
|
|
31
34
|
*/
|
|
32
35
|
|
|
33
36
|
import { findSqlInstance } from "./gateway-credentials.js";
|
|
@@ -36,16 +39,20 @@ import { type GcloudRunner, realSleep, runWithRetry, type Sleep } from "./gcloud
|
|
|
36
39
|
import type { EnsureOutcome } from "./runtime-secrets.js";
|
|
37
40
|
import { type GcloudCredentials, lookupFailure } from "./upgrade/gcp-lookup.js";
|
|
38
41
|
|
|
39
|
-
/** The
|
|
42
|
+
/** The gateway logins the Admin API created, which must hold no database role (#633, #1329). */
|
|
43
|
+
export const GATEWAY_DATABASE_LOGINS = ["public_role", "public_staging_role"] as const;
|
|
44
|
+
|
|
45
|
+
/** The gcloud call that leaves a gateway login with no database roles. */
|
|
40
46
|
export function revokePublicRoleMembershipsArgs(target: {
|
|
41
47
|
readonly projectId: string;
|
|
42
48
|
readonly sqlInstance: string;
|
|
49
|
+
readonly role: (typeof GATEWAY_DATABASE_LOGINS)[number];
|
|
43
50
|
}): string[] {
|
|
44
51
|
return [
|
|
45
52
|
"sql",
|
|
46
53
|
"users",
|
|
47
54
|
"assign-roles",
|
|
48
|
-
|
|
55
|
+
target.role,
|
|
49
56
|
`--instance=${target.sqlInstance}`,
|
|
50
57
|
"--type=BUILT_IN",
|
|
51
58
|
"--revoke-existing-roles",
|
|
@@ -55,8 +62,9 @@ export function revokePublicRoleMembershipsArgs(target: {
|
|
|
55
62
|
}
|
|
56
63
|
|
|
57
64
|
/**
|
|
58
|
-
* Revoke every database role the Admin API gave
|
|
59
|
-
* instead of throwing; `failed`
|
|
65
|
+
* Revoke every database role the Admin API gave the gateway logins
|
|
66
|
+
* ({@link GATEWAY_DATABASE_LOGINS}). Reports instead of throwing; `failed`
|
|
67
|
+
* names the next step.
|
|
60
68
|
*/
|
|
61
69
|
export async function revokePublicRoleMemberships(
|
|
62
70
|
install: {
|
|
@@ -67,7 +75,7 @@ export async function revokePublicRoleMemberships(
|
|
|
67
75
|
deps: { readonly run?: GcloudRunner; readonly sleep?: Sleep } = {},
|
|
68
76
|
): Promise<EnsureOutcome> {
|
|
69
77
|
const run = deps.run ?? defaultGcloud;
|
|
70
|
-
const id = "
|
|
78
|
+
const id = "gateway logins' database role memberships";
|
|
71
79
|
const lookup = await findSqlInstance(install, { run });
|
|
72
80
|
if (lookup.status !== "found") {
|
|
73
81
|
return {
|
|
@@ -77,17 +85,23 @@ export async function revokePublicRoleMemberships(
|
|
|
77
85
|
};
|
|
78
86
|
}
|
|
79
87
|
const sqlInstance = lookup.name;
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
for (const role of GATEWAY_DATABASE_LOGINS) {
|
|
89
|
+
const r = await runWithRetry(
|
|
90
|
+
run,
|
|
91
|
+
deps.sleep ?? realSleep,
|
|
92
|
+
revokePublicRoleMembershipsArgs({ projectId: install.projectId, sqlInstance, role }),
|
|
93
|
+
);
|
|
94
|
+
if (!r.ok) {
|
|
95
|
+
return {
|
|
96
|
+
id,
|
|
97
|
+
status: "failed",
|
|
98
|
+
error: `gcloud sql users assign-roles ${role} --revoke-existing-roles on ${sqlInstance}: ${r.stderr.trim()}. The account running this needs cloudsql.users.update on the project; re-run once it has.`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
91
101
|
}
|
|
92
|
-
return {
|
|
102
|
+
return {
|
|
103
|
+
id,
|
|
104
|
+
status: "applied",
|
|
105
|
+
detail: `${GATEWAY_DATABASE_LOGINS.join(" and ")} hold no database roles on ${sqlInstance}`,
|
|
106
|
+
};
|
|
93
107
|
}
|
package/src/release-contract.ts
CHANGED
|
@@ -55,6 +55,9 @@ export const INFRA_CONTRACT_PATHS: readonly string[] = [
|
|
|
55
55
|
"packages/provisioning/src/audit-job-setup.ts",
|
|
56
56
|
// #1217 — the capture job, its service account, bucket and grants.
|
|
57
57
|
"packages/provisioning/src/capture-job-setup.ts",
|
|
58
|
+
// #1140 — the scheduler tick: its service account, workflow, Cloud
|
|
59
|
+
// Scheduler job and grants.
|
|
60
|
+
"packages/provisioning/src/scheduler-setup.ts",
|
|
58
61
|
// #819 — the converge job, its database login and the admin's grant on it.
|
|
59
62
|
"packages/provisioning/src/converge-job-setup.ts",
|
|
60
63
|
// #891 — the edge security headers the stacks attach, and the converge's
|
|
@@ -115,7 +115,7 @@ describe("ensureGeneratedSecrets", () => {
|
|
|
115
115
|
{ ...secretsInstall, replication: regional },
|
|
116
116
|
{ run },
|
|
117
117
|
);
|
|
118
|
-
expect(outcomes.map((o) => o.status)).toEqual(Array(
|
|
118
|
+
expect(outcomes.map((o) => o.status)).toEqual(Array(7).fill("present"));
|
|
119
119
|
expect(lines().some((l) => l.includes(" create ") || l.includes(" add "))).toBe(false);
|
|
120
120
|
});
|
|
121
121
|
|
|
@@ -126,7 +126,7 @@ describe("ensureGeneratedSecrets", () => {
|
|
|
126
126
|
{ ...secretsInstall, replication: regional },
|
|
127
127
|
{ run, generate: () => `generated-value-${++n}` },
|
|
128
128
|
);
|
|
129
|
-
expect(outcomes.map((o) => o.status)).toEqual(Array(
|
|
129
|
+
expect(outcomes.map((o) => o.status)).toEqual(Array(7).fill("applied"));
|
|
130
130
|
const creates = calls.filter((c) => c.line.startsWith("secrets create"));
|
|
131
131
|
// #806 — the seal key is 32 random bytes in base64 (not the injected hex generator).
|
|
132
132
|
const sealKey = creates.find((c) => c.line.includes("mcp-oauth-seal-key"))?.stdin ?? "";
|
|
@@ -145,10 +145,15 @@ describe("ensureGeneratedSecrets", () => {
|
|
|
145
145
|
line: "secrets create caelo-production-public-role-password --project=acme --replication-policy=user-managed --locations=europe-west1 --data-file=- --quiet",
|
|
146
146
|
stdin: "generated-value-3",
|
|
147
147
|
},
|
|
148
|
+
// #1329 — the staging gateway's role password: its own, never the live one's.
|
|
149
|
+
{
|
|
150
|
+
line: "secrets create caelo-production-public-staging-role-password --project=acme --replication-policy=user-managed --locations=europe-west1 --data-file=- --quiet",
|
|
151
|
+
stdin: "generated-value-4",
|
|
152
|
+
},
|
|
148
153
|
// #650 — the staging access gate's key.
|
|
149
154
|
{
|
|
150
155
|
line: "secrets create caelo-production-staging-gate-secret --project=acme --replication-policy=user-managed --locations=europe-west1 --data-file=- --quiet",
|
|
151
|
-
stdin: "generated-value-
|
|
156
|
+
stdin: "generated-value-5",
|
|
152
157
|
},
|
|
153
158
|
// #806 — remote MCP's OAuth client starts as the "not configured"
|
|
154
159
|
// placeholder; the Owner writes the real one from Settings → MCP.
|
|
@@ -178,11 +183,12 @@ describe("ensureGeneratedSecrets", () => {
|
|
|
178
183
|
{ run, generate: () => "generated" },
|
|
179
184
|
);
|
|
180
185
|
const creates = calls.filter((c) => c.line.startsWith("secrets create"));
|
|
181
|
-
expect(creates.map((c) => c.stdin).slice(0,
|
|
186
|
+
expect(creates.map((c) => c.stdin).slice(0, 6)).toEqual([
|
|
182
187
|
"operator-set",
|
|
183
188
|
"generated",
|
|
184
189
|
"generated",
|
|
185
190
|
"generated",
|
|
191
|
+
"generated",
|
|
186
192
|
"{}",
|
|
187
193
|
]);
|
|
188
194
|
for (const c of calls) expect(c.line).not.toContain("operator-set");
|
|
@@ -194,6 +200,7 @@ describe("ensureGeneratedSecrets", () => {
|
|
|
194
200
|
"secrets versions list caelo-production-internal-secret": [ok("")],
|
|
195
201
|
"secrets versions list caelo-production-tool-approval-secret": [ok("v/1")],
|
|
196
202
|
"secrets versions list caelo-production-public-role-password": [ok("v/1")],
|
|
203
|
+
"secrets versions list caelo-production-public-staging-role-password": [ok("v/1")],
|
|
197
204
|
"secrets versions list caelo-production-staging-gate-secret": [ok("v/1")],
|
|
198
205
|
"secrets versions list caelo-production-operator-oauth-client": [ok("v/1")],
|
|
199
206
|
"secrets versions list caelo-production-mcp-oauth-seal-key": [ok("v/1")],
|
|
@@ -209,6 +216,7 @@ describe("ensureGeneratedSecrets", () => {
|
|
|
209
216
|
"present",
|
|
210
217
|
"present",
|
|
211
218
|
"present",
|
|
219
|
+
"present",
|
|
212
220
|
]);
|
|
213
221
|
expect(calls.filter((c) => c.line.startsWith("secrets versions add"))).toEqual([
|
|
214
222
|
{
|
|
@@ -243,7 +251,7 @@ describe("ensureGeneratedSecrets", () => {
|
|
|
243
251
|
{ ...secretsInstall, replication: { kind: "automatic" } },
|
|
244
252
|
{ run, generate: () => "v" },
|
|
245
253
|
);
|
|
246
|
-
expect(outcomes.map((o) => o.status)).toEqual(Array(
|
|
254
|
+
expect(outcomes.map((o) => o.status)).toEqual(Array(7).fill("applied"));
|
|
247
255
|
expect(outcomes.every((o) => o.detail?.includes("gcp.resourceLocations"))).toBe(true);
|
|
248
256
|
const creates = lines().filter((l) => l.startsWith("secrets create"));
|
|
249
257
|
// One refused automatic attempt; then every secret — the refused one
|
|
@@ -253,6 +261,7 @@ describe("ensureGeneratedSecrets", () => {
|
|
|
253
261
|
"secrets create caelo-production-internal-secret --project=acme --replication-policy=user-managed --locations=europe-west1 --data-file=- --quiet",
|
|
254
262
|
"secrets create caelo-production-tool-approval-secret --project=acme --replication-policy=user-managed --locations=europe-west1 --data-file=- --quiet",
|
|
255
263
|
"secrets create caelo-production-public-role-password --project=acme --replication-policy=user-managed --locations=europe-west1 --data-file=- --quiet",
|
|
264
|
+
"secrets create caelo-production-public-staging-role-password --project=acme --replication-policy=user-managed --locations=europe-west1 --data-file=- --quiet",
|
|
256
265
|
// #652 — the staging gate's key follows the same path.
|
|
257
266
|
"secrets create caelo-production-staging-gate-secret --project=acme --replication-policy=user-managed --locations=europe-west1 --data-file=- --quiet",
|
|
258
267
|
"secrets create caelo-production-operator-oauth-client --project=acme --replication-policy=user-managed --locations=europe-west1 --data-file=- --quiet",
|
|
@@ -284,7 +293,7 @@ describe("ensureGeneratedSecrets", () => {
|
|
|
284
293
|
{ run, generate: () => "v" },
|
|
285
294
|
);
|
|
286
295
|
expect(outcomes.every((o) => o.status === "failed")).toBe(true);
|
|
287
|
-
expect(lines().filter((l) => l.startsWith("secrets create"))).toHaveLength(
|
|
296
|
+
expect(lines().filter((l) => l.startsWith("secrets create"))).toHaveLength(7);
|
|
288
297
|
});
|
|
289
298
|
});
|
|
290
299
|
|
|
@@ -329,6 +338,7 @@ describe("rotateRuntimeSecret", () => {
|
|
|
329
338
|
service([
|
|
330
339
|
ref("ADMIN_DATABASE_PASSWORD", "postgres-password"),
|
|
331
340
|
ref("PUBLIC_ADMIN_DATABASE_PASSWORD", "postgres-password"),
|
|
341
|
+
ref("PUBLIC_STAGING_ADMIN_DATABASE_PASSWORD", "postgres-password"),
|
|
332
342
|
ref("CAELO_SECRET_KEK", "secret-kek"),
|
|
333
343
|
ref("CAELO_INTERNAL_SECRET", "internal-secret"),
|
|
334
344
|
ref("CAELO_TOOL_APPROVAL_SECRET", "tool-approval-secret"),
|
|
@@ -574,8 +584,8 @@ describe("migrationJobEnvArgs", () => {
|
|
|
574
584
|
it("gives the job password-less URLs and the password as a secret reference", () => {
|
|
575
585
|
const args = migrationJobEnvArgs("db1.p-label.europe-west1.sql-psa.goog");
|
|
576
586
|
expect(args).toEqual([
|
|
577
|
-
"--set-env-vars=ADMIN_DATABASE_URL=postgresql://admin_role@db1.p-label.europe-west1.sql-psa.goog:5432/cms_admin?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem,PUBLIC_ADMIN_DATABASE_URL=postgresql://admin_role@db1.p-label.europe-west1.sql-psa.goog:5432/cms_public?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem",
|
|
578
|
-
"--set-secrets=ADMIN_DATABASE_PASSWORD=caelo-production-postgres-password:latest,PUBLIC_ADMIN_DATABASE_PASSWORD=caelo-production-postgres-password:latest",
|
|
587
|
+
"--set-env-vars=ADMIN_DATABASE_URL=postgresql://admin_role@db1.p-label.europe-west1.sql-psa.goog:5432/cms_admin?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem,PUBLIC_ADMIN_DATABASE_URL=postgresql://admin_role@db1.p-label.europe-west1.sql-psa.goog:5432/cms_public?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem,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",
|
|
588
|
+
"--set-secrets=ADMIN_DATABASE_PASSWORD=caelo-production-postgres-password:latest,PUBLIC_ADMIN_DATABASE_PASSWORD=caelo-production-postgres-password:latest,PUBLIC_STAGING_ADMIN_DATABASE_PASSWORD=caelo-production-postgres-password:latest",
|
|
579
589
|
]);
|
|
580
590
|
});
|
|
581
591
|
});
|