@caelo-cms/provisioning 0.11.8 → 0.11.10
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/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 +6 -1
- package/dist/adopt-existing.d.ts.map +1 -1
- package/dist/adopt-existing.js +86 -0
- package/dist/adopt-existing.js.map +1 -1
- package/dist/agent/command.d.ts +20 -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 +65 -3
- 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.js +12 -3
- package/dist/cli.js.map +1 -1
- package/dist/cloudsql-tls.d.ts +63 -0
- package/dist/cloudsql-tls.d.ts.map +1 -0
- package/dist/cloudsql-tls.js +289 -0
- package/dist/cloudsql-tls.js.map +1 -0
- package/dist/compose.d.ts +29 -6
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +95 -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-job-gcp.d.ts.map +1 -1
- package/dist/converge-job-gcp.js +2 -11
- package/dist/converge-job-gcp.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 +21 -0
- package/dist/gcp-names.d.ts.map +1 -1
- package/dist/gcp-names.js +24 -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 +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -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 +24 -1
- package/dist/infra-contract.js.map +1 -1
- package/dist/install-state.d.ts +6 -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 +17 -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/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 +9 -0
- package/dist/runtime-secrets.d.ts.map +1 -1
- package/dist/runtime-secrets.js +21 -10
- package/dist/runtime-secrets.js.map +1 -1
- 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 +34 -0
- package/dist/self-hosted-roles.d.ts.map +1 -1
- package/dist/self-hosted-roles.js +52 -0
- package/dist/self-hosted-roles.js.map +1 -1
- 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 +15 -6
- package/dist/self-hosted-upgrade.d.ts.map +1 -1
- package/dist/self-hosted-upgrade.js +72 -23
- package/dist/self-hosted-upgrade.js.map +1 -1
- package/dist/stack-contract.d.ts +170 -25
- package/dist/stack-contract.d.ts.map +1 -1
- package/dist/stack-contract.js +314 -37
- 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/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.map +1 -1
- package/dist/upgrade/converge.js +99 -12
- 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 +13 -3
- package/dist/upgrade/upgrade.d.ts.map +1 -1
- package/dist/upgrade/upgrade.js +34 -2
- package/dist/upgrade/upgrade.js.map +1 -1
- package/dist/upgrader-gcp.d.ts +12 -0
- package/dist/upgrader-gcp.d.ts.map +1 -1
- package/dist/upgrader-gcp.js +49 -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/wizards/gcp-cost.d.ts.map +1 -1
- package/dist/wizards/gcp-cost.js +22 -7
- package/dist/wizards/gcp-cost.js.map +1 -1
- package/dist/wizards/gcp.d.ts.map +1 -1
- package/dist/wizards/gcp.js +43 -17
- package/dist/wizards/gcp.js.map +1 -1
- package/package.json +2 -2
- package/src/admin-domain.test.ts +9 -8
- package/src/admin-domain.ts +13 -3
- package/src/admin-memory.test.ts +1 -0
- package/src/adopt-existing.test.ts +39 -0
- package/src/adopt-existing.ts +83 -1
- package/src/agent/command.test.ts +49 -1
- package/src/agent/command.ts +36 -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 +3 -0
- 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.ts +12 -1
- package/src/cloudsql-tls.test.ts +225 -0
- package/src/cloudsql-tls.ts +352 -0
- package/src/compose.ts +125 -24
- package/src/converge-checkpoint.ts +8 -1
- package/src/converge-job-gcp.ts +2 -11
- package/src/database-tls-builders.test.ts +55 -0
- package/src/deployed-release.test.ts +104 -40
- package/src/deployed-release.ts +30 -9
- package/src/gateway-env-isolation.test.ts +10 -5
- package/src/gcp-names.ts +31 -0
- package/src/gcp-url-map.test.ts +6 -8
- package/src/gcp-url-map.ts +6 -2
- package/src/index.test.ts +7 -1
- package/src/index.ts +1 -0
- package/src/infra-contract.test.ts +5 -3
- package/src/infra-contract.ts +24 -1
- package/src/install-state.ts +26 -4
- package/src/lifecycle.ts +20 -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/operator-access-stacks.test.ts +1 -1
- package/src/operator-access.test.ts +5 -4
- package/src/pool-budget-stacks.test.ts +1 -1
- 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 +20 -7
- package/src/runtime-secrets.ts +50 -9
- package/src/seal-key-rotation.test.ts +2 -2
- package/src/seal-key-rotation.ts +25 -25
- package/src/self-hosted-compose.test.ts +108 -5
- 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.test.ts +54 -0
- package/src/self-hosted-roles.ts +58 -0
- package/src/self-hosted-start.test.ts +34 -2
- package/src/self-hosted-start.ts +20 -2
- package/src/self-hosted-upgrade.test.ts +103 -14
- package/src/self-hosted-upgrade.ts +115 -29
- package/src/stack-contract-parity.test.ts +79 -12
- package/src/stack-contract.test.ts +169 -29
- package/src/stack-contract.ts +362 -42
- package/src/stack-converge-secrets.test.ts +26 -13
- package/src/stack-converge.test.ts +199 -26
- package/src/stack-converge.ts +92 -30
- package/src/state-store.test.ts +2 -1
- package/src/upgrade/app-roll.ts +97 -19
- package/src/upgrade/converge.ts +120 -11
- 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 +353 -44
- package/src/upgrade/upgrade.ts +55 -6
- package/src/upgrader-gcp.ts +65 -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/wizards/gcp-cost.test.ts +50 -0
- package/src/wizards/gcp-cost.ts +24 -7
- package/src/wizards/gcp.ts +55 -14
- package/stacks/aws/index.ts +32 -14
- package/stacks/azure/index.ts +57 -18
- package/stacks/gcp/index.ts +83 -23
- package/stacks/gcp-firebase/index.ts +162 -33
- package/stacks/self-hosted/README.md +2 -1
- package/stacks/self-hosted/index.ts +11 -1
|
@@ -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,7 @@ 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
|
-
// sign-in client file path.
|
|
354
|
-
"CAELO_MCP_OAUTH_SEAL_KEY",
|
|
355
|
-
"CAELO_OPERATOR_OAUTH_CLIENT_FILE",
|
|
363
|
+
// No remote-MCP env: the broker is the MCP-Auth-Proxy's (#1328).
|
|
356
364
|
// #650 — the staging gate key arrives in the same roll.
|
|
357
365
|
"CAELO_STAGING_GATE_SECRET",
|
|
358
366
|
// #845 — and which forwarded address is the visitor.
|
|
@@ -396,7 +404,7 @@ function v01050Install() {
|
|
|
396
404
|
const env = new Map(services.gateway.env);
|
|
397
405
|
env.set("GATEWAY_DATABASE_URL", {
|
|
398
406
|
kind: "value",
|
|
399
|
-
value: `postgresql://gateway_role@${HOST}:5432/cms_admin?sslmode=
|
|
407
|
+
value: `postgresql://gateway_role@${HOST}:5432/cms_admin?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem`,
|
|
400
408
|
});
|
|
401
409
|
env.set("GATEWAY_DATABASE_PASSWORD", {
|
|
402
410
|
kind: "secret",
|
|
@@ -488,7 +496,12 @@ describe("retiring a secret file removes its volume too (#954)", () => {
|
|
|
488
496
|
},
|
|
489
497
|
};
|
|
490
498
|
const plan = (live: Parameters<typeof planSecretFiles>[0]) =>
|
|
491
|
-
planSecretFiles(
|
|
499
|
+
planSecretFiles(
|
|
500
|
+
live,
|
|
501
|
+
[OPERATOR_OAUTH_CLIENT_FILE],
|
|
502
|
+
"production",
|
|
503
|
+
RETIRED_SECRET_FILES["mcp-auth-proxy"],
|
|
504
|
+
);
|
|
492
505
|
|
|
493
506
|
it("a retired mount: removes the file, the mount and the volume behind it in the roll", () => {
|
|
494
507
|
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,33 @@ 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
|
-
"--update-env-vars=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_OPERATOR_OAUTH_CLIENT_STORE=projects/acme/secrets/caelo-production-operator-oauth-client,CAELO_STAGING_HOST=staging.acme.com",
|
|
338
|
+
"--update-env-vars=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_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",
|
|
326
339
|
"--update-secrets=CAELO_STAGING_GATE_SECRET=caelo-production-staging-gate-secret:latest",
|
|
327
340
|
]);
|
|
341
|
+
// #1328 — the Firebase rewrite follows the data gateway (above), which
|
|
342
|
+
// gets no remote-MCP env.
|
|
328
343
|
expect(plan.services.gateway.flags).toEqual([
|
|
329
344
|
// The gateway learns which forwarded address is the visitor (#845).
|
|
330
|
-
|
|
331
|
-
"--update-
|
|
332
|
-
|
|
345
|
+
"--update-env-vars=CAELO_TRUSTED_PROXY_HOPS=1",
|
|
346
|
+
"--update-secrets=CAELO_STAGING_GATE_SECRET=caelo-production-staging-gate-secret:latest",
|
|
347
|
+
]);
|
|
348
|
+
// The old gateway service becomes the proxy in one revision: every
|
|
349
|
+
// database and site var goes, its own contract arrives.
|
|
350
|
+
expect(plan.services["mcp-auth-proxy"].flags).toEqual([
|
|
351
|
+
"--remove-env-vars=PUBLIC_DATABASE_URL,CAELO_ENV,MEDIA_STORAGE_URL",
|
|
352
|
+
"--update-env-vars=CAELO_REMOTE_MCP=available,CAELO_TRUSTED_PROXY_HOPS=1,CAELO_OPERATOR_OAUTH_CLIENT_FILE=/secrets/operator-oauth-client/client.json",
|
|
353
|
+
"--remove-secrets=PUBLIC_DATABASE_PASSWORD",
|
|
354
|
+
"--update-secrets=CAELO_MCP_OAUTH_SEAL_KEY=caelo-production-mcp-oauth-seal-key:latest",
|
|
333
355
|
]);
|
|
334
356
|
});
|
|
335
357
|
|
|
@@ -352,9 +374,10 @@ describe("planContractEnv", () => {
|
|
|
352
374
|
{
|
|
353
375
|
admin: { serviceName: "caelo-production-admin-aaa", liveEnv: live },
|
|
354
376
|
gateway: {
|
|
355
|
-
serviceName: "caelo-production-gateway
|
|
377
|
+
serviceName: "caelo-production-data-gateway",
|
|
356
378
|
liveEnv: liveContainerEnv(serviceJson(CONVERGED_GATEWAY_DB)),
|
|
357
379
|
},
|
|
380
|
+
"mcp-auth-proxy": { serviceName: "caelo-production-gateway-bbb", liveEnv: new Map() },
|
|
358
381
|
},
|
|
359
382
|
{ leaveUntouched: ["CAELO_MCP_IAP_SERVICE_ACCOUNT", "CAELO_OPERATOR_ACCESS_JOB"] },
|
|
360
383
|
);
|
|
@@ -375,18 +398,72 @@ describe("planContractEnv", () => {
|
|
|
375
398
|
{
|
|
376
399
|
admin: { serviceName: "adm", liveEnv: liveContainerEnv(serviceJson(CONVERGED_ADMIN_DB)) },
|
|
377
400
|
gateway: { serviceName: "gw", liveEnv: new Map() },
|
|
401
|
+
"mcp-auth-proxy": { serviceName: "px", liveEnv: new Map() },
|
|
378
402
|
},
|
|
379
403
|
);
|
|
380
404
|
expect(plan.ok).toBe(false);
|
|
381
405
|
expect(!plan.ok && plan.error).toContain("CAELO_FIREBASE_SITE");
|
|
382
406
|
});
|
|
383
407
|
|
|
408
|
+
it("#1338: an install on the instance's IP plans its database URLs only with the converge's host", () => {
|
|
409
|
+
const onIp = (db: { name: string; value?: string }[]) =>
|
|
410
|
+
db.map((e) =>
|
|
411
|
+
e.value
|
|
412
|
+
? {
|
|
413
|
+
...e,
|
|
414
|
+
value: e.value
|
|
415
|
+
.replace("db1.p-label.europe-west1.sql-psa.goog", "10.20.0.3")
|
|
416
|
+
.replace(/\?.*$/, "?sslmode=require"),
|
|
417
|
+
}
|
|
418
|
+
: e,
|
|
419
|
+
);
|
|
420
|
+
const services = {
|
|
421
|
+
admin: {
|
|
422
|
+
serviceName: "adm",
|
|
423
|
+
liveEnv: liveContainerEnv(serviceJson(onIp(CONVERGED_ADMIN_DB) as never)),
|
|
424
|
+
},
|
|
425
|
+
gateway: {
|
|
426
|
+
serviceName: "gw",
|
|
427
|
+
liveEnv: liveContainerEnv(serviceJson(onIp(CONVERGED_GATEWAY_DB) as never)),
|
|
428
|
+
},
|
|
429
|
+
// #1328 — no database URL, so the TLS step has nothing to move on it.
|
|
430
|
+
"mcp-auth-proxy": { serviceName: "px", liveEnv: new Map() },
|
|
431
|
+
};
|
|
432
|
+
const install = {
|
|
433
|
+
provider: "gcp" as const,
|
|
434
|
+
projectId: "a",
|
|
435
|
+
env: "production",
|
|
436
|
+
domain: "a.com",
|
|
437
|
+
stagingHost: "staging.a.com",
|
|
438
|
+
region: "r",
|
|
439
|
+
};
|
|
440
|
+
// Read-only plan: the URLs stay as they are and the plan says why.
|
|
441
|
+
const pending = planContractEnv(install, services);
|
|
442
|
+
if (!pending.ok) throw new Error(pending.error);
|
|
443
|
+
expect(pending.databaseHostPending).toBe(true);
|
|
444
|
+
for (const s of ["admin", "gateway"] as const) {
|
|
445
|
+
expect(pending.services[s].changes.map((c) => c.name)).not.toContainEqual(
|
|
446
|
+
expect.stringMatching(/DATABASE_URL$/),
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
// After the converge's TLS step: verify-full URLs on the DNS name.
|
|
450
|
+
const host = "db1.p-label.europe-west1.sql-psa.goog";
|
|
451
|
+
const moved = planContractEnv(install, services, { databaseHost: host });
|
|
452
|
+
if (!moved.ok) throw new Error(moved.error);
|
|
453
|
+
expect(moved.databaseHostPending).toBe(false);
|
|
454
|
+
const url = moved.services.gateway.changes.find((c) => c.name === "PUBLIC_DATABASE_URL");
|
|
455
|
+
expect(url?.to).toBe(
|
|
456
|
+
`postgresql://public_role@${host}:5432/cms_public?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem`,
|
|
457
|
+
);
|
|
458
|
+
});
|
|
459
|
+
|
|
384
460
|
it("fails loudly when the Cloud SQL host can't be discovered", () => {
|
|
385
461
|
const plan = planContractEnv(
|
|
386
462
|
{ provider: "gcp", projectId: "a", env: "production", domain: "a.com", region: "r" },
|
|
387
463
|
{
|
|
388
464
|
admin: { serviceName: "adm", liveEnv: new Map() },
|
|
389
465
|
gateway: { serviceName: "gw", liveEnv: new Map() },
|
|
466
|
+
"mcp-auth-proxy": { serviceName: "px", liveEnv: new Map() },
|
|
390
467
|
},
|
|
391
468
|
);
|
|
392
469
|
expect(plan.ok).toBe(false);
|
|
@@ -486,7 +563,7 @@ describe("ensureStackInvariants", () => {
|
|
|
486
563
|
],
|
|
487
564
|
"secrets get-iam-policy": [
|
|
488
565
|
policy([
|
|
489
|
-
{ role: "roles/secretmanager.secretAccessor", members: [RUN_SA, GW_SA] },
|
|
566
|
+
{ role: "roles/secretmanager.secretAccessor", members: [RUN_SA, GW_SA, PROXY_SA] },
|
|
490
567
|
// #806 — the admin adds remote-MCP OAuth client versions.
|
|
491
568
|
{ role: "roles/secretmanager.secretVersionAdder", members: [RUN_SA] },
|
|
492
569
|
]),
|
|
@@ -494,17 +571,23 @@ describe("ensureStackInvariants", () => {
|
|
|
494
571
|
"storage buckets get-iam-policy": [
|
|
495
572
|
policy([{ role: "roles/storage.objectAdmin", members: [RUN_SA] }]),
|
|
496
573
|
],
|
|
497
|
-
|
|
574
|
+
// #1328 — the data gateway: the Firebase rewrite target.
|
|
575
|
+
"run services get-iam-policy caelo-production-data-gateway": [
|
|
498
576
|
policy([
|
|
499
577
|
{ role: "roles/run.invoker", members: ["allUsers"] },
|
|
500
578
|
{ role: "roles/run.viewer", members: [RUN_SA] },
|
|
501
579
|
]),
|
|
502
580
|
],
|
|
581
|
+
// The MCP-Auth-Proxy: public for MCP clients.
|
|
582
|
+
"run services get-iam-policy caelo-production-gateway-bbb": [
|
|
583
|
+
policy([{ role: "roles/run.invoker", members: ["allUsers"] }]),
|
|
584
|
+
],
|
|
503
585
|
"run services get-iam-policy caelo-production-admin-aaa": [
|
|
504
586
|
policy([{ role: "roles/run.invoker", members: [IAP_AGENT] }]),
|
|
505
587
|
],
|
|
506
588
|
"compute firewall-rules list": [ok("[]")],
|
|
507
589
|
"run services describe caelo-production-gateway-bbb": [ok(ingressJson("all"))],
|
|
590
|
+
"run services describe caelo-production-data-gateway": [ok(ingressJson("all"))],
|
|
508
591
|
});
|
|
509
592
|
|
|
510
593
|
it("is a read-only no-op on an install that is already in shape", async () => {
|
|
@@ -513,7 +596,8 @@ describe("ensureStackInvariants", () => {
|
|
|
513
596
|
expect(report.mustAbort).toBe(false);
|
|
514
597
|
expect(report.outcomes.every((o) => o.status === "present")).toBe(true);
|
|
515
598
|
expect(report.outcomes).toHaveLength(
|
|
516
|
-
|
|
599
|
+
// The two public services' ingress (#732, #1328).
|
|
600
|
+
stackIamInvariants("gcp-firebase").length + ABSENT_DEFAULT_FIREWALL_RULES.length + 2,
|
|
517
601
|
);
|
|
518
602
|
expect(calls.some((c) => c.startsWith("compute firewall-rules delete"))).toBe(false);
|
|
519
603
|
expect(calls.some((c) => c.includes("add-iam-policy-binding"))).toBe(false);
|
|
@@ -628,7 +712,7 @@ describe("ensureStackInvariants", () => {
|
|
|
628
712
|
answers["projects get-iam-policy"] = [
|
|
629
713
|
policy([{ role: "roles/firebasehosting.admin", members: [RUN_SA] }, ...GW_TELEMETRY]),
|
|
630
714
|
];
|
|
631
|
-
answers["run services get-iam-policy caelo-production-gateway
|
|
715
|
+
answers["run services get-iam-policy caelo-production-data-gateway"] = [
|
|
632
716
|
policy([{ role: "roles/run.invoker", members: ["allUsers"] }]),
|
|
633
717
|
];
|
|
634
718
|
const { run, calls } = fakeGcloud(answers);
|
|
@@ -636,7 +720,7 @@ describe("ensureStackInvariants", () => {
|
|
|
636
720
|
|
|
637
721
|
expect(report.mustAbort).toBe(false);
|
|
638
722
|
expect(calls).toContain(
|
|
639
|
-
`run services add-iam-policy-binding caelo-production-gateway
|
|
723
|
+
`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
724
|
);
|
|
641
725
|
for (const role of ["roles/logging.logWriter", "roles/monitoring.metricWriter"]) {
|
|
642
726
|
expect(calls).toContain(
|
|
@@ -650,7 +734,7 @@ describe("ensureStackInvariants", () => {
|
|
|
650
734
|
|
|
651
735
|
it("aborts when a binding the install needs can't be added", async () => {
|
|
652
736
|
const answers = fullFirebase();
|
|
653
|
-
answers["run services get-iam-policy caelo-production-gateway
|
|
737
|
+
answers["run services get-iam-policy caelo-production-data-gateway"] = [
|
|
654
738
|
policy([{ role: "roles/run.invoker", members: ["allUsers"] }]),
|
|
655
739
|
];
|
|
656
740
|
answers["run services add-iam-policy-binding"] = [
|
|
@@ -739,7 +823,7 @@ describe("ensureStackInvariants", () => {
|
|
|
739
823
|
],
|
|
740
824
|
"secrets get-iam-policy": [
|
|
741
825
|
policy([
|
|
742
|
-
{ role: "roles/secretmanager.secretAccessor", members: [RUN_SA, GW_SA] },
|
|
826
|
+
{ role: "roles/secretmanager.secretAccessor", members: [RUN_SA, GW_SA, PROXY_SA] },
|
|
743
827
|
{ role: "roles/secretmanager.secretVersionAdder", members: [RUN_SA] },
|
|
744
828
|
]),
|
|
745
829
|
],
|
|
@@ -871,10 +955,19 @@ describe("ensureStackInvariants", () => {
|
|
|
871
955
|
expect(gatewayGrants).toEqual([
|
|
872
956
|
`secrets add-iam-policy-binding caelo-production-public-role-password --project=acme --member=${GW_SA} --role=roles/secretmanager.secretAccessor --quiet --format=none`,
|
|
873
957
|
`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
958
|
]);
|
|
959
|
+
// #1328 — the remote-MCP broker's seal key and OAuth client file are
|
|
960
|
+
// the MCP-Auth-Proxy's, and those two secrets are all its SA reads.
|
|
961
|
+
expect(
|
|
962
|
+
calls.filter((c) => c.startsWith("secrets add-iam-policy-binding") && c.includes(PROXY_SA)),
|
|
963
|
+
).toEqual([
|
|
964
|
+
`secrets add-iam-policy-binding caelo-production-mcp-oauth-seal-key --project=acme --member=${PROXY_SA} --role=roles/secretmanager.secretAccessor --quiet --format=none`,
|
|
965
|
+
`secrets add-iam-policy-binding caelo-production-operator-oauth-client --project=acme --member=${PROXY_SA} --role=roles/secretmanager.secretAccessor --quiet --format=none`,
|
|
966
|
+
]);
|
|
967
|
+
// …and it holds no project role.
|
|
968
|
+
expect(
|
|
969
|
+
calls.some((c) => c.startsWith("projects add-iam-policy-binding") && c.includes(PROXY_SA)),
|
|
970
|
+
).toBe(false);
|
|
878
971
|
// #806 — the admin may add versions to the OAuth client secret, and to no other.
|
|
879
972
|
const adders = calls.filter((c) => c.includes("roles/secretmanager.secretVersionAdder"));
|
|
880
973
|
expect(adders).toEqual([
|
|
@@ -900,7 +993,13 @@ describe("removeRetiredIamBindings (#613)", () => {
|
|
|
900
993
|
});
|
|
901
994
|
const outcomes = await removeRetiredIamBindings(gcp, { run, sleep: async () => {} });
|
|
902
995
|
// postgres-password removed; gateway-role-password (#635) already clean.
|
|
903
|
-
expect(outcomes.map((o) => o.status)).toEqual([
|
|
996
|
+
expect(outcomes.map((o) => o.status)).toEqual([
|
|
997
|
+
"removed",
|
|
998
|
+
"absent",
|
|
999
|
+
"absent",
|
|
1000
|
+
"absent",
|
|
1001
|
+
"absent",
|
|
1002
|
+
]);
|
|
904
1003
|
expect(calls).toContain(REMOVE);
|
|
905
1004
|
// The admin's own binding on the same secret is never touched.
|
|
906
1005
|
expect(calls.some((c) => c.includes("remove") && c.includes(RUN_SA))).toBe(false);
|
|
@@ -913,7 +1012,13 @@ describe("removeRetiredIamBindings (#613)", () => {
|
|
|
913
1012
|
],
|
|
914
1013
|
});
|
|
915
1014
|
const outcomes = await removeRetiredIamBindings(firebase, { run, sleep: async () => {} });
|
|
916
|
-
expect(outcomes.map((o) => o.status)).toEqual([
|
|
1015
|
+
expect(outcomes.map((o) => o.status)).toEqual([
|
|
1016
|
+
"absent",
|
|
1017
|
+
"absent",
|
|
1018
|
+
"absent",
|
|
1019
|
+
"absent",
|
|
1020
|
+
"absent",
|
|
1021
|
+
]);
|
|
917
1022
|
expect(calls.filter((c) => c.includes("remove-iam-policy-binding"))).toEqual([]);
|
|
918
1023
|
});
|
|
919
1024
|
|
|
@@ -924,12 +1029,51 @@ describe("removeRetiredIamBindings (#613)", () => {
|
|
|
924
1029
|
],
|
|
925
1030
|
});
|
|
926
1031
|
const outcomes = await removeRetiredIamBindings(gcp, { run, sleep: async () => {} });
|
|
927
|
-
expect(outcomes.map((o) => o.status)).toEqual([
|
|
1032
|
+
expect(outcomes.map((o) => o.status)).toEqual([
|
|
1033
|
+
"absent",
|
|
1034
|
+
"removed",
|
|
1035
|
+
"absent",
|
|
1036
|
+
"absent",
|
|
1037
|
+
"absent",
|
|
1038
|
+
]);
|
|
928
1039
|
expect(calls).toContain(
|
|
929
1040
|
`secrets remove-iam-policy-binding caelo-production-gateway-role-password --project=acme --member=${GW_SA} --role=roles/secretmanager.secretAccessor --quiet --format=none`,
|
|
930
1041
|
);
|
|
931
1042
|
});
|
|
932
1043
|
|
|
1044
|
+
it("#1328: takes the gateway SA off the broker's secrets and the admin's viewer off the proxy", async () => {
|
|
1045
|
+
const { run, calls } = fakeGcloud({
|
|
1046
|
+
"secrets get-iam-policy caelo-production-mcp-oauth-seal-key": [
|
|
1047
|
+
policy([{ role: "roles/secretmanager.secretAccessor", members: [GW_SA, PROXY_SA] }]),
|
|
1048
|
+
],
|
|
1049
|
+
"secrets get-iam-policy caelo-production-operator-oauth-client": [
|
|
1050
|
+
policy([{ role: "roles/secretmanager.secretAccessor", members: [GW_SA, PROXY_SA] }]),
|
|
1051
|
+
],
|
|
1052
|
+
"run services get-iam-policy caelo-production-gateway-bbb": [
|
|
1053
|
+
policy([
|
|
1054
|
+
{ role: "roles/run.invoker", members: ["allUsers"] },
|
|
1055
|
+
{ role: "roles/run.viewer", members: [RUN_SA] },
|
|
1056
|
+
]),
|
|
1057
|
+
],
|
|
1058
|
+
});
|
|
1059
|
+
const outcomes = await removeRetiredIamBindings(firebase, { run, sleep: async () => {} });
|
|
1060
|
+
expect(outcomes.map((o) => o.status)).toEqual([
|
|
1061
|
+
"absent",
|
|
1062
|
+
"absent",
|
|
1063
|
+
"removed",
|
|
1064
|
+
"removed",
|
|
1065
|
+
"removed",
|
|
1066
|
+
]);
|
|
1067
|
+
for (const secret of ["mcp-oauth-seal-key", "operator-oauth-client"]) {
|
|
1068
|
+
expect(calls).toContain(
|
|
1069
|
+
`secrets remove-iam-policy-binding caelo-production-${secret} --project=acme --member=${GW_SA} --role=roles/secretmanager.secretAccessor --quiet --format=none`,
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
// The proxy keeps its own access and its public invoker.
|
|
1073
|
+
expect(calls.some((c) => c.includes("remove") && c.includes(PROXY_SA))).toBe(false);
|
|
1074
|
+
expect(calls.some((c) => c.includes("remove") && c.includes("allUsers"))).toBe(false);
|
|
1075
|
+
});
|
|
1076
|
+
|
|
933
1077
|
it("reports a failed removal instead of throwing", async () => {
|
|
934
1078
|
const { run } = fakeGcloud({
|
|
935
1079
|
"secrets get-iam-policy caelo-production-postgres-password": [
|
|
@@ -942,3 +1086,32 @@ describe("removeRetiredIamBindings (#613)", () => {
|
|
|
942
1086
|
expect(o?.error).toContain("PERMISSION_DENIED");
|
|
943
1087
|
});
|
|
944
1088
|
});
|
|
1089
|
+
|
|
1090
|
+
describe("liveDirectVpc / planNoVpc (#1328)", () => {
|
|
1091
|
+
const withNetwork = JSON.stringify({
|
|
1092
|
+
spec: {
|
|
1093
|
+
template: {
|
|
1094
|
+
metadata: {
|
|
1095
|
+
annotations: {
|
|
1096
|
+
"run.googleapis.com/network-interfaces": '[{"network":"caelo-production-vpc"}]',
|
|
1097
|
+
"run.googleapis.com/vpc-access-egress": "private-ranges-only",
|
|
1098
|
+
},
|
|
1099
|
+
},
|
|
1100
|
+
},
|
|
1101
|
+
},
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
it("reads Direct VPC egress from the revision template", () => {
|
|
1105
|
+
expect(liveDirectVpc(withNetwork)).toBe(true);
|
|
1106
|
+
expect(liveDirectVpc(JSON.stringify({ spec: { template: { metadata: {} } } }))).toBe(false);
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
it("takes a service off the VPC in its roll, and leaves one that is off alone", () => {
|
|
1110
|
+
expect(planNoVpc(true)).toEqual({
|
|
1111
|
+
ok: true,
|
|
1112
|
+
flags: ["--clear-network"],
|
|
1113
|
+
changes: ["VPC egress (private ranges) → none"],
|
|
1114
|
+
});
|
|
1115
|
+
expect(planNoVpc(false)).toEqual({ ok: true, flags: [], changes: [] });
|
|
1116
|
+
});
|
|
1117
|
+
});
|