@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/lifecycle.ts
CHANGED
|
@@ -69,12 +69,7 @@ import {
|
|
|
69
69
|
resolveBackend,
|
|
70
70
|
} from "./state-backend.js";
|
|
71
71
|
import { acquireStateLock, realSyncGcloud, releaseOnSignal } from "./state-store.js";
|
|
72
|
-
import {
|
|
73
|
-
type AppRollPlan,
|
|
74
|
-
type ReleaseImage,
|
|
75
|
-
ROLL_ORDER,
|
|
76
|
-
type RollSlug,
|
|
77
|
-
} from "./upgrade/app-roll.js";
|
|
72
|
+
import type { AppRollPlan, RollSlug } from "./upgrade/app-roll.js";
|
|
78
73
|
import { restoreCommand, takeSqlBackup } from "./upgrade/backup.js";
|
|
79
74
|
import { findServiceUrl, lookupFailure, resolveGcpResourceName } from "./upgrade/gcp-lookup.js";
|
|
80
75
|
import { createIapHealthProbe, type HealthProbe } from "./upgrade/health.js";
|
|
@@ -84,12 +79,10 @@ import {
|
|
|
84
79
|
canonicalReleaseTag,
|
|
85
80
|
ensureCosignAvailable,
|
|
86
81
|
fetchLatestReleaseTag,
|
|
87
|
-
RELEASE_REGISTRY,
|
|
88
|
-
releaseImageRef,
|
|
89
82
|
releaseTag,
|
|
90
83
|
releaseVersionForDigest,
|
|
91
84
|
resolveOpsImageRef,
|
|
92
|
-
|
|
85
|
+
resolveReleaseImages,
|
|
93
86
|
verifyCosignAll,
|
|
94
87
|
} from "./upgrade/release-images.js";
|
|
95
88
|
import { clackReporter } from "./upgrade/reporter.js";
|
|
@@ -463,32 +456,6 @@ export async function upgradeCommand(opts: UpgradeOpts = {}): Promise<void> {
|
|
|
463
456
|
);
|
|
464
457
|
}
|
|
465
458
|
|
|
466
|
-
/**
|
|
467
|
-
* Resolve both release images' digests from the public registry. Via the
|
|
468
|
-
* registry's anonymous V2 API, not `gcloud artifacts docker tags list`: that
|
|
469
|
-
* needs IAM read on Caelo's registry project, which operators don't have.
|
|
470
|
-
*/
|
|
471
|
-
async function resolveReleaseImages(
|
|
472
|
-
tag: string,
|
|
473
|
-
): Promise<{ ok: true; images: Record<RollSlug, ReleaseImage> } | { ok: false; error: string }> {
|
|
474
|
-
const images: Partial<Record<RollSlug, ReleaseImage>> = {};
|
|
475
|
-
for (const slug of ROLL_ORDER) {
|
|
476
|
-
const res = await resolveTagDigest(slug, tag);
|
|
477
|
-
if (!res.ok) {
|
|
478
|
-
const r = RELEASE_REGISTRY;
|
|
479
|
-
return {
|
|
480
|
-
ok: false,
|
|
481
|
-
error:
|
|
482
|
-
`Couldn't resolve image digest for ${slug}:${tag}: the public registry returned ${res.reason}.\n` +
|
|
483
|
-
`Verify the tag exists at https://${r.region}-docker.pkg.dev/${r.project}/${r.repo}/${slug}:${tag}\n` +
|
|
484
|
-
"(latest releases live at https://github.com/caelo-cms/caelo-cms/releases — pass --version vX.Y.Z to pin.)",
|
|
485
|
-
};
|
|
486
|
-
}
|
|
487
|
-
images[slug] = { digest: res.digest, imageRef: releaseImageRef(slug, res.digest) };
|
|
488
|
-
}
|
|
489
|
-
return { ok: true, images: images as Record<RollSlug, ReleaseImage> };
|
|
490
|
-
}
|
|
491
|
-
|
|
492
459
|
/**
|
|
493
460
|
* `upgrade`'s step of the keyless provisioner migration (#696,
|
|
494
461
|
* provisioner-identity.ts) — a converge step (upgrade/converge.ts runs it
|
|
@@ -685,12 +652,11 @@ async function upgradeLockedInstall(
|
|
|
685
652
|
// #696 — upgrade never moves Pulumi state; it says how to.
|
|
686
653
|
const stateNotice = localStateNotice(meta);
|
|
687
654
|
if (stateNotice) note(stateNotice, "Pulumi state is on this machine");
|
|
688
|
-
if (ok
|
|
689
|
-
// #
|
|
690
|
-
//
|
|
691
|
-
// reminder is safe to repeat on every upgrade.
|
|
655
|
+
if (ok) {
|
|
656
|
+
// #1330 — the converge routed the staging host to the staging gateway
|
|
657
|
+
// and printed its DNS records (Settings → Updates shows them as well).
|
|
692
658
|
log.info(
|
|
693
|
-
`Protected staging site:
|
|
659
|
+
`Protected staging site: https://${stagingHost}/ — served by the staging gateway behind the staging gate. Password protection and other access: admin → Settings → Staging access (or ${bold("cms-provision staging-domain enable")}).`,
|
|
694
660
|
);
|
|
695
661
|
}
|
|
696
662
|
return ok;
|
|
@@ -44,8 +44,17 @@ const firebase: ReleaseServicesTarget = {
|
|
|
44
44
|
region: "europe-west1",
|
|
45
45
|
env: "production",
|
|
46
46
|
domain: "acme.com",
|
|
47
|
+
stagingHost: "staging.acme.com",
|
|
47
48
|
mcpAuthProxyImageRef: `${IMAGE}/mcp-auth-proxy@sha256:${"c".repeat(64)}`,
|
|
49
|
+
gatewayImageRef: `${IMAGE}/gateway@sha256:${"b".repeat(64)}`,
|
|
48
50
|
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The outcomes of the #1328 part; the staging gateway's (#1330) come after
|
|
54
|
+
* them and are covered by staging-gateway-setup.test.ts.
|
|
55
|
+
*/
|
|
56
|
+
const proxyPart = (outcomes: readonly { id: string; status: string }[]) =>
|
|
57
|
+
outcomes.filter((o) => !/staging gateway/.test(o.id)).map((o) => o.status);
|
|
49
58
|
const gcp: ReleaseServicesTarget = { ...firebase, provider: "gcp" };
|
|
50
59
|
|
|
51
60
|
/** The old gateway service as `gcloud run services describe --format=json` shows it. */
|
|
@@ -210,7 +219,7 @@ describe("ensureReleaseServices", () => {
|
|
|
210
219
|
};
|
|
211
220
|
},
|
|
212
221
|
});
|
|
213
|
-
expect(outcomes
|
|
222
|
+
expect(proxyPart(outcomes)).toEqual(["applied", "present", "present", "applied"]);
|
|
214
223
|
expect(calls.filter((c) => c.startsWith("secrets add-iam-policy-binding"))).toEqual([
|
|
215
224
|
`secrets add-iam-policy-binding caelo-production-mcp-oauth-seal-key --project=acme --member=serviceAccount:${PROXY_SA} --role=roles/secretmanager.secretAccessor --condition=None --quiet`,
|
|
216
225
|
`secrets add-iam-policy-binding caelo-production-operator-oauth-client --project=acme --member=serviceAccount:${PROXY_SA} --role=roles/secretmanager.secretAccessor --condition=None --quiet`,
|
|
@@ -237,7 +246,7 @@ describe("ensureReleaseServices", () => {
|
|
|
237
246
|
"run services describe caelo-production-data-gateway": ok("caelo-production-data-gateway\n"),
|
|
238
247
|
});
|
|
239
248
|
const outcomes = await ensureReleaseServices(firebase, { run });
|
|
240
|
-
expect(outcomes
|
|
249
|
+
expect(proxyPart(outcomes)).toEqual(["present", "present", "present", "present"]);
|
|
241
250
|
expect(calls.some((c) => c.includes("replace") || c.includes("create"))).toBe(false);
|
|
242
251
|
});
|
|
243
252
|
|
|
@@ -267,7 +276,7 @@ describe("ensureReleaseServices", () => {
|
|
|
267
276
|
"run deploy caelo-production-mcp-auth-proxy": ok(),
|
|
268
277
|
});
|
|
269
278
|
const outcomes = await ensureReleaseServices(gcp, { run });
|
|
270
|
-
expect(outcomes
|
|
279
|
+
expect(proxyPart(outcomes)).toEqual(["applied", "present", "present", "applied"]);
|
|
271
280
|
expect(calls).toContain(gcpProxyDeployArgs(gcp).join(" "));
|
|
272
281
|
});
|
|
273
282
|
|
|
@@ -280,7 +289,7 @@ describe("ensureReleaseServices", () => {
|
|
|
280
289
|
),
|
|
281
290
|
});
|
|
282
291
|
const outcomes = await ensureReleaseServices(gcp, { run });
|
|
283
|
-
expect(outcomes.at(-1)
|
|
292
|
+
expect(proxyPart(outcomes).at(-1)).toBe("present");
|
|
284
293
|
expect(calls.some((c) => c.startsWith("run deploy"))).toBe(false);
|
|
285
294
|
});
|
|
286
295
|
|
|
@@ -292,7 +301,7 @@ describe("ensureReleaseServices", () => {
|
|
|
292
301
|
),
|
|
293
302
|
});
|
|
294
303
|
const outcomes = await ensureReleaseServices(gcp, { run });
|
|
295
|
-
expect(outcomes
|
|
304
|
+
expect(proxyPart(outcomes)).toEqual(["applied", "present", "failed"]);
|
|
296
305
|
expect(calls.some((c) => c.startsWith("run "))).toBe(false);
|
|
297
306
|
});
|
|
298
307
|
|
|
@@ -320,7 +329,7 @@ describe("ensureReleaseServices", () => {
|
|
|
320
329
|
pauses.push(ms);
|
|
321
330
|
},
|
|
322
331
|
});
|
|
323
|
-
expect(outcomes
|
|
332
|
+
expect(proxyPart(outcomes)).toEqual(["applied", "present", "present", "present"]);
|
|
324
333
|
expect(pauses.length).toBe(2);
|
|
325
334
|
expect(calls.filter((c) => c.startsWith("secrets add-iam-policy-binding"))).toHaveLength(2);
|
|
326
335
|
});
|
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
serviceSecrets,
|
|
50
50
|
} from "./stack-contract.js";
|
|
51
51
|
import { listFlag } from "./stack-converge.js";
|
|
52
|
+
import { ensureStagingGatewayFromLiveGateway } from "./staging-gateway-setup.js";
|
|
52
53
|
import { lookupFailure, resolveGcpResourceName } from "./upgrade/gcp-lookup.js";
|
|
53
54
|
|
|
54
55
|
/** The install the services are ensured on. */
|
|
@@ -61,8 +62,15 @@ export interface ReleaseServicesTarget {
|
|
|
61
62
|
readonly domain: string;
|
|
62
63
|
/** The release's digest-pinned MCP-Auth-Proxy image (`…/mcp-auth-proxy@sha256:…`). */
|
|
63
64
|
readonly mcpAuthProxyImageRef: string;
|
|
65
|
+
/** #1330 — the release's digest-pinned gateway image, which the staging gateway runs too. */
|
|
66
|
+
readonly gatewayImageRef: string;
|
|
67
|
+
/** #1330 — the staging host (install.json `stagingDomain`). */
|
|
68
|
+
readonly stagingHost: string;
|
|
64
69
|
}
|
|
65
70
|
|
|
71
|
+
/** What the `gcp` proxy's own service needs. */
|
|
72
|
+
export type ProxyServiceTarget = Omit<ReleaseServicesTarget, "gatewayImageRef" | "stagingHost">;
|
|
73
|
+
|
|
66
74
|
export interface ReleaseServicesDeps {
|
|
67
75
|
readonly run?: GcloudRunner;
|
|
68
76
|
/** Writes a service spec for `gcloud run services replace`; returns its path and a cleanup. */
|
|
@@ -95,6 +103,10 @@ export async function ensureReleaseServices(
|
|
|
95
103
|
? await ensureDataGatewayService(target, run, deps.writeSpecFile ?? writeTempSpec)
|
|
96
104
|
: await ensureMcpAuthProxyServiceGcp(target, run),
|
|
97
105
|
);
|
|
106
|
+
if (outcomes.some((o) => o.status === "failed")) return outcomes;
|
|
107
|
+
outcomes.push(
|
|
108
|
+
...(await ensureStagingGatewayFromLiveGateway(target, { run, sleep: deps.sleep ?? realSleep })),
|
|
109
|
+
);
|
|
98
110
|
return outcomes;
|
|
99
111
|
}
|
|
100
112
|
|
|
@@ -317,7 +329,7 @@ function gcpProxyEnv(target: Pick<ReleaseServicesTarget, "env" | "domain">): Clo
|
|
|
317
329
|
* invokable without a Google identity (MCP clients have none), request-based
|
|
318
330
|
* CPU, no VPC, and its Google sign-in client file as a secret volume.
|
|
319
331
|
*/
|
|
320
|
-
export function gcpProxyDeployArgs(target:
|
|
332
|
+
export function gcpProxyDeployArgs(target: ProxyServiceTarget): string[] {
|
|
321
333
|
const name = fixedCloudRunServiceName("gcp", target.env, "mcp-auth-proxy") as string;
|
|
322
334
|
const env = gcpProxyEnv(target);
|
|
323
335
|
const plain = env.flatMap((e) => ("value" in e ? [`${e.name}=${e.value}`] : []));
|
|
@@ -356,7 +368,7 @@ export function gcpProxyDeployArgs(target: ReleaseServicesTarget): string[] {
|
|
|
356
368
|
* app-roll moves it onto each release's image after that.
|
|
357
369
|
*/
|
|
358
370
|
export async function ensureMcpAuthProxyServiceGcp(
|
|
359
|
-
target:
|
|
371
|
+
target: ProxyServiceTarget,
|
|
360
372
|
run: GcloudRunner,
|
|
361
373
|
): Promise<EnsureOutcome> {
|
|
362
374
|
const name = fixedCloudRunServiceName("gcp", target.env, "mcp-auth-proxy") as string;
|
package/src/release-manifest.ts
CHANGED
|
@@ -254,7 +254,7 @@ export function manifestServiceImages(m: Pick<ReleaseManifest, "version" | "dige
|
|
|
254
254
|
| {
|
|
255
255
|
readonly ok: true;
|
|
256
256
|
readonly images: Readonly<
|
|
257
|
-
Record<"admin" | "gateway" | "mcp-auth-proxy", ManifestServiceImage>
|
|
257
|
+
Record<"admin" | "gateway" | "staging-gateway" | "mcp-auth-proxy", ManifestServiceImage>
|
|
258
258
|
>;
|
|
259
259
|
}
|
|
260
260
|
| { readonly ok: false; readonly error: string } {
|
|
@@ -274,6 +274,8 @@ export function manifestServiceImages(m: Pick<ReleaseManifest, "version" | "dige
|
|
|
274
274
|
images: {
|
|
275
275
|
admin: image("admin", m.digests.admin),
|
|
276
276
|
gateway: image("gateway", m.digests.gateway),
|
|
277
|
+
// #1330 — the staging gateway runs the live gateway's image.
|
|
278
|
+
"staging-gateway": image("gateway", m.digests.gateway),
|
|
277
279
|
"mcp-auth-proxy": image("mcp-auth-proxy", proxy),
|
|
278
280
|
},
|
|
279
281
|
};
|
package/src/remote-mcp.test.ts
CHANGED
|
@@ -413,6 +413,7 @@ const firebase: RemoteMcpTarget = {
|
|
|
413
413
|
services: {
|
|
414
414
|
admin: "caelo-production-admin-aaa",
|
|
415
415
|
gateway: "caelo-production-data-gateway",
|
|
416
|
+
"staging-gateway": "caelo-production-staging-gateway",
|
|
416
417
|
"mcp-auth-proxy": "caelo-production-gateway-bbb",
|
|
417
418
|
},
|
|
418
419
|
};
|
|
@@ -606,7 +607,10 @@ describe("checkRemoteMcpIdentityEnv (#1082)", () => {
|
|
|
606
607
|
),
|
|
607
608
|
});
|
|
608
609
|
const r = await checkRemoteMcpIdentityEnv(firebase, identity, { run });
|
|
609
|
-
expect(r).toEqual({
|
|
610
|
+
expect(r).toEqual({
|
|
611
|
+
ok: true,
|
|
612
|
+
drift: { admin: [], gateway: [], "staging-gateway": [], "mcp-auth-proxy": [] },
|
|
613
|
+
});
|
|
610
614
|
expect(calls.some((c) => c.startsWith("run services update"))).toBe(false);
|
|
611
615
|
});
|
|
612
616
|
|
package/src/remote-mcp.ts
CHANGED
|
@@ -345,8 +345,17 @@ export async function checkRemoteMcpIdentityEnv(
|
|
|
345
345
|
): Promise<RemoteMcpResult<{ readonly drift: Record<CloudRunSlug, EnvChange[]> }>> {
|
|
346
346
|
const run = deps.run ?? defaultGcloud;
|
|
347
347
|
const desired = remoteMcpIdentityEnv(identity);
|
|
348
|
-
const drift: Record<CloudRunSlug, EnvChange[]> = {
|
|
348
|
+
const drift: Record<CloudRunSlug, EnvChange[]> = {
|
|
349
|
+
admin: [],
|
|
350
|
+
gateway: [],
|
|
351
|
+
"staging-gateway": [],
|
|
352
|
+
"mcp-auth-proxy": [],
|
|
353
|
+
};
|
|
349
354
|
for (const slug of CLOUD_RUN_SLUGS) {
|
|
355
|
+
// Nothing to check where the identity names nothing (the visitor
|
|
356
|
+
// gateways), and the staging gateway is the CLI's (#1330), not the
|
|
357
|
+
// stack's: it may not exist yet right after a first `up`.
|
|
358
|
+
if (desired[slug].length === 0) continue;
|
|
350
359
|
const described = await describeService(target, target.services[slug], run);
|
|
351
360
|
if (!described.ok) return described;
|
|
352
361
|
const plan = planEnvUpdate(liveContainerEnv(described.json), desired[slug]);
|
|
@@ -323,7 +323,12 @@ describe("rotateRuntimeSecret", () => {
|
|
|
323
323
|
projectId: "acme",
|
|
324
324
|
region: "europe-west1",
|
|
325
325
|
env: "production",
|
|
326
|
-
services: {
|
|
326
|
+
services: {
|
|
327
|
+
admin: "adm-svc",
|
|
328
|
+
gateway: "gw-svc",
|
|
329
|
+
"staging-gateway": "stg-svc",
|
|
330
|
+
"mcp-auth-proxy": "px-svc",
|
|
331
|
+
},
|
|
327
332
|
sqlInstance: "caelo-production-pg-1",
|
|
328
333
|
};
|
|
329
334
|
const ref = (name: string, secret: string, version = "latest") => ({
|
|
@@ -347,6 +352,10 @@ describe("rotateRuntimeSecret", () => {
|
|
|
347
352
|
"run services describe gw-svc": [
|
|
348
353
|
service([ref("PUBLIC_DATABASE_PASSWORD", "public-role-password")]),
|
|
349
354
|
],
|
|
355
|
+
// #1330 — the staging gateway reads its own login password.
|
|
356
|
+
"run services describe stg-svc": [
|
|
357
|
+
service([ref("PUBLIC_DATABASE_PASSWORD", "public-staging-role-password")]),
|
|
358
|
+
],
|
|
350
359
|
// #1328 — the proxy reads none of the rotatable database secrets.
|
|
351
360
|
"run services describe px-svc": [
|
|
352
361
|
service([ref("CAELO_MCP_OAUTH_SEAL_KEY", "mcp-oauth-seal-key")]),
|
package/src/runtime-secrets.ts
CHANGED
|
@@ -30,6 +30,8 @@ import {
|
|
|
30
30
|
gcpSecretId,
|
|
31
31
|
mcpAuthProxyServiceAccountEmail,
|
|
32
32
|
mcpAuthProxyServiceAccountId,
|
|
33
|
+
stagingGatewayServiceAccountEmail,
|
|
34
|
+
stagingGatewayServiceAccountId,
|
|
33
35
|
} from "./gcp-names.js";
|
|
34
36
|
import {
|
|
35
37
|
CLI_GENERATED_SECRETS,
|
|
@@ -123,6 +125,25 @@ export function ensureMcpAuthProxyServiceAccount(
|
|
|
123
125
|
);
|
|
124
126
|
}
|
|
125
127
|
|
|
128
|
+
/**
|
|
129
|
+
* #1330 — create the staging gateway's run SA unless it exists. Its rights:
|
|
130
|
+
* `secretAccessor` on `public-staging-role-password` (runtimeSecretBindings),
|
|
131
|
+
* the log and metric writer, and object viewer on the staging bucket
|
|
132
|
+
* (staging-gateway-setup.ts) — nothing of the live site's.
|
|
133
|
+
*/
|
|
134
|
+
export function ensureStagingGatewayServiceAccount(
|
|
135
|
+
install: { readonly projectId: string; readonly env: string },
|
|
136
|
+
deps: RuntimeDeps = {},
|
|
137
|
+
): Promise<EnsureOutcome> {
|
|
138
|
+
return ensureServiceAccount(
|
|
139
|
+
install.projectId,
|
|
140
|
+
stagingGatewayServiceAccountId(install.env),
|
|
141
|
+
stagingGatewayServiceAccountEmail(install.projectId, install.env),
|
|
142
|
+
`Caelo ${install.env} staging gateway`,
|
|
143
|
+
deps,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
126
147
|
async function ensureServiceAccount(
|
|
127
148
|
projectId: string,
|
|
128
149
|
accountId: string,
|
|
@@ -177,6 +177,12 @@ describe("schedulerWorkflowSource", () => {
|
|
|
177
177
|
expect(src).toContain(`exp: \${now + 600}`);
|
|
178
178
|
});
|
|
179
179
|
|
|
180
|
+
it("declares the tick as JSON, or SvelteKit refuses it as a cross-site form post", () => {
|
|
181
|
+
const tick = src.slice(src.indexOf("- tick:"), src.indexOf("- done:"));
|
|
182
|
+
expect(tick).toContain("Content-Type: application/json");
|
|
183
|
+
expect(tick).toContain("body: {}");
|
|
184
|
+
});
|
|
185
|
+
|
|
180
186
|
it("posts the tick with the signed JWT and a bounded timeout", () => {
|
|
181
187
|
expect(src).toContain('url: "https://admin.acme.com/api/internal/scheduler/tick"');
|
|
182
188
|
expect(src).toContain(`Authorization: \${"Bearer " + signed.body.signedJwt}`);
|
package/src/scheduler-setup.ts
CHANGED
|
@@ -165,6 +165,11 @@ export function tickOrigin(
|
|
|
165
165
|
* Credentials API, authenticated with the workflow's own OAuth token), then
|
|
166
166
|
* POST the tick with it. A refused or failed tick (HTTP ≥ 400) fails the
|
|
167
167
|
* execution, so it shows in the workflow's execution history and logs.
|
|
168
|
+
*
|
|
169
|
+
* The tick carries `Content-Type: application/json` and a JSON body, like the
|
|
170
|
+
* self-hosted clock (admin-core scheduler/clock.ts), because the admin's
|
|
171
|
+
* SvelteKit treats a POST with no content type and no `Origin` as a
|
|
172
|
+
* cross-site form submission: it answers 403 before the route runs.
|
|
168
173
|
*/
|
|
169
174
|
export function schedulerWorkflowSource(opts: {
|
|
170
175
|
readonly serviceAccount: string;
|
|
@@ -198,6 +203,8 @@ main:
|
|
|
198
203
|
url: "${origin}${SCHEDULER_TICK_PATH}"
|
|
199
204
|
headers:
|
|
200
205
|
Authorization: \${"Bearer " + signed.body.signedJwt}
|
|
206
|
+
Content-Type: application/json
|
|
207
|
+
body: {}
|
|
201
208
|
timeout: ${SCHEDULER_TICK_TIMEOUT_SECONDS}
|
|
202
209
|
result: answer
|
|
203
210
|
- done:
|
|
@@ -109,12 +109,12 @@ describe("backupSelfHosted", () => {
|
|
|
109
109
|
expect(out).toEqual({ ok: true, path: to });
|
|
110
110
|
expect(probe(r.calls.map((c) => c.cmd))).toEqual([
|
|
111
111
|
"exec -T postgres pg_isready -U caelo",
|
|
112
|
-
"stop caelo-admin caelo-gateway",
|
|
112
|
+
"stop caelo-admin caelo-gateway caelo-staging-gateway",
|
|
113
113
|
"exec -T postgres pg_dumpall -U caelo --globals-only",
|
|
114
114
|
"exec -T postgres pg_dump -U caelo -Fc cms_admin",
|
|
115
115
|
"exec -T postgres pg_dump -U caelo -Fc cms_public",
|
|
116
116
|
"run --rm --no-deps -T --entrypoint tar caelo-admin -C / -cf - srv/caelo data/media",
|
|
117
|
-
"up -d caelo-admin caelo-gateway",
|
|
117
|
+
"up -d caelo-admin caelo-gateway caelo-staging-gateway",
|
|
118
118
|
"exec -T caelo-admin bun -e <probe>",
|
|
119
119
|
"exec -T caelo-gateway bun -e <probe>",
|
|
120
120
|
]);
|
|
@@ -146,7 +146,9 @@ describe("backupSelfHosted", () => {
|
|
|
146
146
|
...quiet,
|
|
147
147
|
});
|
|
148
148
|
expect(out.ok).toBe(false);
|
|
149
|
-
expect(r.calls.map((c) => c.cmd)).toContain(
|
|
149
|
+
expect(r.calls.map((c) => c.cmd)).toContain(
|
|
150
|
+
"up -d caelo-admin caelo-gateway caelo-staging-gateway",
|
|
151
|
+
);
|
|
150
152
|
});
|
|
151
153
|
|
|
152
154
|
it("refuses when Postgres is not running and changes nothing", async () => {
|
|
@@ -194,7 +196,7 @@ describe("restoreSelfHosted", () => {
|
|
|
194
196
|
const out = await restoreSelfHosted(archive, restoreDeps(r));
|
|
195
197
|
expect(out.ok).toBe(true);
|
|
196
198
|
expect(probe(r.calls.map((c) => c.cmd))).toEqual([
|
|
197
|
-
"stop caelo-admin caelo-gateway",
|
|
199
|
+
"stop caelo-admin caelo-gateway caelo-staging-gateway",
|
|
198
200
|
"up -d --wait postgres",
|
|
199
201
|
"exec -T postgres psql -U caelo -d postgres -v ON_ERROR_STOP=1 -c DROP DATABASE IF EXISTS cms_admin WITH (FORCE) -c DROP DATABASE IF EXISTS cms_public WITH (FORCE)",
|
|
200
202
|
"<converge roles>",
|
|
@@ -28,6 +28,8 @@ const base: Omit<ComposeSpec, "images"> = {
|
|
|
28
28
|
caeloSecretKek: "0".repeat(64),
|
|
29
29
|
dbTlsServerCertSha256: "abababababababababababababababababababababababababababababababab",
|
|
30
30
|
internalSecret: "i".repeat(48),
|
|
31
|
+
stagingHost: "staging.example.com",
|
|
32
|
+
stagingGateSecret: "g".repeat(64),
|
|
31
33
|
anthropicApiKey: "sk-ant-x",
|
|
32
34
|
diskSize: "20Gi",
|
|
33
35
|
ports: { http: 80, https: 443 },
|
|
@@ -63,6 +63,7 @@ const MEMBER_EXPR: Record<IamInvariant["member"], string> = {
|
|
|
63
63
|
// The secret loop resolves the SA per service through `serviceSa`.
|
|
64
64
|
"gateway-sa": "gatewaySa.email",
|
|
65
65
|
"mcp-auth-proxy-sa": "mcpAuthProxySa.email",
|
|
66
|
+
"staging-gateway-sa": "stagingGatewaySa.email",
|
|
66
67
|
"static-publisher-sa": "staticPublisherSa.email",
|
|
67
68
|
"iap-service-agent": "iapServiceIdentity.email",
|
|
68
69
|
allUsers: 'member: "allUsers"',
|
|
@@ -166,15 +167,18 @@ describe.each(["gcp", "gcp-firebase"] as const)("%s stack ↔ upgrade parity", (
|
|
|
166
167
|
/serviceName: "gateway",[\s\S]*?contractEnv: \[\s*\.\.\.gatewayEnvContract\(envContractInputs\),\s*\.\.\.poolBudget\.env\.gateway,?\s*\]/,
|
|
167
168
|
);
|
|
168
169
|
if (provider === "gcp-firebase") {
|
|
169
|
-
// The proxy: its contract
|
|
170
|
-
//
|
|
170
|
+
// The proxy: its contract and its identity env, nothing else (#1331:
|
|
171
|
+
// no forward origin any more).
|
|
171
172
|
expect(src).toMatch(
|
|
172
|
-
/serviceName: "mcp-auth-proxy",[\s\S]*?contractEnv: \[\s*\.\.\.mcpAuthProxyEnvContract\(\{ provider: "gcp-firebase", env \}\),\s*\.\.\.remoteMcpEnv\["mcp-auth-proxy"\]
|
|
173
|
+
/serviceName: "mcp-auth-proxy",[\s\S]*?contractEnv: \[\s*\.\.\.mcpAuthProxyEnvContract\(\{ provider: "gcp-firebase", env \}\),\s*\.\.\.remoteMcpEnv\["mcp-auth-proxy"\],?\s*\]/,
|
|
173
174
|
);
|
|
175
|
+
expect(src).not.toContain("INTERIM_FORWARD_ORIGIN");
|
|
174
176
|
} else {
|
|
175
177
|
// gcp: the proxy is CLI-owned (mcp-auth-proxy-setup.ts), not a stack service.
|
|
176
178
|
expect(src).not.toContain('serviceName: "mcp-auth-proxy"');
|
|
177
179
|
}
|
|
180
|
+
// #1330 — the staging gateway is CLI-owned on both (staging-gateway-setup.ts).
|
|
181
|
+
expect(src).not.toContain('serviceName: "staging-gateway"');
|
|
178
182
|
expect(src).toContain("envs: [...args.contractEnv],");
|
|
179
183
|
});
|
|
180
184
|
|
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
defaultStagingHost,
|
|
15
15
|
fixedCloudRunServiceName,
|
|
16
16
|
gatewayEnvContract,
|
|
17
|
-
INTERIM_FORWARD_ORIGIN_ENV,
|
|
18
17
|
iamMember,
|
|
19
18
|
MCP_AUTH_PROXY_TRUSTED_PROXY_HOPS,
|
|
20
19
|
mcpAuthProxyEnvContract,
|
|
@@ -24,12 +23,15 @@ import {
|
|
|
24
23
|
RETIRED_SERVICE_ENV,
|
|
25
24
|
runtimeSecretBindings,
|
|
26
25
|
SECRETS_SET_UP_AHEAD_OF_THEIR_READER,
|
|
26
|
+
SERVICE_IMAGE,
|
|
27
27
|
SERVICE_SECRET_ENV,
|
|
28
28
|
SERVICE_SECRET_FILES,
|
|
29
29
|
SERVICE_VPC_EGRESS,
|
|
30
30
|
STACK_IAM_NOT_ENSURED,
|
|
31
|
+
STAGING_GATEWAY_TRUSTED_PROXY_HOPS,
|
|
31
32
|
serviceSecrets,
|
|
32
33
|
stackIamInvariants,
|
|
34
|
+
stagingGatewayEnvContract,
|
|
33
35
|
} from "./stack-contract.js";
|
|
34
36
|
|
|
35
37
|
const base = {
|
|
@@ -108,6 +110,8 @@ describe("adminEnvContract", () => {
|
|
|
108
110
|
CAELO_CAPTURE_JOB: "projects/acme/locations/europe-west1/jobs/caelo-production-capture",
|
|
109
111
|
CAELO_STATIC_BUCKET: "acme-caelo-production-static",
|
|
110
112
|
CAELO_STAGING_BUCKET: "acme-caelo-production-staging",
|
|
113
|
+
// #1330 — every provider serves staging at its own host now.
|
|
114
|
+
CAELO_STAGING_HOST: "staging.acme.com",
|
|
111
115
|
...ADMIN_SECRETS,
|
|
112
116
|
});
|
|
113
117
|
});
|
|
@@ -119,10 +123,13 @@ describe("adminEnvContract", () => {
|
|
|
119
123
|
...base,
|
|
120
124
|
provider: "gcp-firebase",
|
|
121
125
|
firebaseSiteId: "caelo-production-site-abc123",
|
|
126
|
+
firebaseStagingSiteId: "caelo-production-stg-6eace1",
|
|
122
127
|
gatewayService: "caelo-production-gateway-1a2b3c4",
|
|
123
128
|
}),
|
|
124
129
|
),
|
|
125
130
|
).toEqual({
|
|
131
|
+
CAELO_FIREBASE_STAGING_SITE: "caelo-production-stg-6eace1",
|
|
132
|
+
CAELO_STAGING_BUCKET: "acme-caelo-production-staging",
|
|
126
133
|
CAELO_PROVIDER: "gcp-firebase",
|
|
127
134
|
CAELO_ENV: "production",
|
|
128
135
|
MEDIA_STORAGE_URL: "gs://acme-caelo-production-media",
|
|
@@ -210,8 +217,8 @@ describe("gatewayEnvContract", () => {
|
|
|
210
217
|
PUBLIC_DATABASE_URL:
|
|
211
218
|
"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",
|
|
212
219
|
PUBLIC_DATABASE_PASSWORD: "secret:caelo-production-public-role-password:latest",
|
|
213
|
-
|
|
214
|
-
//
|
|
220
|
+
// No remote-MCP env: the broker is the MCP-Auth-Proxy's (#1328); no
|
|
221
|
+
// staging gate key: the staging gateway's, through its projection (#1330).
|
|
215
222
|
});
|
|
216
223
|
});
|
|
217
224
|
|
|
@@ -222,10 +229,53 @@ describe("gatewayEnvContract", () => {
|
|
|
222
229
|
expect(asRecord(gateway).CAELO_TRUSTED_PROXY_HOPS).toBe("2");
|
|
223
230
|
});
|
|
224
231
|
|
|
225
|
-
it("#
|
|
232
|
+
it("#1330 — the live gateway knows no staging host and holds no staging gate key", () => {
|
|
226
233
|
const fb = asRecord(gatewayEnvContract({ ...base, provider: "gcp-firebase" }));
|
|
227
234
|
expect(fb.CAELO_STAGING_HOST).toBeUndefined();
|
|
228
|
-
expect(fb.CAELO_STAGING_GATE_SECRET).
|
|
235
|
+
expect(fb.CAELO_STAGING_GATE_SECRET).toBeUndefined();
|
|
236
|
+
expect(RETIRED_SERVICE_ENV.gateway).toContain("CAELO_STAGING_GATE_SECRET");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("#1330 — the staging gateway is the gateway image set to staging: its one login, its storage, nothing live", () => {
|
|
240
|
+
for (const provider of ["gcp", "gcp-firebase"] as const) {
|
|
241
|
+
expect(asRecord(stagingGatewayEnvContract({ ...base, provider }))).toEqual({
|
|
242
|
+
CAELO_PROVIDER: provider,
|
|
243
|
+
CAELO_ENV: "production",
|
|
244
|
+
CAELO_GATEWAY_ENVIRONMENT: "staging",
|
|
245
|
+
CAELO_TRUSTED_PROXY_HOPS: String(STAGING_GATEWAY_TRUSTED_PROXY_HOPS[provider]),
|
|
246
|
+
PUBLIC_DATABASE_URL:
|
|
247
|
+
"postgresql://public_staging_role@db1.p-label.europe-west1.sql-psa.goog:5432/cms_public_staging?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem",
|
|
248
|
+
CAELO_STAGING_STORAGE: "gs://acme-caelo-production-staging",
|
|
249
|
+
PUBLIC_DATABASE_PASSWORD: "secret:caelo-production-public-staging-role-password:latest",
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("pins the trusted hop count per provider, so a forged X-Forwarded-For entry is never trusted", () => {
|
|
255
|
+
expect(STAGING_GATEWAY_TRUSTED_PROXY_HOPS).toEqual({
|
|
256
|
+
gcp: 2,
|
|
257
|
+
"gcp-firebase": 1,
|
|
258
|
+
"self-hosted": 1,
|
|
259
|
+
aws: 2,
|
|
260
|
+
azure: 1,
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("shares no secret with the live gateway and reads exactly its own login", () => {
|
|
265
|
+
expect(serviceSecrets("staging-gateway")).toEqual(["public-staging-role-password"]);
|
|
266
|
+
const live = new Set(serviceSecrets("gateway"));
|
|
267
|
+
expect(serviceSecrets("staging-gateway").some((x) => live.has(x))).toBe(false);
|
|
268
|
+
expect(serviceSecrets("gateway")).not.toContain("staging-gate-secret");
|
|
269
|
+
expect(SECRETS_SET_UP_AHEAD_OF_THEIR_READER["public-staging-role-password"]).toBeUndefined();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("runs the live gateway's image under its own fixed name", () => {
|
|
273
|
+
expect(SERVICE_IMAGE["staging-gateway"]).toBe("gateway");
|
|
274
|
+
for (const provider of ["gcp", "gcp-firebase"] as const) {
|
|
275
|
+
expect(fixedCloudRunServiceName(provider, "production", "staging-gateway")).toBe(
|
|
276
|
+
"caelo-production-staging-gateway",
|
|
277
|
+
);
|
|
278
|
+
}
|
|
229
279
|
});
|
|
230
280
|
|
|
231
281
|
it("#650 — the admin's staging host seed is the install setting, any host", () => {
|
|
@@ -235,6 +285,7 @@ describe("gatewayEnvContract", () => {
|
|
|
235
285
|
stagingHost: "preview.other.de",
|
|
236
286
|
provider: "gcp-firebase",
|
|
237
287
|
firebaseSiteId: "s",
|
|
288
|
+
firebaseStagingSiteId: "s-stg",
|
|
238
289
|
gatewayService: "g",
|
|
239
290
|
}),
|
|
240
291
|
);
|
|
@@ -243,7 +294,7 @@ describe("gatewayEnvContract", () => {
|
|
|
243
294
|
});
|
|
244
295
|
|
|
245
296
|
it("the gateway's run SA reads no admin-only secret (admin password, KEK, internal, tool approval)", () => {
|
|
246
|
-
expect(serviceSecrets("gateway")).toEqual(["public-role-password"
|
|
297
|
+
expect(serviceSecrets("gateway")).toEqual(["public-role-password"]);
|
|
247
298
|
for (const v of gateway) {
|
|
248
299
|
expect(v.name).not.toMatch(/KEK|INTERNAL|TOOL_APPROVAL|PUBLIC_ADMIN/);
|
|
249
300
|
}
|
|
@@ -336,10 +387,11 @@ describe("runtime secrets", () => {
|
|
|
336
387
|
secret: "public-role-password",
|
|
337
388
|
stackResource: "gateway-public-role-password-binding",
|
|
338
389
|
},
|
|
390
|
+
// #1330 — the staging gateway: its own login only.
|
|
339
391
|
{
|
|
340
|
-
service: "gateway",
|
|
341
|
-
secret: "staging-
|
|
342
|
-
stackResource: "gateway-staging-
|
|
392
|
+
service: "staging-gateway",
|
|
393
|
+
secret: "public-staging-role-password",
|
|
394
|
+
stackResource: "staging-gateway-public-staging-role-password-binding",
|
|
343
395
|
},
|
|
344
396
|
// #1328 — the broker's seal key (env) and Google sign-in client (a
|
|
345
397
|
// file, which counts like an env secret: the SA reads it).
|
|
@@ -385,6 +437,8 @@ describe("self-hosted compose", () => {
|
|
|
385
437
|
caeloSecretKek: "k",
|
|
386
438
|
dbTlsServerCertSha256: "abababababababababababababababababababababababababababababababab",
|
|
387
439
|
internalSecret: "i".repeat(48),
|
|
440
|
+
stagingHost: "staging.example.com",
|
|
441
|
+
stagingGateSecret: "g".repeat(64),
|
|
388
442
|
diskSize: "10G",
|
|
389
443
|
ports: { http: 80, https: 443 },
|
|
390
444
|
project: "caelo-example-com",
|
|
@@ -424,14 +478,19 @@ describe("stackIamInvariants", () => {
|
|
|
424
478
|
}
|
|
425
479
|
});
|
|
426
480
|
|
|
427
|
-
it("grants the gateway SA only its own role password
|
|
481
|
+
it("grants the gateway SA only its own role password + telemetry roles (#1330: no staging gate key)", () => {
|
|
428
482
|
for (const provider of ["gcp", "gcp-firebase"] as const) {
|
|
429
483
|
const gateway = stackIamInvariants(provider).filter((i) => i.member === "gateway-sa");
|
|
430
484
|
expect(gateway.map((i) => `${i.role} ${JSON.stringify(i.target)}`).sort()).toEqual([
|
|
431
485
|
'roles/logging.logWriter {"kind":"project"}',
|
|
432
486
|
'roles/monitoring.metricWriter {"kind":"project"}',
|
|
433
487
|
'roles/secretmanager.secretAccessor {"kind":"secret","name":"public-role-password"}',
|
|
434
|
-
|
|
488
|
+
]);
|
|
489
|
+
const staging = stackIamInvariants(provider).filter((i) => i.member === "staging-gateway-sa");
|
|
490
|
+
expect(staging.map((i) => `${i.role} ${JSON.stringify(i.target)}`).sort()).toEqual([
|
|
491
|
+
'roles/logging.logWriter {"kind":"project"}',
|
|
492
|
+
'roles/monitoring.metricWriter {"kind":"project"}',
|
|
493
|
+
'roles/secretmanager.secretAccessor {"kind":"secret","name":"public-staging-role-password"}',
|
|
435
494
|
]);
|
|
436
495
|
}
|
|
437
496
|
});
|
|
@@ -508,10 +567,11 @@ describe("MCP-Auth-Proxy contract (#1328)", () => {
|
|
|
508
567
|
CAELO_REMOTE_MCP: "unavailable",
|
|
509
568
|
CAELO_TRUSTED_PROXY_HOPS: String(MCP_AUTH_PROXY_TRUSTED_PROXY_HOPS.azure),
|
|
510
569
|
});
|
|
511
|
-
//
|
|
570
|
+
// #1331 — the interim forwarder is gone; the roll removes its env.
|
|
512
571
|
expect(
|
|
513
572
|
mcpAuthProxyEnvContract({ provider: "gcp-firebase", env: "production" }).map((v) => v.name),
|
|
514
|
-
).not.toContain(
|
|
573
|
+
).not.toContain("CAELO_INTERIM_FORWARD_ORIGIN");
|
|
574
|
+
expect(RETIRED_SERVICE_ENV["mcp-auth-proxy"]).toContain("CAELO_INTERIM_FORWARD_ORIGIN");
|
|
515
575
|
});
|
|
516
576
|
|
|
517
577
|
it("the proxy's SA reads exactly its two secrets and holds no other invariant", () => {
|