@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
package/src/upgrade/upgrade.ts
CHANGED
|
@@ -4,14 +4,17 @@
|
|
|
4
4
|
* `cms-provision upgrade` for one gcp / gcp-firebase install: converge, then
|
|
5
5
|
* app-roll (epic #696). The order, and why:
|
|
6
6
|
*
|
|
7
|
-
* 1. plan
|
|
7
|
+
* 1. plan: release digests, the services, the env contract — read-only
|
|
8
|
+
* but for the services an older install lacks (#1328, additive and
|
|
9
|
+
* serving nothing yet: mcp-auth-proxy-setup.ts)
|
|
8
10
|
* 2. verify the images' signatures
|
|
9
11
|
* 3. pre-upgrade Cloud SQL backup — the first change; if it fails, the
|
|
10
12
|
* install is exactly as it was
|
|
11
13
|
* 4. converge, the part before the roll (keyless provisioner, IAM, secrets,
|
|
12
14
|
* MCP access)
|
|
13
15
|
* 5. app-roll: health baseline, migrations, the converge steps that must
|
|
14
|
-
* follow migrations, admin
|
|
16
|
+
* follow migrations, admin, gateway and MCP-Auth-Proxy rolls, deep
|
|
17
|
+
* health check, rollback
|
|
15
18
|
* on failure, recorded digests
|
|
16
19
|
* 6. converge, the part after the roll; a complete converge records the
|
|
17
20
|
* install's infrastructure contract version
|
|
@@ -29,6 +32,7 @@
|
|
|
29
32
|
|
|
30
33
|
import type { GcloudRunner, Sleep } from "../gcloud-retry.js";
|
|
31
34
|
import type { ImageDigests } from "../install-state.js";
|
|
35
|
+
import { ensureReleaseServices } from "../mcp-auth-proxy-setup.js";
|
|
32
36
|
import type { HttpFetch } from "../runtime-secrets.js";
|
|
33
37
|
import {
|
|
34
38
|
type AppRollPlan,
|
|
@@ -75,9 +79,17 @@ export interface GcpUpgradeTarget {
|
|
|
75
79
|
*/
|
|
76
80
|
export interface UpgradeCheckpoint {
|
|
77
81
|
readonly backup?: SqlBackup;
|
|
78
|
-
readonly priorRevisions?:
|
|
82
|
+
readonly priorRevisions?: PriorRevisions;
|
|
79
83
|
}
|
|
80
84
|
|
|
85
|
+
/**
|
|
86
|
+
* The rollback target per service. The MCP-Auth-Proxy's is absent in a
|
|
87
|
+
* checkpoint a release before #1328 wrote; its planned revision is kept then.
|
|
88
|
+
*/
|
|
89
|
+
export type PriorRevisions = Readonly<Record<"admin" | "gateway", string>> & {
|
|
90
|
+
readonly "mcp-auth-proxy"?: string;
|
|
91
|
+
};
|
|
92
|
+
|
|
81
93
|
export interface GcpUpgradeDeps {
|
|
82
94
|
readonly run: GcloudRunner;
|
|
83
95
|
readonly reporter: UpgradeReporter;
|
|
@@ -165,6 +177,41 @@ export async function upgradeGcpInstallOutcome(
|
|
|
165
177
|
r.error(images.error);
|
|
166
178
|
return failed;
|
|
167
179
|
}
|
|
180
|
+
// #1328 — the services the plan must find: the MCP-Auth-Proxy's account
|
|
181
|
+
// and, on an install whose stack predates it, the data gateway
|
|
182
|
+
// (gcp-firebase) or the proxy's own service (gcp). Additive, serving
|
|
183
|
+
// nothing yet, so the install keeps running as it is when this or
|
|
184
|
+
// anything after it fails. The gcp proxy runs the new release's image,
|
|
185
|
+
// so that image's signature is checked before it is deployed.
|
|
186
|
+
const proxyImage = images.images["mcp-auth-proxy"];
|
|
187
|
+
if (
|
|
188
|
+
target.provider === "gcp" &&
|
|
189
|
+
!target.skipVerify &&
|
|
190
|
+
!(await deps.verifyImages([{ slug: "mcp-auth-proxy", digest: proxyImage.digest }]))
|
|
191
|
+
) {
|
|
192
|
+
sPre.failed("The MCP-Auth-Proxy image's signature did not verify");
|
|
193
|
+
return failed;
|
|
194
|
+
}
|
|
195
|
+
const ensured = await ensureReleaseServices(
|
|
196
|
+
{
|
|
197
|
+
provider: target.provider,
|
|
198
|
+
projectId,
|
|
199
|
+
region: target.region,
|
|
200
|
+
env: target.env,
|
|
201
|
+
domain: target.domain,
|
|
202
|
+
mcpAuthProxyImageRef: proxyImage.imageRef,
|
|
203
|
+
},
|
|
204
|
+
{ run },
|
|
205
|
+
);
|
|
206
|
+
const ensureFailed = ensured.filter((o) => o.status === "failed");
|
|
207
|
+
if (ensureFailed.length > 0) {
|
|
208
|
+
sPre.failed("The services this release runs could not be set up. Nothing was changed.");
|
|
209
|
+
for (const o of ensureFailed) r.error(` FAILED: ${o.id}\n ${o.error ?? ""}`);
|
|
210
|
+
return failed;
|
|
211
|
+
}
|
|
212
|
+
for (const o of ensured.filter((o) => o.status === "applied")) {
|
|
213
|
+
r.info(` created: ${o.id}${o.detail ? ` (${o.detail})` : ""}`);
|
|
214
|
+
}
|
|
168
215
|
const planned = await planAppRoll(
|
|
169
216
|
{
|
|
170
217
|
provider: target.provider,
|
|
@@ -269,7 +316,9 @@ export async function upgradeGcpInstallOutcome(
|
|
|
269
316
|
},
|
|
270
317
|
);
|
|
271
318
|
if (!rolled.ok) return { ok: false, rolledBack: rolled.rolledBack };
|
|
272
|
-
r.success(
|
|
319
|
+
r.success(
|
|
320
|
+
`Upgrade to ${target.targetTag} complete (admin, gateway and MCP-Auth-Proxy revisions serving).`,
|
|
321
|
+
);
|
|
273
322
|
r.info(
|
|
274
323
|
`Pre-upgrade backup: ${backup.backup.backupId} (restore: ${restoreCommand(backup.backup)})`,
|
|
275
324
|
);
|
|
@@ -314,8 +363,8 @@ function withPriorRevisions(
|
|
|
314
363
|
return {
|
|
315
364
|
...plan,
|
|
316
365
|
services: plan.services.map((s) => {
|
|
317
|
-
const before = recorded[s.slug];
|
|
318
|
-
if (before === s.priorRevision) return s;
|
|
366
|
+
const before: string | undefined = recorded[s.slug];
|
|
367
|
+
if (before === undefined || before === s.priorRevision) return s;
|
|
319
368
|
r.info(
|
|
320
369
|
`Continuing an interrupted update: ${s.slug} goes back to ${before} (what served before it started) if the new release fails its checks.`,
|
|
321
370
|
);
|
package/src/upgrader-gcp.ts
CHANGED
|
@@ -16,6 +16,13 @@
|
|
|
16
16
|
import { mkdtempSync } from "node:fs";
|
|
17
17
|
import { tmpdir } from "node:os";
|
|
18
18
|
import { join } from "node:path";
|
|
19
|
+
import { isIpLiteral } from "@caelo-cms/shared";
|
|
20
|
+
import {
|
|
21
|
+
ensureCloudSqlVerifyFull,
|
|
22
|
+
liveDatabaseUrls,
|
|
23
|
+
moveServicesToDnsHost,
|
|
24
|
+
} from "./cloudsql-tls.js";
|
|
25
|
+
import { findSqlInstance } from "./gateway-credentials.js";
|
|
19
26
|
import { gcloud } from "./gcloud.js";
|
|
20
27
|
import { type GcloudRunner, realSleep } from "./gcloud-retry.js";
|
|
21
28
|
import { GCP_STACK_ENV } from "./gcp-names.js";
|
|
@@ -23,10 +30,16 @@ import type { InstallMetadata } from "./install-state.js";
|
|
|
23
30
|
import { readAdminConfig, runMigrationsViaCloudRunJob } from "./migration-runner.js";
|
|
24
31
|
import { fetchVerifiedReleaseManifest } from "./release-manifest.js";
|
|
25
32
|
import type { HttpFetch } from "./runtime-secrets.js";
|
|
33
|
+
import { DATABASE_URL_VARS } from "./stack-contract.js";
|
|
34
|
+
import { liveContainerEnv } from "./stack-converge.js";
|
|
26
35
|
import { opsBucketName } from "./state-backend.js";
|
|
27
36
|
import { acquireBucketStateLock, GcsStateStore, realSyncGcloud } from "./state-store.js";
|
|
28
|
-
import type { AppRollPlan } from "./upgrade/app-roll.js";
|
|
29
|
-
import {
|
|
37
|
+
import type { AppRollPlan, RollSlug } from "./upgrade/app-roll.js";
|
|
38
|
+
import {
|
|
39
|
+
describeServiceJson,
|
|
40
|
+
findServiceUrl,
|
|
41
|
+
resolveGcpResourceName,
|
|
42
|
+
} from "./upgrade/gcp-lookup.js";
|
|
30
43
|
import { createIapHealthProbe } from "./upgrade/health.js";
|
|
31
44
|
import { createReleaseImageForms } from "./upgrade/image-forms.js";
|
|
32
45
|
import { type IamTester, restIamTester } from "./upgrader-iam-preflight.js";
|
|
@@ -58,6 +71,54 @@ export async function upgraderDatabaseHost(
|
|
|
58
71
|
return cfg.databaseHost;
|
|
59
72
|
}
|
|
60
73
|
|
|
74
|
+
/**
|
|
75
|
+
* #1338 — the database host for the converge job (#819), after making sure
|
|
76
|
+
* a verify-full connection to it can work. The job opens its database
|
|
77
|
+
* queue before it converges anything, with this release's code, which
|
|
78
|
+
* connects with verify-full only; on an install whose services still dial
|
|
79
|
+
* the instance's IP it first runs the converge's database TLS step
|
|
80
|
+
* (cloudsql-tls.ts: shared CA, private DNS name, services moved onto it).
|
|
81
|
+
* The job runs as the install's `caelo-provisioner`, which holds
|
|
82
|
+
* `roles/cloudsql.admin` and `roles/dns.admin` (gcloud.ts PROVISIONER_ROLES).
|
|
83
|
+
* Idempotent: the converge's own step finds everything in shape.
|
|
84
|
+
*/
|
|
85
|
+
export async function convergeDatabaseHost(
|
|
86
|
+
placement: Pick<UpgraderPlacement, "projectId" | "region">,
|
|
87
|
+
run: GcloudRunner = gcloud,
|
|
88
|
+
http?: HttpFetch,
|
|
89
|
+
): Promise<string> {
|
|
90
|
+
const host = await upgraderDatabaseHost(placement, run);
|
|
91
|
+
if (!isIpLiteral(host)) return host;
|
|
92
|
+
const install = { projectId: placement.projectId, env: GCP_STACK_ENV };
|
|
93
|
+
const lookup = await findSqlInstance(install, { run });
|
|
94
|
+
if (lookup.status !== "found") {
|
|
95
|
+
throw new Error(`could not find the install's Cloud SQL instance (${lookup.status})`);
|
|
96
|
+
}
|
|
97
|
+
const deps = { run, ...(http ? { http } : {}) };
|
|
98
|
+
const tls = await ensureCloudSqlVerifyFull({ ...install, sqlInstance: lookup.name }, deps);
|
|
99
|
+
if (!tls.ok) throw new Error(`database TLS could not be set up: ${tls.error}`);
|
|
100
|
+
const services: { serviceName: string; liveUrls: Record<string, string> }[] = [];
|
|
101
|
+
for (const slug of ["admin", "gateway"] as const) {
|
|
102
|
+
const found = await resolveGcpResourceName(
|
|
103
|
+
run,
|
|
104
|
+
"run-service",
|
|
105
|
+
`caelo-${GCP_STACK_ENV}-${slug}`,
|
|
106
|
+
placement.projectId,
|
|
107
|
+
placement.region,
|
|
108
|
+
);
|
|
109
|
+
if (found.status !== "found") throw new Error(`could not find the ${slug} service`);
|
|
110
|
+
const json = await describeServiceJson(run, placement.projectId, placement.region, found.name);
|
|
111
|
+
if (!json) throw new Error(`could not read the ${slug} service's configuration`);
|
|
112
|
+
services.push({
|
|
113
|
+
serviceName: found.name,
|
|
114
|
+
liveUrls: liveDatabaseUrls(liveContainerEnv(json), DATABASE_URL_VARS[slug]),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
const moved = await moveServicesToDnsHost(placement, services, tls.host, deps);
|
|
118
|
+
if (!moved.ok) throw new Error(moved.error);
|
|
119
|
+
return tls.host;
|
|
120
|
+
}
|
|
121
|
+
|
|
61
122
|
/**
|
|
62
123
|
* The IAP-backed deep health probe for `plan`'s install (the upgrader job;
|
|
63
124
|
* the converge job, #819, too). `accessJustChanged`: a converge right
|
|
@@ -71,7 +132,7 @@ export async function healthProbeFor(
|
|
|
71
132
|
context: { readonly accessJustChanged: boolean },
|
|
72
133
|
) {
|
|
73
134
|
const { install } = plan;
|
|
74
|
-
const urls: Partial<Record<
|
|
135
|
+
const urls: Partial<Record<RollSlug, string>> = {};
|
|
75
136
|
for (const s of plan.services) {
|
|
76
137
|
const url = await findServiceUrl(run, install.projectId, install.region, s.serviceName);
|
|
77
138
|
if (!url) {
|
|
@@ -88,6 +149,7 @@ export async function healthProbeFor(
|
|
|
88
149
|
domain: install.domain,
|
|
89
150
|
adminServiceUrl: urls.admin as string,
|
|
90
151
|
gatewayServiceUrl: urls.gateway as string,
|
|
152
|
+
mcpAuthProxyServiceUrl: urls["mcp-auth-proxy"] as string,
|
|
91
153
|
},
|
|
92
154
|
{ run, http, sleep: realSleep, accessJustChanged: context.accessJustChanged },
|
|
93
155
|
),
|
|
@@ -73,6 +73,11 @@ function setupGrants(): Map<string, Set<string>> {
|
|
|
73
73
|
`projects/${p}/serviceAccounts/caelo-production-gateway-sa@${p}.iam.gserviceaccount.com`,
|
|
74
74
|
of("roles/iam.serviceAccountUser"),
|
|
75
75
|
],
|
|
76
|
+
// #1328 — the MCP-Auth-Proxy's revisions run as its own account.
|
|
77
|
+
[
|
|
78
|
+
`projects/${p}/serviceAccounts/caelo-production-mcp-proxy-sa@${p}.iam.gserviceaccount.com`,
|
|
79
|
+
of("roles/iam.serviceAccountUser"),
|
|
80
|
+
],
|
|
76
81
|
[
|
|
77
82
|
`projects/${p}/serviceAccounts/caelo-production-upgrader@${p}.iam.gserviceaccount.com`,
|
|
78
83
|
of("roles/iam.serviceAccountUser"),
|
|
@@ -154,6 +159,20 @@ describe("upgraderAccessChecks", () => {
|
|
|
154
159
|
expect(preflightMissingReason(r.missing, UPGRADER_PREFLIGHT_VOICE)).toContain("capture job");
|
|
155
160
|
});
|
|
156
161
|
|
|
162
|
+
it("#1328: the MCP-Auth-Proxy's account is checked, since every app-roll rolls the proxy", async () => {
|
|
163
|
+
const proxySa =
|
|
164
|
+
"projects/acme/serviceAccounts/caelo-production-mcp-proxy-sa@acme.iam.gserviceaccount.com";
|
|
165
|
+
const grants = setupGrants();
|
|
166
|
+
grants.delete(proxySa);
|
|
167
|
+
const r = await runIamPreflight(upgraderAccessChecks(TARGET), tester(grants));
|
|
168
|
+
expect(r.kind).toBe("missing");
|
|
169
|
+
if (r.kind !== "missing") return;
|
|
170
|
+
expect(r.missing.map((m) => [resourceName(m.check.resource), m.missing])).toEqual([
|
|
171
|
+
[proxySa, ["iam.serviceAccounts.actAs"]],
|
|
172
|
+
]);
|
|
173
|
+
expect(preflightMissingReason(r.missing, UPGRADER_PREFLIGHT_VOICE)).toContain("MCP-Auth-Proxy");
|
|
174
|
+
});
|
|
175
|
+
|
|
157
176
|
it("never needs a permission only a forbidden role would give (no compute, no IAM admin)", () => {
|
|
158
177
|
const all = upgraderAccessChecks(TARGET).flatMap((c) => c.permissions);
|
|
159
178
|
for (const p of all) {
|
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
auditServiceAccountEmail,
|
|
55
55
|
captureServiceAccountEmail,
|
|
56
56
|
gatewayServiceAccountEmail,
|
|
57
|
+
mcpAuthProxyServiceAccountEmail,
|
|
57
58
|
mcpIapServiceAccountEmail,
|
|
58
59
|
runServiceAccountEmail,
|
|
59
60
|
upgraderJobName,
|
|
@@ -186,6 +187,11 @@ export function upgraderAccessChecks(target: {
|
|
|
186
187
|
resource: sa(gatewayServiceAccountEmail(projectId, env)),
|
|
187
188
|
permissions: ["iam.serviceAccounts.actAs"],
|
|
188
189
|
},
|
|
190
|
+
{
|
|
191
|
+
need: "run the MCP-Auth-Proxy as its service account",
|
|
192
|
+
resource: sa(mcpAuthProxyServiceAccountEmail(projectId, env)),
|
|
193
|
+
permissions: ["iam.serviceAccounts.actAs"],
|
|
194
|
+
},
|
|
189
195
|
{
|
|
190
196
|
// #875 — every app-roll moves the audit job onto the new admin image
|
|
191
197
|
// (upgrade/admin-image-jobs.ts), and upgrader-setup.ts grants actAs on
|
package/src/upgrader-job.test.ts
CHANGED
|
@@ -57,14 +57,16 @@ const revisionJson = (deployed: string | null, resolved: string) =>
|
|
|
57
57
|
status: { imageDigest: `${MIRROR}@${resolved}` },
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
-
/** Serving revisions deployed as `admin` / `gateway` (index digests). */
|
|
60
|
+
/** Serving revisions deployed as `admin` / `gateway` (index digests); the proxy serves D("3"). */
|
|
61
61
|
const servingAs = (admin: string, gateway: string): [RegExp, Answer] => [
|
|
62
62
|
/^run revisions describe (\S+)/,
|
|
63
63
|
(l) =>
|
|
64
64
|
ok(
|
|
65
65
|
l.includes("-admin-")
|
|
66
66
|
? revisionJson(admin, `sha256:${"e".repeat(64)}`)
|
|
67
|
-
:
|
|
67
|
+
: l.includes(PROXY)
|
|
68
|
+
? revisionJson(D("3"), P("3"))
|
|
69
|
+
: revisionJson(gateway, `sha256:${"d".repeat(64)}`),
|
|
68
70
|
),
|
|
69
71
|
];
|
|
70
72
|
|
|
@@ -79,13 +81,16 @@ const releaseTags = (slug: "admin" | "gateway", tags: Record<string, string>): [
|
|
|
79
81
|
];
|
|
80
82
|
|
|
81
83
|
const ADMIN = "caelo-production-admin-aaa";
|
|
82
|
-
|
|
84
|
+
// #1328 — gcp-firebase: the visitor gateway is the fixed-name data gateway,
|
|
85
|
+
// the MCP-Auth-Proxy the service that was the gateway (E13 a).
|
|
86
|
+
const GATEWAY = "caelo-production-data-gateway";
|
|
87
|
+
const PROXY = "caelo-production-gateway-bbb";
|
|
83
88
|
|
|
84
89
|
const MANIFEST: ReleaseManifest = {
|
|
85
90
|
manifestVersion: 1,
|
|
86
91
|
version: "0.11.0",
|
|
87
92
|
channel: "stable",
|
|
88
|
-
digests: { admin: D("a"), gateway: D("b"), ops: D("c") },
|
|
93
|
+
digests: { admin: D("a"), gateway: D("b"), ops: D("c"), "mcp-auth-proxy": D("d") },
|
|
89
94
|
minFromVersion: "0.10.0",
|
|
90
95
|
infraContractVersion: 2,
|
|
91
96
|
migrations: { contract: false, lastContractVersion: null, head: "0267_platform_updates" },
|
|
@@ -121,14 +126,23 @@ const META: InstallMetadata = {
|
|
|
121
126
|
ownerEmail: "owner@acme.com",
|
|
122
127
|
region: "europe-west1",
|
|
123
128
|
createdAt: "2026-01-01T00:00:00Z",
|
|
124
|
-
imageDigests: { admin: D("1"), gateway: D("2") },
|
|
129
|
+
imageDigests: { admin: D("1"), gateway: D("2"), "mcp-auth-proxy": D("3") },
|
|
125
130
|
releaseVersion: "0.10.45",
|
|
126
131
|
infraContractVersion: 2,
|
|
127
132
|
};
|
|
128
133
|
|
|
129
|
-
const manifestOf = (
|
|
134
|
+
const manifestOf = (
|
|
135
|
+
version: string,
|
|
136
|
+
admin: string,
|
|
137
|
+
gateway: string,
|
|
138
|
+
proxy = D("3"),
|
|
139
|
+
): ReleaseManifestResult => ({
|
|
130
140
|
ok: true,
|
|
131
|
-
manifest: {
|
|
141
|
+
manifest: {
|
|
142
|
+
...MANIFEST,
|
|
143
|
+
version,
|
|
144
|
+
digests: { admin, gateway, ops: D("c"), "mcp-auth-proxy": proxy },
|
|
145
|
+
},
|
|
132
146
|
});
|
|
133
147
|
|
|
134
148
|
const serviceJson = (image: string, env: unknown[]) =>
|
|
@@ -141,11 +155,13 @@ const ADMIN_JSON = serviceJson("registry/admin@sha256:old", [
|
|
|
141
155
|
{ name: "CAELO_FIREBASE_SITE", value: "caelo-production-site-abc123" },
|
|
142
156
|
{
|
|
143
157
|
name: "ADMIN_DATABASE_URL",
|
|
144
|
-
value:
|
|
158
|
+
value:
|
|
159
|
+
"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",
|
|
145
160
|
},
|
|
146
161
|
secretRef("ADMIN_DATABASE_PASSWORD", "postgres-password"),
|
|
147
162
|
]);
|
|
148
163
|
const GATEWAY_JSON = serviceJson("registry/gateway@sha256:old", []);
|
|
164
|
+
const PROXY_JSON = serviceJson("registry/mcp-auth-proxy@sha256:old", []);
|
|
149
165
|
|
|
150
166
|
type Answer = GcloudResult | ((line: string) => GcloudResult);
|
|
151
167
|
|
|
@@ -153,15 +169,23 @@ function fakeGcloud(events: string[], overrides: [RegExp, Answer][] = []) {
|
|
|
153
169
|
const rules: [RegExp, Answer][] = [
|
|
154
170
|
...overrides,
|
|
155
171
|
[/^run services list .*caelo-production-admin/, ok(`${ADMIN}\n`)],
|
|
156
|
-
[/^run services list .*caelo-production-gateway/, ok(`${GATEWAY}\n`)],
|
|
172
|
+
[/^run services list .*caelo-production-data-gateway/, ok(`${GATEWAY}\n`)],
|
|
173
|
+
[/^run services list .*caelo-production-gateway/, ok(`${PROXY}\n`)],
|
|
157
174
|
[/describe (\S+) .*revisionName/, (l) => ok(`${l.split(" ")[3]}-00001-old\n`)],
|
|
158
175
|
[new RegExp(`describe ${ADMIN} .*--format=json`), ok(ADMIN_JSON)],
|
|
159
176
|
[new RegExp(`describe ${GATEWAY} .*--format=json`), ok(GATEWAY_JSON)],
|
|
177
|
+
[new RegExp(`describe ${PROXY} .*--format=json`), ok(PROXY_JSON)],
|
|
160
178
|
[/describe (\S+) .*status\.url/, (l) => ok(`https://${l.split(" ")[3]}-xyz.a.run.app\n`)],
|
|
161
179
|
[
|
|
162
180
|
/^run revisions describe (\S+)/,
|
|
163
181
|
(l) =>
|
|
164
|
-
ok(
|
|
182
|
+
ok(
|
|
183
|
+
l.includes("-admin-")
|
|
184
|
+
? revisionJson(D("1"), P("1"))
|
|
185
|
+
: l.includes(PROXY)
|
|
186
|
+
? revisionJson(D("3"), P("3"))
|
|
187
|
+
: revisionJson(D("2"), P("2")),
|
|
188
|
+
),
|
|
165
189
|
],
|
|
166
190
|
[/^sql instances list/, ok("caelo-production-pg-abc\n")],
|
|
167
191
|
[/^sql backups list/, ok("1760000000000\n")],
|
|
@@ -307,9 +331,11 @@ function fakeImageForms(order: string[], down: boolean): ReleaseImageForms {
|
|
|
307
331
|
return {
|
|
308
332
|
ofIndex,
|
|
309
333
|
of: async (m, slug) => {
|
|
334
|
+
const index = m.digests[slug];
|
|
335
|
+
if (!index) return { ok: false, transient: false, message: `no ${slug} image` };
|
|
310
336
|
const signed = m.platformDigests?.[slug];
|
|
311
|
-
if (signed) return { ok: true, digests: [
|
|
312
|
-
return ofIndex(slug,
|
|
337
|
+
if (signed) return { ok: true, digests: [index, signed], amd64: signed };
|
|
338
|
+
return ofIndex(slug, index);
|
|
313
339
|
},
|
|
314
340
|
};
|
|
315
341
|
}
|
|
@@ -393,6 +419,21 @@ describe("runUpgraderJob", () => {
|
|
|
393
419
|
]);
|
|
394
420
|
});
|
|
395
421
|
|
|
422
|
+
it("#1328: a target manifest without an MCP-Auth-Proxy image stops before anything changes", async () => {
|
|
423
|
+
const { "mcp-auth-proxy": _none, ...digests } = MANIFEST.digests;
|
|
424
|
+
const { deps, order, q, written } = setup({
|
|
425
|
+
manifest: { ok: true, manifest: { ...MANIFEST, digests } },
|
|
426
|
+
});
|
|
427
|
+
const r = await runUpgraderJob(deps);
|
|
428
|
+
expect(r.status).toBe("failed");
|
|
429
|
+
expect(q.end()?.codes).toEqual(["manifest"]);
|
|
430
|
+
expect(q.end()?.reason).toContain("names no mcp-auth-proxy image");
|
|
431
|
+
expect(order.some((l) => l.startsWith("gcloud sql backups create"))).toBe(false);
|
|
432
|
+
expect(order.some((l) => l.startsWith("migrate "))).toBe(false);
|
|
433
|
+
expect(order.some((l) => l.startsWith("gcloud run services update"))).toBe(false);
|
|
434
|
+
expect(written).toEqual([]);
|
|
435
|
+
});
|
|
436
|
+
|
|
396
437
|
it("#815: decides first, locks right before the backup, holds the lock to the end", async () => {
|
|
397
438
|
const ok = setup();
|
|
398
439
|
await runUpgraderJob(ok.deps);
|
|
@@ -429,6 +470,7 @@ describe("runUpgraderJob", () => {
|
|
|
429
470
|
"iam projects/acme/serviceAccounts/caelo-production-audit@acme.iam.gserviceaccount.com",
|
|
430
471
|
"iam projects/acme/serviceAccounts/caelo-production-capture@acme.iam.gserviceaccount.com",
|
|
431
472
|
"iam projects/acme/serviceAccounts/caelo-production-gateway-sa@acme.iam.gserviceaccount.com",
|
|
473
|
+
"iam projects/acme/serviceAccounts/caelo-production-mcp-proxy-sa@acme.iam.gserviceaccount.com",
|
|
432
474
|
"iam projects/acme/serviceAccounts/caelo-production-run-sa@acme.iam.gserviceaccount.com",
|
|
433
475
|
"iam projects/acme/serviceAccounts/caelo-production-upgrader@acme.iam.gserviceaccount.com",
|
|
434
476
|
"iam projects/caelo-website/locations/europe-west1/repositories/caelo-cms-images",
|
|
@@ -616,12 +658,21 @@ describe("runUpgraderJob", () => {
|
|
|
616
658
|
|
|
617
659
|
it("a revision deployed by tag matches by the digest Cloud Run resolved", async () => {
|
|
618
660
|
const { deps, written } = setup({
|
|
619
|
-
meta: {
|
|
661
|
+
meta: {
|
|
662
|
+
...META,
|
|
663
|
+
imageDigests: { admin: P("1"), gateway: D("2"), "mcp-auth-proxy": D("3") },
|
|
664
|
+
},
|
|
620
665
|
gcloud: [
|
|
621
666
|
[
|
|
622
667
|
/^run revisions describe (\S+)/,
|
|
623
668
|
(l) =>
|
|
624
|
-
ok(
|
|
669
|
+
ok(
|
|
670
|
+
l.includes("-admin-")
|
|
671
|
+
? revisionJson(null, P("1"))
|
|
672
|
+
: l.includes(PROXY)
|
|
673
|
+
? revisionJson(D("3"), P("3"))
|
|
674
|
+
: revisionJson(D("2"), P("2")),
|
|
675
|
+
),
|
|
625
676
|
],
|
|
626
677
|
],
|
|
627
678
|
});
|
|
@@ -664,14 +715,20 @@ describe("runUpgraderJob", () => {
|
|
|
664
715
|
|
|
665
716
|
it("#815: drift + a stop verdict: the records are corrected under the lock, the verdict stands", async () => {
|
|
666
717
|
const drift = {
|
|
667
|
-
meta: {
|
|
718
|
+
meta: {
|
|
719
|
+
...META,
|
|
720
|
+
imageDigests: { admin: D("9"), gateway: D("2"), "mcp-auth-proxy": D("3") },
|
|
721
|
+
infraContractVersion: 1,
|
|
722
|
+
},
|
|
668
723
|
manifests: { "0.10.45": manifestOf("0.10.45", D("1"), D("2")) },
|
|
669
724
|
};
|
|
670
725
|
const free = setup(drift);
|
|
671
726
|
expect((await runUpgraderJob(free.deps)).status).toBe("needs_cloud_shell");
|
|
672
727
|
expect(free.q.end()?.codes).toEqual(["infra-contract"]);
|
|
673
728
|
expect(free.written).toEqual([
|
|
674
|
-
expect.objectContaining({
|
|
729
|
+
expect.objectContaining({
|
|
730
|
+
imageDigests: { admin: D("1"), gateway: D("2"), "mcp-auth-proxy": D("3") },
|
|
731
|
+
}),
|
|
675
732
|
]);
|
|
676
733
|
expect(free.order.indexOf("lock")).toBeGreaterThan(step(free.order, /^manifest 0\.11\.0/));
|
|
677
734
|
expect(free.order).toContain("unlock");
|
|
@@ -814,7 +871,8 @@ describe("runUpgraderJob", () => {
|
|
|
814
871
|
const servingPlatform = (admin: string, gateway: string): [RegExp, Answer] => [
|
|
815
872
|
/^run revisions describe (\S+)/,
|
|
816
873
|
(l) => {
|
|
817
|
-
|
|
874
|
+
// The MCP-Auth-Proxy serves its recorded index D("3") throughout.
|
|
875
|
+
const d = l.includes("-admin-") ? admin : l.includes(PROXY) ? D("3") : gateway;
|
|
818
876
|
return ok(revisionJson(d, d));
|
|
819
877
|
},
|
|
820
878
|
];
|
|
@@ -824,12 +882,17 @@ describe("runUpgraderJob", () => {
|
|
|
824
882
|
const v064 = {
|
|
825
883
|
...MANIFEST,
|
|
826
884
|
version: "0.10.64",
|
|
827
|
-
digests: {
|
|
885
|
+
digests: {
|
|
886
|
+
admin: ADMIN_INDEX,
|
|
887
|
+
gateway: GATEWAY_INDEX,
|
|
888
|
+
ops: D("c"),
|
|
889
|
+
"mcp-auth-proxy": D("3"),
|
|
890
|
+
},
|
|
828
891
|
};
|
|
829
892
|
const { deps, q, written } = setup({
|
|
830
893
|
meta: {
|
|
831
894
|
...META,
|
|
832
|
-
imageDigests: { admin: ADMIN_INDEX, gateway: GATEWAY_INDEX },
|
|
895
|
+
imageDigests: { admin: ADMIN_INDEX, gateway: GATEWAY_INDEX, "mcp-auth-proxy": D("3") },
|
|
833
896
|
releaseVersion: "0.10.64",
|
|
834
897
|
},
|
|
835
898
|
manifests: { "0.10.64": { ok: true, manifest: v064 } },
|
|
@@ -839,7 +902,7 @@ describe("runUpgraderJob", () => {
|
|
|
839
902
|
expect((await runUpgraderJob(deps)).status).toBe("succeeded");
|
|
840
903
|
expect(written[0]).toMatchObject({
|
|
841
904
|
releaseVersion: "0.10.64",
|
|
842
|
-
imageDigests: { admin: ADMIN_INDEX, gateway: GATEWAY_INDEX },
|
|
905
|
+
imageDigests: { admin: ADMIN_INDEX, gateway: GATEWAY_INDEX, "mcp-auth-proxy": D("3") },
|
|
843
906
|
platformDigests: { admin: ADMIN_AMD64, gateway: GATEWAY_AMD64 },
|
|
844
907
|
});
|
|
845
908
|
expect(q.events.some((e) => e.message.startsWith("0.10.64 → 0.11.0"))).toBe(true);
|
|
@@ -861,6 +924,7 @@ describe("runUpgraderJob", () => {
|
|
|
861
924
|
expect(order.filter((l) => l.startsWith("index "))).toEqual([
|
|
862
925
|
`index admin@${D("a")}`,
|
|
863
926
|
`index gateway@${D("b")}`,
|
|
927
|
+
`index mcp-auth-proxy@${D("d")}`,
|
|
864
928
|
]);
|
|
865
929
|
expect(q.events.some((e) => e.message.includes("refreshed its install records"))).toBe(false);
|
|
866
930
|
});
|
|
@@ -868,14 +932,14 @@ describe("runUpgraderJob", () => {
|
|
|
868
932
|
it("the roll records the new release's platform forms, signed ones first", async () => {
|
|
869
933
|
const signed = {
|
|
870
934
|
...MANIFEST,
|
|
871
|
-
platformDigests: { admin: P("x"), gateway: P("y"), ops: P("z") },
|
|
935
|
+
platformDigests: { admin: P("x"), gateway: P("y"), ops: P("z"), "mcp-auth-proxy": P("w") },
|
|
872
936
|
};
|
|
873
937
|
const { deps, order, written } = setup({ manifest: { ok: true, manifest: signed } });
|
|
874
938
|
expect((await runUpgraderJob(deps)).status).toBe("succeeded");
|
|
875
939
|
expect(written.at(-1)).toMatchObject({
|
|
876
940
|
releaseVersion: "0.11.0",
|
|
877
|
-
imageDigests: { admin: D("a"), gateway: D("b") },
|
|
878
|
-
platformDigests: { admin: P("x"), gateway: P("y") },
|
|
941
|
+
imageDigests: { admin: D("a"), gateway: D("b"), "mcp-auth-proxy": D("d") },
|
|
942
|
+
platformDigests: { admin: P("x"), gateway: P("y"), "mcp-auth-proxy": P("w") },
|
|
879
943
|
});
|
|
880
944
|
expect(order.some((l) => l.startsWith("index "))).toBe(false);
|
|
881
945
|
});
|
|
@@ -886,7 +950,11 @@ describe("runUpgraderJob", () => {
|
|
|
886
950
|
imageForms: "down",
|
|
887
951
|
});
|
|
888
952
|
expect((await runUpgraderJob(deps)).status).toBe("succeeded");
|
|
889
|
-
expect(written.at(-1)?.imageDigests).toEqual({
|
|
953
|
+
expect(written.at(-1)?.imageDigests).toEqual({
|
|
954
|
+
admin: D("a"),
|
|
955
|
+
gateway: D("b"),
|
|
956
|
+
"mcp-auth-proxy": D("d"),
|
|
957
|
+
});
|
|
890
958
|
expect(written.at(-1)?.platformDigests).toBeUndefined();
|
|
891
959
|
expect(q.events.some((e) => e.level === "warn" && e.message.includes("linux/amd64"))).toBe(
|
|
892
960
|
true,
|
|
@@ -995,7 +1063,9 @@ describe("runUpgraderJob", () => {
|
|
|
995
1063
|
manifests: { "0.10.45": manifestOf("0.10.45", D("1"), D("2")) },
|
|
996
1064
|
});
|
|
997
1065
|
expect((await runUpgraderJob(deps)).status).toBe("needs_cloud_shell");
|
|
998
|
-
expect(q.end()?.reason).toContain(
|
|
1066
|
+
expect(q.end()?.reason).toContain(
|
|
1067
|
+
"The admin and the MCP-Auth-Proxy serve v0.10.45's image, the gateway does not",
|
|
1068
|
+
);
|
|
999
1069
|
expect(written).toEqual([]);
|
|
1000
1070
|
});
|
|
1001
1071
|
|
|
@@ -1021,8 +1091,12 @@ describe("runUpgraderJob", () => {
|
|
|
1021
1091
|
expect((await runUpgraderJob(deps)).status).toBe("needs_cloud_shell");
|
|
1022
1092
|
const end = q.end();
|
|
1023
1093
|
expect(end?.codes).toEqual(["install-state-drift"]);
|
|
1024
|
-
expect(end?.reason).toContain(
|
|
1025
|
-
|
|
1094
|
+
expect(end?.reason).toContain(
|
|
1095
|
+
"The gateway and the MCP-Auth-Proxy serve v0.10.45's image, the admin does not",
|
|
1096
|
+
);
|
|
1097
|
+
expect(end?.reason).toContain(
|
|
1098
|
+
"the admin and the MCP-Auth-Proxy serve v0.10.44's image, the gateway does not",
|
|
1099
|
+
);
|
|
1026
1100
|
expect(written).toEqual([]);
|
|
1027
1101
|
});
|
|
1028
1102
|
|
package/src/upgrader-job.ts
CHANGED
|
@@ -56,15 +56,21 @@ import type {
|
|
|
56
56
|
StoredUpdatePolicy,
|
|
57
57
|
} from "@caelo-cms/shared";
|
|
58
58
|
import type { GcloudRunner, Sleep } from "./gcloud-retry.js";
|
|
59
|
-
import {
|
|
59
|
+
import { upgraderJobName } from "./gcp-names.js";
|
|
60
60
|
import type { InstallMetadata } from "./install-state.js";
|
|
61
|
-
import
|
|
61
|
+
import {
|
|
62
|
+
manifestServiceImages,
|
|
63
|
+
type ReleaseManifest,
|
|
64
|
+
type ReleaseManifestResult,
|
|
65
|
+
} from "./release-manifest.js";
|
|
66
|
+
import { cloudRunServicePrefix } from "./stack-contract.js";
|
|
62
67
|
import { StateLockHeldError } from "./state-store.js";
|
|
63
68
|
import {
|
|
64
69
|
type AppRollPlan,
|
|
65
70
|
executeAppRoll,
|
|
66
71
|
planAppRoll,
|
|
67
72
|
type ReleaseImage,
|
|
73
|
+
ROLL_ORDER,
|
|
68
74
|
type RollSlug,
|
|
69
75
|
} from "./upgrade/app-roll.js";
|
|
70
76
|
import { restoreCommand, takeSqlBackup } from "./upgrade/backup.js";
|
|
@@ -389,7 +395,7 @@ export async function runUpgraderJob(deps: UpgraderJobDeps): Promise<UpgraderRun
|
|
|
389
395
|
// install.json must describe what actually serves: if a previous run
|
|
390
396
|
// rolled but could not record it, the recorded version is stale and
|
|
391
397
|
// the downgrade / minFromVersion checks would judge against it.
|
|
392
|
-
const serving = await servingDigests(deps);
|
|
398
|
+
const serving = await servingDigests(deps, meta.provider);
|
|
393
399
|
if (!serving.ok) {
|
|
394
400
|
return stopWith("failed", ["lookup"], `Nothing was changed: ${serving.error}.`);
|
|
395
401
|
}
|
|
@@ -424,6 +430,9 @@ export async function runUpgraderJob(deps: UpgraderJobDeps): Promise<UpgraderRun
|
|
|
424
430
|
imageDigests: {
|
|
425
431
|
admin: found.manifest.digests.admin,
|
|
426
432
|
gateway: found.manifest.digests.gateway,
|
|
433
|
+
...(found.manifest.digests["mcp-auth-proxy"]
|
|
434
|
+
? { "mcp-auth-proxy": found.manifest.digests["mcp-auth-proxy"] }
|
|
435
|
+
: {}),
|
|
427
436
|
},
|
|
428
437
|
...(found.platformDigests ? { platformDigests: found.platformDigests } : {}),
|
|
429
438
|
releaseVersion: found.manifest.version,
|
|
@@ -500,13 +509,10 @@ export async function runUpgraderJob(deps: UpgraderJobDeps): Promise<UpgraderRun
|
|
|
500
509
|
r.info("The upgrader has every permission this update needs.");
|
|
501
510
|
|
|
502
511
|
const m = decision.manifest;
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
imageRef: releaseImageRef("gateway", m.digests.gateway),
|
|
508
|
-
},
|
|
509
|
-
};
|
|
512
|
+
const resolved = manifestServiceImages(m);
|
|
513
|
+
if (!resolved.ok)
|
|
514
|
+
return stopWith("failed", ["manifest"], `Nothing was changed: ${resolved.error}.`);
|
|
515
|
+
const images: Record<RollSlug, ReleaseImage> = resolved.images;
|
|
510
516
|
const planned = await planAppRoll(
|
|
511
517
|
{
|
|
512
518
|
provider: meta.provider,
|
|
@@ -694,7 +700,10 @@ export async function recordUpdateEnd(
|
|
|
694
700
|
}
|
|
695
701
|
|
|
696
702
|
/** Every digest the revisions serving traffic are known by, per service. */
|
|
697
|
-
async function servingDigests(
|
|
703
|
+
async function servingDigests(
|
|
704
|
+
deps: UpgraderJobDeps,
|
|
705
|
+
provider: InstallMetadata["provider"],
|
|
706
|
+
): Promise<
|
|
698
707
|
| {
|
|
699
708
|
ok: true;
|
|
700
709
|
digests: ServingDigests;
|
|
@@ -705,8 +714,12 @@ async function servingDigests(deps: UpgraderJobDeps): Promise<
|
|
|
705
714
|
> {
|
|
706
715
|
const digests: Partial<Record<RollSlug, readonly string[]>> = {};
|
|
707
716
|
const services: Partial<Record<RollSlug, string>> = {};
|
|
708
|
-
|
|
709
|
-
|
|
717
|
+
if (provider !== "gcp" && provider !== "gcp-firebase") {
|
|
718
|
+
return { ok: false, error: `the upgrader runs on Google Cloud installs only, not ${provider}` };
|
|
719
|
+
}
|
|
720
|
+
for (const slug of ROLL_ORDER) {
|
|
721
|
+
// #1328 — which service plays which role differs per provider.
|
|
722
|
+
const prefix = cloudRunServicePrefix(provider, deps.env, slug);
|
|
710
723
|
const service = await resolveGcpResourceName(
|
|
711
724
|
deps.run,
|
|
712
725
|
"run-service",
|
|
@@ -739,7 +752,7 @@ async function templateDigests(
|
|
|
739
752
|
services: Readonly<Record<RollSlug, string>>,
|
|
740
753
|
): Promise<ServingDigests> {
|
|
741
754
|
const out: Partial<Record<RollSlug, readonly string[]>> = {};
|
|
742
|
-
for (const slug of
|
|
755
|
+
for (const slug of ROLL_ORDER) {
|
|
743
756
|
const d = await findTemplateImageDigest(deps.run, deps.projectId, deps.region, services[slug]);
|
|
744
757
|
out[slug] = d ? [d] : [];
|
|
745
758
|
}
|