@caelo-cms/provisioning 0.11.12 → 0.11.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin-domain.d.ts +4 -22
- package/dist/admin-domain.d.ts.map +1 -1
- package/dist/admin-domain.js +4 -58
- package/dist/admin-domain.js.map +1 -1
- package/dist/agent-db.js +29 -5
- package/dist/audit-job-setup.d.ts.map +1 -1
- package/dist/audit-job-setup.js +14 -19
- package/dist/audit-job-setup.js.map +1 -1
- package/dist/caddy.d.ts +2 -1
- package/dist/caddy.d.ts.map +1 -1
- package/dist/caddy.js +21 -3
- package/dist/caddy.js.map +1 -1
- package/dist/cli.js +111 -62
- package/dist/cli.js.map +1 -1
- package/dist/compose.d.ts +22 -2
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +53 -3
- package/dist/compose.js.map +1 -1
- package/dist/converge-checkpoint.d.ts +1 -0
- package/dist/converge-checkpoint.d.ts.map +1 -1
- package/dist/converge-checkpoint.js +1 -0
- package/dist/converge-checkpoint.js.map +1 -1
- package/dist/deployed-release.d.ts.map +1 -1
- package/dist/deployed-release.js +40 -8
- package/dist/deployed-release.js.map +1 -1
- package/dist/gcp-names.d.ts +33 -14
- package/dist/gcp-names.d.ts.map +1 -1
- package/dist/gcp-names.js +43 -0
- package/dist/gcp-names.js.map +1 -1
- package/dist/gcp-url-map.d.ts +19 -4
- package/dist/gcp-url-map.d.ts.map +1 -1
- package/dist/gcp-url-map.js +33 -5
- package/dist/gcp-url-map.js.map +1 -1
- package/dist/infra-contract.d.ts +1 -1
- package/dist/infra-contract.d.ts.map +1 -1
- package/dist/infra-contract.js +22 -1
- package/dist/infra-contract.js.map +1 -1
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +5 -29
- package/dist/lifecycle.js.map +1 -1
- package/dist/mcp-auth-proxy-setup.d.ts +8 -2
- package/dist/mcp-auth-proxy-setup.d.ts.map +1 -1
- package/dist/mcp-auth-proxy-setup.js +4 -0
- package/dist/mcp-auth-proxy-setup.js.map +1 -1
- package/dist/release-manifest.d.ts +1 -1
- package/dist/release-manifest.d.ts.map +1 -1
- package/dist/release-manifest.js +2 -0
- package/dist/release-manifest.js.map +1 -1
- package/dist/remote-mcp.d.ts.map +1 -1
- package/dist/remote-mcp.js +11 -1
- package/dist/remote-mcp.js.map +1 -1
- package/dist/runtime-secrets.d.ts +10 -0
- package/dist/runtime-secrets.d.ts.map +1 -1
- package/dist/runtime-secrets.js +10 -1
- package/dist/runtime-secrets.js.map +1 -1
- package/dist/scheduler-setup.d.ts +5 -0
- package/dist/scheduler-setup.d.ts.map +1 -1
- package/dist/scheduler-setup.js +7 -0
- package/dist/scheduler-setup.js.map +1 -1
- package/dist/stack-contract.d.ts +61 -20
- package/dist/stack-contract.d.ts.map +1 -1
- package/dist/stack-contract.js +173 -35
- package/dist/stack-contract.js.map +1 -1
- package/dist/stack-converge.d.ts.map +1 -1
- package/dist/stack-converge.js +4 -2
- package/dist/stack-converge.js.map +1 -1
- package/dist/staging-admin.d.ts +2 -2
- package/dist/staging-admin.d.ts.map +1 -1
- package/dist/staging-gateway-setup.d.ts +140 -0
- package/dist/staging-gateway-setup.d.ts.map +1 -0
- package/dist/staging-gateway-setup.js +848 -0
- package/dist/staging-gateway-setup.js.map +1 -0
- package/dist/upgrade/app-roll.d.ts +4 -4
- package/dist/upgrade/app-roll.d.ts.map +1 -1
- package/dist/upgrade/app-roll.js +7 -5
- package/dist/upgrade/app-roll.js.map +1 -1
- package/dist/upgrade/converge.d.ts.map +1 -1
- package/dist/upgrade/converge.js +36 -27
- package/dist/upgrade/converge.js.map +1 -1
- package/dist/upgrade/image-forms.d.ts +3 -3
- package/dist/upgrade/image-forms.d.ts.map +1 -1
- package/dist/upgrade/image-forms.js +2 -2
- package/dist/upgrade/image-forms.js.map +1 -1
- package/dist/upgrade/release-images.d.ts +20 -0
- package/dist/upgrade/release-images.d.ts.map +1 -1
- package/dist/upgrade/release-images.js +28 -0
- package/dist/upgrade/release-images.js.map +1 -1
- package/dist/upgrade/upgrade.d.ts +3 -1
- package/dist/upgrade/upgrade.d.ts.map +1 -1
- package/dist/upgrade/upgrade.js +22 -1
- package/dist/upgrade/upgrade.js.map +1 -1
- package/dist/upgrader-job.d.ts.map +1 -1
- package/dist/upgrader-job.js +30 -5
- package/dist/upgrader-job.js.map +1 -1
- package/dist/upgrader-records.d.ts +2 -2
- package/dist/upgrader-records.d.ts.map +1 -1
- package/dist/upgrader-records.js +2 -2
- package/dist/upgrader-records.js.map +1 -1
- package/dist/wizard.d.ts.map +1 -1
- package/dist/wizard.js +3 -3
- package/dist/wizard.js.map +1 -1
- package/dist/wizards/gcp.d.ts.map +1 -1
- package/dist/wizards/gcp.js +48 -9
- package/dist/wizards/gcp.js.map +1 -1
- package/package.json +2 -2
- package/src/admin-domain.test.ts +0 -91
- package/src/admin-domain.ts +4 -64
- package/src/admin-memory.test.ts +2 -0
- package/src/audit-job-setup.test.ts +11 -11
- package/src/audit-job-setup.ts +14 -19
- package/src/caddy-redirects.docker.test.ts +10 -5
- package/src/caddy-redirects.test.ts +7 -5
- package/src/caddy-validate.test.ts +1 -1
- package/src/caddy.test.ts +12 -6
- package/src/caddy.ts +23 -4
- package/src/cli.ts +138 -77
- package/src/compose.ts +67 -2
- package/src/converge-checkpoint.ts +1 -0
- package/src/database-tls-builders.test.ts +3 -1
- package/src/deployed-release.test.ts +21 -0
- package/src/deployed-release.ts +42 -7
- package/src/gateway-env-isolation.test.ts +2 -0
- package/src/gcp-names.ts +53 -0
- package/src/gcp-url-map.test.ts +26 -2
- package/src/gcp-url-map.ts +36 -5
- package/src/index.test.ts +9 -3
- package/src/infra-contract.test.ts +5 -3
- package/src/infra-contract.ts +22 -1
- package/src/lifecycle.ts +6 -40
- package/src/mcp-auth-proxy-setup.test.ts +15 -6
- package/src/mcp-auth-proxy-setup.ts +14 -2
- package/src/release-manifest.ts +3 -1
- package/src/remote-mcp-stack-config.test.ts +1 -0
- package/src/remote-mcp.test.ts +5 -1
- package/src/remote-mcp.ts +10 -1
- package/src/runtime-secrets.test.ts +10 -1
- package/src/runtime-secrets.ts +21 -0
- package/src/scheduler-setup.test.ts +6 -0
- package/src/scheduler-setup.ts +7 -0
- package/src/self-hosted-backup.test.ts +6 -4
- package/src/self-hosted-compose.test.ts +2 -0
- package/src/stack-contract-parity.test.ts +7 -3
- package/src/stack-contract.test.ts +73 -13
- package/src/stack-contract.ts +195 -42
- package/src/stack-converge-secrets.test.ts +7 -2
- package/src/stack-converge.test.ts +37 -9
- package/src/stack-converge.ts +4 -1
- package/src/staging-admin.ts +2 -2
- package/src/staging-gateway-setup.test.ts +361 -0
- package/src/staging-gateway-setup.ts +1083 -0
- package/src/upgrade/app-roll.ts +7 -4
- package/src/upgrade/converge.ts +40 -26
- package/src/upgrade/image-forms.ts +7 -7
- package/src/upgrade/release-images.test.ts +28 -0
- package/src/upgrade/release-images.ts +44 -0
- package/src/upgrade/upgrade.test.ts +63 -11
- package/src/upgrade/upgrade.ts +29 -2
- package/src/upgrader-job.test.ts +23 -0
- package/src/upgrader-job.ts +45 -11
- package/src/upgrader-records.ts +7 -7
- package/src/wizard.ts +2 -3
- package/src/wizards/gcp.ts +73 -9
- package/stacks/gcp/index.ts +47 -6
- package/stacks/gcp-firebase/index.ts +44 -12
- package/stacks/self-hosted/index.ts +9 -2
package/src/upgrade/app-roll.ts
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
gatewayServiceAccountEmail,
|
|
30
30
|
mcpAuthProxyServiceAccountEmail,
|
|
31
31
|
runServiceAccountEmail,
|
|
32
|
+
stagingGatewayServiceAccountEmail,
|
|
32
33
|
} from "../gcp-names.js";
|
|
33
34
|
import type { ImageDigests } from "../install-state.js";
|
|
34
35
|
import {
|
|
@@ -82,10 +83,10 @@ import type { UpgradeReporter } from "./reporter.js";
|
|
|
82
83
|
export type RollSlug = CloudRunSlug;
|
|
83
84
|
|
|
84
85
|
/**
|
|
85
|
-
* The roll order: the
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
86
|
+
* The roll order: the gateways follow the admin, never the other way round
|
|
87
|
+
* (the admin's migrations and projection come first), the staging gateway
|
|
88
|
+
* (#1330) right after the live one with the same image, and the
|
|
89
|
+
* MCP-Auth-Proxy last (#1328).
|
|
89
90
|
*/
|
|
90
91
|
export const ROLL_ORDER: readonly RollSlug[] = CLOUD_RUN_SLUGS;
|
|
91
92
|
|
|
@@ -96,6 +97,8 @@ export function rollServiceAccount(slug: RollSlug, projectId: string, env: strin
|
|
|
96
97
|
return runServiceAccountEmail(projectId, env);
|
|
97
98
|
case "gateway":
|
|
98
99
|
return gatewayServiceAccountEmail(projectId, env);
|
|
100
|
+
case "staging-gateway":
|
|
101
|
+
return stagingGatewayServiceAccountEmail(projectId, env);
|
|
99
102
|
case "mcp-auth-proxy":
|
|
100
103
|
return mcpAuthProxyServiceAccountEmail(projectId, env);
|
|
101
104
|
}
|
package/src/upgrade/converge.ts
CHANGED
|
@@ -63,7 +63,6 @@ import {
|
|
|
63
63
|
type CloudRunEnvVar,
|
|
64
64
|
CONVERGE_JOB_ENV_VAR,
|
|
65
65
|
DATABASE_URL_VARS,
|
|
66
|
-
INTERIM_FORWARD_ORIGIN_ENV,
|
|
67
66
|
MCP_ENV_VAR,
|
|
68
67
|
OPERATOR_ACCESS_JOB_ENV_VAR,
|
|
69
68
|
parseSchedulerIntervalMinutes,
|
|
@@ -78,10 +77,11 @@ import {
|
|
|
78
77
|
removeRetiredIamBindings,
|
|
79
78
|
} from "../stack-converge.js";
|
|
80
79
|
import { ensureStagingDatabase } from "../staging-database.js";
|
|
80
|
+
import { ensureStagingGatewayEdge } from "../staging-gateway-setup.js";
|
|
81
81
|
import { ensureUpgraderJob, resolveUpgraderTarget } from "../upgrader-setup.js";
|
|
82
82
|
import { ensureUrlMapRedirectsAccess } from "../url-map-redirects-access.js";
|
|
83
83
|
import type { AppRollHooks, AppRollPlan, RollPlan, RollSlug } from "./app-roll.js";
|
|
84
|
-
import {
|
|
84
|
+
import { lookupFailure } from "./gcp-lookup.js";
|
|
85
85
|
import type { UpgradeReporter } from "./reporter.js";
|
|
86
86
|
|
|
87
87
|
/**
|
|
@@ -160,6 +160,7 @@ export function startConverge(
|
|
|
160
160
|
const services = {
|
|
161
161
|
admin: service("admin").serviceName,
|
|
162
162
|
gateway: service("gateway").serviceName,
|
|
163
|
+
"staging-gateway": service("staging-gateway").serviceName,
|
|
163
164
|
"mcp-auth-proxy": service("mcp-auth-proxy").serviceName,
|
|
164
165
|
};
|
|
165
166
|
const target = {
|
|
@@ -450,17 +451,41 @@ export function startConverge(
|
|
|
450
451
|
incomplete = true;
|
|
451
452
|
}
|
|
452
453
|
|
|
453
|
-
async function
|
|
454
|
-
// #
|
|
455
|
-
//
|
|
456
|
-
//
|
|
457
|
-
//
|
|
458
|
-
// the
|
|
459
|
-
//
|
|
460
|
-
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
454
|
+
async function stagingEdge(): Promise<void> {
|
|
455
|
+
// #1330 — route the staging host to the staging gateway and nowhere
|
|
456
|
+
// else: gcp-firebase's second Hosting site (and the old Cloud Run domain
|
|
457
|
+
// mapping deleted), gcp's host rule, backend and certificate
|
|
458
|
+
// (staging-gateway-setup.ts). Only staging depends on it, so a failure
|
|
459
|
+
// warns; the records the staging host needs are printed (Settings →
|
|
460
|
+
// Updates shows them too).
|
|
461
|
+
const s = r.step(`Routing the staging host ${install.stagingHost} to the staging gateway...`);
|
|
462
|
+
const edge = await ensureStagingGatewayEdge(
|
|
463
|
+
{
|
|
464
|
+
provider: install.provider,
|
|
465
|
+
projectId: install.projectId,
|
|
466
|
+
region: install.region,
|
|
467
|
+
env: install.env,
|
|
468
|
+
domain: install.domain,
|
|
469
|
+
stagingHost: install.stagingHost,
|
|
470
|
+
},
|
|
471
|
+
{ run, ...(deps.http ? { http: deps.http } : {}) },
|
|
472
|
+
);
|
|
473
|
+
for (const line of edge.done) r.info(` ${line}`);
|
|
474
|
+
if (edge.ok) {
|
|
475
|
+
s.done(`The staging host is served by the staging gateway, behind the staging gate`);
|
|
476
|
+
if (edge.dnsRecords.length > 0) {
|
|
477
|
+
r.info(
|
|
478
|
+
` DNS for ${install.stagingHost} (add or change at your registrar if it differs; Settings → Updates shows the same):`,
|
|
479
|
+
);
|
|
480
|
+
for (const d of edge.dnsRecords) r.info(` ${d.name} ${d.type} ${d.value}`);
|
|
481
|
+
}
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
s.warned(`The staging host is not routed yet: ${edge.error}`);
|
|
485
|
+
r.warn(
|
|
486
|
+
`The upgrade continues; ${install.stagingHost} answers nothing until this succeeds (the live site is not affected). Re-run upgrade once the cause is fixed.`,
|
|
487
|
+
);
|
|
488
|
+
incomplete = true;
|
|
464
489
|
}
|
|
465
490
|
|
|
466
491
|
/** The admin's origin behind IAP, once {@link remoteMcpIdentity} read it. */
|
|
@@ -847,6 +872,7 @@ export function startConverge(
|
|
|
847
872
|
await auditJob();
|
|
848
873
|
await captureJob();
|
|
849
874
|
await remoteMcpEdge();
|
|
875
|
+
await stagingEdge();
|
|
850
876
|
return true;
|
|
851
877
|
},
|
|
852
878
|
|
|
@@ -859,19 +885,7 @@ export function startConverge(
|
|
|
859
885
|
await convergeJob();
|
|
860
886
|
await schedulerTick();
|
|
861
887
|
await operatorAccess();
|
|
862
|
-
|
|
863
|
-
try {
|
|
864
|
-
forward = await interimForwardEnv();
|
|
865
|
-
} catch (e) {
|
|
866
|
-
r.error(
|
|
867
|
-
`The MCP-Auth-Proxy cannot be pointed at the data gateway: ${e instanceof Error ? e.message : String(e)}. Nothing was rolled; re-run upgrade once it is fixed.`,
|
|
868
|
-
);
|
|
869
|
-
return { ok: false };
|
|
870
|
-
}
|
|
871
|
-
const extraEnv: Partial<Record<RollSlug, CloudRunEnvVar[]>> = {
|
|
872
|
-
...(identity ?? {}),
|
|
873
|
-
"mcp-auth-proxy": [...(identity?.["mcp-auth-proxy"] ?? []), ...forward],
|
|
874
|
-
};
|
|
888
|
+
const extraEnv: Partial<Record<RollSlug, CloudRunEnvVar[]>> = { ...(identity ?? {}) };
|
|
875
889
|
return {
|
|
876
890
|
ok: true,
|
|
877
891
|
leaveUntouched: [...leaveUntouched],
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
import { z } from "zod";
|
|
31
31
|
import type { ImageDigests } from "../install-state.js";
|
|
32
32
|
import type { ReleaseManifest } from "../release-manifest.js";
|
|
33
|
-
import {
|
|
33
|
+
import { SERVICE_IMAGE_NAMES, type ServiceImageName } from "../stack-contract.js";
|
|
34
34
|
import { fetchRegistryManifest, IMAGE_DIGEST, RELEASE_REGISTRY } from "./release-images.js";
|
|
35
35
|
|
|
36
36
|
/** The digests one release image is known by. */
|
|
@@ -58,10 +58,10 @@ export interface ReleaseImageForms {
|
|
|
58
58
|
/** The forms of release `m`'s image `slug` (signed platform digests win). */
|
|
59
59
|
of(
|
|
60
60
|
m: Pick<ReleaseManifest, "version" | "digests" | "platformDigests">,
|
|
61
|
-
slug:
|
|
61
|
+
slug: ServiceImageName,
|
|
62
62
|
): Promise<ImageFormsResult>;
|
|
63
63
|
/** The forms of the index `indexDigest` of image `slug`, read from the mirror. */
|
|
64
|
-
ofIndex(slug:
|
|
64
|
+
ofIndex(slug: ServiceImageName, indexDigest: string): Promise<ImageFormsResult>;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
const INDEX_MEDIA_TYPES = new Set([
|
|
@@ -133,7 +133,7 @@ export function createReleaseImageForms(fetchImpl: typeof fetch = fetch): Releas
|
|
|
133
133
|
const cache = new Map<string, Promise<ImageFormsResult>>();
|
|
134
134
|
const { region, project, repo } = RELEASE_REGISTRY;
|
|
135
135
|
|
|
136
|
-
async function lookup(slug:
|
|
136
|
+
async function lookup(slug: ServiceImageName, indexDigest: string): Promise<ImageFormsResult> {
|
|
137
137
|
const where = `the ${slug} image index ${indexDigest} in the release mirror`;
|
|
138
138
|
const fetched = await fetchRegistryManifest(
|
|
139
139
|
`${region}-docker.pkg.dev`,
|
|
@@ -165,7 +165,7 @@ export function createReleaseImageForms(fetchImpl: typeof fetch = fetch): Releas
|
|
|
165
165
|
return formsFromIndexJson(indexDigest, json);
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
const ofIndex = (slug:
|
|
168
|
+
const ofIndex = (slug: ServiceImageName, indexDigest: string): Promise<ImageFormsResult> => {
|
|
169
169
|
const key = `${slug}@${indexDigest}`;
|
|
170
170
|
let pending = cache.get(key);
|
|
171
171
|
if (!pending) {
|
|
@@ -212,8 +212,8 @@ export async function platformDigestsOf(
|
|
|
212
212
|
| { readonly ok: true; readonly platformDigests: ImageDigests }
|
|
213
213
|
| { readonly ok: false; readonly message: string }
|
|
214
214
|
> {
|
|
215
|
-
const out: Partial<Record<
|
|
216
|
-
for (const slug of
|
|
215
|
+
const out: Partial<Record<ServiceImageName, string>> = {};
|
|
216
|
+
for (const slug of SERVICE_IMAGE_NAMES) {
|
|
217
217
|
const index = digests[slug];
|
|
218
218
|
// #1328 — digests recorded before the MCP-Auth-Proxy existed carry none.
|
|
219
219
|
if (!index) continue;
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
RELEASE_SIGNER_IDENTITY_REGEXP,
|
|
11
11
|
releaseTag,
|
|
12
12
|
resolveRegistryDigest,
|
|
13
|
+
resolveReleaseImages,
|
|
13
14
|
} from "./release-images.js";
|
|
14
15
|
|
|
15
16
|
const DIGEST = `sha256:${"d".repeat(64)}`;
|
|
@@ -138,3 +139,30 @@ describe("cosignInstallHint (#816)", () => {
|
|
|
138
139
|
expect(cosignInstallHint("darwin")).toContain("brew install cosign");
|
|
139
140
|
});
|
|
140
141
|
});
|
|
142
|
+
|
|
143
|
+
describe("resolveReleaseImages (#1330)", () => {
|
|
144
|
+
it("looks up each release image once and gives the staging gateway the gateway's", async () => {
|
|
145
|
+
const asked: string[] = [];
|
|
146
|
+
const r = await resolveReleaseImages("0.12.0", async (image) => {
|
|
147
|
+
asked.push(image);
|
|
148
|
+
return { ok: true, digest: `sha256:${image === "gateway" ? "b" : "a"}${"0".repeat(63)}` };
|
|
149
|
+
});
|
|
150
|
+
expect(asked).toEqual(["admin", "gateway", "mcp-auth-proxy"]);
|
|
151
|
+
expect(r.ok).toBe(true);
|
|
152
|
+
if (!r.ok) return;
|
|
153
|
+
expect(r.images["staging-gateway"]).toEqual(r.images.gateway);
|
|
154
|
+
expect(r.images["staging-gateway"].imageRef).toContain("/gateway@sha256:");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("names the image whose tag is missing", async () => {
|
|
158
|
+
const r = await resolveReleaseImages("0.12.0", async (image) =>
|
|
159
|
+
image === "gateway"
|
|
160
|
+
? { ok: false, reason: "404" }
|
|
161
|
+
: { ok: true, digest: `sha256:${"a".repeat(64)}` },
|
|
162
|
+
);
|
|
163
|
+
expect(r.ok).toBe(false);
|
|
164
|
+
if (r.ok) return;
|
|
165
|
+
expect(r.error).toContain("gateway:0.12.0");
|
|
166
|
+
expect(r.error).not.toContain("staging-gateway");
|
|
167
|
+
});
|
|
168
|
+
});
|
|
@@ -16,6 +16,13 @@
|
|
|
16
16
|
import { IMAGE_DIGEST } from "@caelo-cms/shared";
|
|
17
17
|
import { confirm, isCancel, log, spinner } from "@clack/prompts";
|
|
18
18
|
import { green, red } from "kleur/colors";
|
|
19
|
+
import {
|
|
20
|
+
CLOUD_RUN_SLUGS,
|
|
21
|
+
type CloudRunSlug,
|
|
22
|
+
SERVICE_IMAGE,
|
|
23
|
+
SERVICE_IMAGE_NAMES,
|
|
24
|
+
type ServiceImageName,
|
|
25
|
+
} from "../stack-contract.js";
|
|
19
26
|
|
|
20
27
|
/** The Caelo services shipped as release images (#1328 added the MCP-Auth-Proxy). */
|
|
21
28
|
export type ReleaseService = "admin" | "gateway" | "mcp-auth-proxy";
|
|
@@ -468,3 +475,40 @@ export async function verifyCosignAll(
|
|
|
468
475
|
}
|
|
469
476
|
return true;
|
|
470
477
|
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Resolve each release image's digest from the public registry, and give
|
|
481
|
+
* every service the image it runs (#1330: the staging gateway runs the
|
|
482
|
+
* gateway's — there is no `staging-gateway` image to look up). Via the
|
|
483
|
+
* registry's anonymous V2 API, not `gcloud artifacts docker tags list`: that
|
|
484
|
+
* needs IAM read on Caelo's registry project, which operators don't have.
|
|
485
|
+
*
|
|
486
|
+
* @param resolve - the tag lookup (tests pass a fake)
|
|
487
|
+
*/
|
|
488
|
+
export async function resolveReleaseImages(
|
|
489
|
+
tag: string,
|
|
490
|
+
resolve: (image: string, tag: string) => Promise<DigestResult> = resolveTagDigest,
|
|
491
|
+
): Promise<
|
|
492
|
+
| { ok: true; images: Record<CloudRunSlug, { digest: string; imageRef: string }> }
|
|
493
|
+
| { ok: false; error: string }
|
|
494
|
+
> {
|
|
495
|
+
const byImage: Partial<Record<ServiceImageName, { digest: string; imageRef: string }>> = {};
|
|
496
|
+
for (const image of SERVICE_IMAGE_NAMES) {
|
|
497
|
+
const res = await resolve(image, tag);
|
|
498
|
+
if (!res.ok) {
|
|
499
|
+
const r = RELEASE_REGISTRY;
|
|
500
|
+
return {
|
|
501
|
+
ok: false,
|
|
502
|
+
error:
|
|
503
|
+
`Couldn't resolve image digest for ${image}:${tag}: the public registry returned ${res.reason}.\n` +
|
|
504
|
+
`Verify the tag exists at https://${r.region}-docker.pkg.dev/${r.project}/${r.repo}/${image}:${tag}\n` +
|
|
505
|
+
"(latest releases live at https://github.com/caelo-cms/caelo-cms/releases — pass --version vX.Y.Z to pin.)",
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
byImage[image] = { digest: res.digest, imageRef: releaseImageRef(image, res.digest) };
|
|
509
|
+
}
|
|
510
|
+
const images = Object.fromEntries(
|
|
511
|
+
CLOUD_RUN_SLUGS.map((slug) => [slug, byImage[SERVICE_IMAGE[slug]]]),
|
|
512
|
+
) as Record<CloudRunSlug, { digest: string; imageRef: string }>;
|
|
513
|
+
return { ok: true, images };
|
|
514
|
+
}
|
|
@@ -41,6 +41,8 @@ const GATEWAY = "caelo-production-data-gateway";
|
|
|
41
41
|
const PROXY = "caelo-production-gateway-bbb";
|
|
42
42
|
/** gcp: the proxy's fixed name (the gateway keeps its auto-named service). */
|
|
43
43
|
const GCP_PROXY = "caelo-production-mcp-auth-proxy";
|
|
44
|
+
/** #1330 — the staging gateway, under its fixed name on both providers. */
|
|
45
|
+
const STAGING = "caelo-production-staging-gateway";
|
|
44
46
|
const SQL = "caelo-production-pg-abc";
|
|
45
47
|
const BACKUP_ID = "1760000000000";
|
|
46
48
|
const DIGEST = {
|
|
@@ -51,6 +53,8 @@ const DIGEST = {
|
|
|
51
53
|
const IMAGES: Record<RollSlug, ReleaseImage> = {
|
|
52
54
|
admin: { digest: DIGEST.admin, imageRef: `registry/admin@${DIGEST.admin}` },
|
|
53
55
|
gateway: { digest: DIGEST.gateway, imageRef: `registry/gateway@${DIGEST.gateway}` },
|
|
56
|
+
// #1330 — the staging gateway runs the gateway's image.
|
|
57
|
+
"staging-gateway": { digest: DIGEST.gateway, imageRef: `registry/gateway@${DIGEST.gateway}` },
|
|
54
58
|
"mcp-auth-proxy": {
|
|
55
59
|
digest: DIGEST["mcp-auth-proxy"],
|
|
56
60
|
imageRef: `registry/mcp-auth-proxy@${DIGEST["mcp-auth-proxy"]}`,
|
|
@@ -115,6 +119,16 @@ const GATEWAY_JSON = serviceJson("registry/gateway@sha256:old", [
|
|
|
115
119
|
secretRef("PUBLIC_DATABASE_PASSWORD", "public-role-password"),
|
|
116
120
|
]);
|
|
117
121
|
|
|
122
|
+
/** #1330 — the staging gateway as the converge deployed it. */
|
|
123
|
+
const STAGING_JSON = serviceJson("registry/gateway@sha256:old", [
|
|
124
|
+
{
|
|
125
|
+
name: "PUBLIC_DATABASE_URL",
|
|
126
|
+
value:
|
|
127
|
+
"postgresql://public_staging_role@db1.p-label.europe-west1.sql-psa.goog:5432/cms_public_staging?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem",
|
|
128
|
+
},
|
|
129
|
+
secretRef("PUBLIC_DATABASE_PASSWORD", "public-staging-role-password"),
|
|
130
|
+
]);
|
|
131
|
+
|
|
118
132
|
/**
|
|
119
133
|
* gcp-firebase: the service that was the gateway, before this upgrade turns
|
|
120
134
|
* it into the MCP-Auth-Proxy — the visitor gateway's database login, the
|
|
@@ -150,6 +164,28 @@ function fakeInstall(events: string[], overrides: [RegExp, Answer][] = []) {
|
|
|
150
164
|
// On gcp this prefix is the gateway's, on gcp-firebase the proxy's.
|
|
151
165
|
[/^run services list .*caelo-production-gateway/, ok(`${PROXY}\n`)],
|
|
152
166
|
[/^run services list .*caelo-production-mcp-auth-proxy/, ok(`${GCP_PROXY}\n`)],
|
|
167
|
+
[/^run services list .*caelo-production-staging-gateway/, ok(`${STAGING}\n`)],
|
|
168
|
+
[new RegExp(`describe ${STAGING} .*--format=json`), ok(STAGING_JSON)],
|
|
169
|
+
// #1330 — the gcp staging edge: one WAF policy, one HTTPS proxy, the site's URL map.
|
|
170
|
+
[/^compute security-policies list/, ok("caelo-production-waf-1\n")],
|
|
171
|
+
[
|
|
172
|
+
/^compute target-https-proxies list/,
|
|
173
|
+
ok(JSON.stringify([{ name: "caelo-production-https-proxy-1", sslCertificates: [] }])),
|
|
174
|
+
],
|
|
175
|
+
[
|
|
176
|
+
/^compute url-maps list/,
|
|
177
|
+
ok(
|
|
178
|
+
JSON.stringify([
|
|
179
|
+
{
|
|
180
|
+
name: "caelo-production-url-map-1",
|
|
181
|
+
hostRules: [{ hosts: ["acme.com"], pathMatcher: "public" }],
|
|
182
|
+
pathMatchers: [
|
|
183
|
+
{ name: "public", pathRules: [{ paths: ["/api/*"], service: "gw-backend" }] },
|
|
184
|
+
],
|
|
185
|
+
},
|
|
186
|
+
]),
|
|
187
|
+
),
|
|
188
|
+
],
|
|
153
189
|
[/describe (\S+) .*revisionName/, (l) => ok(`${l.split(" ")[3]}-00001-old\n`)],
|
|
154
190
|
[new RegExp(`describe ${ADMIN} .*--format=json`), ok(ADMIN_JSON)],
|
|
155
191
|
[new RegExp(`describe ${GATEWAY} .*--format=json`), ok(GATEWAY_JSON)],
|
|
@@ -298,7 +334,7 @@ const ANY_WRITE =
|
|
|
298
334
|
* install lacks (mcp-auth-proxy-setup.ts). Additive; nothing serves on them.
|
|
299
335
|
*/
|
|
300
336
|
const PRE_PLAN_WRITE =
|
|
301
|
-
/^(iam service-accounts create caelo-production-mcp-proxy-sa |secrets add-iam-policy-binding caelo-production-(mcp-oauth-seal-key|operator-oauth-client) .*--member=serviceAccount:caelo-production-mcp-proxy-sa@|run services replace |run deploy caelo-production-mcp-auth-proxy )/;
|
|
337
|
+
/^(iam service-accounts create caelo-production-(mcp-proxy-sa|stg-gw-sa) |secrets add-iam-policy-binding caelo-production-(mcp-oauth-seal-key|operator-oauth-client) .*--member=serviceAccount:caelo-production-mcp-proxy-sa@|secrets add-iam-policy-binding caelo-production-public-staging-role-password .*--member=serviceAccount:caelo-production-stg-gw-sa@|storage buckets (create|add-iam-policy-binding) gs:\/\/acme-caelo-production-staging |run services replace |run deploy caelo-production-(mcp-auth-proxy|staging-gateway) |run services add-iam-policy-binding caelo-production-staging-gateway .*--member=allUsers )/;
|
|
302
338
|
|
|
303
339
|
const indexOf = (events: string[], pattern: RegExp): number => {
|
|
304
340
|
const i = events.findIndex((e) => pattern.test(e));
|
|
@@ -606,11 +642,14 @@ describe("upgradeGcpInstall — converge, then app-roll", () => {
|
|
|
606
642
|
.slice(deep)
|
|
607
643
|
.filter((e) => e.includes("update-traffic") && e.includes("--to-revisions"));
|
|
608
644
|
// Newest first: the proxy (back to the gateway release it was), the
|
|
609
|
-
// gateway, the admin.
|
|
645
|
+
// staging gateway (#1330), the gateway, the admin.
|
|
610
646
|
expect(rollbacks).toEqual([
|
|
611
647
|
expect.stringContaining(
|
|
612
648
|
`${PROXY} --region europe-west1 --project acme --to-revisions ${PROXY}-00001-old=100`,
|
|
613
649
|
),
|
|
650
|
+
expect.stringContaining(
|
|
651
|
+
`${STAGING} --region europe-west1 --project acme --to-revisions ${STAGING}-00001-old=100`,
|
|
652
|
+
),
|
|
614
653
|
expect.stringContaining(
|
|
615
654
|
`${GATEWAY} --region europe-west1 --project acme --to-revisions ${GATEWAY}-00001-old=100`,
|
|
616
655
|
),
|
|
@@ -752,6 +791,7 @@ describe("upgradeGcpInstall — converge, then app-roll", () => {
|
|
|
752
791
|
const before = {
|
|
753
792
|
admin: `${ADMIN}-00000-before`,
|
|
754
793
|
gateway: `${GATEWAY}-00000-before`,
|
|
794
|
+
"staging-gateway": `${STAGING}-00000-before`,
|
|
755
795
|
"mcp-auth-proxy": `${PROXY}-00000-before`,
|
|
756
796
|
};
|
|
757
797
|
expect(
|
|
@@ -772,12 +812,13 @@ describe("upgradeGcpInstall — converge, then app-roll", () => {
|
|
|
772
812
|
const rollbacks = events.filter((e) => e.includes("--to-revisions"));
|
|
773
813
|
expect(rollbacks).toEqual([
|
|
774
814
|
expect.stringContaining(`--to-revisions ${before["mcp-auth-proxy"]}=100`),
|
|
815
|
+
expect.stringContaining(`--to-revisions ${before["staging-gateway"]}=100`),
|
|
775
816
|
expect.stringContaining(`--to-revisions ${before.gateway}=100`),
|
|
776
817
|
expect.stringContaining(`--to-revisions ${before.admin}=100`),
|
|
777
818
|
]);
|
|
778
819
|
});
|
|
779
820
|
|
|
780
|
-
it("#1328 — a checkpoint without the proxy's revision (an older release wrote it) keeps the planned one", async () => {
|
|
821
|
+
it("#1328/#1330 — a checkpoint without the proxy's or staging gateway's revision (an older release wrote it) keeps the planned one", async () => {
|
|
781
822
|
const events: string[] = [];
|
|
782
823
|
const { deps } = upgradeDeps(events, { deep: { ok: false, error: "unhealthy" } });
|
|
783
824
|
const before = { admin: `${ADMIN}-00000-before`, gateway: `${GATEWAY}-00000-before` };
|
|
@@ -792,6 +833,7 @@ describe("upgradeGcpInstall — converge, then app-roll", () => {
|
|
|
792
833
|
).toBe(false);
|
|
793
834
|
expect(events.filter((e) => e.includes("--to-revisions"))).toEqual([
|
|
794
835
|
expect.stringContaining(`--to-revisions ${PROXY}-00001-old=100`),
|
|
836
|
+
expect.stringContaining(`--to-revisions ${STAGING}-00001-old=100`),
|
|
795
837
|
expect.stringContaining(`--to-revisions ${before.gateway}=100`),
|
|
796
838
|
expect.stringContaining(`--to-revisions ${before.admin}=100`),
|
|
797
839
|
]);
|
|
@@ -916,11 +958,12 @@ describe("upgradeGcpInstall — converge, then app-roll", () => {
|
|
|
916
958
|
expect(gateway).not.toContain("operator-oauth-client");
|
|
917
959
|
expect(gateway).not.toContain("--timeout");
|
|
918
960
|
expect(admin).not.toContain("--timeout");
|
|
919
|
-
// No extra revision: everything rides the image rolls
|
|
920
|
-
|
|
961
|
+
// No extra revision: everything rides the image rolls (four services
|
|
962
|
+
// since the staging gateway, #1330).
|
|
963
|
+
expect(calls.filter((c) => /^run services update /.test(c))).toHaveLength(4);
|
|
921
964
|
});
|
|
922
965
|
|
|
923
|
-
it("#1328 — the gcp-firebase proxy roll leaves the VPC, drops every database login and forwards
|
|
966
|
+
it("#1328/#1331 — the gcp-firebase proxy roll leaves the VPC, drops every database login and forwards nothing", async () => {
|
|
924
967
|
const events: string[] = [];
|
|
925
968
|
const { deps, calls } = upgradeDeps(events);
|
|
926
969
|
expect(await upgradeGcpInstall(TARGET, deps)).toBe(true);
|
|
@@ -931,7 +974,8 @@ describe("upgradeGcpInstall — converge, then app-roll", () => {
|
|
|
931
974
|
);
|
|
932
975
|
expect(proxy).toContain("--clear-network");
|
|
933
976
|
expect(proxy).toContain("CAELO_REMOTE_MCP=available");
|
|
934
|
-
|
|
977
|
+
// #1331 — the interim forwarder to the data gateway is gone.
|
|
978
|
+
expect(proxy).not.toContain("CAELO_INTERIM_FORWARD_ORIGIN");
|
|
935
979
|
const removedEnv = proxy.match(/--remove-env-vars=(\S+)/)?.[1]?.split(",") ?? [];
|
|
936
980
|
const removedSecrets = proxy.match(/--remove-secrets=(\S+)/)?.[1]?.split(",") ?? [];
|
|
937
981
|
expect(removedEnv).toContain("PUBLIC_DATABASE_URL");
|
|
@@ -1031,10 +1075,12 @@ describe("upgradeGcpInstall — converge, then app-roll", () => {
|
|
|
1031
1075
|
const admin = rolls.find((c) => c.startsWith(`run services update ${ADMIN} `)) ?? "";
|
|
1032
1076
|
const gateway = rolls.find((c) => c.startsWith(`run services update ${GATEWAY} `)) ?? "";
|
|
1033
1077
|
const proxy = rolls.find((c) => c.startsWith(`run services update ${PROXY} `)) ?? "";
|
|
1078
|
+
const staging = rolls.find((c) => c.startsWith(`run services update ${STAGING} `)) ?? "";
|
|
1034
1079
|
expect(gateway).toContain("--cpu-throttling");
|
|
1080
|
+
expect(staging).toContain("--cpu-throttling");
|
|
1035
1081
|
expect(proxy).toContain("--cpu-throttling");
|
|
1036
1082
|
expect(admin).not.toContain("cpu-throttling");
|
|
1037
|
-
expect(calls.filter((c) => /^run services update /.test(c))).toHaveLength(
|
|
1083
|
+
expect(calls.filter((c) => /^run services update /.test(c))).toHaveLength(4);
|
|
1038
1084
|
});
|
|
1039
1085
|
|
|
1040
1086
|
it("#806/#1328 — on gcp, moves the remote-MCP paths and the Cloud Armor rule to the proxy's backend before the roll", async () => {
|
|
@@ -1122,7 +1168,13 @@ describe("upgradeGcpInstall — converge, then app-roll", () => {
|
|
|
1122
1168
|
events,
|
|
1123
1169
|
/^gcloud compute security-policies rules delete 900 --security-policy=caelo-production-waf-1 /,
|
|
1124
1170
|
);
|
|
1125
|
-
|
|
1171
|
+
// The remote-MCP routes first, then (#1330) the staging host's own rule.
|
|
1172
|
+
expect(puts).toHaveLength(2);
|
|
1173
|
+
const staging = JSON.parse(puts[1] as string) as typeof urlMap;
|
|
1174
|
+
expect(staging.hostRules).toContainEqual({
|
|
1175
|
+
hosts: ["staging.acme.com"],
|
|
1176
|
+
pathMatcher: "staging",
|
|
1177
|
+
});
|
|
1126
1178
|
const put = JSON.parse(puts[0] as string) as typeof urlMap;
|
|
1127
1179
|
const rules = put.pathMatchers[0]?.pathRules ?? [];
|
|
1128
1180
|
expect(rules).toEqual([
|
|
@@ -1190,7 +1242,7 @@ describe("upgradeGcpInstall — converge, then app-roll", () => {
|
|
|
1190
1242
|
healthProbe: async () => ({ ok: true, probe: throwing }),
|
|
1191
1243
|
}),
|
|
1192
1244
|
).toBe(false);
|
|
1193
|
-
expect(events.filter((e) => e.includes("--to-revisions"))).toHaveLength(
|
|
1245
|
+
expect(events.filter((e) => e.includes("--to-revisions"))).toHaveLength(4);
|
|
1194
1246
|
expect(recorded.digests).toEqual([]);
|
|
1195
1247
|
});
|
|
1196
1248
|
|
|
@@ -1351,7 +1403,7 @@ describe("app-roll and converge stay on their side", () => {
|
|
|
1351
1403
|
);
|
|
1352
1404
|
expect(result).toEqual({ ok: true, digests: DIGEST });
|
|
1353
1405
|
expect(calls.filter((c) => CONVERGE_WRITE.test(c))).toEqual([]);
|
|
1354
|
-
expect(calls.filter((c) => /^run services update /.test(c))).toHaveLength(
|
|
1406
|
+
expect(calls.filter((c) => /^run services update /.test(c))).toHaveLength(4);
|
|
1355
1407
|
});
|
|
1356
1408
|
|
|
1357
1409
|
it("#1338: without a converge, a plan whose services dial the database's IP stops before migrations", async () => {
|
package/src/upgrade/upgrade.ts
CHANGED
|
@@ -34,6 +34,7 @@ import type { GcloudRunner, Sleep } from "../gcloud-retry.js";
|
|
|
34
34
|
import type { ImageDigests } from "../install-state.js";
|
|
35
35
|
import { ensureReleaseServices } from "../mcp-auth-proxy-setup.js";
|
|
36
36
|
import type { HttpFetch } from "../runtime-secrets.js";
|
|
37
|
+
import { SERVICE_IMAGE } from "../stack-contract.js";
|
|
37
38
|
import {
|
|
38
39
|
type AppRollPlan,
|
|
39
40
|
executeAppRoll,
|
|
@@ -90,9 +91,11 @@ export interface UpgradeCheckpoint {
|
|
|
90
91
|
|
|
91
92
|
/**
|
|
92
93
|
* The rollback target per service. The MCP-Auth-Proxy's is absent in a
|
|
93
|
-
* checkpoint a release before #1328 wrote
|
|
94
|
+
* checkpoint a release before #1328 wrote, the staging gateway's in one from
|
|
95
|
+
* before #1330; their planned revisions are kept then.
|
|
94
96
|
*/
|
|
95
97
|
export type PriorRevisions = Readonly<Record<"admin" | "gateway", string>> & {
|
|
98
|
+
readonly "staging-gateway"?: string;
|
|
96
99
|
readonly "mcp-auth-proxy"?: string;
|
|
97
100
|
};
|
|
98
101
|
|
|
@@ -189,7 +192,12 @@ export async function upgradeGcpInstallOutcome(
|
|
|
189
192
|
// nothing yet, so the install keeps running as it is when this or
|
|
190
193
|
// anything after it fails. The gcp proxy runs the new release's image,
|
|
191
194
|
// so that image's signature is checked before it is deployed.
|
|
195
|
+
//
|
|
196
|
+
// #1330 — and the staging gateway (both providers), deployed with the new
|
|
197
|
+
// release's gateway image when the install has none yet, so that image's
|
|
198
|
+
// signature is checked first too.
|
|
192
199
|
const proxyImage = images.images["mcp-auth-proxy"];
|
|
200
|
+
const gatewayImage = images.images.gateway;
|
|
193
201
|
if (
|
|
194
202
|
target.provider === "gcp" &&
|
|
195
203
|
!target.skipVerify &&
|
|
@@ -198,6 +206,13 @@ export async function upgradeGcpInstallOutcome(
|
|
|
198
206
|
sPre.failed("The MCP-Auth-Proxy image's signature did not verify");
|
|
199
207
|
return failed;
|
|
200
208
|
}
|
|
209
|
+
if (
|
|
210
|
+
!target.skipVerify &&
|
|
211
|
+
!(await deps.verifyImages([{ slug: "gateway", digest: gatewayImage.digest }]))
|
|
212
|
+
) {
|
|
213
|
+
sPre.failed("The gateway image's signature did not verify");
|
|
214
|
+
return failed;
|
|
215
|
+
}
|
|
201
216
|
const ensured = await ensureReleaseServices(
|
|
202
217
|
{
|
|
203
218
|
provider: target.provider,
|
|
@@ -205,7 +220,9 @@ export async function upgradeGcpInstallOutcome(
|
|
|
205
220
|
region: target.region,
|
|
206
221
|
env: target.env,
|
|
207
222
|
domain: target.domain,
|
|
223
|
+
stagingHost: target.stagingHost,
|
|
208
224
|
mcpAuthProxyImageRef: proxyImage.imageRef,
|
|
225
|
+
gatewayImageRef: gatewayImage.imageRef,
|
|
209
226
|
},
|
|
210
227
|
{ run },
|
|
211
228
|
);
|
|
@@ -244,7 +261,17 @@ export async function upgradeGcpInstallOutcome(
|
|
|
244
261
|
// 2. P21 ship 4 — signatures against the Caelo release workflow's
|
|
245
262
|
// identity: refuses a compromised registry, a typosquat or a fork's image.
|
|
246
263
|
if (!target.skipVerify) {
|
|
247
|
-
|
|
264
|
+
// One check per release image: the staging gateway (#1330) runs the
|
|
265
|
+
// gateway's, which has no repository of its own to verify against.
|
|
266
|
+
const images = [
|
|
267
|
+
...new Map(
|
|
268
|
+
plan.services.map((s) => [
|
|
269
|
+
`${SERVICE_IMAGE[s.slug]}@${s.digest}`,
|
|
270
|
+
{ slug: SERVICE_IMAGE[s.slug], digest: s.digest },
|
|
271
|
+
]),
|
|
272
|
+
).values(),
|
|
273
|
+
];
|
|
274
|
+
if (!(await deps.verifyImages(images))) return failed;
|
|
248
275
|
} else {
|
|
249
276
|
r.warn("--skip-verify set — image signatures NOT verified.");
|
|
250
277
|
}
|
package/src/upgrader-job.test.ts
CHANGED
|
@@ -171,6 +171,11 @@ function fakeGcloud(events: string[], overrides: [RegExp, Answer][] = []) {
|
|
|
171
171
|
[/^run services list .*caelo-production-admin/, ok(`${ADMIN}\n`)],
|
|
172
172
|
[/^run services list .*caelo-production-data-gateway/, ok(`${GATEWAY}\n`)],
|
|
173
173
|
[/^run services list .*caelo-production-gateway/, ok(`${PROXY}\n`)],
|
|
174
|
+
// #1330 — the staging gateway, serving the gateway's image (below).
|
|
175
|
+
[
|
|
176
|
+
/^run services list .*caelo-production-staging-gateway/,
|
|
177
|
+
ok("caelo-production-staging-gateway\n"),
|
|
178
|
+
],
|
|
174
179
|
[/describe (\S+) .*revisionName/, (l) => ok(`${l.split(" ")[3]}-00001-old\n`)],
|
|
175
180
|
[new RegExp(`describe ${ADMIN} .*--format=json`), ok(ADMIN_JSON)],
|
|
176
181
|
[new RegExp(`describe ${GATEWAY} .*--format=json`), ok(GATEWAY_JSON)],
|
|
@@ -478,6 +483,24 @@ describe("runUpgraderJob", () => {
|
|
|
478
483
|
);
|
|
479
484
|
});
|
|
480
485
|
|
|
486
|
+
it("#1330: a staging gateway that serves another image than the gateway stops the run; nothing changed", async () => {
|
|
487
|
+
const { deps, order, q, written } = setup({
|
|
488
|
+
gcloud: [
|
|
489
|
+
[
|
|
490
|
+
/^run revisions describe caelo-production-staging-gateway/,
|
|
491
|
+
ok(revisionJson(D("9"), P("9"))),
|
|
492
|
+
],
|
|
493
|
+
],
|
|
494
|
+
});
|
|
495
|
+
const r = await runUpgraderJob(deps);
|
|
496
|
+
expect(r).toMatchObject({ exitCode: 1, status: "failed" });
|
|
497
|
+
expect(q.end()?.reason).toContain(
|
|
498
|
+
"the staging gateway (caelo-production-staging-gateway) serves",
|
|
499
|
+
);
|
|
500
|
+
expect(order.some((l) => /^gcloud run services update /.test(l))).toBe(false);
|
|
501
|
+
expect(written).toEqual([]);
|
|
502
|
+
});
|
|
503
|
+
|
|
481
504
|
it("#818: missing grants end needs-Cloud-Shell with EVERY missing permission named; nothing changed", async () => {
|
|
482
505
|
const { deps, order, q, written } = setup({
|
|
483
506
|
iamWithheld: {
|