@caelo-cms/provisioning 0.11.9 → 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 +44 -2
- 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
package/src/remote-mcp.ts
CHANGED
|
@@ -12,13 +12,13 @@
|
|
|
12
12
|
* (#1082: the stack is their one owner — stack-contract.ts says why
|
|
13
13
|
* they cannot be plain resource inputs), and
|
|
14
14
|
* {@link checkRemoteMcpIdentityEnv}, the read-only check after it.
|
|
15
|
-
* - {@link ensureRemoteMcpEdge} (`gcp` only): the
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
15
|
+
* - {@link ensureRemoteMcpEdge} (`gcp` only): the MCP-Auth-Proxy's
|
|
16
|
+
* load-balancer part (#1328) — its service, serverless NEG, backend and
|
|
17
|
+
* own Cloud Armor policy with the `/api/mcp/*` rule (remote-mcp-edge.ts),
|
|
18
|
+
* and the URL map's rule sending the remote-MCP paths to it (the stack
|
|
19
|
+
* ignores `pathRules` after creation, gcp-url-map.ts).
|
|
20
20
|
*
|
|
21
|
-
* The secrets, the IAM bindings, the
|
|
21
|
+
* The secrets, the IAM bindings, the proxy's secret volume and timeout
|
|
22
22
|
* come through the shared contract like everything else
|
|
23
23
|
* (runtime-secrets.ts, stack-contract.ts, upgrade/app-roll.ts).
|
|
24
24
|
*
|
|
@@ -28,13 +28,14 @@
|
|
|
28
28
|
*
|
|
29
29
|
* Nothing here enables remote MCP: that happens when the Owner sets up the
|
|
30
30
|
* install's Google sign-in client (Settings → Google sign-in). Until then
|
|
31
|
-
* the
|
|
31
|
+
* the MCP-Auth-Proxy answers its fixed 404.
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
|
-
import {
|
|
34
|
+
import { GCP_MCP_AUTH_PROXY_PATHS } from "@caelo-cms/shared";
|
|
35
35
|
import { gcloud as defaultGcloud } from "./gcloud.js";
|
|
36
36
|
import type { GcloudRunner } from "./gcloud-retry.js";
|
|
37
|
-
import { gcpNamePrefix } from "./gcp-names.js";
|
|
37
|
+
import { gcpNamePrefix, mcpAuthProxyEdgeNames } from "./gcp-names.js";
|
|
38
|
+
import { ensureMcpAuthProxyServiceGcp } from "./mcp-auth-proxy-setup.js";
|
|
38
39
|
import {
|
|
39
40
|
isMcpArmorRule,
|
|
40
41
|
type LiveArmorRule,
|
|
@@ -43,7 +44,10 @@ import {
|
|
|
43
44
|
} from "./remote-mcp-edge.js";
|
|
44
45
|
import type { HttpFetch } from "./runtime-secrets.js";
|
|
45
46
|
import {
|
|
47
|
+
CLOUD_RUN_SLUGS,
|
|
46
48
|
type CloudRunSlug,
|
|
49
|
+
cloudRunServicePrefix,
|
|
50
|
+
fixedCloudRunServiceName,
|
|
47
51
|
type GcpProvider,
|
|
48
52
|
REMOTE_MCP_ENV,
|
|
49
53
|
type RemoteMcpIdentity,
|
|
@@ -209,12 +213,15 @@ async function resolveServiceIdentity(
|
|
|
209
213
|
const value = r.ok ? r.stdout.trim() : "";
|
|
210
214
|
return value.startsWith("https://") ? value : null;
|
|
211
215
|
};
|
|
216
|
+
// #1328 — the broker answers on the MCP-Auth-Proxy's URL: the service
|
|
217
|
+
// that was the gateway before (E13 a), so the URL connectors hold stays.
|
|
218
|
+
const proxyService = target.services["mcp-auth-proxy"];
|
|
212
219
|
const adminUrl = await url(target.services.admin);
|
|
213
|
-
const
|
|
214
|
-
if (!adminUrl || !
|
|
220
|
+
const proxyUrl = await url(proxyService);
|
|
221
|
+
if (!adminUrl || !proxyUrl) {
|
|
215
222
|
return {
|
|
216
223
|
ok: false,
|
|
217
|
-
error: `read the Cloud Run URL of the ${adminUrl ? "
|
|
224
|
+
error: `read the Cloud Run URL of the ${adminUrl ? "MCP-Auth-Proxy" : "admin"} service (${adminUrl ? proxyService : target.services.admin})`,
|
|
218
225
|
};
|
|
219
226
|
}
|
|
220
227
|
return {
|
|
@@ -225,7 +232,7 @@ async function resolveServiceIdentity(
|
|
|
225
232
|
region: target.region,
|
|
226
233
|
adminServiceName: target.services.admin,
|
|
227
234
|
adminServiceUrl: adminUrl,
|
|
228
|
-
|
|
235
|
+
mcpAuthProxyServiceUrl: proxyUrl,
|
|
229
236
|
}),
|
|
230
237
|
};
|
|
231
238
|
}
|
|
@@ -262,8 +269,15 @@ export async function findRemoteMcpTarget(
|
|
|
262
269
|
): Promise<RemoteMcpResult<{ readonly target: RemoteMcpTarget }>> {
|
|
263
270
|
const run = deps.run ?? defaultGcloud;
|
|
264
271
|
const services: Partial<Record<CloudRunSlug, string>> = {};
|
|
265
|
-
for (const slug of
|
|
266
|
-
|
|
272
|
+
for (const slug of CLOUD_RUN_SLUGS) {
|
|
273
|
+
// A service created under a fixed name needs no lookup — and on `gcp`
|
|
274
|
+
// the MCP-Auth-Proxy is created after the stack's first `up`.
|
|
275
|
+
const fixed = fixedCloudRunServiceName(base.provider, base.env, slug);
|
|
276
|
+
if (fixed) {
|
|
277
|
+
services[slug] = fixed;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
const prefix = cloudRunServicePrefix(base.provider, base.env, slug);
|
|
267
281
|
const lookup = await resolveGcpResourceName(
|
|
268
282
|
run,
|
|
269
283
|
"run-service",
|
|
@@ -331,8 +345,8 @@ export async function checkRemoteMcpIdentityEnv(
|
|
|
331
345
|
): Promise<RemoteMcpResult<{ readonly drift: Record<CloudRunSlug, EnvChange[]> }>> {
|
|
332
346
|
const run = deps.run ?? defaultGcloud;
|
|
333
347
|
const desired = remoteMcpIdentityEnv(identity);
|
|
334
|
-
const drift: Record<CloudRunSlug, EnvChange[]> = { admin: [], gateway: [] };
|
|
335
|
-
for (const slug of
|
|
348
|
+
const drift: Record<CloudRunSlug, EnvChange[]> = { admin: [], gateway: [], "mcp-auth-proxy": [] };
|
|
349
|
+
for (const slug of CLOUD_RUN_SLUGS) {
|
|
336
350
|
const described = await describeService(target, target.services[slug], run);
|
|
337
351
|
if (!described.ok) return described;
|
|
338
352
|
const plan = planEnvUpdate(liveContainerEnv(described.json), desired[slug]);
|
|
@@ -364,16 +378,32 @@ export interface LiveUrlMap {
|
|
|
364
378
|
}
|
|
365
379
|
|
|
366
380
|
export type UrlMapPathsPlan =
|
|
367
|
-
| {
|
|
381
|
+
| {
|
|
382
|
+
readonly ok: true;
|
|
383
|
+
readonly map: LiveUrlMap;
|
|
384
|
+
/** What changed, for the log; empty when the map was already right. */
|
|
385
|
+
readonly changes: string[];
|
|
386
|
+
}
|
|
368
387
|
| { readonly ok: false; readonly error: string };
|
|
369
388
|
|
|
389
|
+
/** Whether a rule's `service` (a full self-link) is the backend service named `name`. */
|
|
390
|
+
function targetsBackend(rule: UrlMapPathRule, name: string): boolean {
|
|
391
|
+
return rule.service?.endsWith(`/backendServices/${name}`) ?? false;
|
|
392
|
+
}
|
|
393
|
+
|
|
370
394
|
/**
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
*
|
|
395
|
+
* #1328 — the URL map with the remote-MCP paths ({@link GCP_MCP_AUTH_PROXY_PATHS})
|
|
396
|
+
* on their own rule to the MCP-Auth-Proxy's backend, and none of them left
|
|
397
|
+
* on the gateway's `/api/*` rule (#806 put the discovery paths there). Only
|
|
398
|
+
* those two rules change; redirect rules the admin wrote stay as they are.
|
|
399
|
+
*
|
|
400
|
+
* @param proxyBackend the proxy backend's self-link
|
|
375
401
|
*/
|
|
376
|
-
export function planUrlMapPaths(
|
|
402
|
+
export function planUrlMapPaths(
|
|
403
|
+
maps: readonly LiveUrlMap[],
|
|
404
|
+
siteHost: string,
|
|
405
|
+
proxyBackend: string,
|
|
406
|
+
): UrlMapPathsPlan {
|
|
377
407
|
const routing = maps.flatMap((m) =>
|
|
378
408
|
(m.hostRules ?? [])
|
|
379
409
|
.filter((h) => h.hosts.includes(siteHost))
|
|
@@ -387,47 +417,69 @@ export function planUrlMapPaths(maps: readonly LiveUrlMap[], siteHost: string):
|
|
|
387
417
|
}
|
|
388
418
|
const { map, pathMatcher } = routing[0] as (typeof routing)[number];
|
|
389
419
|
const matcher = (map.pathMatchers ?? []).find((m) => m.name === pathMatcher);
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
)
|
|
393
|
-
if (!matcher || !gatewayRule) {
|
|
420
|
+
const isGatewayRule = (r: UrlMapPathRule) =>
|
|
421
|
+
r.service !== undefined && r.urlRedirect === undefined && r.paths.includes("/api/*");
|
|
422
|
+
if (!matcher || !(matcher.pathRules ?? []).some(isGatewayRule)) {
|
|
394
423
|
return {
|
|
395
424
|
ok: false,
|
|
396
425
|
error: `URL map ${map.name} has no /api/* rule to the gateway in path matcher ${pathMatcher}; re-run the installer to restore the load balancer`,
|
|
397
426
|
};
|
|
398
427
|
}
|
|
399
|
-
const
|
|
400
|
-
|
|
428
|
+
const proxyName = proxyBackend.slice(proxyBackend.lastIndexOf("/") + 1);
|
|
429
|
+
const changes: string[] = [];
|
|
430
|
+
const rules: UrlMapPathRule[] = [];
|
|
431
|
+
let proxyRule = false;
|
|
432
|
+
for (const r of matcher.pathRules ?? []) {
|
|
433
|
+
if (isGatewayRule(r)) {
|
|
434
|
+
const moved = r.paths.filter((p) => GCP_MCP_AUTH_PROXY_PATHS.includes(p));
|
|
435
|
+
if (moved.length > 0) changes.push(`moved ${moved.join(", ")} off the gateway's rule`);
|
|
436
|
+
rules.push({ ...r, paths: r.paths.filter((p) => !GCP_MCP_AUTH_PROXY_PATHS.includes(p)) });
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
if (targetsBackend(r, proxyName)) {
|
|
440
|
+
proxyRule = true;
|
|
441
|
+
const same =
|
|
442
|
+
r.paths.length === GCP_MCP_AUTH_PROXY_PATHS.length &&
|
|
443
|
+
GCP_MCP_AUTH_PROXY_PATHS.every((p) => r.paths.includes(p));
|
|
444
|
+
if (!same)
|
|
445
|
+
changes.push(`set the MCP-Auth-Proxy rule to ${GCP_MCP_AUTH_PROXY_PATHS.join(", ")}`);
|
|
446
|
+
rules.push({ ...r, paths: [...GCP_MCP_AUTH_PROXY_PATHS] });
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
rules.push(r);
|
|
450
|
+
}
|
|
451
|
+
if (!proxyRule) {
|
|
452
|
+
changes.push(`routed ${GCP_MCP_AUTH_PROXY_PATHS.join(", ")} to the MCP-Auth-Proxy`);
|
|
453
|
+
// Before the admin's redirect rules: GCP orders nothing by position for
|
|
454
|
+
// path rules (the longest match wins), but a stable place keeps diffs small.
|
|
455
|
+
const at = rules.findIndex(isGatewayRule) + 1;
|
|
456
|
+
rules.splice(at, 0, { paths: [...GCP_MCP_AUTH_PROXY_PATHS], service: proxyBackend });
|
|
457
|
+
}
|
|
458
|
+
if (changes.length === 0) return { ok: true, map, changes };
|
|
401
459
|
const next: LiveUrlMap = {
|
|
402
460
|
...map,
|
|
403
461
|
pathMatchers: (map.pathMatchers ?? []).map((m) =>
|
|
404
|
-
m !== matcher
|
|
405
|
-
? m
|
|
406
|
-
: {
|
|
407
|
-
...m,
|
|
408
|
-
pathRules: (m.pathRules ?? []).map((r) =>
|
|
409
|
-
r === gatewayRule ? { ...r, paths: [...r.paths, ...added] } : r,
|
|
410
|
-
),
|
|
411
|
-
},
|
|
462
|
+
m !== matcher ? m : { ...m, pathRules: rules },
|
|
412
463
|
),
|
|
413
464
|
};
|
|
414
|
-
return { ok: true, map: next,
|
|
465
|
+
return { ok: true, map: next, changes };
|
|
415
466
|
}
|
|
416
467
|
|
|
417
468
|
const COMPUTE_API = "https://compute.googleapis.com/compute/v1";
|
|
418
469
|
const OPERATION_POLLS = 60;
|
|
419
470
|
|
|
420
471
|
/**
|
|
421
|
-
* Route the remote-MCP
|
|
422
|
-
*
|
|
423
|
-
*
|
|
424
|
-
*
|
|
425
|
-
*
|
|
472
|
+
* Route the remote-MCP paths to the MCP-Auth-Proxy on a `gcp` install: read
|
|
473
|
+
* the URL map, apply {@link planUrlMapPaths} and write it back through the
|
|
474
|
+
* Compute API (`urlMaps.update` carries the map's fingerprint, so a
|
|
475
|
+
* concurrent write — the admin publishing redirects — fails instead of
|
|
476
|
+
* being lost; a re-run then succeeds).
|
|
426
477
|
*/
|
|
427
478
|
export async function ensureUrlMapRemoteMcpPaths(
|
|
428
479
|
target: Pick<RemoteMcpTarget, "projectId" | "domain">,
|
|
480
|
+
proxyBackend: string,
|
|
429
481
|
deps: { run?: GcloudRunner; http?: HttpFetch; sleep?: (ms: number) => Promise<void> } = {},
|
|
430
|
-
): Promise<RemoteMcpResult<{ readonly
|
|
482
|
+
): Promise<RemoteMcpResult<{ readonly changes: string[] }>> {
|
|
431
483
|
const run = deps.run ?? defaultGcloud;
|
|
432
484
|
const http = deps.http ?? (fetch as unknown as HttpFetch);
|
|
433
485
|
const sleep = deps.sleep ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)));
|
|
@@ -442,9 +494,9 @@ export async function ensureUrlMapRemoteMcpPaths(
|
|
|
442
494
|
if (!list.ok) return { ok: false, error: `list URL maps: ${list.stderr.trim()}` };
|
|
443
495
|
const maps = parseJson(list.stdout || "[]");
|
|
444
496
|
if (!Array.isArray(maps)) return { ok: false, error: "list URL maps: output is not a JSON list" };
|
|
445
|
-
const plan = planUrlMapPaths(maps as LiveUrlMap[], target.domain);
|
|
497
|
+
const plan = planUrlMapPaths(maps as LiveUrlMap[], target.domain, proxyBackend);
|
|
446
498
|
if (!plan.ok) return plan;
|
|
447
|
-
if (plan.
|
|
499
|
+
if (plan.changes.length === 0) return { ok: true, changes: [] };
|
|
448
500
|
const token = await run(["auth", "print-access-token"]);
|
|
449
501
|
const bearer = token.ok ? token.stdout.trim() : "";
|
|
450
502
|
if (!bearer)
|
|
@@ -481,13 +533,43 @@ export async function ensureUrlMapRemoteMcpPaths(
|
|
|
481
533
|
error: `URL-map update failed: ${errors.map((e) => e.message).join("; ")}`,
|
|
482
534
|
};
|
|
483
535
|
}
|
|
484
|
-
return { ok: true,
|
|
536
|
+
return { ok: true, changes: plan.changes };
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** A Cloud Armor policy's rules, or why they could not be read; `missing` when it does not exist. */
|
|
540
|
+
async function describePolicyRules(
|
|
541
|
+
run: GcloudRunner,
|
|
542
|
+
policy: string,
|
|
543
|
+
project: string,
|
|
544
|
+
): Promise<
|
|
545
|
+
| { readonly ok: true; readonly rules: LiveArmorRule[] }
|
|
546
|
+
| { readonly ok: false; readonly missing: boolean; readonly error: string }
|
|
547
|
+
> {
|
|
548
|
+
const describe = await run([
|
|
549
|
+
"compute",
|
|
550
|
+
"security-policies",
|
|
551
|
+
"describe",
|
|
552
|
+
policy,
|
|
553
|
+
project,
|
|
554
|
+
"--format=json",
|
|
555
|
+
]);
|
|
556
|
+
if (!describe.ok) {
|
|
557
|
+
return { ok: false, missing: NOT_FOUND.test(describe.stderr), error: describe.stderr.trim() };
|
|
558
|
+
}
|
|
559
|
+
const described = parseJson(describe.stdout) as { rules?: LiveArmorRule[] } | null;
|
|
560
|
+
if (!described)
|
|
561
|
+
return { ok: false, missing: false, error: `describe ${policy}: output is not JSON` };
|
|
562
|
+
return { ok: true, rules: described.rules ?? [] };
|
|
485
563
|
}
|
|
486
564
|
|
|
565
|
+
const NOT_FOUND = /NOT_FOUND|not found|was not found|does not exist/i;
|
|
566
|
+
|
|
487
567
|
/**
|
|
488
|
-
*
|
|
489
|
-
* ({@link MCP_ARMOR_RULE}
|
|
490
|
-
* to spec.
|
|
568
|
+
* #1328 — the MCP-Auth-Proxy's own Cloud Armor policy with the `/api/mcp/*`
|
|
569
|
+
* rule ({@link MCP_ARMOR_RULE}; remote-mcp-edge.ts says why): created when
|
|
570
|
+
* missing, a rule at its priority brought back to spec. Everything else the
|
|
571
|
+
* policy's default rule allows: the proxy rate-limits every route itself
|
|
572
|
+
* (apps/mcp-auth-proxy rate-limit.ts).
|
|
491
573
|
*/
|
|
492
574
|
export async function ensureMcpArmorRule(
|
|
493
575
|
target: Pick<RemoteMcpTarget, "projectId" | "env">,
|
|
@@ -495,6 +577,56 @@ export async function ensureMcpArmorRule(
|
|
|
495
577
|
): Promise<
|
|
496
578
|
RemoteMcpResult<{ readonly policy: string; readonly action: "present" | "created" | "updated" }>
|
|
497
579
|
> {
|
|
580
|
+
const run = deps.run ?? defaultGcloud;
|
|
581
|
+
const project = `--project=${target.projectId}`;
|
|
582
|
+
const { policy } = mcpAuthProxyEdgeNames(target.env);
|
|
583
|
+
let described = await describePolicyRules(run, policy, project);
|
|
584
|
+
if (!described.ok && described.missing) {
|
|
585
|
+
const created = await run([
|
|
586
|
+
"compute",
|
|
587
|
+
"security-policies",
|
|
588
|
+
"create",
|
|
589
|
+
policy,
|
|
590
|
+
project,
|
|
591
|
+
`--description=Caelo ${target.env} MCP-Auth-Proxy — remote-MCP throttle`,
|
|
592
|
+
"--quiet",
|
|
593
|
+
]);
|
|
594
|
+
if (!created.ok) return { ok: false, error: `create ${policy}: ${created.stderr.trim()}` };
|
|
595
|
+
described = await describePolicyRules(run, policy, project);
|
|
596
|
+
}
|
|
597
|
+
if (!described.ok) return { ok: false, error: `describe ${policy}: ${described.error}` };
|
|
598
|
+
const existing = described.rules.find((r) => r.priority === MCP_ARMOR_RULE.priority);
|
|
599
|
+
if (existing && isMcpArmorRule(existing)) return { ok: true, policy, action: "present" };
|
|
600
|
+
const verb = existing ? "update" : "create";
|
|
601
|
+
const r = await run([
|
|
602
|
+
"compute",
|
|
603
|
+
"security-policies",
|
|
604
|
+
"rules",
|
|
605
|
+
verb,
|
|
606
|
+
String(MCP_ARMOR_RULE.priority),
|
|
607
|
+
`--security-policy=${policy}`,
|
|
608
|
+
project,
|
|
609
|
+
...mcpArmorRuleFlags(),
|
|
610
|
+
"--quiet",
|
|
611
|
+
]);
|
|
612
|
+
if (!r.ok)
|
|
613
|
+
return {
|
|
614
|
+
ok: false,
|
|
615
|
+
error: `${verb} rule ${MCP_ARMOR_RULE.priority} on ${policy}: ${r.stderr.trim()}`,
|
|
616
|
+
};
|
|
617
|
+
return { ok: true, policy, action: existing ? "updated" : "created" };
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* #1328 — drop the `/api/mcp/*` rule #806 put on the gateway's policy, once
|
|
622
|
+
* the URL map no longer sends those paths to the gateway: there it would
|
|
623
|
+
* only exempt matching paths from the OWASP packs and the ban. A rule at the
|
|
624
|
+
* priority that is not exactly the MCP rule is someone's own and stays.
|
|
625
|
+
*/
|
|
626
|
+
export async function retireGatewayMcpArmorRule(
|
|
627
|
+
target: Pick<RemoteMcpTarget, "projectId" | "env">,
|
|
628
|
+
deps: { run?: GcloudRunner } = {},
|
|
629
|
+
): Promise<RemoteMcpResult<{ readonly removed: string | null }>> {
|
|
498
630
|
const run = deps.run ?? defaultGcloud;
|
|
499
631
|
const project = `--project=${target.projectId}`;
|
|
500
632
|
const prefix = `${gcpNamePrefix(target.env)}-waf`;
|
|
@@ -515,56 +647,183 @@ export async function ensureMcpArmorRule(
|
|
|
515
647
|
};
|
|
516
648
|
}
|
|
517
649
|
const policy = names[0] as string;
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
policy,
|
|
523
|
-
project,
|
|
524
|
-
"--format=json",
|
|
525
|
-
]);
|
|
526
|
-
if (!describe.ok) return { ok: false, error: `describe ${policy}: ${describe.stderr.trim()}` };
|
|
527
|
-
const described = parseJson(describe.stdout) as { rules?: LiveArmorRule[] } | null;
|
|
528
|
-
if (!described) return { ok: false, error: `describe ${policy}: output is not JSON` };
|
|
529
|
-
const rules = described.rules ?? [];
|
|
530
|
-
const existing = rules.find((r) => r.priority === MCP_ARMOR_RULE.priority);
|
|
531
|
-
if (existing && isMcpArmorRule(existing)) return { ok: true, policy, action: "present" };
|
|
532
|
-
const verb = existing ? "update" : "create";
|
|
650
|
+
const described = await describePolicyRules(run, policy, project);
|
|
651
|
+
if (!described.ok) return { ok: false, error: `describe ${policy}: ${described.error}` };
|
|
652
|
+
const rule = described.rules.find((r) => r.priority === MCP_ARMOR_RULE.priority);
|
|
653
|
+
if (!rule || !isMcpArmorRule(rule)) return { ok: true, removed: null };
|
|
533
654
|
const r = await run([
|
|
534
655
|
"compute",
|
|
535
656
|
"security-policies",
|
|
536
657
|
"rules",
|
|
537
|
-
|
|
658
|
+
"delete",
|
|
538
659
|
String(MCP_ARMOR_RULE.priority),
|
|
539
660
|
`--security-policy=${policy}`,
|
|
540
661
|
project,
|
|
541
|
-
...mcpArmorRuleFlags(),
|
|
542
662
|
"--quiet",
|
|
543
663
|
]);
|
|
544
|
-
if (!r.ok)
|
|
664
|
+
if (!r.ok) {
|
|
545
665
|
return {
|
|
546
666
|
ok: false,
|
|
547
|
-
error:
|
|
667
|
+
error: `delete rule ${MCP_ARMOR_RULE.priority} on ${policy}: ${r.stderr.trim()}`,
|
|
548
668
|
};
|
|
549
|
-
|
|
669
|
+
}
|
|
670
|
+
return { ok: true, removed: policy };
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* #1328 — the proxy's serverless NEG and backend service (EXTERNAL_MANAGED,
|
|
675
|
+
* its own Cloud Armor policy), created when missing. Returns the backend's
|
|
676
|
+
* self-link for the URL map.
|
|
677
|
+
*/
|
|
678
|
+
export async function ensureMcpAuthProxyBackend(
|
|
679
|
+
target: Pick<RemoteMcpTarget, "projectId" | "region" | "env">,
|
|
680
|
+
deps: { run?: GcloudRunner } = {},
|
|
681
|
+
): Promise<RemoteMcpResult<{ readonly backend: string; readonly done: string[] }>> {
|
|
682
|
+
const run = deps.run ?? defaultGcloud;
|
|
683
|
+
const project = `--project=${target.projectId}`;
|
|
684
|
+
const names = mcpAuthProxyEdgeNames(target.env);
|
|
685
|
+
const service = fixedCloudRunServiceName("gcp", target.env, "mcp-auth-proxy") as string;
|
|
686
|
+
const done: string[] = [];
|
|
687
|
+
const neg = await run([
|
|
688
|
+
"compute",
|
|
689
|
+
"network-endpoint-groups",
|
|
690
|
+
"describe",
|
|
691
|
+
names.neg,
|
|
692
|
+
`--region=${target.region}`,
|
|
693
|
+
project,
|
|
694
|
+
"--format=value(name)",
|
|
695
|
+
]);
|
|
696
|
+
if (!neg.ok) {
|
|
697
|
+
if (!NOT_FOUND.test(neg.stderr))
|
|
698
|
+
return { ok: false, error: `describe ${names.neg}: ${neg.stderr.trim()}` };
|
|
699
|
+
const created = await run([
|
|
700
|
+
"compute",
|
|
701
|
+
"network-endpoint-groups",
|
|
702
|
+
"create",
|
|
703
|
+
names.neg,
|
|
704
|
+
`--region=${target.region}`,
|
|
705
|
+
project,
|
|
706
|
+
"--network-endpoint-type=serverless",
|
|
707
|
+
`--cloud-run-service=${service}`,
|
|
708
|
+
"--quiet",
|
|
709
|
+
]);
|
|
710
|
+
if (!created.ok) return { ok: false, error: `create ${names.neg}: ${created.stderr.trim()}` };
|
|
711
|
+
done.push(`created serverless NEG ${names.neg} → ${service}`);
|
|
712
|
+
}
|
|
713
|
+
const backend = await run([
|
|
714
|
+
"compute",
|
|
715
|
+
"backend-services",
|
|
716
|
+
"describe",
|
|
717
|
+
names.backend,
|
|
718
|
+
"--global",
|
|
719
|
+
project,
|
|
720
|
+
"--format=json",
|
|
721
|
+
]);
|
|
722
|
+
let backendJson: { selfLink?: string; securityPolicy?: string; backends?: { group?: string }[] };
|
|
723
|
+
if (!backend.ok) {
|
|
724
|
+
if (!NOT_FOUND.test(backend.stderr)) {
|
|
725
|
+
return { ok: false, error: `describe ${names.backend}: ${backend.stderr.trim()}` };
|
|
726
|
+
}
|
|
727
|
+
const created = await run([
|
|
728
|
+
"compute",
|
|
729
|
+
"backend-services",
|
|
730
|
+
"create",
|
|
731
|
+
names.backend,
|
|
732
|
+
"--global",
|
|
733
|
+
project,
|
|
734
|
+
"--load-balancing-scheme=EXTERNAL_MANAGED",
|
|
735
|
+
"--protocol=HTTPS",
|
|
736
|
+
`--security-policy=${names.policy}`,
|
|
737
|
+
"--format=json",
|
|
738
|
+
"--quiet",
|
|
739
|
+
]);
|
|
740
|
+
if (!created.ok)
|
|
741
|
+
return { ok: false, error: `create ${names.backend}: ${created.stderr.trim()}` };
|
|
742
|
+
done.push(`created backend service ${names.backend} (Cloud Armor ${names.policy})`);
|
|
743
|
+
const parsed = parseJson(created.stdout);
|
|
744
|
+
backendJson = (Array.isArray(parsed) ? parsed[0] : parsed) ?? {};
|
|
745
|
+
} else {
|
|
746
|
+
backendJson = (parseJson(backend.stdout) ?? {}) as typeof backendJson;
|
|
747
|
+
if (!backendJson.securityPolicy?.endsWith(`/securityPolicies/${names.policy}`)) {
|
|
748
|
+
const set = await run([
|
|
749
|
+
"compute",
|
|
750
|
+
"backend-services",
|
|
751
|
+
"update",
|
|
752
|
+
names.backend,
|
|
753
|
+
"--global",
|
|
754
|
+
project,
|
|
755
|
+
`--security-policy=${names.policy}`,
|
|
756
|
+
"--quiet",
|
|
757
|
+
]);
|
|
758
|
+
if (!set.ok) return { ok: false, error: `attach ${names.policy}: ${set.stderr.trim()}` };
|
|
759
|
+
done.push(`attached Cloud Armor ${names.policy} to ${names.backend}`);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
if (
|
|
763
|
+
!(backendJson.backends ?? []).some((b) =>
|
|
764
|
+
b.group?.endsWith(`/networkEndpointGroups/${names.neg}`),
|
|
765
|
+
)
|
|
766
|
+
) {
|
|
767
|
+
const added = await run([
|
|
768
|
+
"compute",
|
|
769
|
+
"backend-services",
|
|
770
|
+
"add-backend",
|
|
771
|
+
names.backend,
|
|
772
|
+
"--global",
|
|
773
|
+
project,
|
|
774
|
+
`--network-endpoint-group=${names.neg}`,
|
|
775
|
+
`--network-endpoint-group-region=${target.region}`,
|
|
776
|
+
"--quiet",
|
|
777
|
+
]);
|
|
778
|
+
if (!added.ok)
|
|
779
|
+
return { ok: false, error: `add ${names.neg} to ${names.backend}: ${added.stderr.trim()}` };
|
|
780
|
+
done.push(`added ${names.neg} to ${names.backend}`);
|
|
781
|
+
}
|
|
782
|
+
const selfLink =
|
|
783
|
+
backendJson.selfLink ??
|
|
784
|
+
`https://www.googleapis.com/compute/v1/projects/${target.projectId}/global/backendServices/${names.backend}`;
|
|
785
|
+
return { ok: true, backend: selfLink, done };
|
|
550
786
|
}
|
|
551
787
|
|
|
552
|
-
/**
|
|
788
|
+
/**
|
|
789
|
+
* The MCP-Auth-Proxy's load-balancer part on `gcp`, in the order that never
|
|
790
|
+
* routes a request to something that is not there yet: its service (created
|
|
791
|
+
* once with the release's image; mcp-auth-proxy-setup.ts), its Cloud Armor
|
|
792
|
+
* policy, NEG and backend, the URL map's MCP rule, and last the old MCP rule
|
|
793
|
+
* off the gateway's policy. A no-op success on `gcp-firebase`, where the
|
|
794
|
+
* proxy is reached on its own run.app URL.
|
|
795
|
+
*/
|
|
553
796
|
export async function ensureRemoteMcpEdge(
|
|
554
|
-
target: Pick<RemoteMcpTarget, "provider" | "projectId" | "env" | "domain"
|
|
797
|
+
target: Pick<RemoteMcpTarget, "provider" | "projectId" | "region" | "env" | "domain"> & {
|
|
798
|
+
/** The release's digest-pinned proxy image, for a proxy service that does not exist yet. */
|
|
799
|
+
readonly mcpAuthProxyImageRef: string;
|
|
800
|
+
},
|
|
555
801
|
deps: { run?: GcloudRunner; http?: HttpFetch; sleep?: (ms: number) => Promise<void> } = {},
|
|
556
802
|
): Promise<RemoteMcpResult<{ readonly done: string[] }>> {
|
|
557
803
|
if (target.provider !== "gcp") return { ok: true, done: [] };
|
|
804
|
+
const run = deps.run ?? defaultGcloud;
|
|
558
805
|
const done: string[] = [];
|
|
559
|
-
const
|
|
806
|
+
const service = await ensureMcpAuthProxyServiceGcp({ ...target, provider: "gcp" }, run);
|
|
807
|
+
if (service.status === "failed")
|
|
808
|
+
return { ok: false, error: `${service.id}: ${service.error ?? ""}` };
|
|
809
|
+
done.push(`${service.id}: ${service.status}`);
|
|
810
|
+
const armor = await ensureMcpArmorRule(target, { run });
|
|
560
811
|
if (!armor.ok) return { ok: false, error: `Cloud Armor: ${armor.error}` };
|
|
561
812
|
done.push(`Cloud Armor rule ${MCP_ARMOR_RULE.priority} on ${armor.policy}: ${armor.action}`);
|
|
562
|
-
const
|
|
813
|
+
const backend = await ensureMcpAuthProxyBackend(target, { run });
|
|
814
|
+
if (!backend.ok) return { ok: false, error: `backend: ${backend.error}` };
|
|
815
|
+
done.push(...backend.done);
|
|
816
|
+
const paths = await ensureUrlMapRemoteMcpPaths(target, backend.backend, deps);
|
|
563
817
|
if (!paths.ok) return { ok: false, error: `URL map: ${paths.error}` };
|
|
564
818
|
done.push(
|
|
565
|
-
paths.
|
|
566
|
-
? `URL map:
|
|
567
|
-
: "URL map:
|
|
819
|
+
paths.changes.length > 0
|
|
820
|
+
? `URL map: ${paths.changes.join("; ")}`
|
|
821
|
+
: "URL map: remote-MCP paths already routed to the MCP-Auth-Proxy",
|
|
568
822
|
);
|
|
823
|
+
const retired = await retireGatewayMcpArmorRule(target, { run });
|
|
824
|
+
if (!retired.ok) return { ok: false, error: `gateway Cloud Armor: ${retired.error}` };
|
|
825
|
+
if (retired.removed) {
|
|
826
|
+
done.push(`removed the remote-MCP rule ${MCP_ARMOR_RULE.priority} from ${retired.removed}`);
|
|
827
|
+
}
|
|
569
828
|
return { ok: true, done };
|
|
570
829
|
}
|
|
@@ -314,7 +314,7 @@ describe("rotateRuntimeSecret", () => {
|
|
|
314
314
|
projectId: "acme",
|
|
315
315
|
region: "europe-west1",
|
|
316
316
|
env: "production",
|
|
317
|
-
services: { admin: "adm-svc", gateway: "gw-svc" },
|
|
317
|
+
services: { admin: "adm-svc", gateway: "gw-svc", "mcp-auth-proxy": "px-svc" },
|
|
318
318
|
sqlInstance: "caelo-production-pg-1",
|
|
319
319
|
};
|
|
320
320
|
const ref = (name: string, secret: string, version = "latest") => ({
|
|
@@ -337,6 +337,10 @@ describe("rotateRuntimeSecret", () => {
|
|
|
337
337
|
"run services describe gw-svc": [
|
|
338
338
|
service([ref("PUBLIC_DATABASE_PASSWORD", "public-role-password")]),
|
|
339
339
|
],
|
|
340
|
+
// #1328 — the proxy reads none of the rotatable database secrets.
|
|
341
|
+
"run services describe px-svc": [
|
|
342
|
+
service([ref("CAELO_MCP_OAUTH_SEAL_KEY", "mcp-oauth-seal-key")]),
|
|
343
|
+
],
|
|
340
344
|
"auth print-access-token": [ok("tok\n")],
|
|
341
345
|
});
|
|
342
346
|
|
|
@@ -451,7 +455,10 @@ describe("rotateRuntimeSecret", () => {
|
|
|
451
455
|
const { run, lines } = fakeGcloud({
|
|
452
456
|
"run services describe adm-svc": [
|
|
453
457
|
service([
|
|
454
|
-
{
|
|
458
|
+
{
|
|
459
|
+
name: "ADMIN_DATABASE_URL",
|
|
460
|
+
value: "postgres://admin_role:pw@db1.p-label.europe-west1.sql-psa.goog:5432/cms_admin",
|
|
461
|
+
},
|
|
455
462
|
]),
|
|
456
463
|
],
|
|
457
464
|
});
|
|
@@ -534,17 +541,23 @@ describe("parseAdminConfig", () => {
|
|
|
534
541
|
it("marks an admin still on an inline-password URL (jobs on its image need upgrade first)", () => {
|
|
535
542
|
const cfg = parseAdminConfig(
|
|
536
543
|
admin([
|
|
537
|
-
{
|
|
544
|
+
{
|
|
545
|
+
name: "ADMIN_DATABASE_URL",
|
|
546
|
+
value: "postgres://admin_role:pw@db1.p-label.europe-west1.sql-psa.goog:5432/cms_admin",
|
|
547
|
+
},
|
|
538
548
|
]),
|
|
539
549
|
);
|
|
540
|
-
expect(cfg?.databaseHost).toBe("
|
|
550
|
+
expect(cfg?.databaseHost).toBe("db1.p-label.europe-west1.sql-psa.goog");
|
|
541
551
|
expect(cfg?.secretEnv).toBe(false);
|
|
542
552
|
});
|
|
543
553
|
|
|
544
554
|
it("marks an admin that reads the password from Secret Manager", () => {
|
|
545
555
|
const cfg = parseAdminConfig(
|
|
546
556
|
admin([
|
|
547
|
-
{
|
|
557
|
+
{
|
|
558
|
+
name: "ADMIN_DATABASE_URL",
|
|
559
|
+
value: "postgresql://admin_role@db1.p-label.europe-west1.sql-psa.goog:5432/cms_admin",
|
|
560
|
+
},
|
|
548
561
|
{
|
|
549
562
|
name: "ADMIN_DATABASE_PASSWORD",
|
|
550
563
|
valueFrom: {
|
|
@@ -559,9 +572,9 @@ describe("parseAdminConfig", () => {
|
|
|
559
572
|
|
|
560
573
|
describe("migrationJobEnvArgs", () => {
|
|
561
574
|
it("gives the job password-less URLs and the password as a secret reference", () => {
|
|
562
|
-
const args = migrationJobEnvArgs("
|
|
575
|
+
const args = migrationJobEnvArgs("db1.p-label.europe-west1.sql-psa.goog");
|
|
563
576
|
expect(args).toEqual([
|
|
564
|
-
"--set-env-vars=ADMIN_DATABASE_URL=postgresql://admin_role@
|
|
577
|
+
"--set-env-vars=ADMIN_DATABASE_URL=postgresql://admin_role@db1.p-label.europe-west1.sql-psa.goog:5432/cms_admin?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem,PUBLIC_ADMIN_DATABASE_URL=postgresql://admin_role@db1.p-label.europe-west1.sql-psa.goog:5432/cms_public?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem",
|
|
565
578
|
"--set-secrets=ADMIN_DATABASE_PASSWORD=caelo-production-postgres-password:latest,PUBLIC_ADMIN_DATABASE_PASSWORD=caelo-production-postgres-password:latest",
|
|
566
579
|
]);
|
|
567
580
|
});
|