@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
|
@@ -16,6 +16,7 @@ import { describe, expect, it } from "bun:test";
|
|
|
16
16
|
import { databaseUrlFromEnv } from "@caelo-cms/shared";
|
|
17
17
|
import type { GcloudResult } from "./gcloud.js";
|
|
18
18
|
import {
|
|
19
|
+
CLOUD_RUN_SLUGS,
|
|
19
20
|
type CloudRunSlug,
|
|
20
21
|
OPERATOR_OAUTH_CLIENT_FILE,
|
|
21
22
|
RETIRED_SECRET_FILES,
|
|
@@ -32,7 +33,7 @@ import {
|
|
|
32
33
|
} from "./stack-converge.js";
|
|
33
34
|
|
|
34
35
|
const PW = "4f2a9c0d1e2b3a4f5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b";
|
|
35
|
-
const HOST = "
|
|
36
|
+
const HOST = "db1.p-label.europe-west1.sql-psa.goog";
|
|
36
37
|
|
|
37
38
|
/** Secret Manager as the fake revisions resolve it. */
|
|
38
39
|
const SECRET_VALUES: Record<string, string> = {
|
|
@@ -141,7 +142,7 @@ function expectGatewayOnItsOwnLogins(env: Record<string, string>): void {
|
|
|
141
142
|
expect(Object.values(env).join("\n")).not.toContain("g".repeat(64));
|
|
142
143
|
expect(Object.values(env).join("\n")).not.toContain("cms_admin");
|
|
143
144
|
expect(credentials(databaseUrlFromEnv(["PUBLIC_DATABASE_URL"], env))).toBe(
|
|
144
|
-
`public_role:${"p".repeat(64)}@${HOST}:5432/cms_public?sslmode=
|
|
145
|
+
`public_role:${"p".repeat(64)}@${HOST}:5432/cms_public?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem`,
|
|
145
146
|
);
|
|
146
147
|
}
|
|
147
148
|
|
|
@@ -157,7 +158,7 @@ const serviceJson = (env: unknown[]) =>
|
|
|
157
158
|
/** What `gcloud run services describe` showed on a live v0.10.29 gcp install. */
|
|
158
159
|
function v01029Install(scheme: "postgresql" | "postgres") {
|
|
159
160
|
const url = (role: string, db: string) =>
|
|
160
|
-
`${scheme}://${role}:${PW}@${HOST}:5432/${db}?sslmode=
|
|
161
|
+
`${scheme}://${role}:${PW}@${HOST}:5432/${db}?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem`;
|
|
161
162
|
const common = {
|
|
162
163
|
CAELO_PROVIDER: "gcp",
|
|
163
164
|
CAELO_ENV: "production",
|
|
@@ -200,20 +201,30 @@ const INSTALL = {
|
|
|
200
201
|
stagingHost: "staging.acme.com",
|
|
201
202
|
region: "europe-west1",
|
|
202
203
|
};
|
|
204
|
+
/** The fixtures' services; the proxy is added by {@link upgrade} when absent. */
|
|
205
|
+
type InstallServices = Record<"admin" | "gateway", FakeService> & {
|
|
206
|
+
"mcp-auth-proxy"?: FakeService;
|
|
207
|
+
};
|
|
208
|
+
|
|
203
209
|
const SERVICE_ACCOUNTS: Record<CloudRunSlug, string> = {
|
|
204
210
|
admin: "caelo-production-run-sa@acme.iam.gserviceaccount.com",
|
|
205
211
|
gateway: "caelo-production-gateway-sa@acme.iam.gserviceaccount.com",
|
|
212
|
+
"mcp-auth-proxy": "caelo-production-mcp-proxy-sa@acme.iam.gserviceaccount.com",
|
|
206
213
|
};
|
|
207
214
|
|
|
208
215
|
/** Plan + apply one upgrade roll on the fake services, as upgradeCommand does. */
|
|
209
|
-
function upgrade(services:
|
|
216
|
+
function upgrade(services: InstallServices) {
|
|
217
|
+
// #1328 — the gcp MCP-Auth-Proxy: created by the converge before the plan,
|
|
218
|
+
// with no env yet in these fixtures (they are about database logins).
|
|
219
|
+
services["mcp-auth-proxy"] ??= new FakeService(new Map(), SERVICE_ACCOUNTS["mcp-auth-proxy"]);
|
|
210
220
|
const plan = planContractEnv(INSTALL, {
|
|
211
221
|
admin: { serviceName: "adm", liveEnv: services.admin.env },
|
|
212
222
|
gateway: { serviceName: "gw", liveEnv: services.gateway.env },
|
|
223
|
+
"mcp-auth-proxy": { serviceName: "proxy", liveEnv: services["mcp-auth-proxy"].env },
|
|
213
224
|
});
|
|
214
225
|
if (!plan.ok) throw new Error(plan.error);
|
|
215
|
-
for (const slug of
|
|
216
|
-
services[slug].update(
|
|
226
|
+
for (const slug of CLOUD_RUN_SLUGS) {
|
|
227
|
+
(services[slug] as FakeService).update(
|
|
217
228
|
serviceRollArgs({
|
|
218
229
|
serviceName: slug,
|
|
219
230
|
region: INSTALL.region,
|
|
@@ -297,7 +308,7 @@ describe.each(["postgresql", "postgres"] as const)(
|
|
|
297
308
|
*/
|
|
298
309
|
function v01035Install() {
|
|
299
310
|
const url = (role: string, db: string) =>
|
|
300
|
-
`postgresql://${role}@${HOST}:5432/${db}?sslmode=
|
|
311
|
+
`postgresql://${role}@${HOST}:5432/${db}?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem`;
|
|
301
312
|
const ref = (name: string, secret: string) => ({
|
|
302
313
|
name,
|
|
303
314
|
valueFrom: { secretKeyRef: { name: `caelo-production-${secret}`, key: "latest" } },
|
|
@@ -349,10 +360,9 @@ describe("upgrading a v0.10.35 install: the gateway leaves admin_role (#613)", (
|
|
|
349
360
|
expect(plan.ok && plan.services.gateway.changes.map((c) => c.name).sort()).toEqual([
|
|
350
361
|
"ADMIN_DATABASE_PASSWORD",
|
|
351
362
|
"ADMIN_DATABASE_URL",
|
|
352
|
-
// #
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
"CAELO_OPERATOR_OAUTH_CLIENT_FILE",
|
|
363
|
+
// #1329 — the gateway says it is the live site's.
|
|
364
|
+
"CAELO_GATEWAY_ENVIRONMENT",
|
|
365
|
+
// No remote-MCP env: the broker is the MCP-Auth-Proxy's (#1328).
|
|
356
366
|
// #650 — the staging gate key arrives in the same roll.
|
|
357
367
|
"CAELO_STAGING_GATE_SECRET",
|
|
358
368
|
// #845 — and which forwarded address is the visitor.
|
|
@@ -396,7 +406,7 @@ function v01050Install() {
|
|
|
396
406
|
const env = new Map(services.gateway.env);
|
|
397
407
|
env.set("GATEWAY_DATABASE_URL", {
|
|
398
408
|
kind: "value",
|
|
399
|
-
value: `postgresql://gateway_role@${HOST}:5432/cms_admin?sslmode=
|
|
409
|
+
value: `postgresql://gateway_role@${HOST}:5432/cms_admin?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem`,
|
|
400
410
|
});
|
|
401
411
|
env.set("GATEWAY_DATABASE_PASSWORD", {
|
|
402
412
|
kind: "secret",
|
|
@@ -488,7 +498,12 @@ describe("retiring a secret file removes its volume too (#954)", () => {
|
|
|
488
498
|
},
|
|
489
499
|
};
|
|
490
500
|
const plan = (live: Parameters<typeof planSecretFiles>[0]) =>
|
|
491
|
-
planSecretFiles(
|
|
501
|
+
planSecretFiles(
|
|
502
|
+
live,
|
|
503
|
+
[OPERATOR_OAUTH_CLIENT_FILE],
|
|
504
|
+
"production",
|
|
505
|
+
RETIRED_SECRET_FILES["mcp-auth-proxy"],
|
|
506
|
+
);
|
|
492
507
|
|
|
493
508
|
it("a retired mount: removes the file, the mount and the volume behind it in the roll", () => {
|
|
494
509
|
const r = plan({
|
|
@@ -13,9 +13,11 @@ import {
|
|
|
13
13
|
type LiveEnvValue,
|
|
14
14
|
liveContainerEnv,
|
|
15
15
|
liveDatabaseHost,
|
|
16
|
+
liveDirectVpc,
|
|
16
17
|
liveServiceAccount,
|
|
17
18
|
planContractEnv,
|
|
18
19
|
planEnvUpdate,
|
|
20
|
+
planNoVpc,
|
|
19
21
|
policyGrants,
|
|
20
22
|
removeRetiredIamBindings,
|
|
21
23
|
rollService,
|
|
@@ -56,6 +58,7 @@ const policy = (bindings: { role: string; members: string[]; condition?: unknown
|
|
|
56
58
|
|
|
57
59
|
const RUN_SA = "serviceAccount:caelo-production-run-sa@acme.iam.gserviceaccount.com";
|
|
58
60
|
const GW_SA = "serviceAccount:caelo-production-gateway-sa@acme.iam.gserviceaccount.com";
|
|
61
|
+
const PROXY_SA = "serviceAccount:caelo-production-mcp-proxy-sa@acme.iam.gserviceaccount.com";
|
|
59
62
|
const GW_TELEMETRY = [
|
|
60
63
|
{ role: "roles/logging.logWriter", members: [GW_SA] },
|
|
61
64
|
{ role: "roles/monitoring.metricWriter", members: [GW_SA] },
|
|
@@ -67,7 +70,11 @@ const firebase: InstallTarget = {
|
|
|
67
70
|
projectId: "acme",
|
|
68
71
|
region: "europe-west1",
|
|
69
72
|
env: "production",
|
|
70
|
-
services: {
|
|
73
|
+
services: {
|
|
74
|
+
admin: "caelo-production-admin-aaa",
|
|
75
|
+
gateway: "caelo-production-data-gateway",
|
|
76
|
+
"mcp-auth-proxy": "caelo-production-gateway-bbb",
|
|
77
|
+
},
|
|
71
78
|
};
|
|
72
79
|
const gcp: InstallTarget = { ...firebase, provider: "gcp" };
|
|
73
80
|
|
|
@@ -126,13 +133,13 @@ describe("liveContainerEnv / liveServiceAccount / liveDatabaseHost", () => {
|
|
|
126
133
|
|
|
127
134
|
it("finds the Cloud SQL host in ADMIN_DATABASE_URL with or without an inline password", () => {
|
|
128
135
|
for (const url of [
|
|
129
|
-
"postgresql://admin_role:pw@
|
|
130
|
-
"postgresql://admin_role@
|
|
136
|
+
"postgresql://admin_role:pw@db1.p-label.europe-west1.sql-psa.goog:5432/cms_admin?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem",
|
|
137
|
+
"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",
|
|
131
138
|
]) {
|
|
132
139
|
const host = liveDatabaseHost(
|
|
133
140
|
new Map([["ADMIN_DATABASE_URL", { kind: "value", value: url }]]),
|
|
134
141
|
);
|
|
135
|
-
expect(host).toEqual({ ok: true, host: "
|
|
142
|
+
expect(host).toEqual({ ok: true, host: "db1.p-label.europe-west1.sql-psa.goog" });
|
|
136
143
|
}
|
|
137
144
|
expect(liveDatabaseHost(new Map()).ok).toBe(false);
|
|
138
145
|
});
|
|
@@ -261,11 +268,13 @@ const secretRef = (name: string, secret: string) => ({
|
|
|
261
268
|
const CONVERGED_ADMIN_DB = [
|
|
262
269
|
{
|
|
263
270
|
name: "ADMIN_DATABASE_URL",
|
|
264
|
-
value:
|
|
271
|
+
value:
|
|
272
|
+
"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",
|
|
265
273
|
},
|
|
266
274
|
{
|
|
267
275
|
name: "PUBLIC_ADMIN_DATABASE_URL",
|
|
268
|
-
value:
|
|
276
|
+
value:
|
|
277
|
+
"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",
|
|
269
278
|
},
|
|
270
279
|
secretRef("ADMIN_DATABASE_PASSWORD", "postgres-password"),
|
|
271
280
|
secretRef("PUBLIC_ADMIN_DATABASE_PASSWORD", "postgres-password"),
|
|
@@ -276,7 +285,8 @@ const CONVERGED_ADMIN_DB = [
|
|
|
276
285
|
const CONVERGED_GATEWAY_DB = [
|
|
277
286
|
{
|
|
278
287
|
name: "PUBLIC_DATABASE_URL",
|
|
279
|
-
value:
|
|
288
|
+
value:
|
|
289
|
+
"postgresql://public_role@db1.p-label.europe-west1.sql-psa.goog:5432/cms_public?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem",
|
|
280
290
|
},
|
|
281
291
|
secretRef("PUBLIC_DATABASE_PASSWORD", "public-role-password"),
|
|
282
292
|
];
|
|
@@ -315,21 +325,35 @@ describe("planContractEnv", () => {
|
|
|
315
325
|
},
|
|
316
326
|
{
|
|
317
327
|
admin: { serviceName: "caelo-production-admin-aaa", liveEnv: adminLive },
|
|
318
|
-
|
|
328
|
+
// #1328 — the data gateway, a copy of the old gateway service.
|
|
329
|
+
gateway: { serviceName: "caelo-production-data-gateway", liveEnv: gatewayLive },
|
|
330
|
+
// The old gateway service, which becomes the MCP-Auth-Proxy (E13 a).
|
|
331
|
+
"mcp-auth-proxy": { serviceName: "caelo-production-gateway-bbb", liveEnv: gatewayLive },
|
|
319
332
|
},
|
|
320
333
|
);
|
|
321
334
|
if (!plan.ok) throw new Error(plan.error);
|
|
322
335
|
// #650 — the same roll converges the protected staging domain's host
|
|
323
336
|
// and gate key onto an install that predates it.
|
|
324
337
|
expect(plan.services.admin.flags).toEqual([
|
|
325
|
-
|
|
326
|
-
"--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",
|
|
327
341
|
]);
|
|
342
|
+
// #1328 — the Firebase rewrite follows the data gateway (above), which
|
|
343
|
+
// gets no remote-MCP env.
|
|
328
344
|
expect(plan.services.gateway.flags).toEqual([
|
|
329
|
-
// The gateway learns which forwarded address is the visitor (#845)
|
|
330
|
-
// #
|
|
331
|
-
"--update-env-vars=CAELO_TRUSTED_PROXY_HOPS=1
|
|
332
|
-
"--update-secrets=CAELO_STAGING_GATE_SECRET=caelo-production-staging-gate-secret:latest
|
|
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",
|
|
348
|
+
"--update-secrets=CAELO_STAGING_GATE_SECRET=caelo-production-staging-gate-secret:latest",
|
|
349
|
+
]);
|
|
350
|
+
// The old gateway service becomes the proxy in one revision: every
|
|
351
|
+
// database and site var goes, its own contract arrives.
|
|
352
|
+
expect(plan.services["mcp-auth-proxy"].flags).toEqual([
|
|
353
|
+
"--remove-env-vars=PUBLIC_DATABASE_URL,CAELO_ENV,MEDIA_STORAGE_URL",
|
|
354
|
+
"--update-env-vars=CAELO_REMOTE_MCP=available,CAELO_TRUSTED_PROXY_HOPS=1,CAELO_OPERATOR_OAUTH_CLIENT_FILE=/secrets/operator-oauth-client/client.json",
|
|
355
|
+
"--remove-secrets=PUBLIC_DATABASE_PASSWORD",
|
|
356
|
+
"--update-secrets=CAELO_MCP_OAUTH_SEAL_KEY=caelo-production-mcp-oauth-seal-key:latest",
|
|
333
357
|
]);
|
|
334
358
|
});
|
|
335
359
|
|
|
@@ -352,9 +376,10 @@ describe("planContractEnv", () => {
|
|
|
352
376
|
{
|
|
353
377
|
admin: { serviceName: "caelo-production-admin-aaa", liveEnv: live },
|
|
354
378
|
gateway: {
|
|
355
|
-
serviceName: "caelo-production-gateway
|
|
379
|
+
serviceName: "caelo-production-data-gateway",
|
|
356
380
|
liveEnv: liveContainerEnv(serviceJson(CONVERGED_GATEWAY_DB)),
|
|
357
381
|
},
|
|
382
|
+
"mcp-auth-proxy": { serviceName: "caelo-production-gateway-bbb", liveEnv: new Map() },
|
|
358
383
|
},
|
|
359
384
|
{ leaveUntouched: ["CAELO_MCP_IAP_SERVICE_ACCOUNT", "CAELO_OPERATOR_ACCESS_JOB"] },
|
|
360
385
|
);
|
|
@@ -375,18 +400,72 @@ describe("planContractEnv", () => {
|
|
|
375
400
|
{
|
|
376
401
|
admin: { serviceName: "adm", liveEnv: liveContainerEnv(serviceJson(CONVERGED_ADMIN_DB)) },
|
|
377
402
|
gateway: { serviceName: "gw", liveEnv: new Map() },
|
|
403
|
+
"mcp-auth-proxy": { serviceName: "px", liveEnv: new Map() },
|
|
378
404
|
},
|
|
379
405
|
);
|
|
380
406
|
expect(plan.ok).toBe(false);
|
|
381
407
|
expect(!plan.ok && plan.error).toContain("CAELO_FIREBASE_SITE");
|
|
382
408
|
});
|
|
383
409
|
|
|
410
|
+
it("#1338: an install on the instance's IP plans its database URLs only with the converge's host", () => {
|
|
411
|
+
const onIp = (db: { name: string; value?: string }[]) =>
|
|
412
|
+
db.map((e) =>
|
|
413
|
+
e.value
|
|
414
|
+
? {
|
|
415
|
+
...e,
|
|
416
|
+
value: e.value
|
|
417
|
+
.replace("db1.p-label.europe-west1.sql-psa.goog", "10.20.0.3")
|
|
418
|
+
.replace(/\?.*$/, "?sslmode=require"),
|
|
419
|
+
}
|
|
420
|
+
: e,
|
|
421
|
+
);
|
|
422
|
+
const services = {
|
|
423
|
+
admin: {
|
|
424
|
+
serviceName: "adm",
|
|
425
|
+
liveEnv: liveContainerEnv(serviceJson(onIp(CONVERGED_ADMIN_DB) as never)),
|
|
426
|
+
},
|
|
427
|
+
gateway: {
|
|
428
|
+
serviceName: "gw",
|
|
429
|
+
liveEnv: liveContainerEnv(serviceJson(onIp(CONVERGED_GATEWAY_DB) as never)),
|
|
430
|
+
},
|
|
431
|
+
// #1328 — no database URL, so the TLS step has nothing to move on it.
|
|
432
|
+
"mcp-auth-proxy": { serviceName: "px", liveEnv: new Map() },
|
|
433
|
+
};
|
|
434
|
+
const install = {
|
|
435
|
+
provider: "gcp" as const,
|
|
436
|
+
projectId: "a",
|
|
437
|
+
env: "production",
|
|
438
|
+
domain: "a.com",
|
|
439
|
+
stagingHost: "staging.a.com",
|
|
440
|
+
region: "r",
|
|
441
|
+
};
|
|
442
|
+
// Read-only plan: the URLs stay as they are and the plan says why.
|
|
443
|
+
const pending = planContractEnv(install, services);
|
|
444
|
+
if (!pending.ok) throw new Error(pending.error);
|
|
445
|
+
expect(pending.databaseHostPending).toBe(true);
|
|
446
|
+
for (const s of ["admin", "gateway"] as const) {
|
|
447
|
+
expect(pending.services[s].changes.map((c) => c.name)).not.toContainEqual(
|
|
448
|
+
expect.stringMatching(/DATABASE_URL$/),
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
// After the converge's TLS step: verify-full URLs on the DNS name.
|
|
452
|
+
const host = "db1.p-label.europe-west1.sql-psa.goog";
|
|
453
|
+
const moved = planContractEnv(install, services, { databaseHost: host });
|
|
454
|
+
if (!moved.ok) throw new Error(moved.error);
|
|
455
|
+
expect(moved.databaseHostPending).toBe(false);
|
|
456
|
+
const url = moved.services.gateway.changes.find((c) => c.name === "PUBLIC_DATABASE_URL");
|
|
457
|
+
expect(url?.to).toBe(
|
|
458
|
+
`postgresql://public_role@${host}:5432/cms_public?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem`,
|
|
459
|
+
);
|
|
460
|
+
});
|
|
461
|
+
|
|
384
462
|
it("fails loudly when the Cloud SQL host can't be discovered", () => {
|
|
385
463
|
const plan = planContractEnv(
|
|
386
464
|
{ provider: "gcp", projectId: "a", env: "production", domain: "a.com", region: "r" },
|
|
387
465
|
{
|
|
388
466
|
admin: { serviceName: "adm", liveEnv: new Map() },
|
|
389
467
|
gateway: { serviceName: "gw", liveEnv: new Map() },
|
|
468
|
+
"mcp-auth-proxy": { serviceName: "px", liveEnv: new Map() },
|
|
390
469
|
},
|
|
391
470
|
);
|
|
392
471
|
expect(plan.ok).toBe(false);
|
|
@@ -486,7 +565,7 @@ describe("ensureStackInvariants", () => {
|
|
|
486
565
|
],
|
|
487
566
|
"secrets get-iam-policy": [
|
|
488
567
|
policy([
|
|
489
|
-
{ role: "roles/secretmanager.secretAccessor", members: [RUN_SA, GW_SA] },
|
|
568
|
+
{ role: "roles/secretmanager.secretAccessor", members: [RUN_SA, GW_SA, PROXY_SA] },
|
|
490
569
|
// #806 — the admin adds remote-MCP OAuth client versions.
|
|
491
570
|
{ role: "roles/secretmanager.secretVersionAdder", members: [RUN_SA] },
|
|
492
571
|
]),
|
|
@@ -494,17 +573,23 @@ describe("ensureStackInvariants", () => {
|
|
|
494
573
|
"storage buckets get-iam-policy": [
|
|
495
574
|
policy([{ role: "roles/storage.objectAdmin", members: [RUN_SA] }]),
|
|
496
575
|
],
|
|
497
|
-
|
|
576
|
+
// #1328 — the data gateway: the Firebase rewrite target.
|
|
577
|
+
"run services get-iam-policy caelo-production-data-gateway": [
|
|
498
578
|
policy([
|
|
499
579
|
{ role: "roles/run.invoker", members: ["allUsers"] },
|
|
500
580
|
{ role: "roles/run.viewer", members: [RUN_SA] },
|
|
501
581
|
]),
|
|
502
582
|
],
|
|
583
|
+
// The MCP-Auth-Proxy: public for MCP clients.
|
|
584
|
+
"run services get-iam-policy caelo-production-gateway-bbb": [
|
|
585
|
+
policy([{ role: "roles/run.invoker", members: ["allUsers"] }]),
|
|
586
|
+
],
|
|
503
587
|
"run services get-iam-policy caelo-production-admin-aaa": [
|
|
504
588
|
policy([{ role: "roles/run.invoker", members: [IAP_AGENT] }]),
|
|
505
589
|
],
|
|
506
590
|
"compute firewall-rules list": [ok("[]")],
|
|
507
591
|
"run services describe caelo-production-gateway-bbb": [ok(ingressJson("all"))],
|
|
592
|
+
"run services describe caelo-production-data-gateway": [ok(ingressJson("all"))],
|
|
508
593
|
});
|
|
509
594
|
|
|
510
595
|
it("is a read-only no-op on an install that is already in shape", async () => {
|
|
@@ -513,7 +598,8 @@ describe("ensureStackInvariants", () => {
|
|
|
513
598
|
expect(report.mustAbort).toBe(false);
|
|
514
599
|
expect(report.outcomes.every((o) => o.status === "present")).toBe(true);
|
|
515
600
|
expect(report.outcomes).toHaveLength(
|
|
516
|
-
|
|
601
|
+
// The two public services' ingress (#732, #1328).
|
|
602
|
+
stackIamInvariants("gcp-firebase").length + ABSENT_DEFAULT_FIREWALL_RULES.length + 2,
|
|
517
603
|
);
|
|
518
604
|
expect(calls.some((c) => c.startsWith("compute firewall-rules delete"))).toBe(false);
|
|
519
605
|
expect(calls.some((c) => c.includes("add-iam-policy-binding"))).toBe(false);
|
|
@@ -628,7 +714,7 @@ describe("ensureStackInvariants", () => {
|
|
|
628
714
|
answers["projects get-iam-policy"] = [
|
|
629
715
|
policy([{ role: "roles/firebasehosting.admin", members: [RUN_SA] }, ...GW_TELEMETRY]),
|
|
630
716
|
];
|
|
631
|
-
answers["run services get-iam-policy caelo-production-gateway
|
|
717
|
+
answers["run services get-iam-policy caelo-production-data-gateway"] = [
|
|
632
718
|
policy([{ role: "roles/run.invoker", members: ["allUsers"] }]),
|
|
633
719
|
];
|
|
634
720
|
const { run, calls } = fakeGcloud(answers);
|
|
@@ -636,7 +722,7 @@ describe("ensureStackInvariants", () => {
|
|
|
636
722
|
|
|
637
723
|
expect(report.mustAbort).toBe(false);
|
|
638
724
|
expect(calls).toContain(
|
|
639
|
-
`run services add-iam-policy-binding caelo-production-gateway
|
|
725
|
+
`run services add-iam-policy-binding caelo-production-data-gateway --region=europe-west1 --project=acme --member=${RUN_SA} --role=roles/run.viewer --quiet --format=none`,
|
|
640
726
|
);
|
|
641
727
|
for (const role of ["roles/logging.logWriter", "roles/monitoring.metricWriter"]) {
|
|
642
728
|
expect(calls).toContain(
|
|
@@ -650,7 +736,7 @@ describe("ensureStackInvariants", () => {
|
|
|
650
736
|
|
|
651
737
|
it("aborts when a binding the install needs can't be added", async () => {
|
|
652
738
|
const answers = fullFirebase();
|
|
653
|
-
answers["run services get-iam-policy caelo-production-gateway
|
|
739
|
+
answers["run services get-iam-policy caelo-production-data-gateway"] = [
|
|
654
740
|
policy([{ role: "roles/run.invoker", members: ["allUsers"] }]),
|
|
655
741
|
];
|
|
656
742
|
answers["run services add-iam-policy-binding"] = [
|
|
@@ -739,7 +825,7 @@ describe("ensureStackInvariants", () => {
|
|
|
739
825
|
],
|
|
740
826
|
"secrets get-iam-policy": [
|
|
741
827
|
policy([
|
|
742
|
-
{ role: "roles/secretmanager.secretAccessor", members: [RUN_SA, GW_SA] },
|
|
828
|
+
{ role: "roles/secretmanager.secretAccessor", members: [RUN_SA, GW_SA, PROXY_SA] },
|
|
743
829
|
{ role: "roles/secretmanager.secretVersionAdder", members: [RUN_SA] },
|
|
744
830
|
]),
|
|
745
831
|
],
|
|
@@ -871,10 +957,19 @@ describe("ensureStackInvariants", () => {
|
|
|
871
957
|
expect(gatewayGrants).toEqual([
|
|
872
958
|
`secrets add-iam-policy-binding caelo-production-public-role-password --project=acme --member=${GW_SA} --role=roles/secretmanager.secretAccessor --quiet --format=none`,
|
|
873
959
|
`secrets add-iam-policy-binding caelo-production-staging-gate-secret --project=acme --member=${GW_SA} --role=roles/secretmanager.secretAccessor --quiet --format=none`,
|
|
874
|
-
// #806 — the remote-MCP broker's seal key and OAuth client file.
|
|
875
|
-
`secrets add-iam-policy-binding caelo-production-mcp-oauth-seal-key --project=acme --member=${GW_SA} --role=roles/secretmanager.secretAccessor --quiet --format=none`,
|
|
876
|
-
`secrets add-iam-policy-binding caelo-production-operator-oauth-client --project=acme --member=${GW_SA} --role=roles/secretmanager.secretAccessor --quiet --format=none`,
|
|
877
960
|
]);
|
|
961
|
+
// #1328 — the remote-MCP broker's seal key and OAuth client file are
|
|
962
|
+
// the MCP-Auth-Proxy's, and those two secrets are all its SA reads.
|
|
963
|
+
expect(
|
|
964
|
+
calls.filter((c) => c.startsWith("secrets add-iam-policy-binding") && c.includes(PROXY_SA)),
|
|
965
|
+
).toEqual([
|
|
966
|
+
`secrets add-iam-policy-binding caelo-production-mcp-oauth-seal-key --project=acme --member=${PROXY_SA} --role=roles/secretmanager.secretAccessor --quiet --format=none`,
|
|
967
|
+
`secrets add-iam-policy-binding caelo-production-operator-oauth-client --project=acme --member=${PROXY_SA} --role=roles/secretmanager.secretAccessor --quiet --format=none`,
|
|
968
|
+
]);
|
|
969
|
+
// …and it holds no project role.
|
|
970
|
+
expect(
|
|
971
|
+
calls.some((c) => c.startsWith("projects add-iam-policy-binding") && c.includes(PROXY_SA)),
|
|
972
|
+
).toBe(false);
|
|
878
973
|
// #806 — the admin may add versions to the OAuth client secret, and to no other.
|
|
879
974
|
const adders = calls.filter((c) => c.includes("roles/secretmanager.secretVersionAdder"));
|
|
880
975
|
expect(adders).toEqual([
|
|
@@ -900,7 +995,13 @@ describe("removeRetiredIamBindings (#613)", () => {
|
|
|
900
995
|
});
|
|
901
996
|
const outcomes = await removeRetiredIamBindings(gcp, { run, sleep: async () => {} });
|
|
902
997
|
// postgres-password removed; gateway-role-password (#635) already clean.
|
|
903
|
-
expect(outcomes.map((o) => o.status)).toEqual([
|
|
998
|
+
expect(outcomes.map((o) => o.status)).toEqual([
|
|
999
|
+
"removed",
|
|
1000
|
+
"absent",
|
|
1001
|
+
"absent",
|
|
1002
|
+
"absent",
|
|
1003
|
+
"absent",
|
|
1004
|
+
]);
|
|
904
1005
|
expect(calls).toContain(REMOVE);
|
|
905
1006
|
// The admin's own binding on the same secret is never touched.
|
|
906
1007
|
expect(calls.some((c) => c.includes("remove") && c.includes(RUN_SA))).toBe(false);
|
|
@@ -913,7 +1014,13 @@ describe("removeRetiredIamBindings (#613)", () => {
|
|
|
913
1014
|
],
|
|
914
1015
|
});
|
|
915
1016
|
const outcomes = await removeRetiredIamBindings(firebase, { run, sleep: async () => {} });
|
|
916
|
-
expect(outcomes.map((o) => o.status)).toEqual([
|
|
1017
|
+
expect(outcomes.map((o) => o.status)).toEqual([
|
|
1018
|
+
"absent",
|
|
1019
|
+
"absent",
|
|
1020
|
+
"absent",
|
|
1021
|
+
"absent",
|
|
1022
|
+
"absent",
|
|
1023
|
+
]);
|
|
917
1024
|
expect(calls.filter((c) => c.includes("remove-iam-policy-binding"))).toEqual([]);
|
|
918
1025
|
});
|
|
919
1026
|
|
|
@@ -924,12 +1031,51 @@ describe("removeRetiredIamBindings (#613)", () => {
|
|
|
924
1031
|
],
|
|
925
1032
|
});
|
|
926
1033
|
const outcomes = await removeRetiredIamBindings(gcp, { run, sleep: async () => {} });
|
|
927
|
-
expect(outcomes.map((o) => o.status)).toEqual([
|
|
1034
|
+
expect(outcomes.map((o) => o.status)).toEqual([
|
|
1035
|
+
"absent",
|
|
1036
|
+
"removed",
|
|
1037
|
+
"absent",
|
|
1038
|
+
"absent",
|
|
1039
|
+
"absent",
|
|
1040
|
+
]);
|
|
928
1041
|
expect(calls).toContain(
|
|
929
1042
|
`secrets remove-iam-policy-binding caelo-production-gateway-role-password --project=acme --member=${GW_SA} --role=roles/secretmanager.secretAccessor --quiet --format=none`,
|
|
930
1043
|
);
|
|
931
1044
|
});
|
|
932
1045
|
|
|
1046
|
+
it("#1328: takes the gateway SA off the broker's secrets and the admin's viewer off the proxy", async () => {
|
|
1047
|
+
const { run, calls } = fakeGcloud({
|
|
1048
|
+
"secrets get-iam-policy caelo-production-mcp-oauth-seal-key": [
|
|
1049
|
+
policy([{ role: "roles/secretmanager.secretAccessor", members: [GW_SA, PROXY_SA] }]),
|
|
1050
|
+
],
|
|
1051
|
+
"secrets get-iam-policy caelo-production-operator-oauth-client": [
|
|
1052
|
+
policy([{ role: "roles/secretmanager.secretAccessor", members: [GW_SA, PROXY_SA] }]),
|
|
1053
|
+
],
|
|
1054
|
+
"run services get-iam-policy caelo-production-gateway-bbb": [
|
|
1055
|
+
policy([
|
|
1056
|
+
{ role: "roles/run.invoker", members: ["allUsers"] },
|
|
1057
|
+
{ role: "roles/run.viewer", members: [RUN_SA] },
|
|
1058
|
+
]),
|
|
1059
|
+
],
|
|
1060
|
+
});
|
|
1061
|
+
const outcomes = await removeRetiredIamBindings(firebase, { run, sleep: async () => {} });
|
|
1062
|
+
expect(outcomes.map((o) => o.status)).toEqual([
|
|
1063
|
+
"absent",
|
|
1064
|
+
"absent",
|
|
1065
|
+
"removed",
|
|
1066
|
+
"removed",
|
|
1067
|
+
"removed",
|
|
1068
|
+
]);
|
|
1069
|
+
for (const secret of ["mcp-oauth-seal-key", "operator-oauth-client"]) {
|
|
1070
|
+
expect(calls).toContain(
|
|
1071
|
+
`secrets remove-iam-policy-binding caelo-production-${secret} --project=acme --member=${GW_SA} --role=roles/secretmanager.secretAccessor --quiet --format=none`,
|
|
1072
|
+
);
|
|
1073
|
+
}
|
|
1074
|
+
// The proxy keeps its own access and its public invoker.
|
|
1075
|
+
expect(calls.some((c) => c.includes("remove") && c.includes(PROXY_SA))).toBe(false);
|
|
1076
|
+
expect(calls.some((c) => c.includes("remove") && c.includes("allUsers"))).toBe(false);
|
|
1077
|
+
});
|
|
1078
|
+
|
|
933
1079
|
it("reports a failed removal instead of throwing", async () => {
|
|
934
1080
|
const { run } = fakeGcloud({
|
|
935
1081
|
"secrets get-iam-policy caelo-production-postgres-password": [
|
|
@@ -942,3 +1088,32 @@ describe("removeRetiredIamBindings (#613)", () => {
|
|
|
942
1088
|
expect(o?.error).toContain("PERMISSION_DENIED");
|
|
943
1089
|
});
|
|
944
1090
|
});
|
|
1091
|
+
|
|
1092
|
+
describe("liveDirectVpc / planNoVpc (#1328)", () => {
|
|
1093
|
+
const withNetwork = JSON.stringify({
|
|
1094
|
+
spec: {
|
|
1095
|
+
template: {
|
|
1096
|
+
metadata: {
|
|
1097
|
+
annotations: {
|
|
1098
|
+
"run.googleapis.com/network-interfaces": '[{"network":"caelo-production-vpc"}]',
|
|
1099
|
+
"run.googleapis.com/vpc-access-egress": "private-ranges-only",
|
|
1100
|
+
},
|
|
1101
|
+
},
|
|
1102
|
+
},
|
|
1103
|
+
},
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
it("reads Direct VPC egress from the revision template", () => {
|
|
1107
|
+
expect(liveDirectVpc(withNetwork)).toBe(true);
|
|
1108
|
+
expect(liveDirectVpc(JSON.stringify({ spec: { template: { metadata: {} } } }))).toBe(false);
|
|
1109
|
+
});
|
|
1110
|
+
|
|
1111
|
+
it("takes a service off the VPC in its roll, and leaves one that is off alone", () => {
|
|
1112
|
+
expect(planNoVpc(true)).toEqual({
|
|
1113
|
+
ok: true,
|
|
1114
|
+
flags: ["--clear-network"],
|
|
1115
|
+
changes: ["VPC egress (private ranges) → none"],
|
|
1116
|
+
});
|
|
1117
|
+
expect(planNoVpc(false)).toEqual({ ok: true, flags: [], changes: [] });
|
|
1118
|
+
});
|
|
1119
|
+
});
|