@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
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* #1328 — the Cloud Run services an upgrade to the MCP-Auth-Proxy release
|
|
5
|
+
* must find before it plans its rolls, created through gcloud on installs
|
|
6
|
+
* whose stack predates them (`cms-provision upgrade` and the converge job
|
|
7
|
+
* never run Pulumi). Run as the operator's or the provisioner's identity,
|
|
8
|
+
* before the upgrade's plan: each step only ADDS a service, account or grant
|
|
9
|
+
* that serves no traffic yet, so a failure leaves the running install as it
|
|
10
|
+
* was (converge release 1 of the hardening plan, #763, is additive).
|
|
11
|
+
*
|
|
12
|
+
* - Both providers: the proxy's own service account and its
|
|
13
|
+
* `secretAccessor` on exactly its two secrets — before any service runs
|
|
14
|
+
* as it, because Cloud Run checks a revision's secret access at deploy.
|
|
15
|
+
* - `gcp-firebase` (E13 a): the visitor gateway's new service
|
|
16
|
+
* `caelo-<env>-data-gateway`, a copy of the service that was the gateway
|
|
17
|
+
* (same image, env, secrets, VPC, account), so the app-roll can then roll
|
|
18
|
+
* it like any other service and turn the old one into the proxy. Copied,
|
|
19
|
+
* not built from the contract, so the health baseline sees the release
|
|
20
|
+
* that serves today on all three services.
|
|
21
|
+
* - `gcp`: the proxy's own service `caelo-<env>-mcp-auth-proxy`, deployed
|
|
22
|
+
* with the release's proxy image (there is no older proxy to copy). Its
|
|
23
|
+
* load-balancer part is remote-mcp.ts `ensureRemoteMcpEdge`, later in
|
|
24
|
+
* the converge.
|
|
25
|
+
*
|
|
26
|
+
* Fresh installs get the same from their stack (gcp-firebase) or from the
|
|
27
|
+
* wizard after the first `pulumi up` (gcp, like the jobs: CLI-owned).
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
31
|
+
import { tmpdir } from "node:os";
|
|
32
|
+
import { join } from "node:path";
|
|
33
|
+
import { gcloud as defaultGcloud } from "./gcloud.js";
|
|
34
|
+
import type { GcloudRunner } from "./gcloud-retry.js";
|
|
35
|
+
import { gcpSecretId, mcpAuthProxyServiceAccountEmail } from "./gcp-names.js";
|
|
36
|
+
import { type EnsureOutcome, ensureMcpAuthProxyServiceAccount } from "./runtime-secrets.js";
|
|
37
|
+
import {
|
|
38
|
+
type CloudRunEnvVar,
|
|
39
|
+
cloudRunServicePrefix,
|
|
40
|
+
DEFAULT_MCP_AUTH_PROXY_MAX_INSTANCES,
|
|
41
|
+
fixedCloudRunServiceName,
|
|
42
|
+
type GcpProvider,
|
|
43
|
+
MCP_AUTH_PROXY_TIMEOUT_SECONDS,
|
|
44
|
+
mcpAuthProxyEnvContract,
|
|
45
|
+
OPERATOR_OAUTH_CLIENT_FILE,
|
|
46
|
+
remoteMcpIdentity,
|
|
47
|
+
remoteMcpIdentityEnv,
|
|
48
|
+
secretFilePath,
|
|
49
|
+
serviceSecrets,
|
|
50
|
+
} from "./stack-contract.js";
|
|
51
|
+
import { listFlag } from "./stack-converge.js";
|
|
52
|
+
import { lookupFailure, resolveGcpResourceName } from "./upgrade/gcp-lookup.js";
|
|
53
|
+
|
|
54
|
+
/** The install the services are ensured on. */
|
|
55
|
+
export interface ReleaseServicesTarget {
|
|
56
|
+
readonly provider: GcpProvider;
|
|
57
|
+
readonly projectId: string;
|
|
58
|
+
readonly region: string;
|
|
59
|
+
/** Pulumi stack name (`production`). */
|
|
60
|
+
readonly env: string;
|
|
61
|
+
readonly domain: string;
|
|
62
|
+
/** The release's digest-pinned MCP-Auth-Proxy image (`…/mcp-auth-proxy@sha256:…`). */
|
|
63
|
+
readonly mcpAuthProxyImageRef: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface ReleaseServicesDeps {
|
|
67
|
+
readonly run?: GcloudRunner;
|
|
68
|
+
/** Writes a service spec for `gcloud run services replace`; returns its path and a cleanup. */
|
|
69
|
+
readonly writeSpecFile?: (json: string) => {
|
|
70
|
+
readonly path: string;
|
|
71
|
+
readonly cleanup: () => void;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const NOT_FOUND = /NOT_FOUND|not found|does not exist|could not be found/i;
|
|
76
|
+
|
|
77
|
+
/** Ensure everything {@link ReleaseServicesTarget} needs; outcomes in order, failures included. */
|
|
78
|
+
export async function ensureReleaseServices(
|
|
79
|
+
target: ReleaseServicesTarget,
|
|
80
|
+
deps: ReleaseServicesDeps = {},
|
|
81
|
+
): Promise<EnsureOutcome[]> {
|
|
82
|
+
const run = deps.run ?? defaultGcloud;
|
|
83
|
+
const outcomes: EnsureOutcome[] = [];
|
|
84
|
+
const account = await ensureMcpAuthProxyServiceAccount(target, { run });
|
|
85
|
+
outcomes.push(account);
|
|
86
|
+
if (account.status === "failed") return outcomes;
|
|
87
|
+
outcomes.push(...(await ensureMcpAuthProxySecretAccess(target, run)));
|
|
88
|
+
if (outcomes.some((o) => o.status === "failed")) return outcomes;
|
|
89
|
+
outcomes.push(
|
|
90
|
+
target.provider === "gcp-firebase"
|
|
91
|
+
? await ensureDataGatewayService(target, run, deps.writeSpecFile ?? writeTempSpec)
|
|
92
|
+
: await ensureMcpAuthProxyServiceGcp(target, run),
|
|
93
|
+
);
|
|
94
|
+
return outcomes;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* `secretAccessor` for the proxy's account on each secret it reads
|
|
99
|
+
* (stack-contract.ts `serviceSecrets("mcp-auth-proxy")`) and nothing else.
|
|
100
|
+
* `add-iam-policy-binding` is idempotent; the stack invariants ensure the
|
|
101
|
+
* same bindings again later in the converge.
|
|
102
|
+
*/
|
|
103
|
+
async function ensureMcpAuthProxySecretAccess(
|
|
104
|
+
target: Pick<ReleaseServicesTarget, "projectId" | "env">,
|
|
105
|
+
run: GcloudRunner,
|
|
106
|
+
): Promise<EnsureOutcome[]> {
|
|
107
|
+
const member = `serviceAccount:${mcpAuthProxyServiceAccountEmail(target.projectId, target.env)}`;
|
|
108
|
+
const outcomes: EnsureOutcome[] = [];
|
|
109
|
+
for (const secret of serviceSecrets("mcp-auth-proxy")) {
|
|
110
|
+
const id = `secretAccessor → MCP-Auth-Proxy on ${secret}`;
|
|
111
|
+
const r = await run([
|
|
112
|
+
"secrets",
|
|
113
|
+
"add-iam-policy-binding",
|
|
114
|
+
gcpSecretId(target.env, secret),
|
|
115
|
+
`--project=${target.projectId}`,
|
|
116
|
+
`--member=${member}`,
|
|
117
|
+
"--role=roles/secretmanager.secretAccessor",
|
|
118
|
+
"--condition=None",
|
|
119
|
+
"--quiet",
|
|
120
|
+
]);
|
|
121
|
+
outcomes.push(
|
|
122
|
+
r.ok ? { id, status: "present" } : { id, status: "failed", error: r.stderr.trim() },
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return outcomes;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ===========================================================================
|
|
129
|
+
// gcp-firebase: the data gateway
|
|
130
|
+
// ===========================================================================
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The Knative service spec of `serviceJson` (`gcloud run services describe
|
|
134
|
+
* --format=json`) as a new service named `name`, for `gcloud run services
|
|
135
|
+
* replace`: the configuration only — image, env, secrets, volumes, VPC,
|
|
136
|
+
* scaling, account, ingress — without the source's identity (uid, revision
|
|
137
|
+
* names, status), its tool stamps or its traffic pins.
|
|
138
|
+
*
|
|
139
|
+
* @throws when the source is not a gateway image (a re-run must never copy
|
|
140
|
+
* the MCP-Auth-Proxy the old service became)
|
|
141
|
+
*/
|
|
142
|
+
export function cloneServiceSpec(serviceJson: string, name: string): string {
|
|
143
|
+
const svc = JSON.parse(serviceJson) as {
|
|
144
|
+
apiVersion?: string;
|
|
145
|
+
kind?: string;
|
|
146
|
+
metadata?: { labels?: Record<string, string>; annotations?: Record<string, string> };
|
|
147
|
+
spec?: {
|
|
148
|
+
template?: {
|
|
149
|
+
metadata?: { labels?: Record<string, string>; annotations?: Record<string, string> };
|
|
150
|
+
spec?: { containers?: { image?: string }[] } & Record<string, unknown>;
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
const image = svc.spec?.template?.spec?.containers?.[0]?.image ?? "";
|
|
155
|
+
if (!/\/gateway[@:]/.test(image)) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`the source service runs ${image || "no image"}, not a Caelo gateway image — the visitor gateway cannot be copied from it`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
const keepLabels = (labels: Record<string, string> | undefined) =>
|
|
161
|
+
Object.fromEntries(
|
|
162
|
+
Object.entries(labels ?? {}).filter(
|
|
163
|
+
([k]) =>
|
|
164
|
+
!k.startsWith("cloud.googleapis.com/") &&
|
|
165
|
+
!k.startsWith("run.googleapis.com/") &&
|
|
166
|
+
!k.startsWith("client.knative.dev/") &&
|
|
167
|
+
!k.startsWith("serving.knative.dev/") &&
|
|
168
|
+
!k.startsWith("goog-"),
|
|
169
|
+
),
|
|
170
|
+
);
|
|
171
|
+
// Service-level: only the settings; the rest are server stamps.
|
|
172
|
+
const KEEP_SERVICE_ANNOTATIONS = [
|
|
173
|
+
"run.googleapis.com/ingress",
|
|
174
|
+
"run.googleapis.com/launch-stage",
|
|
175
|
+
];
|
|
176
|
+
const annotations = Object.fromEntries(
|
|
177
|
+
Object.entries(svc.metadata?.annotations ?? {}).filter(([k]) =>
|
|
178
|
+
KEEP_SERVICE_ANNOTATIONS.includes(k),
|
|
179
|
+
),
|
|
180
|
+
);
|
|
181
|
+
const templateAnnotations = Object.fromEntries(
|
|
182
|
+
Object.entries(svc.spec?.template?.metadata?.annotations ?? {}).filter(
|
|
183
|
+
([k]) => k !== "run.googleapis.com/client-name" && k !== "run.googleapis.com/client-version",
|
|
184
|
+
),
|
|
185
|
+
);
|
|
186
|
+
return JSON.stringify({
|
|
187
|
+
apiVersion: svc.apiVersion ?? "serving.knative.dev/v1",
|
|
188
|
+
kind: svc.kind ?? "Service",
|
|
189
|
+
metadata: { name, labels: keepLabels(svc.metadata?.labels), annotations },
|
|
190
|
+
spec: {
|
|
191
|
+
template: {
|
|
192
|
+
metadata: {
|
|
193
|
+
labels: keepLabels(svc.spec?.template?.metadata?.labels),
|
|
194
|
+
annotations: templateAnnotations,
|
|
195
|
+
},
|
|
196
|
+
spec: svc.spec?.template?.spec ?? {},
|
|
197
|
+
},
|
|
198
|
+
traffic: [{ percent: 100, latestRevision: true }],
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function writeTempSpec(json: string): { path: string; cleanup: () => void } {
|
|
204
|
+
const dir = mkdtempSync(join(tmpdir(), "caelo-service-"));
|
|
205
|
+
const path = join(dir, "service.json");
|
|
206
|
+
writeFileSync(path, json, { mode: 0o600 });
|
|
207
|
+
return { path, cleanup: () => rmSync(dir, { recursive: true, force: true }) };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* The gcp-firebase data gateway (`caelo-<env>-data-gateway`): present, or
|
|
212
|
+
* created as a copy of the `caelo-<env>-gateway*` service. Its public invoker
|
|
213
|
+
* and the admin's `run.viewer` (the Firebase rewrite) follow from the stack
|
|
214
|
+
* invariants later in the converge.
|
|
215
|
+
*/
|
|
216
|
+
async function ensureDataGatewayService(
|
|
217
|
+
target: ReleaseServicesTarget,
|
|
218
|
+
run: GcloudRunner,
|
|
219
|
+
writeSpecFile: NonNullable<ReleaseServicesDeps["writeSpecFile"]>,
|
|
220
|
+
): Promise<EnsureOutcome> {
|
|
221
|
+
const name = fixedCloudRunServiceName(target.provider, target.env, "gateway") as string;
|
|
222
|
+
const id = `Cloud Run service ${name} (the visitor gateway)`;
|
|
223
|
+
const where = [`--region=${target.region}`, `--project=${target.projectId}`];
|
|
224
|
+
const existing = await run([
|
|
225
|
+
"run",
|
|
226
|
+
"services",
|
|
227
|
+
"describe",
|
|
228
|
+
name,
|
|
229
|
+
...where,
|
|
230
|
+
"--format=value(metadata.name)",
|
|
231
|
+
]);
|
|
232
|
+
if (existing.ok) return { id, status: "present" };
|
|
233
|
+
if (!NOT_FOUND.test(existing.stderr)) {
|
|
234
|
+
return { id, status: "failed", error: existing.stderr.trim() };
|
|
235
|
+
}
|
|
236
|
+
// The source: the service that was the gateway and becomes the proxy.
|
|
237
|
+
const sourcePrefix = cloudRunServicePrefix(target.provider, target.env, "mcp-auth-proxy");
|
|
238
|
+
const source = await resolveGcpResourceName(
|
|
239
|
+
run,
|
|
240
|
+
"run-service",
|
|
241
|
+
sourcePrefix,
|
|
242
|
+
target.projectId,
|
|
243
|
+
target.region,
|
|
244
|
+
);
|
|
245
|
+
if (source.status !== "found") {
|
|
246
|
+
return {
|
|
247
|
+
id,
|
|
248
|
+
status: "failed",
|
|
249
|
+
error: lookupFailure(source, `${sourcePrefix}* Cloud Run service`, "operator"),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
const described = await run([
|
|
253
|
+
"run",
|
|
254
|
+
"services",
|
|
255
|
+
"describe",
|
|
256
|
+
source.name,
|
|
257
|
+
...where,
|
|
258
|
+
"--format=json",
|
|
259
|
+
]);
|
|
260
|
+
if (!described.ok) {
|
|
261
|
+
return { id, status: "failed", error: `describe ${source.name}: ${described.stderr.trim()}` };
|
|
262
|
+
}
|
|
263
|
+
let spec: string;
|
|
264
|
+
try {
|
|
265
|
+
spec = cloneServiceSpec(described.stdout, name);
|
|
266
|
+
} catch (e) {
|
|
267
|
+
return {
|
|
268
|
+
id,
|
|
269
|
+
status: "failed",
|
|
270
|
+
error: `${e instanceof Error ? e.message : String(e)}. Re-run the installer (it creates ${name} from the stack), then upgrade again.`,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
const file = writeSpecFile(spec);
|
|
274
|
+
try {
|
|
275
|
+
const replaced = await run(["run", "services", "replace", file.path, ...where, "--quiet"]);
|
|
276
|
+
return replaced.ok
|
|
277
|
+
? { id, status: "applied", detail: `copied from ${source.name}` }
|
|
278
|
+
: { id, status: "failed", error: replaced.stderr.trim() };
|
|
279
|
+
} finally {
|
|
280
|
+
file.cleanup();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ===========================================================================
|
|
285
|
+
// gcp: the proxy's service
|
|
286
|
+
// ===========================================================================
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* The full env the `gcp` proxy is deployed with: its contract plus the
|
|
290
|
+
* remote-MCP identity it can derive without a lookup (its public origin is
|
|
291
|
+
* the site's host, the admin's is `admin.<domain>`). The IAP audience is
|
|
292
|
+
* the admin's alone.
|
|
293
|
+
*/
|
|
294
|
+
function gcpProxyEnv(target: Pick<ReleaseServicesTarget, "env" | "domain">): CloudRunEnvVar[] {
|
|
295
|
+
const identity = remoteMcpIdentityEnv(
|
|
296
|
+
remoteMcpIdentity({
|
|
297
|
+
provider: "gcp",
|
|
298
|
+
domain: target.domain,
|
|
299
|
+
// Only the admin's audience uses these; the proxy's env does not.
|
|
300
|
+
projectNumber: "0",
|
|
301
|
+
adminBackendServiceId: "0",
|
|
302
|
+
}),
|
|
303
|
+
)["mcp-auth-proxy"];
|
|
304
|
+
return [...mcpAuthProxyEnvContract({ provider: "gcp", env: target.env }), ...identity];
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* The `gcloud run deploy` argv of the `gcp` proxy: load-balancer-only
|
|
309
|
+
* ingress (its two hops, stack-contract.ts MCP_AUTH_PROXY_TRUSTED_PROXY_HOPS),
|
|
310
|
+
* invokable without a Google identity (MCP clients have none), request-based
|
|
311
|
+
* CPU, no VPC, and its Google sign-in client file as a secret volume.
|
|
312
|
+
*/
|
|
313
|
+
export function gcpProxyDeployArgs(target: ReleaseServicesTarget): string[] {
|
|
314
|
+
const name = fixedCloudRunServiceName("gcp", target.env, "mcp-auth-proxy") as string;
|
|
315
|
+
const env = gcpProxyEnv(target);
|
|
316
|
+
const plain = env.flatMap((e) => ("value" in e ? [`${e.name}=${e.value}`] : []));
|
|
317
|
+
const secrets = [
|
|
318
|
+
...env.flatMap((e) =>
|
|
319
|
+
"valueSource" in e
|
|
320
|
+
? [`${e.name}=${e.valueSource.secretKeyRef.secret}:${e.valueSource.secretKeyRef.version}`]
|
|
321
|
+
: [],
|
|
322
|
+
),
|
|
323
|
+
`${secretFilePath(OPERATOR_OAUTH_CLIENT_FILE)}=${gcpSecretId(target.env, OPERATOR_OAUTH_CLIENT_FILE.secret)}:${OPERATOR_OAUTH_CLIENT_FILE.version}`,
|
|
324
|
+
];
|
|
325
|
+
return [
|
|
326
|
+
"run",
|
|
327
|
+
"deploy",
|
|
328
|
+
name,
|
|
329
|
+
`--image=${target.mcpAuthProxyImageRef}`,
|
|
330
|
+
`--region=${target.region}`,
|
|
331
|
+
`--project=${target.projectId}`,
|
|
332
|
+
`--service-account=${mcpAuthProxyServiceAccountEmail(target.projectId, target.env)}`,
|
|
333
|
+
"--ingress=internal-and-cloud-load-balancing",
|
|
334
|
+
"--allow-unauthenticated",
|
|
335
|
+
"--min-instances=0",
|
|
336
|
+
`--max-instances=${DEFAULT_MCP_AUTH_PROXY_MAX_INSTANCES}`,
|
|
337
|
+
"--cpu=1",
|
|
338
|
+
"--memory=512Mi",
|
|
339
|
+
"--cpu-throttling",
|
|
340
|
+
`--timeout=${MCP_AUTH_PROXY_TIMEOUT_SECONDS}`,
|
|
341
|
+
listFlag("--set-env-vars", plain),
|
|
342
|
+
listFlag("--set-secrets", secrets),
|
|
343
|
+
"--quiet",
|
|
344
|
+
];
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* The `gcp` proxy service: present, or deployed. Deployed once; the
|
|
349
|
+
* app-roll moves it onto each release's image after that.
|
|
350
|
+
*/
|
|
351
|
+
export async function ensureMcpAuthProxyServiceGcp(
|
|
352
|
+
target: ReleaseServicesTarget,
|
|
353
|
+
run: GcloudRunner,
|
|
354
|
+
): Promise<EnsureOutcome> {
|
|
355
|
+
const name = fixedCloudRunServiceName("gcp", target.env, "mcp-auth-proxy") as string;
|
|
356
|
+
const id = `Cloud Run service ${name} (the MCP-Auth-Proxy)`;
|
|
357
|
+
const existing = await run([
|
|
358
|
+
"run",
|
|
359
|
+
"services",
|
|
360
|
+
"describe",
|
|
361
|
+
name,
|
|
362
|
+
`--region=${target.region}`,
|
|
363
|
+
`--project=${target.projectId}`,
|
|
364
|
+
"--format=value(metadata.name)",
|
|
365
|
+
]);
|
|
366
|
+
if (existing.ok) return { id, status: "present" };
|
|
367
|
+
if (!NOT_FOUND.test(existing.stderr)) {
|
|
368
|
+
return { id, status: "failed", error: existing.stderr.trim() };
|
|
369
|
+
}
|
|
370
|
+
const deployed = await run(gcpProxyDeployArgs(target));
|
|
371
|
+
return deployed.ok
|
|
372
|
+
? { id, status: "applied" }
|
|
373
|
+
: { id, status: "failed", error: deployed.stderr.trim() };
|
|
374
|
+
}
|
|
@@ -48,7 +48,7 @@ describe.each(["gcp", "gcp-firebase"])("%s stack — MCP through IAP", (provider
|
|
|
48
48
|
env: "production",
|
|
49
49
|
domain: "d.com",
|
|
50
50
|
region: "r",
|
|
51
|
-
databaseUrls: databaseUrls("
|
|
51
|
+
databaseUrls: databaseUrls("db1.p-label.europe-west1.sql-psa.goog"),
|
|
52
52
|
});
|
|
53
53
|
expect(env).toContainEqual({
|
|
54
54
|
name: "CAELO_MCP_IAP_SERVICE_ACCOUNT",
|
package/src/media-volume.test.ts
CHANGED
|
@@ -152,7 +152,7 @@ describe("admin media volume contract", () => {
|
|
|
152
152
|
domain: "acme.com",
|
|
153
153
|
stagingHost: "staging.acme.com",
|
|
154
154
|
region: "europe-west1",
|
|
155
|
-
databaseUrls: databaseUrls("
|
|
155
|
+
databaseUrls: databaseUrls("db1.p-label.europe-west1.sql-psa.goog"),
|
|
156
156
|
});
|
|
157
157
|
expect(env).toContainEqual({ name: "MEDIA_ROOT_DIR", value: MEDIA_MOUNT_PATH });
|
|
158
158
|
});
|
|
@@ -45,7 +45,7 @@ describe("admin env contract — operator access", () => {
|
|
|
45
45
|
domain: "acme.com",
|
|
46
46
|
stagingHost: "staging.acme.com",
|
|
47
47
|
region: "europe-west1",
|
|
48
|
-
databaseUrls: databaseUrls("
|
|
48
|
+
databaseUrls: databaseUrls("db1.p-label.europe-west1.sql-psa.goog"),
|
|
49
49
|
});
|
|
50
50
|
expect(env).toContainEqual({
|
|
51
51
|
name: "CAELO_OPERATOR_ACCESS_JOB",
|
|
@@ -51,7 +51,7 @@ const target: OperatorAccessSyncTarget = {
|
|
|
51
51
|
imageRef: "europe-west1-docker.pkg.dev/r/admin@sha256:new",
|
|
52
52
|
network: "net",
|
|
53
53
|
subnet: "sub",
|
|
54
|
-
databaseHost: "
|
|
54
|
+
databaseHost: "db1.p-label.europe-west1.sql-psa.goog",
|
|
55
55
|
};
|
|
56
56
|
|
|
57
57
|
const roleJson = JSON.stringify({ includedPermissions: [...OPERATOR_ACCESS_ROLE.permissions] });
|
|
@@ -201,7 +201,7 @@ describe("ensureOperatorAccessSync", () => {
|
|
|
201
201
|
"--args=--bun,/app/packages/admin-core/src/security/operator-access/sync-job.ts",
|
|
202
202
|
);
|
|
203
203
|
expect(deploy).toContain(
|
|
204
|
-
"--set-env-vars=^|^CAELO_OPERATOR_ACCESS_IAP_WEB=cloud_run-europe-west1/services/caelo-production-admin-abc|CAELO_MCP_IAP_SERVICE_ACCOUNT=caelo-mcp@p.iam.gserviceaccount.com|CAELO_OPERATOR_ACCESS_STATIC_MEMBERS=user:owner@x.com|CAELO_OPERATOR_ACCESS_DB_HOST=
|
|
204
|
+
"--set-env-vars=^|^CAELO_OPERATOR_ACCESS_IAP_WEB=cloud_run-europe-west1/services/caelo-production-admin-abc|CAELO_MCP_IAP_SERVICE_ACCOUNT=caelo-mcp@p.iam.gserviceaccount.com|CAELO_OPERATOR_ACCESS_STATIC_MEMBERS=user:owner@x.com|CAELO_OPERATOR_ACCESS_DB_HOST=db1.p-label.europe-west1.sql-psa.goog|CAELO_OPERATOR_OAUTH_CLIENT_SECRET=projects/p/secrets/caelo-production-operator-oauth-client",
|
|
205
205
|
);
|
|
206
206
|
});
|
|
207
207
|
|
|
@@ -322,7 +322,8 @@ describe("resolveOperatorAccessTarget", () => {
|
|
|
322
322
|
env: [
|
|
323
323
|
{
|
|
324
324
|
name: "ADMIN_DATABASE_URL",
|
|
325
|
-
value:
|
|
325
|
+
value:
|
|
326
|
+
"postgresql://admin_role@db1.p-label.europe-west1.sql-psa.goog:5432/cms_admin",
|
|
326
327
|
},
|
|
327
328
|
],
|
|
328
329
|
},
|
|
@@ -351,7 +352,7 @@ describe("resolveOperatorAccessTarget", () => {
|
|
|
351
352
|
imageRef: "img@sha256:new",
|
|
352
353
|
network: "net",
|
|
353
354
|
subnet: "sub",
|
|
354
|
-
databaseHost: "
|
|
355
|
+
databaseHost: "db1.p-label.europe-west1.sql-psa.goog",
|
|
355
356
|
},
|
|
356
357
|
});
|
|
357
358
|
});
|
|
@@ -240,6 +240,23 @@ describe("ReleaseManifestSchema", () => {
|
|
|
240
240
|
const partial = { ...MANIFEST, platformDigests: { admin: d("1"), gateway: d("2") } };
|
|
241
241
|
expect(ReleaseManifestSchema.safeParse(partial).success).toBe(false);
|
|
242
242
|
});
|
|
243
|
+
|
|
244
|
+
it("#1328: mcp-auth-proxy is optional (older signed manifests lack it) and checked when present", () => {
|
|
245
|
+
// The canonical fixture is a pre-#1328 manifest: it names no proxy image.
|
|
246
|
+
expect("mcp-auth-proxy" in MANIFEST.digests).toBe(false);
|
|
247
|
+
const digests = { ...MANIFEST.digests, "mcp-auth-proxy": d("4") };
|
|
248
|
+
const platformDigests = {
|
|
249
|
+
admin: d("1"),
|
|
250
|
+
gateway: d("2"),
|
|
251
|
+
ops: d("3"),
|
|
252
|
+
"mcp-auth-proxy": d("5"),
|
|
253
|
+
};
|
|
254
|
+
const parsed = ReleaseManifestSchema.safeParse({ ...MANIFEST, digests, platformDigests });
|
|
255
|
+
expect(parsed.success && parsed.data.digests["mcp-auth-proxy"]).toBe(d("4"));
|
|
256
|
+
expect(parsed.success && parsed.data.platformDigests?.["mcp-auth-proxy"]).toBe(d("5"));
|
|
257
|
+
const bad = { ...MANIFEST, digests: { ...MANIFEST.digests, "mcp-auth-proxy": "sha256:nope" } };
|
|
258
|
+
expect(ReleaseManifestSchema.safeParse(bad).success).toBe(false);
|
|
259
|
+
});
|
|
243
260
|
});
|
|
244
261
|
|
|
245
262
|
describe("compareImageDigests", () => {
|
|
@@ -270,6 +287,14 @@ describe("compareImageDigests", () => {
|
|
|
270
287
|
{ image: "admin", form: "platform", expected: null, actual: d("1") },
|
|
271
288
|
]);
|
|
272
289
|
});
|
|
290
|
+
|
|
291
|
+
it("#1328: a resolved image the manifest does not sign (a pre-proxy release) never matches", () => {
|
|
292
|
+
expect(compareImageDigests(MANIFEST, { "mcp-auth-proxy": d("4") })).toEqual([
|
|
293
|
+
{ image: "mcp-auth-proxy", form: "index", expected: null, actual: d("4") },
|
|
294
|
+
]);
|
|
295
|
+
const signed = { ...MANIFEST, digests: { ...MANIFEST.digests, "mcp-auth-proxy": d("4") } };
|
|
296
|
+
expect(compareImageDigests(signed, { "mcp-auth-proxy": d("4") })).toEqual([]);
|
|
297
|
+
});
|
|
273
298
|
});
|
|
274
299
|
|
|
275
300
|
describe("assessAutoInstall", () => {
|
package/src/release-manifest.ts
CHANGED
|
@@ -35,7 +35,11 @@ import {
|
|
|
35
35
|
type ReleaseManifest,
|
|
36
36
|
} from "@caelo-cms/shared";
|
|
37
37
|
import { infraStepsText } from "./infra-contract.js";
|
|
38
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
RELEASE_OIDC_ISSUER,
|
|
40
|
+
RELEASE_WORKFLOWS_URL,
|
|
41
|
+
releaseImageRef,
|
|
42
|
+
} from "./upgrade/release-images.js";
|
|
39
43
|
|
|
40
44
|
// The manifest's shape and location live in @caelo-cms/shared (the admin's
|
|
41
45
|
// auto-update scheduler reads them too); re-exported so this module stays
|
|
@@ -223,11 +227,58 @@ export interface DigestMismatch {
|
|
|
223
227
|
readonly image: ReleaseImage;
|
|
224
228
|
/** Which digest differs: the image index, or its linux/amd64 manifest (#1025). */
|
|
225
229
|
readonly form: "index" | "platform";
|
|
226
|
-
/**
|
|
230
|
+
/**
|
|
231
|
+
* The manifest's digest; null when the manifest names none for this form
|
|
232
|
+
* (no platform digests, or an image the release predates —
|
|
233
|
+
* OPTIONAL_RELEASE_IMAGES).
|
|
234
|
+
*/
|
|
227
235
|
readonly expected: string | null;
|
|
228
236
|
readonly actual: string | null;
|
|
229
237
|
}
|
|
230
238
|
|
|
239
|
+
/** One service's digest-pinned image in the public mirror. */
|
|
240
|
+
export interface ManifestServiceImage {
|
|
241
|
+
readonly digest: string;
|
|
242
|
+
readonly imageRef: string;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* The images the app-roll moves the Cloud Run services onto, from the signed
|
|
247
|
+
* manifest's digests — never a registry tag lookup.
|
|
248
|
+
*
|
|
249
|
+
* Fails for a manifest without an MCP-Auth-Proxy digest: such a release
|
|
250
|
+
* predates #1328, and this code rolls three services, so installing it
|
|
251
|
+
* would leave the proxy on an image of another release.
|
|
252
|
+
*/
|
|
253
|
+
export function manifestServiceImages(m: Pick<ReleaseManifest, "version" | "digests">):
|
|
254
|
+
| {
|
|
255
|
+
readonly ok: true;
|
|
256
|
+
readonly images: Readonly<
|
|
257
|
+
Record<"admin" | "gateway" | "mcp-auth-proxy", ManifestServiceImage>
|
|
258
|
+
>;
|
|
259
|
+
}
|
|
260
|
+
| { readonly ok: false; readonly error: string } {
|
|
261
|
+
const proxy = m.digests["mcp-auth-proxy"];
|
|
262
|
+
if (!proxy) {
|
|
263
|
+
return {
|
|
264
|
+
ok: false,
|
|
265
|
+
error: `the signed manifest of ${m.version} names no mcp-auth-proxy image: that release predates the MCP-Auth-Proxy (#1328) and cannot be installed by this release's upgrader. Install it from a terminal with the version's own CLI (bunx @caelo-cms/provisioning@${m.version} upgrade).`,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
const image = (slug: string, digest: string) => ({
|
|
269
|
+
digest,
|
|
270
|
+
imageRef: releaseImageRef(slug, digest),
|
|
271
|
+
});
|
|
272
|
+
return {
|
|
273
|
+
ok: true,
|
|
274
|
+
images: {
|
|
275
|
+
admin: image("admin", m.digests.admin),
|
|
276
|
+
gateway: image("gateway", m.digests.gateway),
|
|
277
|
+
"mcp-auth-proxy": image("mcp-auth-proxy", proxy),
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
231
282
|
/**
|
|
232
283
|
* Compare the digests a registry resolved for the release's tags with the
|
|
233
284
|
* signed manifest. Only images present in `resolved` (or `resolvedPlatform`)
|
|
@@ -246,9 +297,13 @@ export function compareImageDigests(
|
|
|
246
297
|
const out: DigestMismatch[] = [];
|
|
247
298
|
for (const image of RELEASE_IMAGES) {
|
|
248
299
|
if (image in resolved) {
|
|
300
|
+
// An image the signed manifest does not name (a release from before
|
|
301
|
+
// the image existed) can never match: a caller that resolved it is
|
|
302
|
+
// asking about an image the signature does not cover.
|
|
249
303
|
const actual = resolved[image] ?? null;
|
|
250
|
-
|
|
251
|
-
|
|
304
|
+
const expected = manifest.digests[image] ?? null;
|
|
305
|
+
if (actual === null || actual !== expected) {
|
|
306
|
+
out.push({ image, form: "index", expected, actual });
|
|
252
307
|
}
|
|
253
308
|
}
|
|
254
309
|
if (image in resolvedPlatform) {
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
adminEnvContract,
|
|
16
16
|
type CloudRunEnvVar,
|
|
17
17
|
databaseUrls,
|
|
18
|
-
|
|
18
|
+
mcpAuthProxyEnvContract,
|
|
19
19
|
parseRemoteMcpIdentityConfig,
|
|
20
20
|
REMOTE_MCP_ENV,
|
|
21
21
|
REMOTE_MCP_IDENTITY_CONFIG,
|
|
@@ -41,24 +41,28 @@ const contractInputs = {
|
|
|
41
41
|
domain: "viu.one",
|
|
42
42
|
region: "europe-west1",
|
|
43
43
|
stagingHost: "staging.viu.one",
|
|
44
|
-
databaseUrls: databaseUrls("
|
|
44
|
+
databaseUrls: databaseUrls("db1.p-label.europe-west1.sql-psa.goog"),
|
|
45
45
|
} as const;
|
|
46
46
|
const contract = {
|
|
47
47
|
admin: adminEnvContract({
|
|
48
48
|
...contractInputs,
|
|
49
49
|
firebaseSiteId: "caelo-production-site-eb4697",
|
|
50
|
-
gatewayService: "caelo-production-gateway
|
|
50
|
+
gatewayService: "caelo-production-data-gateway",
|
|
51
51
|
}),
|
|
52
|
-
|
|
52
|
+
// #1328 — the broker's identity env is the MCP-Auth-Proxy's, the service
|
|
53
|
+
// that was the gateway (E13 a), so its URL is the old gateway's.
|
|
54
|
+
"mcp-auth-proxy": mcpAuthProxyEnvContract(contractInputs),
|
|
53
55
|
};
|
|
54
56
|
|
|
57
|
+
type IdentitySlug = keyof typeof contract;
|
|
58
|
+
|
|
55
59
|
/** The env a stack declares for one service, given the config value. */
|
|
56
|
-
function declared(slug:
|
|
60
|
+
function declared(slug: IdentitySlug, config: string | undefined): CloudRunEnvVar[] {
|
|
57
61
|
return [...contract[slug], ...remoteMcpIdentityStackEnv(config)[slug]];
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
/** The live services after an installer run plus its old gcloud post-step: gcloud reorders. */
|
|
61
|
-
function live(slug:
|
|
65
|
+
function live(slug: IdentitySlug): CloudRunEnvVar[] {
|
|
62
66
|
return [...contract[slug], ...remoteMcpIdentityEnv(identity)[slug]].reverse();
|
|
63
67
|
}
|
|
64
68
|
|
|
@@ -89,7 +93,11 @@ describe("remote-MCP identity as stack config (#1082)", () => {
|
|
|
89
93
|
|
|
90
94
|
it("declares nothing while the config is unset (a fresh install's first up)", () => {
|
|
91
95
|
expect(parseRemoteMcpIdentityConfig(undefined)).toBeNull();
|
|
92
|
-
expect(remoteMcpIdentityStackEnv(undefined)).toEqual({
|
|
96
|
+
expect(remoteMcpIdentityStackEnv(undefined)).toEqual({
|
|
97
|
+
admin: [],
|
|
98
|
+
gateway: [],
|
|
99
|
+
"mcp-auth-proxy": [],
|
|
100
|
+
});
|
|
93
101
|
});
|
|
94
102
|
|
|
95
103
|
it("refuses a corrupt or foreign value loudly instead of dropping the vars", () => {
|
|
@@ -111,9 +119,11 @@ describe("remote-MCP identity as stack config (#1082)", () => {
|
|
|
111
119
|
.admin.map((v) => v.name)
|
|
112
120
|
.sort(),
|
|
113
121
|
).toEqual(Object.values(REMOTE_MCP_ENV).sort());
|
|
122
|
+
// #1328 — the visitor gateway carries none of it.
|
|
123
|
+
expect(remoteMcpIdentityStackEnv(serializeRemoteMcpIdentity(identity)).gateway).toEqual([]);
|
|
114
124
|
});
|
|
115
125
|
|
|
116
|
-
it.each(["admin", "
|
|
126
|
+
it.each(["admin", "mcp-auth-proxy"] as const)(
|
|
117
127
|
"%s: with the config the CLI writes, a refreshed plan removes and changes nothing",
|
|
118
128
|
(slug) => {
|
|
119
129
|
const plan = envPlan(declared(slug, serializeRemoteMcpIdentity(identity)), live(slug));
|
|
@@ -128,7 +138,7 @@ describe("remote-MCP identity as stack config (#1082)", () => {
|
|
|
128
138
|
"CAELO_IAP_AUDIENCE",
|
|
129
139
|
"CAELO_MCP_PUBLIC_URL",
|
|
130
140
|
]);
|
|
131
|
-
expect(envPlan(declared("
|
|
141
|
+
expect(envPlan(declared("mcp-auth-proxy", undefined), live("mcp-auth-proxy")).removed).toEqual([
|
|
132
142
|
"CAELO_ADMIN_URL",
|
|
133
143
|
"CAELO_MCP_PUBLIC_URL",
|
|
134
144
|
]);
|