@caelo-cms/provisioning 0.11.9 → 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/admin-domain.d.ts +10 -1
- package/dist/admin-domain.d.ts.map +1 -1
- package/dist/admin-domain.js +13 -3
- package/dist/admin-domain.js.map +1 -1
- package/dist/adopt-existing.d.ts +12 -3
- package/dist/adopt-existing.d.ts.map +1 -1
- package/dist/adopt-existing.js +143 -0
- package/dist/adopt-existing.js.map +1 -1
- package/dist/agent/command.d.ts +23 -0
- package/dist/agent/command.d.ts.map +1 -1
- package/dist/agent/command.js +32 -9
- package/dist/agent/command.js.map +1 -1
- package/dist/agent/run.d.ts +2 -0
- package/dist/agent/run.d.ts.map +1 -1
- package/dist/agent/run.js +5 -1
- package/dist/agent/run.js.map +1 -1
- package/dist/agent-db.js +69 -10
- package/dist/aws-task-roles.d.ts +4 -2
- package/dist/aws-task-roles.d.ts.map +1 -1
- package/dist/aws-task-roles.js +4 -1
- package/dist/aws-task-roles.js.map +1 -1
- package/dist/caddy.d.ts +21 -4
- package/dist/caddy.d.ts.map +1 -1
- package/dist/caddy.js +27 -3
- package/dist/caddy.js.map +1 -1
- package/dist/cli-routing.js +1 -1
- package/dist/cli-routing.js.map +1 -1
- package/dist/cli.js +68 -7
- package/dist/cli.js.map +1 -1
- package/dist/cloudsql-tls.d.ts +93 -0
- package/dist/cloudsql-tls.d.ts.map +1 -0
- package/dist/cloudsql-tls.js +295 -0
- package/dist/cloudsql-tls.js.map +1 -0
- package/dist/compose.d.ts +43 -6
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +148 -23
- package/dist/compose.js.map +1 -1
- package/dist/converge-checkpoint.d.ts +1 -0
- package/dist/converge-checkpoint.d.ts.map +1 -1
- package/dist/converge-checkpoint.js +8 -1
- package/dist/converge-checkpoint.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 +11 -13
- 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/deployed-release.d.ts +4 -1
- package/dist/deployed-release.d.ts.map +1 -1
- package/dist/deployed-release.js +18 -8
- package/dist/deployed-release.js.map +1 -1
- package/dist/gcp-names.d.ts +34 -0
- package/dist/gcp-names.d.ts.map +1 -1
- package/dist/gcp-names.js +45 -0
- package/dist/gcp-names.js.map +1 -1
- package/dist/gcp-url-map.d.ts.map +1 -1
- package/dist/gcp-url-map.js +6 -2
- package/dist/gcp-url-map.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- 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 +45 -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 +13 -0
- package/dist/install-state.d.ts.map +1 -1
- package/dist/install-state.js +17 -4
- package/dist/install-state.js.map +1 -1
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +20 -8
- package/dist/lifecycle.js.map +1 -1
- package/dist/mcp-auth-proxy-setup.d.ts +48 -0
- package/dist/mcp-auth-proxy-setup.d.ts.map +1 -0
- package/dist/mcp-auth-proxy-setup.js +280 -0
- package/dist/mcp-auth-proxy-setup.js.map +1 -0
- 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/release-manifest.d.ts +25 -1
- package/dist/release-manifest.d.ts.map +1 -1
- package/dist/release-manifest.js +36 -3
- package/dist/release-manifest.js.map +1 -1
- package/dist/remote-mcp.d.ts +55 -18
- package/dist/remote-mcp.d.ts.map +1 -1
- package/dist/remote-mcp.js +307 -77
- package/dist/remote-mcp.js.map +1 -1
- package/dist/runtime-secrets.d.ts +11 -0
- package/dist/runtime-secrets.d.ts.map +1 -1
- package/dist/runtime-secrets.js +40 -25
- 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/seal-key-rotation.d.ts +7 -7
- package/dist/seal-key-rotation.d.ts.map +1 -1
- package/dist/seal-key-rotation.js +22 -22
- package/dist/seal-key-rotation.js.map +1 -1
- package/dist/self-hosted-db-tls.d.ts +37 -0
- package/dist/self-hosted-db-tls.d.ts.map +1 -0
- package/dist/self-hosted-db-tls.js +220 -0
- package/dist/self-hosted-db-tls.js.map +1 -0
- package/dist/self-hosted-release.d.ts +3 -1
- package/dist/self-hosted-release.d.ts.map +1 -1
- package/dist/self-hosted-release.js +33 -9
- package/dist/self-hosted-release.js.map +1 -1
- package/dist/self-hosted-roles.d.ts +43 -2
- package/dist/self-hosted-roles.d.ts.map +1 -1
- package/dist/self-hosted-roles.js +87 -1
- 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-start.d.ts +0 -21
- package/dist/self-hosted-start.d.ts.map +1 -1
- package/dist/self-hosted-start.js +39 -1
- package/dist/self-hosted-start.js.map +1 -1
- package/dist/self-hosted-upgrade.d.ts +19 -7
- package/dist/self-hosted-upgrade.d.ts.map +1 -1
- package/dist/self-hosted-upgrade.js +78 -25
- package/dist/self-hosted-upgrade.js.map +1 -1
- package/dist/stack-contract.d.ts +234 -28
- package/dist/stack-contract.d.ts.map +1 -1
- package/dist/stack-contract.js +397 -39
- package/dist/stack-contract.js.map +1 -1
- package/dist/stack-converge.d.ts +31 -0
- package/dist/stack-converge.d.ts.map +1 -1
- package/dist/stack-converge.js +71 -30
- package/dist/stack-converge.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/app-roll.d.ts +25 -4
- package/dist/upgrade/app-roll.d.ts.map +1 -1
- package/dist/upgrade/app-roll.js +63 -17
- package/dist/upgrade/app-roll.js.map +1 -1
- package/dist/upgrade/converge.d.ts +5 -0
- package/dist/upgrade/converge.d.ts.map +1 -1
- package/dist/upgrade/converge.js +207 -20
- package/dist/upgrade/converge.js.map +1 -1
- package/dist/upgrade/health.d.ts +31 -7
- package/dist/upgrade/health.d.ts.map +1 -1
- package/dist/upgrade/health.js +41 -17
- package/dist/upgrade/health.js.map +1 -1
- package/dist/upgrade/image-forms.d.ts +1 -1
- package/dist/upgrade/image-forms.d.ts.map +1 -1
- package/dist/upgrade/image-forms.js +24 -7
- package/dist/upgrade/image-forms.js.map +1 -1
- package/dist/upgrade/release-images.d.ts +12 -3
- package/dist/upgrade/release-images.d.ts.map +1 -1
- package/dist/upgrade/release-images.js +13 -4
- package/dist/upgrade/release-images.js.map +1 -1
- package/dist/upgrade/upgrade.d.ts +19 -3
- package/dist/upgrade/upgrade.d.ts.map +1 -1
- package/dist/upgrade/upgrade.js +37 -2
- package/dist/upgrade/upgrade.js.map +1 -1
- package/dist/upgrader-gcp.d.ts +13 -0
- package/dist/upgrader-gcp.d.ts.map +1 -1
- package/dist/upgrader-gcp.js +50 -1
- package/dist/upgrader-gcp.js.map +1 -1
- package/dist/upgrader-iam-preflight.d.ts.map +1 -1
- package/dist/upgrader-iam-preflight.js +6 -1
- package/dist/upgrader-iam-preflight.js.map +1 -1
- package/dist/upgrader-job.d.ts +1 -1
- package/dist/upgrader-job.d.ts.map +1 -1
- package/dist/upgrader-job.js +20 -14
- package/dist/upgrader-job.js.map +1 -1
- package/dist/upgrader-records.d.ts +1 -1
- package/dist/upgrader-records.d.ts.map +1 -1
- package/dist/upgrader-records.js +26 -7
- package/dist/upgrader-records.js.map +1 -1
- package/dist/upgrader-setup.d.ts.map +1 -1
- package/dist/upgrader-setup.js +10 -5
- package/dist/upgrader-setup.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 +103 -18
- package/dist/wizards/gcp-cost.js.map +1 -1
- package/dist/wizards/gcp.d.ts.map +1 -1
- package/dist/wizards/gcp.js +129 -27
- package/dist/wizards/gcp.js.map +1 -1
- package/package.json +2 -2
- package/src/adapter.ts +6 -0
- package/src/admin-domain.test.ts +9 -8
- package/src/admin-domain.ts +13 -3
- package/src/admin-memory.test.ts +2 -1
- package/src/adopt-existing.test.ts +85 -0
- package/src/adopt-existing.ts +141 -3
- package/src/agent/command.test.ts +49 -1
- package/src/agent/command.ts +39 -9
- package/src/agent/run.test.ts +19 -0
- package/src/agent/run.ts +11 -2
- package/src/aws-task-roles.ts +5 -2
- package/src/caddy-redirects.docker.test.ts +2 -0
- package/src/caddy-redirects.test.ts +4 -1
- package/src/caddy-validate.test.ts +7 -1
- package/src/caddy.test.ts +39 -1
- package/src/caddy.ts +36 -4
- package/src/checkpoint-secrets.test.ts +14 -6
- package/src/cli-routing.test.ts +1 -1
- package/src/cli-routing.ts +1 -1
- package/src/cli.ts +71 -5
- package/src/cloudsql-tls.test.ts +255 -0
- package/src/cloudsql-tls.ts +359 -0
- package/src/compose.ts +193 -24
- package/src/converge-checkpoint.ts +8 -1
- package/src/converge-db-trust.test.ts +237 -0
- package/src/converge-db-trust.ts +212 -0
- package/src/converge-job-gcp.ts +11 -12
- 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 +56 -0
- package/src/deployed-release.test.ts +107 -40
- package/src/deployed-release.ts +30 -9
- package/src/gateway-env-isolation.test.ts +50 -12
- package/src/gcp-names.ts +56 -0
- package/src/gcp-url-map.test.ts +6 -8
- package/src/gcp-url-map.ts +6 -2
- package/src/index.test.ts +10 -4
- package/src/index.ts +7 -0
- package/src/infra-contract.test.ts +7 -3
- package/src/infra-contract.ts +45 -1
- package/src/install-job.ts +4 -1
- package/src/install-state.ts +33 -4
- package/src/lifecycle.ts +23 -11
- package/src/mcp-auth-proxy-setup.test.ts +305 -0
- package/src/mcp-auth-proxy-setup.ts +374 -0
- package/src/mcp-iap-stacks.test.ts +1 -1
- package/src/media-volume.test.ts +1 -1
- package/src/migration-runner.ts +14 -9
- package/src/migration-targets.ts +12 -0
- package/src/operator-access-stacks.test.ts +1 -1
- package/src/operator-access.test.ts +20 -4
- package/src/operator-access.ts +14 -0
- package/src/pool-budget-stack.ts +15 -0
- package/src/pool-budget-stacks.test.ts +39 -14
- 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/release-manifest.test.ts +25 -0
- package/src/release-manifest.ts +59 -4
- package/src/remote-mcp-stack-config.test.ts +19 -9
- package/src/remote-mcp.test.ts +252 -57
- package/src/remote-mcp.ts +336 -77
- package/src/runtime-secrets.test.ts +37 -14
- package/src/runtime-secrets.ts +87 -27
- package/src/scheduler-setup.test.ts +201 -0
- package/src/scheduler-setup.ts +375 -0
- package/src/seal-key-rotation.test.ts +2 -2
- package/src/seal-key-rotation.ts +25 -25
- package/src/self-hosted-backup.test.ts +1 -0
- package/src/self-hosted-compose.test.ts +180 -6
- package/src/self-hosted-db-tls.test.ts +111 -0
- package/src/self-hosted-db-tls.ts +287 -0
- package/src/self-hosted-release-resolve.test.ts +71 -0
- package/src/self-hosted-release.ts +41 -11
- package/src/self-hosted-roles.integration.test.ts +2 -0
- package/src/self-hosted-roles.test.ts +87 -3
- package/src/self-hosted-roles.ts +101 -3
- 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 +35 -2
- package/src/self-hosted-start.ts +20 -2
- package/src/self-hosted-upgrade.test.ts +104 -14
- package/src/self-hosted-upgrade.ts +123 -31
- package/src/stack-contract-parity.test.ts +79 -12
- package/src/stack-contract.test.ts +199 -31
- package/src/stack-contract.ts +472 -45
- package/src/stack-converge-secrets.test.ts +28 -13
- package/src/stack-converge.test.ts +203 -28
- package/src/stack-converge.ts +92 -30
- package/src/staging-database.test.ts +114 -0
- package/src/staging-database.ts +148 -0
- package/src/state-store.test.ts +2 -1
- package/src/upgrade/app-roll.ts +97 -19
- package/src/upgrade/converge.ts +249 -19
- package/src/upgrade/health.test.ts +63 -25
- package/src/upgrade/health.ts +54 -22
- package/src/upgrade/image-forms.test.ts +34 -0
- package/src/upgrade/image-forms.ts +23 -8
- package/src/upgrade/release-images.test.ts +5 -1
- package/src/upgrade/release-images.ts +22 -6
- package/src/upgrade/upgrade.test.ts +438 -46
- package/src/upgrade/upgrade.ts +64 -6
- package/src/upgrader-gcp.ts +66 -3
- package/src/upgrader-iam-preflight.test.ts +19 -0
- package/src/upgrader-iam-preflight.ts +6 -0
- package/src/upgrader-job.test.ts +101 -27
- package/src/upgrader-job.ts +27 -14
- package/src/upgrader-records.test.ts +97 -12
- package/src/upgrader-records.ts +28 -11
- package/src/upgrader-setup.test.ts +2 -0
- package/src/upgrader-setup.ts +10 -4
- package/src/wizard.ts +19 -0
- package/src/wizards/gcp-cost.test.ts +103 -0
- package/src/wizards/gcp-cost.ts +125 -18
- package/src/wizards/gcp.ts +164 -22
- package/stacks/aws/Pulumi.yaml +1 -1
- package/stacks/aws/index.ts +72 -14
- package/stacks/azure/index.ts +108 -21
- package/stacks/gcp/Pulumi.yaml +1 -1
- package/stacks/gcp/README.md +1 -0
- package/stacks/gcp/index.ts +133 -24
- package/stacks/gcp-firebase/Pulumi.yaml +1 -1
- package/stacks/gcp-firebase/index.ts +212 -34
- package/stacks/self-hosted/Pulumi.yaml +3 -0
- package/stacks/self-hosted/README.md +6 -1
- package/stacks/self-hosted/index.ts +40 -5
package/src/media-volume.test.ts
CHANGED
|
@@ -152,7 +152,7 @@ describe("admin media volume contract", () => {
|
|
|
152
152
|
domain: "acme.com",
|
|
153
153
|
stagingHost: "staging.acme.com",
|
|
154
154
|
region: "europe-west1",
|
|
155
|
-
databaseUrls: databaseUrls("
|
|
155
|
+
databaseUrls: databaseUrls("db1.p-label.europe-west1.sql-psa.goog"),
|
|
156
156
|
});
|
|
157
157
|
expect(env).toContainEqual({ name: "MEDIA_ROOT_DIR", value: MEDIA_MOUNT_PATH });
|
|
158
158
|
});
|
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;
|
|
@@ -45,7 +45,7 @@ describe("admin env contract — operator access", () => {
|
|
|
45
45
|
domain: "acme.com",
|
|
46
46
|
stagingHost: "staging.acme.com",
|
|
47
47
|
region: "europe-west1",
|
|
48
|
-
databaseUrls: databaseUrls("
|
|
48
|
+
databaseUrls: databaseUrls("db1.p-label.europe-west1.sql-psa.goog"),
|
|
49
49
|
});
|
|
50
50
|
expect(env).toContainEqual({
|
|
51
51
|
name: "CAELO_OPERATOR_ACCESS_JOB",
|
|
@@ -51,7 +51,7 @@ const target: OperatorAccessSyncTarget = {
|
|
|
51
51
|
imageRef: "europe-west1-docker.pkg.dev/r/admin@sha256:new",
|
|
52
52
|
network: "net",
|
|
53
53
|
subnet: "sub",
|
|
54
|
-
databaseHost: "
|
|
54
|
+
databaseHost: "db1.p-label.europe-west1.sql-psa.goog",
|
|
55
55
|
};
|
|
56
56
|
|
|
57
57
|
const roleJson = JSON.stringify({ includedPermissions: [...OPERATOR_ACCESS_ROLE.permissions] });
|
|
@@ -201,10 +201,25 @@ describe("ensureOperatorAccessSync", () => {
|
|
|
201
201
|
"--args=--bun,/app/packages/admin-core/src/security/operator-access/sync-job.ts",
|
|
202
202
|
);
|
|
203
203
|
expect(deploy).toContain(
|
|
204
|
-
"--set-env-vars=^|^CAELO_OPERATOR_ACCESS_IAP_WEB=cloud_run-europe-west1/services/caelo-production-admin-abc|CAELO_MCP_IAP_SERVICE_ACCOUNT=caelo-mcp@p.iam.gserviceaccount.com|CAELO_OPERATOR_ACCESS_STATIC_MEMBERS=user:owner@x.com|CAELO_OPERATOR_ACCESS_DB_HOST=
|
|
204
|
+
"--set-env-vars=^|^CAELO_OPERATOR_ACCESS_IAP_WEB=cloud_run-europe-west1/services/caelo-production-admin-abc|CAELO_MCP_IAP_SERVICE_ACCOUNT=caelo-mcp@p.iam.gserviceaccount.com|CAELO_OPERATOR_ACCESS_STATIC_MEMBERS=user:owner@x.com|CAELO_OPERATOR_ACCESS_DB_HOST=db1.p-label.europe-west1.sql-psa.goog|CAELO_OPERATOR_OAUTH_CLIENT_SECRET=projects/p/secrets/caelo-production-operator-oauth-client",
|
|
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 () => {} });
|
|
@@ -322,7 +337,8 @@ describe("resolveOperatorAccessTarget", () => {
|
|
|
322
337
|
env: [
|
|
323
338
|
{
|
|
324
339
|
name: "ADMIN_DATABASE_URL",
|
|
325
|
-
value:
|
|
340
|
+
value:
|
|
341
|
+
"postgresql://admin_role@db1.p-label.europe-west1.sql-psa.goog:5432/cms_admin",
|
|
326
342
|
},
|
|
327
343
|
],
|
|
328
344
|
},
|
|
@@ -351,7 +367,7 @@ describe("resolveOperatorAccessTarget", () => {
|
|
|
351
367
|
imageRef: "img@sha256:new",
|
|
352
368
|
network: "net",
|
|
353
369
|
subnet: "sub",
|
|
354
|
-
databaseHost: "
|
|
370
|
+
databaseHost: "db1.p-label.europe-west1.sql-psa.goog",
|
|
355
371
|
},
|
|
356
372
|
});
|
|
357
373
|
});
|
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,10 +82,11 @@ 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
|
-
).toThrow(/
|
|
89
|
+
).toThrow(/CONNECTION LIMIT, #1338/);
|
|
67
90
|
});
|
|
68
91
|
});
|
|
69
92
|
|
|
@@ -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
|
|
@@ -240,6 +240,23 @@ describe("ReleaseManifestSchema", () => {
|
|
|
240
240
|
const partial = { ...MANIFEST, platformDigests: { admin: d("1"), gateway: d("2") } };
|
|
241
241
|
expect(ReleaseManifestSchema.safeParse(partial).success).toBe(false);
|
|
242
242
|
});
|
|
243
|
+
|
|
244
|
+
it("#1328: mcp-auth-proxy is optional (older signed manifests lack it) and checked when present", () => {
|
|
245
|
+
// The canonical fixture is a pre-#1328 manifest: it names no proxy image.
|
|
246
|
+
expect("mcp-auth-proxy" in MANIFEST.digests).toBe(false);
|
|
247
|
+
const digests = { ...MANIFEST.digests, "mcp-auth-proxy": d("4") };
|
|
248
|
+
const platformDigests = {
|
|
249
|
+
admin: d("1"),
|
|
250
|
+
gateway: d("2"),
|
|
251
|
+
ops: d("3"),
|
|
252
|
+
"mcp-auth-proxy": d("5"),
|
|
253
|
+
};
|
|
254
|
+
const parsed = ReleaseManifestSchema.safeParse({ ...MANIFEST, digests, platformDigests });
|
|
255
|
+
expect(parsed.success && parsed.data.digests["mcp-auth-proxy"]).toBe(d("4"));
|
|
256
|
+
expect(parsed.success && parsed.data.platformDigests?.["mcp-auth-proxy"]).toBe(d("5"));
|
|
257
|
+
const bad = { ...MANIFEST, digests: { ...MANIFEST.digests, "mcp-auth-proxy": "sha256:nope" } };
|
|
258
|
+
expect(ReleaseManifestSchema.safeParse(bad).success).toBe(false);
|
|
259
|
+
});
|
|
243
260
|
});
|
|
244
261
|
|
|
245
262
|
describe("compareImageDigests", () => {
|
|
@@ -270,6 +287,14 @@ describe("compareImageDigests", () => {
|
|
|
270
287
|
{ image: "admin", form: "platform", expected: null, actual: d("1") },
|
|
271
288
|
]);
|
|
272
289
|
});
|
|
290
|
+
|
|
291
|
+
it("#1328: a resolved image the manifest does not sign (a pre-proxy release) never matches", () => {
|
|
292
|
+
expect(compareImageDigests(MANIFEST, { "mcp-auth-proxy": d("4") })).toEqual([
|
|
293
|
+
{ image: "mcp-auth-proxy", form: "index", expected: null, actual: d("4") },
|
|
294
|
+
]);
|
|
295
|
+
const signed = { ...MANIFEST, digests: { ...MANIFEST.digests, "mcp-auth-proxy": d("4") } };
|
|
296
|
+
expect(compareImageDigests(signed, { "mcp-auth-proxy": d("4") })).toEqual([]);
|
|
297
|
+
});
|
|
273
298
|
});
|
|
274
299
|
|
|
275
300
|
describe("assessAutoInstall", () => {
|
package/src/release-manifest.ts
CHANGED
|
@@ -35,7 +35,11 @@ import {
|
|
|
35
35
|
type ReleaseManifest,
|
|
36
36
|
} from "@caelo-cms/shared";
|
|
37
37
|
import { infraStepsText } from "./infra-contract.js";
|
|
38
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
RELEASE_OIDC_ISSUER,
|
|
40
|
+
RELEASE_WORKFLOWS_URL,
|
|
41
|
+
releaseImageRef,
|
|
42
|
+
} from "./upgrade/release-images.js";
|
|
39
43
|
|
|
40
44
|
// The manifest's shape and location live in @caelo-cms/shared (the admin's
|
|
41
45
|
// auto-update scheduler reads them too); re-exported so this module stays
|
|
@@ -223,11 +227,58 @@ export interface DigestMismatch {
|
|
|
223
227
|
readonly image: ReleaseImage;
|
|
224
228
|
/** Which digest differs: the image index, or its linux/amd64 manifest (#1025). */
|
|
225
229
|
readonly form: "index" | "platform";
|
|
226
|
-
/**
|
|
230
|
+
/**
|
|
231
|
+
* The manifest's digest; null when the manifest names none for this form
|
|
232
|
+
* (no platform digests, or an image the release predates —
|
|
233
|
+
* OPTIONAL_RELEASE_IMAGES).
|
|
234
|
+
*/
|
|
227
235
|
readonly expected: string | null;
|
|
228
236
|
readonly actual: string | null;
|
|
229
237
|
}
|
|
230
238
|
|
|
239
|
+
/** One service's digest-pinned image in the public mirror. */
|
|
240
|
+
export interface ManifestServiceImage {
|
|
241
|
+
readonly digest: string;
|
|
242
|
+
readonly imageRef: string;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* The images the app-roll moves the Cloud Run services onto, from the signed
|
|
247
|
+
* manifest's digests — never a registry tag lookup.
|
|
248
|
+
*
|
|
249
|
+
* Fails for a manifest without an MCP-Auth-Proxy digest: such a release
|
|
250
|
+
* predates #1328, and this code rolls three services, so installing it
|
|
251
|
+
* would leave the proxy on an image of another release.
|
|
252
|
+
*/
|
|
253
|
+
export function manifestServiceImages(m: Pick<ReleaseManifest, "version" | "digests">):
|
|
254
|
+
| {
|
|
255
|
+
readonly ok: true;
|
|
256
|
+
readonly images: Readonly<
|
|
257
|
+
Record<"admin" | "gateway" | "mcp-auth-proxy", ManifestServiceImage>
|
|
258
|
+
>;
|
|
259
|
+
}
|
|
260
|
+
| { readonly ok: false; readonly error: string } {
|
|
261
|
+
const proxy = m.digests["mcp-auth-proxy"];
|
|
262
|
+
if (!proxy) {
|
|
263
|
+
return {
|
|
264
|
+
ok: false,
|
|
265
|
+
error: `the signed manifest of ${m.version} names no mcp-auth-proxy image: that release predates the MCP-Auth-Proxy (#1328) and cannot be installed by this release's upgrader. Install it from a terminal with the version's own CLI (bunx @caelo-cms/provisioning@${m.version} upgrade).`,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
const image = (slug: string, digest: string) => ({
|
|
269
|
+
digest,
|
|
270
|
+
imageRef: releaseImageRef(slug, digest),
|
|
271
|
+
});
|
|
272
|
+
return {
|
|
273
|
+
ok: true,
|
|
274
|
+
images: {
|
|
275
|
+
admin: image("admin", m.digests.admin),
|
|
276
|
+
gateway: image("gateway", m.digests.gateway),
|
|
277
|
+
"mcp-auth-proxy": image("mcp-auth-proxy", proxy),
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
231
282
|
/**
|
|
232
283
|
* Compare the digests a registry resolved for the release's tags with the
|
|
233
284
|
* signed manifest. Only images present in `resolved` (or `resolvedPlatform`)
|
|
@@ -246,9 +297,13 @@ export function compareImageDigests(
|
|
|
246
297
|
const out: DigestMismatch[] = [];
|
|
247
298
|
for (const image of RELEASE_IMAGES) {
|
|
248
299
|
if (image in resolved) {
|
|
300
|
+
// An image the signed manifest does not name (a release from before
|
|
301
|
+
// the image existed) can never match: a caller that resolved it is
|
|
302
|
+
// asking about an image the signature does not cover.
|
|
249
303
|
const actual = resolved[image] ?? null;
|
|
250
|
-
|
|
251
|
-
|
|
304
|
+
const expected = manifest.digests[image] ?? null;
|
|
305
|
+
if (actual === null || actual !== expected) {
|
|
306
|
+
out.push({ image, form: "index", expected, actual });
|
|
252
307
|
}
|
|
253
308
|
}
|
|
254
309
|
if (image in resolvedPlatform) {
|