@caelo-cms/provisioning 0.11.12 → 0.11.14
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 +4 -22
- package/dist/admin-domain.d.ts.map +1 -1
- package/dist/admin-domain.js +4 -58
- package/dist/admin-domain.js.map +1 -1
- package/dist/agent-db.js +29 -5
- package/dist/audit-job-setup.d.ts.map +1 -1
- package/dist/audit-job-setup.js +14 -19
- package/dist/audit-job-setup.js.map +1 -1
- package/dist/caddy.d.ts +2 -1
- package/dist/caddy.d.ts.map +1 -1
- package/dist/caddy.js +21 -3
- package/dist/caddy.js.map +1 -1
- package/dist/cli.js +111 -62
- package/dist/cli.js.map +1 -1
- package/dist/compose.d.ts +22 -2
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +53 -3
- 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 +1 -0
- package/dist/converge-checkpoint.js.map +1 -1
- package/dist/deployed-release.d.ts.map +1 -1
- package/dist/deployed-release.js +40 -8
- package/dist/deployed-release.js.map +1 -1
- package/dist/gcp-names.d.ts +33 -14
- package/dist/gcp-names.d.ts.map +1 -1
- package/dist/gcp-names.js +43 -0
- package/dist/gcp-names.js.map +1 -1
- package/dist/gcp-url-map.d.ts +19 -4
- package/dist/gcp-url-map.d.ts.map +1 -1
- package/dist/gcp-url-map.js +33 -5
- package/dist/gcp-url-map.js.map +1 -1
- package/dist/infra-contract.d.ts +1 -1
- package/dist/infra-contract.d.ts.map +1 -1
- package/dist/infra-contract.js +22 -1
- package/dist/infra-contract.js.map +1 -1
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +5 -29
- package/dist/lifecycle.js.map +1 -1
- package/dist/mcp-auth-proxy-setup.d.ts +8 -2
- package/dist/mcp-auth-proxy-setup.d.ts.map +1 -1
- package/dist/mcp-auth-proxy-setup.js +4 -0
- package/dist/mcp-auth-proxy-setup.js.map +1 -1
- package/dist/release-manifest.d.ts +1 -1
- package/dist/release-manifest.d.ts.map +1 -1
- package/dist/release-manifest.js +2 -0
- package/dist/release-manifest.js.map +1 -1
- package/dist/remote-mcp.d.ts.map +1 -1
- package/dist/remote-mcp.js +11 -1
- package/dist/remote-mcp.js.map +1 -1
- package/dist/runtime-secrets.d.ts +10 -0
- package/dist/runtime-secrets.d.ts.map +1 -1
- package/dist/runtime-secrets.js +10 -1
- package/dist/runtime-secrets.js.map +1 -1
- package/dist/scheduler-setup.d.ts +5 -0
- package/dist/scheduler-setup.d.ts.map +1 -1
- package/dist/scheduler-setup.js +7 -0
- package/dist/scheduler-setup.js.map +1 -1
- package/dist/stack-contract.d.ts +61 -20
- package/dist/stack-contract.d.ts.map +1 -1
- package/dist/stack-contract.js +173 -35
- package/dist/stack-contract.js.map +1 -1
- package/dist/stack-converge.d.ts.map +1 -1
- package/dist/stack-converge.js +4 -2
- package/dist/stack-converge.js.map +1 -1
- package/dist/staging-admin.d.ts +2 -2
- package/dist/staging-admin.d.ts.map +1 -1
- package/dist/staging-gateway-setup.d.ts +140 -0
- package/dist/staging-gateway-setup.d.ts.map +1 -0
- package/dist/staging-gateway-setup.js +848 -0
- package/dist/staging-gateway-setup.js.map +1 -0
- package/dist/upgrade/app-roll.d.ts +4 -4
- package/dist/upgrade/app-roll.d.ts.map +1 -1
- package/dist/upgrade/app-roll.js +7 -5
- package/dist/upgrade/app-roll.js.map +1 -1
- package/dist/upgrade/converge.d.ts.map +1 -1
- package/dist/upgrade/converge.js +36 -27
- package/dist/upgrade/converge.js.map +1 -1
- package/dist/upgrade/image-forms.d.ts +3 -3
- package/dist/upgrade/image-forms.d.ts.map +1 -1
- package/dist/upgrade/image-forms.js +2 -2
- package/dist/upgrade/image-forms.js.map +1 -1
- package/dist/upgrade/release-images.d.ts +20 -0
- package/dist/upgrade/release-images.d.ts.map +1 -1
- package/dist/upgrade/release-images.js +28 -0
- package/dist/upgrade/release-images.js.map +1 -1
- package/dist/upgrade/upgrade.d.ts +3 -1
- package/dist/upgrade/upgrade.d.ts.map +1 -1
- package/dist/upgrade/upgrade.js +22 -1
- package/dist/upgrade/upgrade.js.map +1 -1
- package/dist/upgrader-job.d.ts.map +1 -1
- package/dist/upgrader-job.js +30 -5
- package/dist/upgrader-job.js.map +1 -1
- package/dist/upgrader-records.d.ts +2 -2
- package/dist/upgrader-records.d.ts.map +1 -1
- package/dist/upgrader-records.js +2 -2
- package/dist/upgrader-records.js.map +1 -1
- package/dist/wizard.d.ts.map +1 -1
- package/dist/wizard.js +3 -3
- package/dist/wizard.js.map +1 -1
- package/dist/wizards/gcp.d.ts.map +1 -1
- package/dist/wizards/gcp.js +48 -9
- package/dist/wizards/gcp.js.map +1 -1
- package/package.json +2 -2
- package/src/admin-domain.test.ts +0 -91
- package/src/admin-domain.ts +4 -64
- package/src/admin-memory.test.ts +2 -0
- package/src/audit-job-setup.test.ts +11 -11
- package/src/audit-job-setup.ts +14 -19
- package/src/caddy-redirects.docker.test.ts +10 -5
- package/src/caddy-redirects.test.ts +7 -5
- package/src/caddy-validate.test.ts +1 -1
- package/src/caddy.test.ts +12 -6
- package/src/caddy.ts +23 -4
- package/src/cli.ts +138 -77
- package/src/compose.ts +67 -2
- package/src/converge-checkpoint.ts +1 -0
- package/src/database-tls-builders.test.ts +3 -1
- package/src/deployed-release.test.ts +21 -0
- package/src/deployed-release.ts +42 -7
- package/src/gateway-env-isolation.test.ts +2 -0
- package/src/gcp-names.ts +53 -0
- package/src/gcp-url-map.test.ts +26 -2
- package/src/gcp-url-map.ts +36 -5
- package/src/index.test.ts +9 -3
- package/src/infra-contract.test.ts +5 -3
- package/src/infra-contract.ts +22 -1
- package/src/lifecycle.ts +6 -40
- package/src/mcp-auth-proxy-setup.test.ts +15 -6
- package/src/mcp-auth-proxy-setup.ts +14 -2
- package/src/release-manifest.ts +3 -1
- package/src/remote-mcp-stack-config.test.ts +1 -0
- package/src/remote-mcp.test.ts +5 -1
- package/src/remote-mcp.ts +10 -1
- package/src/runtime-secrets.test.ts +10 -1
- package/src/runtime-secrets.ts +21 -0
- package/src/scheduler-setup.test.ts +6 -0
- package/src/scheduler-setup.ts +7 -0
- package/src/self-hosted-backup.test.ts +6 -4
- package/src/self-hosted-compose.test.ts +2 -0
- package/src/stack-contract-parity.test.ts +7 -3
- package/src/stack-contract.test.ts +73 -13
- package/src/stack-contract.ts +195 -42
- package/src/stack-converge-secrets.test.ts +7 -2
- package/src/stack-converge.test.ts +37 -9
- package/src/stack-converge.ts +4 -1
- package/src/staging-admin.ts +2 -2
- package/src/staging-gateway-setup.test.ts +361 -0
- package/src/staging-gateway-setup.ts +1083 -0
- package/src/upgrade/app-roll.ts +7 -4
- package/src/upgrade/converge.ts +40 -26
- package/src/upgrade/image-forms.ts +7 -7
- package/src/upgrade/release-images.test.ts +28 -0
- package/src/upgrade/release-images.ts +44 -0
- package/src/upgrade/upgrade.test.ts +63 -11
- package/src/upgrade/upgrade.ts +29 -2
- package/src/upgrader-job.test.ts +23 -0
- package/src/upgrader-job.ts +45 -11
- package/src/upgrader-records.ts +7 -7
- package/src/wizard.ts +2 -3
- package/src/wizards/gcp.ts +73 -9
- package/stacks/gcp/index.ts +47 -6
- package/stacks/gcp-firebase/index.ts +44 -12
- package/stacks/self-hosted/index.ts +9 -2
package/src/stack-contract.ts
CHANGED
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
operatorAccessJobResource,
|
|
51
51
|
runServiceAccountEmail,
|
|
52
52
|
schedulerServiceAccountEmail,
|
|
53
|
+
stagingGatewayServiceAccountEmail,
|
|
53
54
|
staticPublisherServiceAccountId,
|
|
54
55
|
upgraderJobResource,
|
|
55
56
|
} from "./gcp-names.js";
|
|
@@ -57,15 +58,45 @@ import {
|
|
|
57
58
|
export type GcpProvider = "gcp" | "gcp-firebase";
|
|
58
59
|
/**
|
|
59
60
|
* The install's Cloud Run services, by role: the admin, the visitor data
|
|
60
|
-
* gateway
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
61
|
+
* gateway, (#1330) the staging gateway — the same image as the live one,
|
|
62
|
+
* gate on, serving the staged build and staging's own data
|
|
63
|
+
* (`cms_public_staging`) — and (#1328) the MCP-Auth-Proxy — remote MCP's
|
|
64
|
+
* sign-in broker and its forwarder to the IAP-protected admin, a service of
|
|
65
|
+
* its own so a hole in a visitor handler can never reach the operators'
|
|
66
|
+
* Google tokens. Which deployed service plays which role is
|
|
67
|
+
* {@link cloudRunServicePrefix}.
|
|
64
68
|
*/
|
|
65
|
-
export type CloudRunSlug = "admin" | "gateway" | "mcp-auth-proxy";
|
|
69
|
+
export type CloudRunSlug = "admin" | "gateway" | "staging-gateway" | "mcp-auth-proxy";
|
|
66
70
|
|
|
67
71
|
/** Every {@link CloudRunSlug}, in roll order: the admin first, the proxy last. */
|
|
68
|
-
export const CLOUD_RUN_SLUGS: readonly CloudRunSlug[] = [
|
|
72
|
+
export const CLOUD_RUN_SLUGS: readonly CloudRunSlug[] = [
|
|
73
|
+
"admin",
|
|
74
|
+
"gateway",
|
|
75
|
+
"staging-gateway",
|
|
76
|
+
"mcp-auth-proxy",
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
/** The release images the Cloud Run services run (one per image, not per service). */
|
|
80
|
+
export type ServiceImageName = "admin" | "gateway" | "mcp-auth-proxy";
|
|
81
|
+
|
|
82
|
+
/** Every {@link ServiceImageName}, in roll order. */
|
|
83
|
+
export const SERVICE_IMAGE_NAMES: readonly ServiceImageName[] = [
|
|
84
|
+
"admin",
|
|
85
|
+
"gateway",
|
|
86
|
+
"mcp-auth-proxy",
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The release image each service runs: the staging gateway runs the live
|
|
91
|
+
* gateway's image (#1330 — one image, its environment decides what it
|
|
92
|
+
* serves), every other service its own.
|
|
93
|
+
*/
|
|
94
|
+
export const SERVICE_IMAGE: Readonly<Record<CloudRunSlug, ServiceImageName>> = {
|
|
95
|
+
admin: "admin",
|
|
96
|
+
gateway: "gateway",
|
|
97
|
+
"staging-gateway": "gateway",
|
|
98
|
+
"mcp-auth-proxy": "mcp-auth-proxy",
|
|
99
|
+
};
|
|
69
100
|
|
|
70
101
|
/**
|
|
71
102
|
* The name prefix of the Cloud Run service that plays `slug` on an install
|
|
@@ -84,8 +115,13 @@ export const CLOUD_RUN_SLUGS: readonly CloudRunSlug[] = ["admin", "gateway", "mc
|
|
|
84
115
|
* the MCP address is the site's own host behind the load balancer, so no
|
|
85
116
|
* service name is pinned there and the proxy is the new service.
|
|
86
117
|
*
|
|
118
|
+
* #1330 — the staging gateway is `caelo-<env>-staging-gateway` on both
|
|
119
|
+
* providers, created by the CLI (staging-gateway-setup.ts) under that fixed
|
|
120
|
+
* name.
|
|
121
|
+
*
|
|
87
122
|
* No prefix is a prefix of another (`…-gateway` does not prefix
|
|
88
|
-
* `…-data-gateway`), so a `metadata.name~^<prefix>`
|
|
123
|
+
* `…-data-gateway` or `…-staging-gateway`), so a `metadata.name~^<prefix>`
|
|
124
|
+
* lookup finds one role.
|
|
89
125
|
*/
|
|
90
126
|
export function cloudRunServicePrefix(
|
|
91
127
|
provider: GcpProvider,
|
|
@@ -94,6 +130,7 @@ export function cloudRunServicePrefix(
|
|
|
94
130
|
): string {
|
|
95
131
|
const prefix = gcpNamePrefix(env);
|
|
96
132
|
if (slug === "admin") return `${prefix}-admin`;
|
|
133
|
+
if (slug === "staging-gateway") return `${prefix}-staging-gateway`;
|
|
97
134
|
if (provider === "gcp-firebase") {
|
|
98
135
|
return slug === "gateway" ? `${prefix}-data-gateway` : `${prefix}-gateway`;
|
|
99
136
|
}
|
|
@@ -102,7 +139,8 @@ export function cloudRunServicePrefix(
|
|
|
102
139
|
|
|
103
140
|
/**
|
|
104
141
|
* The exact name of a service that is created under a fixed name (#1328):
|
|
105
|
-
* the `gcp-firebase` data gateway
|
|
142
|
+
* the `gcp-firebase` data gateway, the `gcp` MCP-Auth-Proxy and (#1330) the
|
|
143
|
+
* staging gateway on both. Fixed, not
|
|
106
144
|
* Pulumi-suffixed, because the converge creates them on existing installs
|
|
107
145
|
* with gcloud, and the stack's next `pulumi up` must then find the same
|
|
108
146
|
* service instead of creating a second one. Null for the services Pulumi
|
|
@@ -114,6 +152,7 @@ export function fixedCloudRunServiceName(
|
|
|
114
152
|
slug: CloudRunSlug,
|
|
115
153
|
): string | null {
|
|
116
154
|
const fixed =
|
|
155
|
+
slug === "staging-gateway" ||
|
|
117
156
|
(provider === "gcp-firebase" && slug === "gateway") ||
|
|
118
157
|
(provider === "gcp" && slug === "mcp-auth-proxy");
|
|
119
158
|
return fixed ? cloudRunServicePrefix(provider, env, slug) : null;
|
|
@@ -253,6 +292,8 @@ export function cloudSqlPrivateDnsName(
|
|
|
253
292
|
export const DATABASE_URL_VARS = {
|
|
254
293
|
admin: ["ADMIN_DATABASE_URL", "PUBLIC_ADMIN_DATABASE_URL", "PUBLIC_STAGING_ADMIN_DATABASE_URL"],
|
|
255
294
|
gateway: ["PUBLIC_DATABASE_URL"],
|
|
295
|
+
// #1330 — public_staging_role on cms_public_staging, its only login.
|
|
296
|
+
"staging-gateway": ["PUBLIC_DATABASE_URL"],
|
|
256
297
|
// #1328 — no database login at all. On gcp-firebase the service still
|
|
257
298
|
// carries the gateway's URL until its roll removes it with the rest of
|
|
258
299
|
// the gateway env (RETIRED_SERVICE_ENV), so the TLS step leaves it alone.
|
|
@@ -276,6 +317,8 @@ export type AdminEnvInputs<V = string> =
|
|
|
276
317
|
| (RuntimeEnvInputs<V> & {
|
|
277
318
|
readonly provider: "gcp-firebase";
|
|
278
319
|
readonly firebaseSiteId: V;
|
|
320
|
+
/** #1330 — the second Hosting site that serves the staging host (gcp-names.ts firebaseStagingSiteId). */
|
|
321
|
+
readonly firebaseStagingSiteId: V;
|
|
279
322
|
readonly gatewayService: V;
|
|
280
323
|
});
|
|
281
324
|
|
|
@@ -296,6 +339,9 @@ export function defaultStagingHost(domain: string): string {
|
|
|
296
339
|
/** Env var the admin seeds its staging host setting from. */
|
|
297
340
|
export const STAGING_HOST_ENV = "CAELO_STAGING_HOST";
|
|
298
341
|
|
|
342
|
+
/** #1330 — env var naming the gcp-firebase staging Hosting site on the admin. */
|
|
343
|
+
export const FIREBASE_STAGING_SITE_ENV = "CAELO_FIREBASE_STAGING_SITE";
|
|
344
|
+
|
|
299
345
|
/**
|
|
300
346
|
* The admin env var naming the MCP service account (issue #37). `upgrade`
|
|
301
347
|
* leaves it untouched when it could not set that account up.
|
|
@@ -515,7 +561,10 @@ export const CLI_GENERATED_SECRETS = [
|
|
|
515
561
|
"public-staging-role-password",
|
|
516
562
|
// #650 — the staging access gate's key: signs "Open staging" links and
|
|
517
563
|
// session cookies, derives the crawler credentials. Read by the admin
|
|
518
|
-
//
|
|
564
|
+
// only; since #1330 the admin publishes it to the staging gateway through
|
|
565
|
+
// the staging projection (cms_public_staging), so no gateway reads it
|
|
566
|
+
// from Secret Manager (moved, never re-minted: the crawler credentials
|
|
567
|
+
// derived from it keep working).
|
|
519
568
|
"staging-gate-secret",
|
|
520
569
|
// #914 / #806 — the Google sign-in client starts as the `{}` placeholder
|
|
521
570
|
// (not configured; the Owner fills it under Settings → Google sign-in),
|
|
@@ -533,10 +582,7 @@ export const CLI_GENERATED_SECRETS = [
|
|
|
533
582
|
*/
|
|
534
583
|
export const SECRETS_SET_UP_AHEAD_OF_THEIR_READER: Readonly<
|
|
535
584
|
Partial<Record<(typeof CLI_GENERATED_SECRETS)[number], string>>
|
|
536
|
-
> = {
|
|
537
|
-
"public-staging-role-password":
|
|
538
|
-
"#1329 — public_staging_role's password. The stacks (gcp.sql.User) and the converge (staging-database.ts) set the role's password from it; the staging gateway service (#1330) is its first runtime reader.",
|
|
539
|
-
};
|
|
585
|
+
> = {};
|
|
540
586
|
|
|
541
587
|
/** A CLI-generated secret's first value: random hex, random base64, or a fixed placeholder. */
|
|
542
588
|
export type GeneratedSecretFormat =
|
|
@@ -607,10 +653,15 @@ export const SERVICE_SECRET_ENV: Readonly<
|
|
|
607
653
|
},
|
|
608
654
|
gateway: {
|
|
609
655
|
// CLAUDE.md §2: no cms_admin credential of any role (#613, #635).
|
|
610
|
-
// public_role on cms_public is the gateway's only login.
|
|
656
|
+
// public_role on cms_public is the gateway's only login. Its one
|
|
657
|
+
// credential (#763: each gateway holds exactly one).
|
|
611
658
|
PUBLIC_DATABASE_PASSWORD: "public-role-password",
|
|
612
|
-
|
|
613
|
-
|
|
659
|
+
},
|
|
660
|
+
// #1330 — public_staging_role on cms_public_staging: the staging
|
|
661
|
+
// gateway's one credential. The gate key comes through the staging
|
|
662
|
+
// projection, never from here; the two gateways share no secret.
|
|
663
|
+
"staging-gateway": {
|
|
664
|
+
PUBLIC_DATABASE_PASSWORD: "public-staging-role-password",
|
|
614
665
|
},
|
|
615
666
|
// #1328 — with the Google sign-in client file below, the proxy's whole
|
|
616
667
|
// Secret Manager reach: no database password, no other key.
|
|
@@ -668,6 +719,7 @@ export function secretFilePath(file: SecretFileContract): string {
|
|
|
668
719
|
export const SERVICE_SECRET_FILES: Readonly<Record<CloudRunSlug, readonly SecretFileContract[]>> = {
|
|
669
720
|
admin: [],
|
|
670
721
|
gateway: [],
|
|
722
|
+
"staging-gateway": [],
|
|
671
723
|
"mcp-auth-proxy": [OPERATOR_OAUTH_CLIENT_FILE],
|
|
672
724
|
};
|
|
673
725
|
|
|
@@ -706,6 +758,7 @@ export const RETIRED_SECRET_FILES: Readonly<Record<CloudRunSlug, readonly Retire
|
|
|
706
758
|
why: "the remote-MCP broker moved to the MCP-Auth-Proxy (#1328); the visitor gateway must not hold the Google sign-in client",
|
|
707
759
|
},
|
|
708
760
|
],
|
|
761
|
+
"staging-gateway": [],
|
|
709
762
|
"mcp-auth-proxy": [
|
|
710
763
|
{
|
|
711
764
|
// gcp-firebase: the proxy is the service that was the gateway, which
|
|
@@ -780,7 +833,13 @@ export const RETIRED_SERVICE_ENV: Readonly<Record<CloudRunSlug, readonly string[
|
|
|
780
833
|
"CAELO_MCP_OAUTH_SEAL_KEY_PREVIOUS",
|
|
781
834
|
"CAELO_MCP_PUBLIC_URL",
|
|
782
835
|
"CAELO_ADMIN_URL",
|
|
836
|
+
// #1330/#1331 — the staging gate moved to the staging gateway, which
|
|
837
|
+
// gets its key through the staging projection.
|
|
838
|
+
"CAELO_STAGING_GATE_SECRET",
|
|
783
839
|
],
|
|
840
|
+
// #1330 — a fresh service; nothing older to remove. Listed so the
|
|
841
|
+
// contract names every service.
|
|
842
|
+
"staging-gateway": [],
|
|
784
843
|
// #1328 — on gcp-firebase the proxy is the service that was the gateway:
|
|
785
844
|
// everything that service carried for visitor traffic goes, above all
|
|
786
845
|
// every database login and the staging gate key. The proxy's SA cannot
|
|
@@ -798,6 +857,9 @@ export const RETIRED_SERVICE_ENV: Readonly<Record<CloudRunSlug, readonly string[
|
|
|
798
857
|
"ADMIN_DATABASE_PASSWORD",
|
|
799
858
|
"GATEWAY_DATABASE_URL",
|
|
800
859
|
"GATEWAY_DATABASE_PASSWORD",
|
|
860
|
+
// #1331 — the interim forwarder to the data gateway is gone: the live
|
|
861
|
+
// rewrite names the data gateway, the staging host the staging gateway.
|
|
862
|
+
"CAELO_INTERIM_FORWARD_ORIGIN",
|
|
801
863
|
],
|
|
802
864
|
};
|
|
803
865
|
|
|
@@ -871,6 +933,48 @@ export const GATEWAY_TRUSTED_PROXY_HOPS: Readonly<Record<SupportedProvider, numb
|
|
|
871
933
|
azure: 1,
|
|
872
934
|
};
|
|
873
935
|
|
|
936
|
+
/**
|
|
937
|
+
* #1330 — {@link TRUSTED_PROXY_HOPS_ENV} of the staging gateway per
|
|
938
|
+
* provider: its own topology, so the staging gate's IP allowlist trusts
|
|
939
|
+
* exactly the entry its own front end appends and a forged
|
|
940
|
+
* `X-Forwarded-For` cannot pass it (stack-contract.test.ts pins the table).
|
|
941
|
+
*/
|
|
942
|
+
export const STAGING_GATEWAY_TRUSTED_PROXY_HOPS: Readonly<Record<SupportedProvider, number>> = {
|
|
943
|
+
// The staging host's own host rule on the external Application LB:
|
|
944
|
+
// `<client>,<LB IP>`; ingress is internal-and-LB only, so nothing reaches
|
|
945
|
+
// the service around the LB.
|
|
946
|
+
gcp: 2,
|
|
947
|
+
// The second Hosting site's `**` rewrite, then Cloud Run's front end,
|
|
948
|
+
// which appends a per-request Google proxy (#1325). The visitor entry
|
|
949
|
+
// Hosting writes before it cannot be told from a forged one (the service
|
|
950
|
+
// is invokable on *.run.app too), so the IP-allowlist mode is refused on
|
|
951
|
+
// gcp-firebase and the entry is used only to throttle failed attempts.
|
|
952
|
+
"gcp-firebase": 1,
|
|
953
|
+
// Caddy without `trusted_proxies` replaces the header with its peer; the
|
|
954
|
+
// staging gateway is reachable only on the Compose network.
|
|
955
|
+
"self-hosted": 1,
|
|
956
|
+
// Not deployed yet (#749): the live gateway's topology once it is.
|
|
957
|
+
aws: 2,
|
|
958
|
+
// Not deployed yet (#750).
|
|
959
|
+
azure: 1,
|
|
960
|
+
};
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* #1330 — where the staging gateway reads the staged build:
|
|
964
|
+
* `gs://<staging bucket>` on gcp / gcp-firebase, `file:///srv/staging` on
|
|
965
|
+
* self-hosted (the admin's `output/staging`, mounted read-only). The
|
|
966
|
+
* gateway reads only the build slots under it (`_slots/` on GCS).
|
|
967
|
+
*/
|
|
968
|
+
export const STAGING_STORAGE_ENV = "CAELO_STAGING_STORAGE";
|
|
969
|
+
|
|
970
|
+
/** {@link STAGING_STORAGE_ENV} of a GCP install. */
|
|
971
|
+
export function gcpStagingStorage(projectId: string, env: string): string {
|
|
972
|
+
return `gs://${gcpBucketName(projectId, env, "staging")}`;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/** #1330 — the staging gateway's request timeout: Firebase Hosting cuts a rewrite at 60 s (#1325). */
|
|
976
|
+
export const STAGING_GATEWAY_TIMEOUT_SECONDS = 60;
|
|
977
|
+
|
|
874
978
|
/**
|
|
875
979
|
* The env of the gateway service. Order is stable so the stacks see no
|
|
876
980
|
* spurious diffs.
|
|
@@ -887,6 +991,30 @@ export function gatewayEnvContract<V>(inputs: RuntimeEnvInputs<V>): CloudRunEnvV
|
|
|
887
991
|
];
|
|
888
992
|
}
|
|
889
993
|
|
|
994
|
+
/**
|
|
995
|
+
* #1330 — the env of the staging gateway: the live gateway's image with
|
|
996
|
+
* `CAELO_GATEWAY_ENVIRONMENT=staging`. Its one credential is
|
|
997
|
+
* `public_staging_role` on `cms_public_staging`; it reads the staged build
|
|
998
|
+
* from {@link STAGING_STORAGE_ENV}. No media bucket, no gate key (the
|
|
999
|
+
* admin publishes that to the staging database), nothing of the live data.
|
|
1000
|
+
*/
|
|
1001
|
+
export function stagingGatewayEnvContract<V>(
|
|
1002
|
+
inputs: RuntimeEnvInputs<V>,
|
|
1003
|
+
): CloudRunEnvVar<V | string>[] {
|
|
1004
|
+
return [
|
|
1005
|
+
{ name: "CAELO_PROVIDER", value: inputs.provider },
|
|
1006
|
+
{ name: "CAELO_ENV", value: inputs.env },
|
|
1007
|
+
{ name: GATEWAY_ENVIRONMENT_ENV, value: "staging" },
|
|
1008
|
+
{
|
|
1009
|
+
name: TRUSTED_PROXY_HOPS_ENV,
|
|
1010
|
+
value: String(STAGING_GATEWAY_TRUSTED_PROXY_HOPS[inputs.provider]),
|
|
1011
|
+
},
|
|
1012
|
+
{ name: "PUBLIC_DATABASE_URL", value: inputs.databaseUrls.publicStaging },
|
|
1013
|
+
{ name: STAGING_STORAGE_ENV, value: gcpStagingStorage(inputs.projectId, inputs.env) },
|
|
1014
|
+
...secretEnv("staging-gateway", inputs.env),
|
|
1015
|
+
];
|
|
1016
|
+
}
|
|
1017
|
+
|
|
890
1018
|
/**
|
|
891
1019
|
* #1328 — whether remote MCP can exist on a provider, as the MCP-Auth-Proxy
|
|
892
1020
|
* reads it. It needs the admin behind Google IAP, which only the Google
|
|
@@ -926,26 +1054,12 @@ export const MCP_AUTH_PROXY_TRUSTED_PROXY_HOPS: Readonly<Record<SupportedProvide
|
|
|
926
1054
|
azure: 1,
|
|
927
1055
|
};
|
|
928
1056
|
|
|
929
|
-
/**
|
|
930
|
-
* #1328 — points the `gcp-firebase` MCP-Auth-Proxy at the visitor data
|
|
931
|
-
* gateway for the traffic that still reaches the service that used to be
|
|
932
|
-
* the gateway: the Firebase `/api/**` rewrite of the release that is live
|
|
933
|
-
* until the next Publish live, and the staging host's Cloud Run domain
|
|
934
|
-
* mapping (#650), which only the domain's verified owner could move. The
|
|
935
|
-
* proxy streams that traffic to the data gateway unchanged instead of
|
|
936
|
-
* answering it (apps/mcp-auth-proxy interim-forward.ts). gcp-firebase only.
|
|
937
|
-
*/
|
|
938
|
-
// FIXME(issue #1331): goes with the staging cutover, once neither a mapping
|
|
939
|
-
// nor a published rewrite can point at the proxy any more.
|
|
940
|
-
export const INTERIM_FORWARD_ORIGIN_ENV = "CAELO_INTERIM_FORWARD_ORIGIN";
|
|
941
|
-
|
|
942
1057
|
/**
|
|
943
1058
|
* The env of the MCP-Auth-Proxy. No database URL, no pool size, no site
|
|
944
1059
|
* settings: the broker reads env plus one secret file and nothing else
|
|
945
1060
|
* (apps/mcp-auth-proxy, whose import lint keeps every database client out).
|
|
946
1061
|
* The remote-MCP identity env — its public origin, the admin's URL — comes
|
|
947
|
-
* on top from {@link remoteMcpIdentityEnv}
|
|
948
|
-
* from the stack or the converge ({@link INTERIM_FORWARD_ORIGIN_ENV}).
|
|
1062
|
+
* on top from {@link remoteMcpIdentityEnv}.
|
|
949
1063
|
*/
|
|
950
1064
|
export function mcpAuthProxyEnvContract(inputs: {
|
|
951
1065
|
readonly provider: SupportedProvider;
|
|
@@ -1006,6 +1120,7 @@ export const SERVICE_CPU_IDLE: Readonly<Record<CloudRunSlug, boolean>> = {
|
|
|
1006
1120
|
// between requests (the category-A jobs, the heartbeat at request start).
|
|
1007
1121
|
admin: false,
|
|
1008
1122
|
gateway: true,
|
|
1123
|
+
"staging-gateway": true,
|
|
1009
1124
|
"mcp-auth-proxy": true,
|
|
1010
1125
|
};
|
|
1011
1126
|
|
|
@@ -1018,6 +1133,8 @@ export const SERVICE_CPU_IDLE: Readonly<Record<CloudRunSlug, boolean>> = {
|
|
|
1018
1133
|
export const SERVICE_VPC_EGRESS: Readonly<Record<CloudRunSlug, boolean>> = {
|
|
1019
1134
|
admin: true,
|
|
1020
1135
|
gateway: true,
|
|
1136
|
+
// #1330 — reaches the staging database on Cloud SQL's private IP.
|
|
1137
|
+
"staging-gateway": true,
|
|
1021
1138
|
"mcp-auth-proxy": false,
|
|
1022
1139
|
};
|
|
1023
1140
|
|
|
@@ -1158,9 +1275,10 @@ export function remoteMcpIdentityEnv(
|
|
|
1158
1275
|
? [{ name: REMOTE_MCP_ENV.googleOrgDomain, value: id.googleOrgDomain }]
|
|
1159
1276
|
: []),
|
|
1160
1277
|
],
|
|
1161
|
-
// #1328 — the broker lives in the MCP-Auth-Proxy; the visitor
|
|
1162
|
-
//
|
|
1278
|
+
// #1328 — the broker lives in the MCP-Auth-Proxy; the visitor gateways
|
|
1279
|
+
// get none of it.
|
|
1163
1280
|
gateway: [],
|
|
1281
|
+
"staging-gateway": [],
|
|
1164
1282
|
"mcp-auth-proxy": [
|
|
1165
1283
|
{ name: REMOTE_MCP_ENV.adminUrl, value: id.adminUrl },
|
|
1166
1284
|
{ name: REMOTE_MCP_ENV.mcpPublicUrl, value: id.mcpPublicUrl },
|
|
@@ -1228,7 +1346,9 @@ export function remoteMcpIdentityStackEnv(
|
|
|
1228
1346
|
raw: string | undefined,
|
|
1229
1347
|
): Record<CloudRunSlug, CloudRunEnvVar[]> {
|
|
1230
1348
|
const id = parseRemoteMcpIdentityConfig(raw);
|
|
1231
|
-
return id
|
|
1349
|
+
return id
|
|
1350
|
+
? remoteMcpIdentityEnv(id)
|
|
1351
|
+
: { admin: [], gateway: [], "staging-gateway": [], "mcp-auth-proxy": [] };
|
|
1232
1352
|
}
|
|
1233
1353
|
|
|
1234
1354
|
/** Whether two identities would give the services the same env. */
|
|
@@ -1326,26 +1446,31 @@ export function adminEnvContract<V>(inputs: AdminEnvInputs<V>): CloudRunEnvVar<V
|
|
|
1326
1446
|
value: operatorOAuthClientSecretResource(projectId, env),
|
|
1327
1447
|
},
|
|
1328
1448
|
];
|
|
1449
|
+
// #1330 — the staging bucket: the full staged build in two slots that the
|
|
1450
|
+
// staging gateway serves from (gcp: Stage also keeps its per-run prefixes
|
|
1451
|
+
// there for Publish live and rollback).
|
|
1452
|
+
admin.push({ name: "CAELO_STAGING_BUCKET", value: gcpBucketName(projectId, env, "staging") });
|
|
1329
1453
|
if (inputs.provider === "gcp") {
|
|
1330
1454
|
// v0.2.78 — the GCS StaticPublisher: Stage uploads to staging,
|
|
1331
1455
|
// Confirm-publish copies to static.
|
|
1332
|
-
admin.push(
|
|
1333
|
-
{ name: "CAELO_STATIC_BUCKET", value: gcpBucketName(projectId, env, "static") },
|
|
1334
|
-
{ name: "CAELO_STAGING_BUCKET", value: gcpBucketName(projectId, env, "staging") },
|
|
1335
|
-
);
|
|
1456
|
+
admin.push({ name: "CAELO_STATIC_BUCKET", value: gcpBucketName(projectId, env, "static") });
|
|
1336
1457
|
} else {
|
|
1337
1458
|
// v0.3.1 — the Firebase publisher deploys to this site and declares
|
|
1338
1459
|
// the /api/** rewrite to the gateway service.
|
|
1339
1460
|
admin.push(
|
|
1340
1461
|
{ name: "CAELO_FIREBASE_SITE", value: inputs.firebaseSiteId },
|
|
1462
|
+
// #1330 — the staging host's Hosting site: Settings → Updates reads
|
|
1463
|
+
// the DNS records of its custom domain.
|
|
1464
|
+
{ name: FIREBASE_STAGING_SITE_ENV, value: inputs.firebaseStagingSiteId },
|
|
1341
1465
|
{ name: "CAELO_GATEWAY_SERVICE", value: inputs.gatewayService },
|
|
1342
1466
|
{ name: "CAELO_GATEWAY_REGION", value: inputs.region },
|
|
1343
|
-
// #650 — seeds the admin's staging host setting (staging builds render
|
|
1344
|
-
// it, the gateway's gate answers on it); the admin DB is the truth
|
|
1345
|
-
// afterwards, changed under Settings → Domains.
|
|
1346
|
-
{ name: STAGING_HOST_ENV, value: inputs.stagingHost },
|
|
1347
1467
|
);
|
|
1348
1468
|
}
|
|
1469
|
+
// #650 — seeds the admin's staging host setting (staging pages carry it,
|
|
1470
|
+
// the staging gateway's gate answers on it); the admin DB is the truth
|
|
1471
|
+
// afterwards, changed under Settings → Domains. Every provider since
|
|
1472
|
+
// #1330: each serves staging at its own host.
|
|
1473
|
+
admin.push({ name: STAGING_HOST_ENV, value: inputs.stagingHost });
|
|
1349
1474
|
admin.push(...secretEnv("admin", env));
|
|
1350
1475
|
return admin;
|
|
1351
1476
|
}
|
|
@@ -1378,6 +1503,8 @@ export type IamPrincipal =
|
|
|
1378
1503
|
| "gateway-sa"
|
|
1379
1504
|
/** #1328 — the MCP-Auth-Proxy's SA: two secrets, nothing else. */
|
|
1380
1505
|
| "mcp-auth-proxy-sa"
|
|
1506
|
+
/** #1330 — the staging gateway's SA: its login password and the staging bucket. */
|
|
1507
|
+
| "staging-gateway-sa"
|
|
1381
1508
|
/** The `gcp` stack's static-publisher SA. */
|
|
1382
1509
|
| "static-publisher-sa"
|
|
1383
1510
|
/** IAP's Google-managed service agent (forwards IAP traffic to Cloud Run). */
|
|
@@ -1442,6 +1569,7 @@ export function runtimeSecretBindings(): RuntimeSecretBinding[] {
|
|
|
1442
1569
|
export const SERVICE_PRINCIPAL: Readonly<Record<CloudRunSlug, IamPrincipal>> = {
|
|
1443
1570
|
admin: "run-sa",
|
|
1444
1571
|
gateway: "gateway-sa",
|
|
1572
|
+
"staging-gateway": "staging-gateway-sa",
|
|
1445
1573
|
"mcp-auth-proxy": "mcp-auth-proxy-sa",
|
|
1446
1574
|
};
|
|
1447
1575
|
|
|
@@ -1508,6 +1636,22 @@ const TELEMETRY: readonly IamInvariant[] = [
|
|
|
1508
1636
|
onFailure: "warn",
|
|
1509
1637
|
why: "gateway CPU/memory metrics reach Cloud Monitoring (autoscaling signals)",
|
|
1510
1638
|
},
|
|
1639
|
+
{
|
|
1640
|
+
stackResource: "staging-gateway-log-writer",
|
|
1641
|
+
role: "roles/logging.logWriter",
|
|
1642
|
+
member: "staging-gateway-sa",
|
|
1643
|
+
target: { kind: "project" },
|
|
1644
|
+
onFailure: "warn",
|
|
1645
|
+
why: "staging gateway container logs reach Cloud Logging",
|
|
1646
|
+
},
|
|
1647
|
+
{
|
|
1648
|
+
stackResource: "staging-gateway-metric-writer",
|
|
1649
|
+
role: "roles/monitoring.metricWriter",
|
|
1650
|
+
member: "staging-gateway-sa",
|
|
1651
|
+
target: { kind: "project" },
|
|
1652
|
+
onFailure: "warn",
|
|
1653
|
+
why: "staging gateway CPU/memory metrics reach Cloud Monitoring (autoscaling signals)",
|
|
1654
|
+
},
|
|
1511
1655
|
];
|
|
1512
1656
|
|
|
1513
1657
|
const GCP_ONLY: readonly IamInvariant[] = [
|
|
@@ -1657,6 +1801,13 @@ export const RETIRED_IAM_BINDINGS: readonly RetiredIamBinding[] = [
|
|
|
1657
1801
|
target: { kind: "secret", name: "operator-oauth-client" },
|
|
1658
1802
|
why: "the remote-MCP broker moved to the MCP-Auth-Proxy (#1328); the visitor gateway must not read the Google sign-in client's secret",
|
|
1659
1803
|
},
|
|
1804
|
+
{
|
|
1805
|
+
// #1330/#1331 — the gate key moved to the staging gateway's projection.
|
|
1806
|
+
role: SECRET_ACCESSOR,
|
|
1807
|
+
member: "gateway-sa",
|
|
1808
|
+
target: { kind: "secret", name: "staging-gate-secret" },
|
|
1809
|
+
why: "the staging gate moved to the staging gateway, which gets its key through the staging projection (#1330); the live gateway must not hold it",
|
|
1810
|
+
},
|
|
1660
1811
|
{
|
|
1661
1812
|
// gcp-firebase: the Firebase rewrite targets the data gateway since
|
|
1662
1813
|
// #1328, so the publisher no longer needs to read the proxy.
|
|
@@ -1713,6 +1864,8 @@ export function iamMember(
|
|
|
1713
1864
|
return `serviceAccount:${gatewayServiceAccountEmail(install.projectId, install.env)}`;
|
|
1714
1865
|
case "mcp-auth-proxy-sa":
|
|
1715
1866
|
return `serviceAccount:${mcpAuthProxyServiceAccountEmail(install.projectId, install.env)}`;
|
|
1867
|
+
case "staging-gateway-sa":
|
|
1868
|
+
return `serviceAccount:${stagingGatewayServiceAccountEmail(install.projectId, install.env)}`;
|
|
1716
1869
|
case "static-publisher-sa":
|
|
1717
1870
|
return `serviceAccount:${staticPublisherServiceAccountId(install.env)}@${install.projectId}.iam.gserviceaccount.com`;
|
|
1718
1871
|
case "iap-service-agent":
|
|
@@ -203,12 +203,14 @@ const INSTALL = {
|
|
|
203
203
|
};
|
|
204
204
|
/** The fixtures' services; the proxy is added by {@link upgrade} when absent. */
|
|
205
205
|
type InstallServices = Record<"admin" | "gateway", FakeService> & {
|
|
206
|
+
"staging-gateway"?: FakeService;
|
|
206
207
|
"mcp-auth-proxy"?: FakeService;
|
|
207
208
|
};
|
|
208
209
|
|
|
209
210
|
const SERVICE_ACCOUNTS: Record<CloudRunSlug, string> = {
|
|
210
211
|
admin: "caelo-production-run-sa@acme.iam.gserviceaccount.com",
|
|
211
212
|
gateway: "caelo-production-gateway-sa@acme.iam.gserviceaccount.com",
|
|
213
|
+
"staging-gateway": "caelo-production-stg-gw-sa@acme.iam.gserviceaccount.com",
|
|
212
214
|
"mcp-auth-proxy": "caelo-production-mcp-proxy-sa@acme.iam.gserviceaccount.com",
|
|
213
215
|
};
|
|
214
216
|
|
|
@@ -217,9 +219,12 @@ function upgrade(services: InstallServices) {
|
|
|
217
219
|
// #1328 — the gcp MCP-Auth-Proxy: created by the converge before the plan,
|
|
218
220
|
// with no env yet in these fixtures (they are about database logins).
|
|
219
221
|
services["mcp-auth-proxy"] ??= new FakeService(new Map(), SERVICE_ACCOUNTS["mcp-auth-proxy"]);
|
|
222
|
+
// #1330 — likewise the staging gateway.
|
|
223
|
+
services["staging-gateway"] ??= new FakeService(new Map(), SERVICE_ACCOUNTS["staging-gateway"]);
|
|
220
224
|
const plan = planContractEnv(INSTALL, {
|
|
221
225
|
admin: { serviceName: "adm", liveEnv: services.admin.env },
|
|
222
226
|
gateway: { serviceName: "gw", liveEnv: services.gateway.env },
|
|
227
|
+
"staging-gateway": { serviceName: "stg", liveEnv: services["staging-gateway"].env },
|
|
223
228
|
"mcp-auth-proxy": { serviceName: "proxy", liveEnv: services["mcp-auth-proxy"].env },
|
|
224
229
|
});
|
|
225
230
|
if (!plan.ok) throw new Error(plan.error);
|
|
@@ -363,8 +368,8 @@ describe("upgrading a v0.10.35 install: the gateway leaves admin_role (#613)", (
|
|
|
363
368
|
// #1329 — the gateway says it is the live site's.
|
|
364
369
|
"CAELO_GATEWAY_ENVIRONMENT",
|
|
365
370
|
// No remote-MCP env: the broker is the MCP-Auth-Proxy's (#1328).
|
|
366
|
-
//
|
|
367
|
-
|
|
371
|
+
// No staging gate key: it reaches the staging gateway through the
|
|
372
|
+
// staging projection (#1330), never the live gateway.
|
|
368
373
|
// #845 — and which forwarded address is the visitor.
|
|
369
374
|
"CAELO_TRUSTED_PROXY_HOPS",
|
|
370
375
|
"PUBLIC_DATABASE_PASSWORD",
|
|
@@ -59,9 +59,11 @@ const policy = (bindings: { role: string; members: string[]; condition?: unknown
|
|
|
59
59
|
const RUN_SA = "serviceAccount:caelo-production-run-sa@acme.iam.gserviceaccount.com";
|
|
60
60
|
const GW_SA = "serviceAccount:caelo-production-gateway-sa@acme.iam.gserviceaccount.com";
|
|
61
61
|
const PROXY_SA = "serviceAccount:caelo-production-mcp-proxy-sa@acme.iam.gserviceaccount.com";
|
|
62
|
+
/** #1330 — the staging gateway's run SA. */
|
|
63
|
+
const STG_SA = "serviceAccount:caelo-production-stg-gw-sa@acme.iam.gserviceaccount.com";
|
|
62
64
|
const GW_TELEMETRY = [
|
|
63
|
-
{ role: "roles/logging.logWriter", members: [GW_SA] },
|
|
64
|
-
{ role: "roles/monitoring.metricWriter", members: [GW_SA] },
|
|
65
|
+
{ role: "roles/logging.logWriter", members: [GW_SA, STG_SA] },
|
|
66
|
+
{ role: "roles/monitoring.metricWriter", members: [GW_SA, STG_SA] },
|
|
65
67
|
];
|
|
66
68
|
const IAP_AGENT = "serviceAccount:service-42@gcp-sa-iap.iam.gserviceaccount.com";
|
|
67
69
|
|
|
@@ -73,6 +75,7 @@ const firebase: InstallTarget = {
|
|
|
73
75
|
services: {
|
|
74
76
|
admin: "caelo-production-admin-aaa",
|
|
75
77
|
gateway: "caelo-production-data-gateway",
|
|
78
|
+
"staging-gateway": "caelo-production-staging-gateway",
|
|
76
79
|
"mcp-auth-proxy": "caelo-production-gateway-bbb",
|
|
77
80
|
},
|
|
78
81
|
};
|
|
@@ -328,6 +331,7 @@ describe("planContractEnv", () => {
|
|
|
328
331
|
// #1328 — the data gateway, a copy of the old gateway service.
|
|
329
332
|
gateway: { serviceName: "caelo-production-data-gateway", liveEnv: gatewayLive },
|
|
330
333
|
// The old gateway service, which becomes the MCP-Auth-Proxy (E13 a).
|
|
334
|
+
"staging-gateway": { serviceName: "caelo-production-staging-gateway", liveEnv: new Map() },
|
|
331
335
|
"mcp-auth-proxy": { serviceName: "caelo-production-gateway-bbb", liveEnv: gatewayLive },
|
|
332
336
|
},
|
|
333
337
|
);
|
|
@@ -336,7 +340,7 @@ describe("planContractEnv", () => {
|
|
|
336
340
|
// and gate key onto an install that predates it.
|
|
337
341
|
expect(plan.services.admin.flags).toEqual([
|
|
338
342
|
// #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",
|
|
343
|
+
"--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_STAGING_BUCKET=acme-caelo-production-staging,CAELO_FIREBASE_STAGING_SITE=caelo-production-stg-6eace1,CAELO_GATEWAY_SERVICE=caelo-production-data-gateway,CAELO_STAGING_HOST=staging.acme.com",
|
|
340
344
|
"--update-secrets=PUBLIC_STAGING_ADMIN_DATABASE_PASSWORD=caelo-production-postgres-password:latest,CAELO_STAGING_GATE_SECRET=caelo-production-staging-gate-secret:latest",
|
|
341
345
|
]);
|
|
342
346
|
// #1328 — the Firebase rewrite follows the data gateway (above), which
|
|
@@ -345,7 +349,6 @@ describe("planContractEnv", () => {
|
|
|
345
349
|
// The gateway learns which forwarded address is the visitor (#845),
|
|
346
350
|
// and (#1329) that it is the live site's.
|
|
347
351
|
"--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
352
|
]);
|
|
350
353
|
// The old gateway service becomes the proxy in one revision: every
|
|
351
354
|
// database and site var goes, its own contract arrives.
|
|
@@ -379,6 +382,7 @@ describe("planContractEnv", () => {
|
|
|
379
382
|
serviceName: "caelo-production-data-gateway",
|
|
380
383
|
liveEnv: liveContainerEnv(serviceJson(CONVERGED_GATEWAY_DB)),
|
|
381
384
|
},
|
|
385
|
+
"staging-gateway": { serviceName: "caelo-production-staging-gateway", liveEnv: new Map() },
|
|
382
386
|
"mcp-auth-proxy": { serviceName: "caelo-production-gateway-bbb", liveEnv: new Map() },
|
|
383
387
|
},
|
|
384
388
|
{ leaveUntouched: ["CAELO_MCP_IAP_SERVICE_ACCOUNT", "CAELO_OPERATOR_ACCESS_JOB"] },
|
|
@@ -400,6 +404,7 @@ describe("planContractEnv", () => {
|
|
|
400
404
|
{
|
|
401
405
|
admin: { serviceName: "adm", liveEnv: liveContainerEnv(serviceJson(CONVERGED_ADMIN_DB)) },
|
|
402
406
|
gateway: { serviceName: "gw", liveEnv: new Map() },
|
|
407
|
+
"staging-gateway": { serviceName: "caelo-production-staging-gateway", liveEnv: new Map() },
|
|
403
408
|
"mcp-auth-proxy": { serviceName: "px", liveEnv: new Map() },
|
|
404
409
|
},
|
|
405
410
|
);
|
|
@@ -429,6 +434,7 @@ describe("planContractEnv", () => {
|
|
|
429
434
|
liveEnv: liveContainerEnv(serviceJson(onIp(CONVERGED_GATEWAY_DB) as never)),
|
|
430
435
|
},
|
|
431
436
|
// #1328 — no database URL, so the TLS step has nothing to move on it.
|
|
437
|
+
"staging-gateway": { serviceName: "caelo-production-staging-gateway", liveEnv: new Map() },
|
|
432
438
|
"mcp-auth-proxy": { serviceName: "px", liveEnv: new Map() },
|
|
433
439
|
};
|
|
434
440
|
const install = {
|
|
@@ -465,6 +471,7 @@ describe("planContractEnv", () => {
|
|
|
465
471
|
{
|
|
466
472
|
admin: { serviceName: "adm", liveEnv: new Map() },
|
|
467
473
|
gateway: { serviceName: "gw", liveEnv: new Map() },
|
|
474
|
+
"staging-gateway": { serviceName: "caelo-production-staging-gateway", liveEnv: new Map() },
|
|
468
475
|
"mcp-auth-proxy": { serviceName: "px", liveEnv: new Map() },
|
|
469
476
|
},
|
|
470
477
|
);
|
|
@@ -565,7 +572,7 @@ describe("ensureStackInvariants", () => {
|
|
|
565
572
|
],
|
|
566
573
|
"secrets get-iam-policy": [
|
|
567
574
|
policy([
|
|
568
|
-
{ role: "roles/secretmanager.secretAccessor", members: [RUN_SA, GW_SA, PROXY_SA] },
|
|
575
|
+
{ role: "roles/secretmanager.secretAccessor", members: [RUN_SA, GW_SA, STG_SA, PROXY_SA] },
|
|
569
576
|
// #806 — the admin adds remote-MCP OAuth client versions.
|
|
570
577
|
{ role: "roles/secretmanager.secretVersionAdder", members: [RUN_SA] },
|
|
571
578
|
]),
|
|
@@ -825,7 +832,10 @@ describe("ensureStackInvariants", () => {
|
|
|
825
832
|
],
|
|
826
833
|
"secrets get-iam-policy": [
|
|
827
834
|
policy([
|
|
828
|
-
{
|
|
835
|
+
{
|
|
836
|
+
role: "roles/secretmanager.secretAccessor",
|
|
837
|
+
members: [RUN_SA, GW_SA, STG_SA, PROXY_SA],
|
|
838
|
+
},
|
|
829
839
|
{ role: "roles/secretmanager.secretVersionAdder", members: [RUN_SA] },
|
|
830
840
|
]),
|
|
831
841
|
],
|
|
@@ -952,11 +962,10 @@ describe("ensureStackInvariants", () => {
|
|
|
952
962
|
(c) => c.startsWith("secrets add-iam-policy-binding") && c.includes(GW_SA),
|
|
953
963
|
);
|
|
954
964
|
// #613 — never admin_role's postgres-password; #635 — nor
|
|
955
|
-
// gateway_role's
|
|
956
|
-
//
|
|
965
|
+
// gateway_role's; #1330 — nor the staging gate key: its own login
|
|
966
|
+
// password only.
|
|
957
967
|
expect(gatewayGrants).toEqual([
|
|
958
968
|
`secrets add-iam-policy-binding caelo-production-public-role-password --project=acme --member=${GW_SA} --role=roles/secretmanager.secretAccessor --quiet --format=none`,
|
|
959
|
-
`secrets add-iam-policy-binding caelo-production-staging-gate-secret --project=acme --member=${GW_SA} --role=roles/secretmanager.secretAccessor --quiet --format=none`,
|
|
960
969
|
]);
|
|
961
970
|
// #1328 — the remote-MCP broker's seal key and OAuth client file are
|
|
962
971
|
// the MCP-Auth-Proxy's, and those two secrets are all its SA reads.
|
|
@@ -1001,6 +1010,7 @@ describe("removeRetiredIamBindings (#613)", () => {
|
|
|
1001
1010
|
"absent",
|
|
1002
1011
|
"absent",
|
|
1003
1012
|
"absent",
|
|
1013
|
+
"absent",
|
|
1004
1014
|
]);
|
|
1005
1015
|
expect(calls).toContain(REMOVE);
|
|
1006
1016
|
// The admin's own binding on the same secret is never touched.
|
|
@@ -1020,6 +1030,7 @@ describe("removeRetiredIamBindings (#613)", () => {
|
|
|
1020
1030
|
"absent",
|
|
1021
1031
|
"absent",
|
|
1022
1032
|
"absent",
|
|
1033
|
+
"absent",
|
|
1023
1034
|
]);
|
|
1024
1035
|
expect(calls.filter((c) => c.includes("remove-iam-policy-binding"))).toEqual([]);
|
|
1025
1036
|
});
|
|
@@ -1037,6 +1048,7 @@ describe("removeRetiredIamBindings (#613)", () => {
|
|
|
1037
1048
|
"absent",
|
|
1038
1049
|
"absent",
|
|
1039
1050
|
"absent",
|
|
1051
|
+
"absent",
|
|
1040
1052
|
]);
|
|
1041
1053
|
expect(calls).toContain(
|
|
1042
1054
|
`secrets remove-iam-policy-binding caelo-production-gateway-role-password --project=acme --member=${GW_SA} --role=roles/secretmanager.secretAccessor --quiet --format=none`,
|
|
@@ -1064,6 +1076,7 @@ describe("removeRetiredIamBindings (#613)", () => {
|
|
|
1064
1076
|
"absent",
|
|
1065
1077
|
"removed",
|
|
1066
1078
|
"removed",
|
|
1079
|
+
"absent",
|
|
1067
1080
|
"removed",
|
|
1068
1081
|
]);
|
|
1069
1082
|
for (const secret of ["mcp-oauth-seal-key", "operator-oauth-client"]) {
|
|
@@ -1076,6 +1089,21 @@ describe("removeRetiredIamBindings (#613)", () => {
|
|
|
1076
1089
|
expect(calls.some((c) => c.includes("remove") && c.includes("allUsers"))).toBe(false);
|
|
1077
1090
|
});
|
|
1078
1091
|
|
|
1092
|
+
it("#1330: takes the live gateway SA off the staging gate key", async () => {
|
|
1093
|
+
const { run, calls } = fakeGcloud({
|
|
1094
|
+
"secrets get-iam-policy caelo-production-staging-gate-secret": [
|
|
1095
|
+
policy([{ role: "roles/secretmanager.secretAccessor", members: [RUN_SA, GW_SA] }]),
|
|
1096
|
+
],
|
|
1097
|
+
});
|
|
1098
|
+
const outcomes = await removeRetiredIamBindings(firebase, { run, sleep: async () => {} });
|
|
1099
|
+
expect(outcomes.map((o) => o.status)[4]).toBe("removed");
|
|
1100
|
+
expect(calls).toContain(
|
|
1101
|
+
`secrets remove-iam-policy-binding caelo-production-staging-gate-secret --project=acme --member=${GW_SA} --role=roles/secretmanager.secretAccessor --quiet --format=none`,
|
|
1102
|
+
);
|
|
1103
|
+
// The admin keeps it: it signs the open links and publishes the key.
|
|
1104
|
+
expect(calls.some((c) => c.includes("remove") && c.includes(RUN_SA))).toBe(false);
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1079
1107
|
it("reports a failed removal instead of throwing", async () => {
|
|
1080
1108
|
const { run } = fakeGcloud({
|
|
1081
1109
|
"secrets get-iam-policy caelo-production-postgres-password": [
|