@caelo-cms/provisioning 0.10.55 → 0.10.58
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/agent-db.js +97 -24
- package/dist/cli-routing.js +1 -1
- package/dist/cli-routing.js.map +1 -1
- package/dist/cli.js +4 -1
- package/dist/cli.js.map +1 -1
- package/dist/compose.js +5 -0
- package/dist/compose.js.map +1 -1
- package/dist/converge-checkpoint.d.ts +66 -0
- package/dist/converge-checkpoint.d.ts.map +1 -0
- package/dist/converge-checkpoint.js +61 -0
- package/dist/converge-checkpoint.js.map +1 -0
- package/dist/converge-job-gcp.d.ts +46 -0
- package/dist/converge-job-gcp.d.ts.map +1 -0
- package/dist/converge-job-gcp.js +368 -0
- package/dist/converge-job-gcp.js.map +1 -0
- package/dist/converge-job-setup.d.ts +62 -0
- package/dist/converge-job-setup.d.ts.map +1 -0
- package/dist/converge-job-setup.js +228 -0
- package/dist/converge-job-setup.js.map +1 -0
- package/dist/converge-job.d.ts +141 -0
- package/dist/converge-job.d.ts.map +1 -0
- package/dist/converge-job.js +325 -0
- package/dist/converge-job.js.map +1 -0
- package/dist/gcp-names.d.ts +9 -0
- package/dist/gcp-names.d.ts.map +1 -1
- package/dist/gcp-names.js +13 -0
- package/dist/gcp-names.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- 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 +33 -1
- package/dist/infra-contract.js.map +1 -1
- package/dist/install-job.d.ts +11 -3
- package/dist/install-job.d.ts.map +1 -1
- package/dist/install-job.js +22 -12
- package/dist/install-job.js.map +1 -1
- package/dist/lifecycle.d.ts +13 -1
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +115 -58
- package/dist/lifecycle.js.map +1 -1
- package/dist/operator-access.d.ts +4 -0
- package/dist/operator-access.d.ts.map +1 -1
- package/dist/operator-access.js +43 -7
- package/dist/operator-access.js.map +1 -1
- package/dist/release-contract.d.ts +3 -2
- package/dist/release-contract.d.ts.map +1 -1
- package/dist/release-contract.js +5 -2
- package/dist/release-contract.js.map +1 -1
- package/dist/release-manifest.d.ts +2 -0
- package/dist/release-manifest.d.ts.map +1 -1
- package/dist/release-manifest.js.map +1 -1
- package/dist/remote-mcp-edge.d.ts +114 -0
- package/dist/remote-mcp-edge.d.ts.map +1 -0
- package/dist/remote-mcp-edge.js +116 -0
- package/dist/remote-mcp-edge.js.map +1 -0
- package/dist/remote-mcp.d.ts +139 -0
- package/dist/remote-mcp.d.ts.map +1 -0
- package/dist/remote-mcp.js +434 -0
- package/dist/remote-mcp.js.map +1 -0
- package/dist/runtime-secrets.d.ts +12 -0
- package/dist/runtime-secrets.d.ts.map +1 -1
- package/dist/runtime-secrets.js +32 -4
- package/dist/runtime-secrets.js.map +1 -1
- package/dist/seal-key-rotation.d.ts +38 -0
- package/dist/seal-key-rotation.d.ts.map +1 -0
- package/dist/seal-key-rotation.js +206 -0
- package/dist/seal-key-rotation.js.map +1 -0
- package/dist/stack-contract.d.ts +206 -3
- package/dist/stack-contract.d.ts.map +1 -1
- package/dist/stack-contract.js +219 -4
- package/dist/stack-contract.js.map +1 -1
- package/dist/stack-converge.d.ts +53 -1
- package/dist/stack-converge.d.ts.map +1 -1
- package/dist/stack-converge.js +124 -4
- package/dist/stack-converge.js.map +1 -1
- package/dist/state-lock-lease.d.ts +142 -0
- package/dist/state-lock-lease.d.ts.map +1 -0
- package/dist/state-lock-lease.js +347 -0
- package/dist/state-lock-lease.js.map +1 -0
- package/dist/state-store.d.ts +66 -9
- package/dist/state-store.d.ts.map +1 -1
- package/dist/state-store.js +249 -30
- package/dist/state-store.js.map +1 -1
- package/dist/upgrade/app-roll.d.ts +36 -7
- package/dist/upgrade/app-roll.d.ts.map +1 -1
- package/dist/upgrade/app-roll.js +53 -19
- package/dist/upgrade/app-roll.js.map +1 -1
- package/dist/upgrade/audit-job-roll.d.ts.map +1 -1
- package/dist/upgrade/audit-job-roll.js +3 -1
- package/dist/upgrade/audit-job-roll.js.map +1 -1
- package/dist/upgrade/converge.d.ts.map +1 -1
- package/dist/upgrade/converge.js +125 -12
- package/dist/upgrade/converge.js.map +1 -1
- package/dist/upgrade/gcp-lookup.d.ts +27 -10
- package/dist/upgrade/gcp-lookup.d.ts.map +1 -1
- package/dist/upgrade/gcp-lookup.js +46 -7
- package/dist/upgrade/gcp-lookup.js.map +1 -1
- package/dist/upgrade/record-rolled.d.ts +40 -0
- package/dist/upgrade/record-rolled.d.ts.map +1 -0
- package/dist/upgrade/record-rolled.js +46 -0
- package/dist/upgrade/record-rolled.js.map +1 -0
- package/dist/upgrade/upgrade.d.ts +45 -1
- package/dist/upgrade/upgrade.d.ts.map +1 -1
- package/dist/upgrade/upgrade.js +60 -17
- package/dist/upgrade/upgrade.js.map +1 -1
- package/dist/upgrader-assess.d.ts +15 -1
- package/dist/upgrader-assess.d.ts.map +1 -1
- package/dist/upgrader-assess.js +36 -6
- package/dist/upgrader-assess.js.map +1 -1
- package/dist/upgrader-gcp.d.ts +15 -0
- package/dist/upgrader-gcp.d.ts.map +1 -1
- package/dist/upgrader-gcp.js +42 -7
- package/dist/upgrader-gcp.js.map +1 -1
- package/dist/upgrader-iam-preflight.d.ts +123 -0
- package/dist/upgrader-iam-preflight.d.ts.map +1 -0
- package/dist/upgrader-iam-preflight.js +414 -0
- package/dist/upgrader-iam-preflight.js.map +1 -0
- package/dist/upgrader-job.d.ts +33 -9
- package/dist/upgrader-job.d.ts.map +1 -1
- package/dist/upgrader-job.js +169 -27
- package/dist/upgrader-job.js.map +1 -1
- package/dist/upgrader-records.d.ts +58 -0
- package/dist/upgrader-records.d.ts.map +1 -0
- package/dist/upgrader-records.js +87 -0
- package/dist/upgrader-records.js.map +1 -0
- package/dist/upgrader-reporter.d.ts +3 -2
- package/dist/upgrader-reporter.d.ts.map +1 -1
- package/dist/upgrader-reporter.js +10 -2
- package/dist/upgrader-reporter.js.map +1 -1
- package/dist/upgrader-setup.d.ts +25 -3
- package/dist/upgrader-setup.d.ts.map +1 -1
- package/dist/upgrader-setup.js +123 -11
- package/dist/upgrader-setup.js.map +1 -1
- package/dist/wizards/gcp-cost.d.ts.map +1 -1
- package/dist/wizards/gcp-cost.js +11 -3
- package/dist/wizards/gcp-cost.js.map +1 -1
- package/dist/wizards/gcp-pulumi.d.ts +6 -0
- package/dist/wizards/gcp-pulumi.d.ts.map +1 -1
- package/dist/wizards/gcp-pulumi.js +14 -2
- package/dist/wizards/gcp-pulumi.js.map +1 -1
- package/dist/wizards/gcp.d.ts.map +1 -1
- package/dist/wizards/gcp.js +106 -4
- package/dist/wizards/gcp.js.map +1 -1
- package/package.json +2 -2
- package/src/agent/run.test.ts +14 -1
- package/src/audit-job-setup.test.ts +7 -1
- package/src/cli-routing.ts +1 -1
- package/src/cli.ts +4 -1
- package/src/compose.ts +5 -0
- package/src/converge-checkpoint.ts +75 -0
- package/src/converge-entrypoint.test.ts +213 -0
- package/src/converge-job-gcp.test.ts +222 -0
- package/src/converge-job-gcp.ts +443 -0
- package/src/converge-job-setup.test.ts +152 -0
- package/src/converge-job-setup.ts +268 -0
- package/src/converge-job.test.ts +486 -0
- package/src/converge-job.ts +542 -0
- package/src/gateway-env-isolation.test.ts +8 -1
- package/src/gcp-names.ts +15 -0
- package/src/gcp-url-map.test.ts +9 -1
- package/src/index.ts +8 -0
- package/src/infra-contract.test.ts +8 -4
- package/src/infra-contract.ts +33 -1
- package/src/install-job.test.ts +3 -1
- package/src/install-job.ts +31 -14
- package/src/lifecycle.ts +137 -71
- package/src/operator-access.test.ts +24 -1
- package/src/operator-access.ts +44 -6
- package/src/release-contract.ts +5 -2
- package/src/release-manifest.test.ts +42 -0
- package/src/release-manifest.ts +2 -0
- package/src/remote-mcp-edge.ts +137 -0
- package/src/remote-mcp.test.ts +690 -0
- package/src/remote-mcp.ts +524 -0
- package/src/runtime-secrets.test.ts +31 -6
- package/src/runtime-secrets.ts +41 -3
- package/src/seal-key-rotation.test.ts +131 -0
- package/src/seal-key-rotation.ts +258 -0
- package/src/stack-contract-parity.test.ts +20 -3
- package/src/stack-contract.test.ts +32 -2
- package/src/stack-contract.ts +363 -4
- package/src/stack-converge-secrets.test.ts +4 -0
- package/src/stack-converge.test.ts +21 -5
- package/src/stack-converge.ts +162 -7
- package/src/state-attach.test.ts +7 -1
- package/src/state-lock-lease.test.ts +302 -0
- package/src/state-lock-lease.ts +423 -0
- package/src/state-lock.test.ts +568 -0
- package/src/state-store.ts +347 -27
- package/src/upgrade/app-roll.ts +102 -21
- package/src/upgrade/audit-job-roll.ts +3 -1
- package/src/upgrade/converge.ts +148 -10
- package/src/upgrade/gcp-lookup.test.ts +80 -0
- package/src/upgrade/gcp-lookup.ts +65 -8
- package/src/upgrade/record-rolled.test.ts +105 -0
- package/src/upgrade/record-rolled.ts +91 -0
- package/src/upgrade/upgrade.test.ts +203 -3
- package/src/upgrade/upgrade.ts +120 -24
- package/src/upgrader-assess.test.ts +92 -3
- package/src/upgrader-assess.ts +58 -9
- package/src/upgrader-gcp.ts +47 -8
- package/src/upgrader-iam-preflight.test.ts +393 -0
- package/src/upgrader-iam-preflight.ts +510 -0
- package/src/upgrader-job.test.ts +586 -15
- package/src/upgrader-job.ts +237 -53
- package/src/upgrader-records.ts +151 -0
- package/src/upgrader-reporter.test.ts +6 -0
- package/src/upgrader-reporter.ts +7 -2
- package/src/upgrader-setup.test.ts +3 -3
- package/src/upgrader-setup.ts +128 -11
- package/src/wizards/gcp-cost.ts +12 -3
- package/src/wizards/gcp-pulumi.test.ts +15 -0
- package/src/wizards/gcp-pulumi.ts +18 -2
- package/src/wizards/gcp.ts +134 -4
- package/stacks/aws/index.ts +10 -0
- package/stacks/azure/index.ts +9 -0
- package/stacks/gcp/index.ts +47 -7
- package/stacks/gcp-firebase/index.ts +38 -6
package/src/upgrader-job.test.ts
CHANGED
|
@@ -16,6 +16,7 @@ import type { InstallMetadata } from "./install-state.js";
|
|
|
16
16
|
import type { ReleaseManifest, ReleaseManifestResult } from "./release-manifest.js";
|
|
17
17
|
import { StateLockHeldError } from "./state-store.js";
|
|
18
18
|
import type { HealthOutcome, HealthProbe } from "./upgrade/health.js";
|
|
19
|
+
import { type IamTester, resourceName } from "./upgrader-iam-preflight.js";
|
|
19
20
|
import {
|
|
20
21
|
type ClaimedUpdate,
|
|
21
22
|
runUpgraderJob,
|
|
@@ -28,6 +29,42 @@ import {
|
|
|
28
29
|
const ok = (stdout = ""): GcloudResult => ({ ok: true, stdout, stderr: "", exitCode: 0 });
|
|
29
30
|
const fail = (stderr: string): GcloudResult => ({ ok: false, stdout: "", stderr, exitCode: 1 });
|
|
30
31
|
const D = (c: string) => `sha256:${c.repeat(64)}`;
|
|
32
|
+
/**
|
|
33
|
+
* The linux/amd64 platform manifest inside release index `D(c)`. Cloud Run
|
|
34
|
+
* reports this one as a revision's `status.imageDigest` while the release
|
|
35
|
+
* manifest and install.json name the index (#860 root cause).
|
|
36
|
+
*/
|
|
37
|
+
const P = (c: string) => `sha256:${"f".repeat(63)}${c}`;
|
|
38
|
+
|
|
39
|
+
const MIRROR = "europe-west1-docker.pkg.dev/caelo-website/caelo-cms-images/x";
|
|
40
|
+
|
|
41
|
+
/** `gcloud run revisions describe --format=json` of a revision deployed as `deployed` (null: by tag). */
|
|
42
|
+
const revisionJson = (deployed: string | null, resolved: string) =>
|
|
43
|
+
JSON.stringify({
|
|
44
|
+
spec: { containers: [{ image: deployed ? `${MIRROR}@${deployed}` : `${MIRROR}:0.10.45` }] },
|
|
45
|
+
status: { imageDigest: `${MIRROR}@${resolved}` },
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/** Serving revisions deployed as `admin` / `gateway` (index digests). */
|
|
49
|
+
const servingAs = (admin: string, gateway: string): [RegExp, Answer] => [
|
|
50
|
+
/^run revisions describe (\S+)/,
|
|
51
|
+
(l) =>
|
|
52
|
+
ok(
|
|
53
|
+
l.includes("-admin-")
|
|
54
|
+
? revisionJson(admin, `sha256:${"e".repeat(64)}`)
|
|
55
|
+
: revisionJson(gateway, `sha256:${"d".repeat(64)}`),
|
|
56
|
+
),
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
/** `gcloud artifacts docker tags list` of the mirror: `tags` maps release version → digest. */
|
|
60
|
+
const releaseTags = (slug: "admin" | "gateway", tags: Record<string, string>): [RegExp, Answer] => [
|
|
61
|
+
new RegExp(`^artifacts docker tags list \\S+/${slug} `),
|
|
62
|
+
ok(
|
|
63
|
+
Object.entries(tags)
|
|
64
|
+
.map(([v, d]) => `projects/p/locations/l/repositories/r/packages/${slug}/tags/${v}\t${d}`)
|
|
65
|
+
.join("\n"),
|
|
66
|
+
),
|
|
67
|
+
];
|
|
31
68
|
|
|
32
69
|
const ADMIN = "caelo-production-admin-aaa";
|
|
33
70
|
const GATEWAY = "caelo-production-gateway-bbb";
|
|
@@ -77,6 +114,11 @@ const META: InstallMetadata = {
|
|
|
77
114
|
infraContractVersion: 2,
|
|
78
115
|
};
|
|
79
116
|
|
|
117
|
+
const manifestOf = (version: string, admin: string, gateway: string): ReleaseManifestResult => ({
|
|
118
|
+
ok: true,
|
|
119
|
+
manifest: { ...MANIFEST, version, digests: { admin, gateway, ops: D("c") } },
|
|
120
|
+
});
|
|
121
|
+
|
|
80
122
|
const serviceJson = (image: string, env: unknown[]) =>
|
|
81
123
|
JSON.stringify({ spec: { template: { spec: { containers: [{ image, env }] } } } });
|
|
82
124
|
const secretRef = (name: string, secret: string) => ({
|
|
@@ -106,7 +148,8 @@ function fakeGcloud(events: string[], overrides: [RegExp, Answer][] = []) {
|
|
|
106
148
|
[/describe (\S+) .*status\.url/, (l) => ok(`https://${l.split(" ")[3]}-xyz.a.run.app\n`)],
|
|
107
149
|
[
|
|
108
150
|
/^run revisions describe (\S+)/,
|
|
109
|
-
(l) =>
|
|
151
|
+
(l) =>
|
|
152
|
+
ok(l.includes("-admin-") ? revisionJson(D("1"), P("1")) : revisionJson(D("2"), P("2"))),
|
|
110
153
|
],
|
|
111
154
|
[/^sql instances list/, ok("caelo-production-pg-abc\n")],
|
|
112
155
|
[/^sql backups list/, ok("1760000000000\n")],
|
|
@@ -159,12 +202,20 @@ function setup(
|
|
|
159
202
|
opts: {
|
|
160
203
|
request?: Partial<ClaimedUpdate>;
|
|
161
204
|
manifest?: ReleaseManifestResult;
|
|
205
|
+
/** Manifests of other versions than the target (the serving release's). */
|
|
206
|
+
manifests?: Record<string, ReleaseManifestResult>;
|
|
162
207
|
meta?: InstallMetadata | null;
|
|
163
208
|
gcloud?: [RegExp, Answer][];
|
|
164
209
|
deep?: HealthOutcome;
|
|
165
210
|
migrate?: { ok: boolean; error?: string };
|
|
166
211
|
locked?: boolean;
|
|
167
212
|
lockDenied?: boolean;
|
|
213
|
+
/** The lock broke a dead run's lock on the way, with this message. */
|
|
214
|
+
brokeStaleLock?: string;
|
|
215
|
+
/** IAM's answer: permissions withheld per resource name. */
|
|
216
|
+
iamWithheld?: Record<string, readonly string[]>;
|
|
217
|
+
/** A resource IAM cannot be asked about. */
|
|
218
|
+
iamError?: string;
|
|
168
219
|
} = {},
|
|
169
220
|
) {
|
|
170
221
|
const order: string[] = [];
|
|
@@ -185,14 +236,21 @@ function setup(
|
|
|
185
236
|
region: "europe-west1",
|
|
186
237
|
env: "production",
|
|
187
238
|
execution: "caelo-production-upgrader-x1",
|
|
239
|
+
iam: fakeIam(order, opts.iamWithheld ?? {}, opts.iamError),
|
|
188
240
|
fetchManifest: async (v) => {
|
|
189
241
|
order.push(`manifest ${v}`);
|
|
242
|
+
const other = opts.manifests?.[v];
|
|
243
|
+
if (other) return other;
|
|
244
|
+
if (v !== MANIFEST.version) {
|
|
245
|
+
return { ok: false, error: { code: "registry", message: `no release manifest for v${v}` } };
|
|
246
|
+
}
|
|
190
247
|
return opts.manifest ?? { ok: true, manifest: MANIFEST };
|
|
191
248
|
},
|
|
192
|
-
lockState: () => {
|
|
249
|
+
lockState: (onBreak) => {
|
|
193
250
|
order.push("lock");
|
|
194
251
|
if (opts.locked) throw new StateLockHeldError("held by caelo installer");
|
|
195
252
|
if (opts.lockDenied) throw new Error("storage.objects.list denied");
|
|
253
|
+
if (opts.brokeStaleLock) onBreak(opts.brokeStaleLock);
|
|
196
254
|
return () => order.push("unlock");
|
|
197
255
|
},
|
|
198
256
|
readInstall: () => (opts.meta === undefined ? META : opts.meta),
|
|
@@ -213,6 +271,23 @@ function setup(
|
|
|
213
271
|
return { deps, order, q, written };
|
|
214
272
|
}
|
|
215
273
|
|
|
274
|
+
/** IAM granting everything asked except `withheld` per resource; `error` names a resource it cannot be asked about. */
|
|
275
|
+
function fakeIam(
|
|
276
|
+
order: string[],
|
|
277
|
+
withheld: Record<string, readonly string[]>,
|
|
278
|
+
error?: string,
|
|
279
|
+
): IamTester {
|
|
280
|
+
return {
|
|
281
|
+
granted: async (resource, permissions) => {
|
|
282
|
+
const name = resourceName(resource);
|
|
283
|
+
order.push(`iam ${name}`);
|
|
284
|
+
if (error && name === error) return { ok: false, error: "HTTP 503 backend unavailable" };
|
|
285
|
+
const no = withheld[name] ?? [];
|
|
286
|
+
return { ok: true, granted: permissions.filter((p) => !no.includes(p)) };
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
216
291
|
const step = (order: string[], re: RegExp) => order.findIndex((l) => re.test(l));
|
|
217
292
|
|
|
218
293
|
describe("runUpgraderJob", () => {
|
|
@@ -275,10 +350,12 @@ describe("runUpgraderJob", () => {
|
|
|
275
350
|
]);
|
|
276
351
|
});
|
|
277
352
|
|
|
278
|
-
it("
|
|
353
|
+
it("#815: decides first, locks right before the backup, holds the lock to the end", async () => {
|
|
279
354
|
const ok = setup();
|
|
280
355
|
await runUpgraderJob(ok.deps);
|
|
281
|
-
|
|
356
|
+
const lock = ok.order.indexOf("lock");
|
|
357
|
+
expect(lock).toBeGreaterThan(step(ok.order, /^manifest 0\.11\.0/));
|
|
358
|
+
expect(lock).toBeLessThan(step(ok.order, /^gcloud sql backups create/));
|
|
282
359
|
expect(ok.order.indexOf("unlock")).toBeGreaterThan(
|
|
283
360
|
step(ok.order, /jobs update caelo-production-upgrader/),
|
|
284
361
|
);
|
|
@@ -287,7 +364,83 @@ describe("runUpgraderJob", () => {
|
|
|
287
364
|
const r = await runUpgraderJob(busy.deps);
|
|
288
365
|
expect(r.status).toBe("failed");
|
|
289
366
|
expect(busy.q.end()?.codes).toEqual(["state-locked"]);
|
|
290
|
-
expect(busy.order.some((l) =>
|
|
367
|
+
expect(busy.order.some((l) => /backups create|services update|^migrate/.test(l))).toBe(false);
|
|
368
|
+
expect(busy.order).not.toContain("unlock");
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it("#818: checks IAM after the decision and before the lock, the backup and the roll", async () => {
|
|
372
|
+
const { deps, order } = setup();
|
|
373
|
+
expect((await runUpgraderJob(deps)).status).toBe("succeeded");
|
|
374
|
+
const firstIam = step(order, /^iam /);
|
|
375
|
+
expect(firstIam).toBeGreaterThan(step(order, /^manifest 0\.11\.0/));
|
|
376
|
+
expect(firstIam).toBeLessThan(order.indexOf("lock"));
|
|
377
|
+
expect(firstIam).toBeLessThan(step(order, /^gcloud sql backups create/));
|
|
378
|
+
// Every resource the run acts on is asked about, each once.
|
|
379
|
+
expect(order.filter((l) => l.startsWith("iam ")).sort()).toEqual(
|
|
380
|
+
[
|
|
381
|
+
"iam gs://acme-caelo-ops",
|
|
382
|
+
"iam gs://acme-caelo-ops/install/",
|
|
383
|
+
"iam projects/acme",
|
|
384
|
+
"iam projects/acme/locations/europe-west1/jobs/caelo-production-upgrader",
|
|
385
|
+
"iam projects/acme/serviceAccounts/caelo-mcp@acme.iam.gserviceaccount.com",
|
|
386
|
+
"iam projects/acme/serviceAccounts/caelo-production-audit@acme.iam.gserviceaccount.com",
|
|
387
|
+
"iam projects/acme/serviceAccounts/caelo-production-gateway-sa@acme.iam.gserviceaccount.com",
|
|
388
|
+
"iam projects/acme/serviceAccounts/caelo-production-run-sa@acme.iam.gserviceaccount.com",
|
|
389
|
+
"iam projects/acme/serviceAccounts/caelo-production-upgrader@acme.iam.gserviceaccount.com",
|
|
390
|
+
"iam projects/caelo-website/locations/europe-west1/repositories/caelo-cms-images",
|
|
391
|
+
].sort(),
|
|
392
|
+
);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it("#818: missing grants end needs-Cloud-Shell with EVERY missing permission named; nothing changed", async () => {
|
|
396
|
+
const { deps, order, q, written } = setup({
|
|
397
|
+
iamWithheld: {
|
|
398
|
+
"projects/acme": ["run.jobs.create", "cloudsql.backupRuns.create"],
|
|
399
|
+
"gs://acme-caelo-ops/install/": ["storage.objects.delete"],
|
|
400
|
+
},
|
|
401
|
+
});
|
|
402
|
+
const r = await runUpgraderJob(deps);
|
|
403
|
+
expect(r).toMatchObject({ exitCode: 0, status: "needs_cloud_shell" });
|
|
404
|
+
const end = q.end();
|
|
405
|
+
expect(end?.codes).toEqual(["iam-preflight"]);
|
|
406
|
+
for (const p of ["run.jobs.create", "cloudsql.backupRuns.create", "storage.objects.delete"]) {
|
|
407
|
+
expect(end?.reason).toContain(p);
|
|
408
|
+
}
|
|
409
|
+
// In plain words, with the one next step.
|
|
410
|
+
expect(end?.reason).toContain("back up the database before anything changes");
|
|
411
|
+
expect(end?.reason).toContain("Run the upgrade once with Cloud Shell");
|
|
412
|
+
expect(end?.reason?.startsWith("Nothing was changed")).toBe(true);
|
|
413
|
+
expect(order).not.toContain("lock");
|
|
414
|
+
expect(written).toEqual([]);
|
|
415
|
+
expect(order.some((l) => /backups create|services update|jobs update|^migrate/.test(l))).toBe(
|
|
416
|
+
false,
|
|
417
|
+
);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it("#818: IAM that cannot be asked fails the run before anything changes (never assumed fine)", async () => {
|
|
421
|
+
const { deps, order, q } = setup({ iamError: "projects/acme" });
|
|
422
|
+
const r = await runUpgraderJob(deps);
|
|
423
|
+
expect(r.status).toBe("failed");
|
|
424
|
+
expect(q.end()?.codes).toEqual(["iam-preflight"]);
|
|
425
|
+
expect(q.end()?.reason).toContain("could not check its access");
|
|
426
|
+
expect(order).not.toContain("lock");
|
|
427
|
+
expect(order.some((l) => /backups create|services update|^migrate/.test(l))).toBe(false);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it("#818: a stop verdict never asks IAM (it changes nothing that needs it)", async () => {
|
|
431
|
+
const { deps, order } = setup({ meta: { ...META, infraContractVersion: 1 } });
|
|
432
|
+
expect((await runUpgraderJob(deps)).status).toBe("needs_cloud_shell");
|
|
433
|
+
expect(order.some((l) => l.startsWith("iam "))).toBe(false);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it("#815: a release that needs Cloud Shell says so even when the lock is held or denied", async () => {
|
|
437
|
+
for (const lock of [{ locked: true }, { lockDenied: true }]) {
|
|
438
|
+
const { deps, order, q } = setup({ meta: { ...META, infraContractVersion: 1 }, ...lock });
|
|
439
|
+
expect((await runUpgraderJob(deps)).status).toBe("needs_cloud_shell");
|
|
440
|
+
expect(q.end()?.codes).toEqual(["infra-contract"]);
|
|
441
|
+
// A verdict that changes nothing never even asks for the lock.
|
|
442
|
+
expect(order).not.toContain("lock");
|
|
443
|
+
}
|
|
291
444
|
});
|
|
292
445
|
|
|
293
446
|
it("does not call a lock it cannot take 'busy': an access failure asks for Cloud Shell", async () => {
|
|
@@ -298,9 +451,61 @@ describe("runUpgraderJob", () => {
|
|
|
298
451
|
expect(end?.codes).toEqual(["state-access"]);
|
|
299
452
|
expect(end?.reason).not.toContain("another install operation");
|
|
300
453
|
expect(end?.reason).toContain("storage.objects.list denied");
|
|
301
|
-
expect(denied.order.some((l) =>
|
|
302
|
-
|
|
303
|
-
|
|
454
|
+
expect(denied.order.some((l) => /backups create|services update|^migrate/.test(l))).toBe(false);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it("#815: a dead run's lock broken on the way lands on the timeline", async () => {
|
|
458
|
+
const { deps, q } = setup({
|
|
459
|
+
brokeStaleLock: "Removed a stale state lock: caelo upgrader job (run x0) has failed.",
|
|
460
|
+
});
|
|
461
|
+
expect((await runUpgraderJob(deps)).status).toBe("succeeded");
|
|
462
|
+
expect(q.events).toContainEqual({
|
|
463
|
+
phase: "assess",
|
|
464
|
+
level: "info",
|
|
465
|
+
message: "Removed a stale state lock: caelo upgrader job (run x0) has failed.",
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it("#815: install.json changed by another writer before the lock: stops, writes nothing", async () => {
|
|
470
|
+
const { deps, order, q, written } = setup();
|
|
471
|
+
let reads = 0;
|
|
472
|
+
const r = await runUpgraderJob({
|
|
473
|
+
...deps,
|
|
474
|
+
readInstall: () => {
|
|
475
|
+
reads += 1;
|
|
476
|
+
return reads === 1 ? META : { ...META, releaseVersion: "0.10.46" };
|
|
477
|
+
},
|
|
478
|
+
});
|
|
479
|
+
expect(r.status).toBe("failed");
|
|
480
|
+
expect(q.end()?.codes).toEqual(["install-state-changed"]);
|
|
481
|
+
expect(written).toEqual([]);
|
|
482
|
+
expect(order.some((l) => /backups create|services update|^migrate/.test(l))).toBe(false);
|
|
483
|
+
expect(order).toContain("unlock");
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it("adversarial #815: two runs at once — one installs, the other finds the lock held", async () => {
|
|
487
|
+
let holder: string | null = null;
|
|
488
|
+
const runOf = (id: string) => {
|
|
489
|
+
const s = setup({ request: { id } });
|
|
490
|
+
const lockState = () => {
|
|
491
|
+
if (holder) throw new StateLockHeldError(`held by ${holder}`);
|
|
492
|
+
holder = id;
|
|
493
|
+
return () => {
|
|
494
|
+
holder = null;
|
|
495
|
+
};
|
|
496
|
+
};
|
|
497
|
+
return { ...s, deps: { ...s.deps, lockState } };
|
|
498
|
+
};
|
|
499
|
+
const a = runOf("req-a");
|
|
500
|
+
const b = runOf("req-b");
|
|
501
|
+
const [ra, rb] = await Promise.all([runUpgraderJob(a.deps), runUpgraderJob(b.deps)]);
|
|
502
|
+
const statuses = [ra.status, rb.status].sort();
|
|
503
|
+
expect(statuses).toEqual(["failed", "succeeded"]);
|
|
504
|
+
const loser = ra.status === "failed" ? a : b;
|
|
505
|
+
expect(loser.q.end()?.codes).toEqual(["state-locked"]);
|
|
506
|
+
expect(loser.order.some((l) => /backups create|services update|^migrate/.test(l))).toBe(false);
|
|
507
|
+
expect(loser.written).toEqual([]);
|
|
508
|
+
expect(holder).toBeNull();
|
|
304
509
|
});
|
|
305
510
|
|
|
306
511
|
it("retries recording the end, and fails the run when it cannot", async () => {
|
|
@@ -354,25 +559,391 @@ describe("runUpgraderJob", () => {
|
|
|
354
559
|
expect(q.end()?.codes).toEqual(["install-state"]);
|
|
355
560
|
});
|
|
356
561
|
|
|
357
|
-
it("
|
|
358
|
-
|
|
562
|
+
it("regression #860: Cloud Run's platform digest of the recorded index is no drift", async () => {
|
|
563
|
+
// The fake reports status.imageDigest = P(…) (the platform manifest) and
|
|
564
|
+
// the revision spec = D(…) (the index install.json and the manifest name).
|
|
565
|
+
const { deps, order, q, written } = setup();
|
|
566
|
+
expect((await runUpgraderJob(deps)).status).toBe("succeeded");
|
|
567
|
+
expect(q.events.some((e) => e.message.includes("refreshed its install records"))).toBe(false);
|
|
568
|
+
expect(order.filter((l) => l.startsWith("manifest"))).toEqual(["manifest 0.11.0"]);
|
|
569
|
+
expect(order.some((l) => l.includes("artifacts docker tags list"))).toBe(false);
|
|
570
|
+
expect(written).toHaveLength(1);
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
it("a revision deployed by tag matches by the digest Cloud Run resolved", async () => {
|
|
574
|
+
const { deps, written } = setup({
|
|
575
|
+
meta: { ...META, imageDigests: { admin: P("1"), gateway: D("2") } },
|
|
576
|
+
gcloud: [
|
|
577
|
+
[
|
|
578
|
+
/^run revisions describe (\S+)/,
|
|
579
|
+
(l) =>
|
|
580
|
+
ok(l.includes("-admin-") ? revisionJson(null, P("1")) : revisionJson(D("2"), P("2"))),
|
|
581
|
+
],
|
|
582
|
+
],
|
|
583
|
+
});
|
|
584
|
+
expect((await runUpgraderJob(deps)).status).toBe("succeeded");
|
|
585
|
+
expect(written).toHaveLength(1);
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it("drift to a signed release: refreshes install.json, says so, installs judged against it", async () => {
|
|
589
|
+
// Recorded: 0.10.44 (digests 9/8). Serving: 0.10.45 (digests 1/2).
|
|
590
|
+
const { deps, order, q, written } = setup({
|
|
591
|
+
meta: {
|
|
592
|
+
...META,
|
|
593
|
+
imageDigests: { admin: D("9"), gateway: D("8") },
|
|
594
|
+
releaseVersion: "0.10.44",
|
|
595
|
+
},
|
|
596
|
+
manifests: {
|
|
597
|
+
"0.10.44": manifestOf("0.10.44", D("9"), D("8")),
|
|
598
|
+
"0.10.45": manifestOf("0.10.45", D("1"), D("2")),
|
|
599
|
+
},
|
|
600
|
+
gcloud: [releaseTags("admin", { "0.10.44": D("9"), "0.10.45": D("1") })],
|
|
601
|
+
});
|
|
602
|
+
const r = await runUpgraderJob(deps);
|
|
603
|
+
expect(r).toMatchObject({ exitCode: 0, status: "succeeded" });
|
|
604
|
+
expect(written[0]).toMatchObject({
|
|
605
|
+
releaseVersion: "0.10.45",
|
|
606
|
+
imageDigests: { admin: D("1"), gateway: D("2") },
|
|
607
|
+
domain: "acme.com",
|
|
608
|
+
infraContractVersion: 2,
|
|
609
|
+
});
|
|
610
|
+
expect(q.events).toContainEqual({
|
|
611
|
+
phase: "assess",
|
|
612
|
+
level: "success",
|
|
613
|
+
message: "Caelo refreshed its install records: v0.10.45 serves.",
|
|
614
|
+
});
|
|
615
|
+
// Judged against the serving release, written once the lock is held (#815).
|
|
616
|
+
expect(step(order, /^manifest 0\.10\.45/)).toBeLessThan(step(order, /^manifest 0\.11\.0/));
|
|
617
|
+
expect(q.events.some((e) => e.message.startsWith("0.10.45 → 0.11.0"))).toBe(true);
|
|
618
|
+
expect(written.at(-1)).toMatchObject({ releaseVersion: "0.11.0" });
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
it("#815: drift + a stop verdict: the records are corrected under the lock, the verdict stands", async () => {
|
|
622
|
+
const drift = {
|
|
623
|
+
meta: { ...META, imageDigests: { admin: D("9"), gateway: D("2") }, infraContractVersion: 1 },
|
|
624
|
+
manifests: { "0.10.45": manifestOf("0.10.45", D("1"), D("2")) },
|
|
625
|
+
};
|
|
626
|
+
const free = setup(drift);
|
|
627
|
+
expect((await runUpgraderJob(free.deps)).status).toBe("needs_cloud_shell");
|
|
628
|
+
expect(free.q.end()?.codes).toEqual(["infra-contract"]);
|
|
629
|
+
expect(free.written).toEqual([
|
|
630
|
+
expect.objectContaining({ imageDigests: { admin: D("1"), gateway: D("2") } }),
|
|
631
|
+
]);
|
|
632
|
+
expect(free.order.indexOf("lock")).toBeGreaterThan(step(free.order, /^manifest 0\.11\.0/));
|
|
633
|
+
expect(free.order).toContain("unlock");
|
|
634
|
+
|
|
635
|
+
// A lock that cannot be taken (no access) only defers the correction;
|
|
636
|
+
// it never replaces the verdict.
|
|
637
|
+
const denied = setup({ ...drift, lockDenied: true });
|
|
638
|
+
expect((await runUpgraderJob(denied.deps)).status).toBe("needs_cloud_shell");
|
|
639
|
+
expect(denied.q.end()?.codes).toEqual(["infra-contract"]);
|
|
640
|
+
expect(denied.written).toEqual([]);
|
|
641
|
+
expect(
|
|
642
|
+
denied.q.events.some(
|
|
643
|
+
(e) => e.level === "warn" && e.message.includes("could not be corrected this time"),
|
|
644
|
+
),
|
|
645
|
+
).toBe(true);
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
it("#815 review: drift + a stop verdict while another run holds the lock ends state-locked", async () => {
|
|
649
|
+
// The serving images were read without the lock; the run holding it may
|
|
650
|
+
// be half-way through its own roll (or be the very Cloud Shell upgrade
|
|
651
|
+
// the verdict would ask for). Its half-finished state is no verdict.
|
|
652
|
+
const busy = setup({
|
|
653
|
+
meta: { ...META, imageDigests: { admin: D("9"), gateway: D("2") }, infraContractVersion: 1 },
|
|
654
|
+
manifests: { "0.10.45": manifestOf("0.10.45", D("1"), D("2")) },
|
|
655
|
+
locked: true,
|
|
656
|
+
});
|
|
657
|
+
expect((await runUpgraderJob(busy.deps)).status).toBe("failed");
|
|
658
|
+
expect(busy.q.end()?.codes).toEqual(["state-locked"]);
|
|
659
|
+
expect(busy.written).toEqual([]);
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
it("#815 review: images no release vouches for, while another run holds the lock, are not drift", async () => {
|
|
663
|
+
// A run mid-roll serves the new admin next to the old gateway for a
|
|
664
|
+
// moment: read without the lock that looks exactly like a mixed install.
|
|
665
|
+
const mixed = {
|
|
666
|
+
gcloud: [
|
|
667
|
+
servingAs(D("9"), D("2")),
|
|
668
|
+
releaseTags("admin", { "0.10.44": D("9") }),
|
|
669
|
+
releaseTags("gateway", { "0.10.45": D("2") }),
|
|
670
|
+
],
|
|
671
|
+
meta: {
|
|
672
|
+
...META,
|
|
673
|
+
imageDigests: { admin: D("1"), gateway: D("2") },
|
|
674
|
+
releaseVersion: "0.10.45",
|
|
675
|
+
},
|
|
676
|
+
manifests: {
|
|
677
|
+
"0.10.44": manifestOf("0.10.44", D("9"), D("8")),
|
|
678
|
+
"0.10.45": manifestOf("0.10.45", D("1"), D("2")),
|
|
679
|
+
},
|
|
680
|
+
};
|
|
681
|
+
const busy = setup({ ...mixed, locked: true });
|
|
682
|
+
expect((await runUpgraderJob(busy.deps)).status).toBe("failed");
|
|
683
|
+
expect(busy.q.end()?.codes).toEqual(["state-locked"]);
|
|
684
|
+
expect(busy.written).toEqual([]);
|
|
685
|
+
|
|
686
|
+
// With the lock free (no run is rolling), it is drift.
|
|
687
|
+
const free = setup(mixed);
|
|
688
|
+
expect((await runUpgraderJob(free.deps)).status).toBe("needs_cloud_shell");
|
|
689
|
+
expect(free.q.end()?.codes).toEqual(["install-state-drift"]);
|
|
690
|
+
expect(free.order).toContain("lock");
|
|
691
|
+
expect(free.order).toContain("unlock");
|
|
692
|
+
expect(free.written).toEqual([]);
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
it("#815: drift + a stop verdict: a failed re-read under the lock keeps the verdict", async () => {
|
|
696
|
+
const drift = setup({
|
|
697
|
+
meta: { ...META, imageDigests: { admin: D("9"), gateway: D("2") }, infraContractVersion: 1 },
|
|
698
|
+
manifests: { "0.10.45": manifestOf("0.10.45", D("1"), D("2")) },
|
|
699
|
+
});
|
|
700
|
+
let reads = 0;
|
|
701
|
+
const r = await runUpgraderJob({
|
|
702
|
+
...drift.deps,
|
|
703
|
+
readInstall: () => {
|
|
704
|
+
reads += 1;
|
|
705
|
+
if (reads > 1) throw new Error("storage.objects.get timed out");
|
|
706
|
+
return {
|
|
707
|
+
...META,
|
|
708
|
+
imageDigests: { admin: D("9"), gateway: D("2") },
|
|
709
|
+
infraContractVersion: 1,
|
|
710
|
+
};
|
|
711
|
+
},
|
|
712
|
+
});
|
|
713
|
+
expect(r.status).toBe("needs_cloud_shell");
|
|
714
|
+
expect(drift.q.end()?.codes).toEqual(["infra-contract"]);
|
|
715
|
+
expect(drift.written).toEqual([]);
|
|
716
|
+
expect(drift.order).toContain("unlock");
|
|
717
|
+
expect(
|
|
718
|
+
drift.q.events.some(
|
|
719
|
+
(e) => e.level === "warn" && e.message.includes("storage.objects.get timed out"),
|
|
720
|
+
),
|
|
721
|
+
).toBe(true);
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
it("#815: a failed re-read under the lock before the backup stops as install-state-read", async () => {
|
|
725
|
+
const { deps, order, q, written } = setup();
|
|
726
|
+
let reads = 0;
|
|
727
|
+
const r = await runUpgraderJob({
|
|
728
|
+
...deps,
|
|
729
|
+
readInstall: () => {
|
|
730
|
+
reads += 1;
|
|
731
|
+
if (reads > 1) throw new Error("storage.objects.get timed out");
|
|
732
|
+
return META;
|
|
733
|
+
},
|
|
734
|
+
});
|
|
735
|
+
expect(r.status).toBe("failed");
|
|
736
|
+
expect(q.end()?.codes).toEqual(["install-state-read"]);
|
|
737
|
+
expect(written).toEqual([]);
|
|
738
|
+
expect(order.some((l) => /backups create|services update|^migrate/.test(l))).toBe(false);
|
|
739
|
+
expect(order).toContain("unlock");
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
it("drift where the recorded version is the serving one needs no tag lookup", async () => {
|
|
743
|
+
const { deps, order, written } = setup({
|
|
359
744
|
meta: { ...META, imageDigests: { admin: D("9"), gateway: D("2") } },
|
|
745
|
+
manifests: { "0.10.45": manifestOf("0.10.45", D("1"), D("2")) },
|
|
746
|
+
});
|
|
747
|
+
expect((await runUpgraderJob(deps)).status).toBe("succeeded");
|
|
748
|
+
expect(order.some((l) => l.includes("artifacts docker tags list"))).toBe(false);
|
|
749
|
+
expect(written[0]).toMatchObject({ imageDigests: { admin: D("1"), gateway: D("2") } });
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
it("drift to images of no signed release is refused, says so, and changes nothing", async () => {
|
|
753
|
+
const { deps, order, q, written } = setup({
|
|
754
|
+
gcloud: [servingAs(D("7"), D("6"))],
|
|
755
|
+
manifests: { "0.10.45": manifestOf("0.10.45", D("1"), D("2")) },
|
|
756
|
+
});
|
|
757
|
+
const r = await runUpgraderJob(deps);
|
|
758
|
+
expect(r.status).toBe("needs_cloud_shell");
|
|
759
|
+
const end = q.end();
|
|
760
|
+
expect(end?.codes).toEqual(["install-state-drift"]);
|
|
761
|
+
expect(end?.reason).toContain("do not belong to one signed Caelo release");
|
|
762
|
+
expect(end?.reason).toContain(`admin ${D("7")}`);
|
|
763
|
+
expect(written).toEqual([]);
|
|
764
|
+
expect(order.some((l) => /backups create|services update|^migrate/.test(l))).toBe(false);
|
|
765
|
+
expect(order).not.toContain("manifest 0.11.0");
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
it("regression #865: drift whose release manifest the registry cannot serve fails retryably, not install-state-drift", async () => {
|
|
769
|
+
const { deps, order, q, written } = setup({
|
|
770
|
+
meta: {
|
|
771
|
+
...META,
|
|
772
|
+
imageDigests: { admin: D("9"), gateway: D("8") },
|
|
773
|
+
releaseVersion: "0.10.44",
|
|
774
|
+
},
|
|
775
|
+
manifests: {
|
|
776
|
+
"0.10.44": manifestOf("0.10.44", D("9"), D("8")),
|
|
777
|
+
"0.10.45": {
|
|
778
|
+
ok: false,
|
|
779
|
+
error: { code: "registry", message: "503 Service Unavailable", transient: true },
|
|
780
|
+
},
|
|
781
|
+
},
|
|
782
|
+
gcloud: [releaseTags("admin", { "0.10.44": D("9"), "0.10.45": D("1") })],
|
|
783
|
+
});
|
|
784
|
+
const r = await runUpgraderJob(deps);
|
|
785
|
+
expect(r.status).toBe("failed");
|
|
786
|
+
const end = q.end();
|
|
787
|
+
expect(end?.codes).toEqual(["registry"]);
|
|
788
|
+
expect(end?.reason).toContain("503 Service Unavailable");
|
|
789
|
+
expect(end?.reason).toContain("Try again later");
|
|
790
|
+
expect(end?.reason).not.toContain("Cloud Shell");
|
|
791
|
+
expect(written).toEqual([]);
|
|
792
|
+
expect(order).not.toContain("manifest 0.11.0");
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
it("regression #865: drift whose candidate manifest does not exist (404) ends in install-state-drift, not a retry", async () => {
|
|
796
|
+
const { deps, q, written } = setup({
|
|
797
|
+
meta: {
|
|
798
|
+
...META,
|
|
799
|
+
imageDigests: { admin: D("9"), gateway: D("8") },
|
|
800
|
+
releaseVersion: "0.10.44",
|
|
801
|
+
},
|
|
802
|
+
manifests: {
|
|
803
|
+
"0.10.44": manifestOf("0.10.44", D("9"), D("8")),
|
|
804
|
+
"0.10.45": {
|
|
805
|
+
ok: false,
|
|
806
|
+
error: { code: "registry", message: "no release manifest for v0.10.45 (HTTP 404)" },
|
|
807
|
+
},
|
|
808
|
+
},
|
|
809
|
+
gcloud: [releaseTags("admin", { "0.10.44": D("9"), "0.10.45": D("1") })],
|
|
810
|
+
});
|
|
811
|
+
expect((await runUpgraderJob(deps)).status).toBe("needs_cloud_shell");
|
|
812
|
+
const end = q.end();
|
|
813
|
+
expect(end?.codes).toEqual(["install-state-drift"]);
|
|
814
|
+
expect(end?.reason).toContain("HTTP 404");
|
|
815
|
+
expect(end?.reason).not.toContain("Try again later");
|
|
816
|
+
expect(written).toEqual([]);
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
it("drift with a signature-rejected candidate still ends in install-state-drift", async () => {
|
|
820
|
+
const { deps, q, written } = setup({
|
|
821
|
+
meta: {
|
|
822
|
+
...META,
|
|
823
|
+
imageDigests: { admin: D("9"), gateway: D("8") },
|
|
824
|
+
releaseVersion: "0.10.44",
|
|
825
|
+
},
|
|
826
|
+
manifests: {
|
|
827
|
+
"0.10.44": manifestOf("0.10.44", D("9"), D("8")),
|
|
828
|
+
"0.10.45": { ok: false, error: { code: "signature", message: "not signed" } },
|
|
829
|
+
},
|
|
830
|
+
gcloud: [releaseTags("admin", { "0.10.44": D("9"), "0.10.45": D("1") })],
|
|
360
831
|
});
|
|
361
832
|
expect((await runUpgraderJob(deps)).status).toBe("needs_cloud_shell");
|
|
362
833
|
expect(q.end()?.codes).toEqual(["install-state-drift"]);
|
|
363
|
-
expect(
|
|
834
|
+
expect(written).toEqual([]);
|
|
364
835
|
});
|
|
365
836
|
|
|
366
|
-
it("
|
|
367
|
-
const { deps, q } = setup(
|
|
837
|
+
it("partial match (the admin of a release, the gateway not) is refused", async () => {
|
|
838
|
+
const { deps, q, written } = setup({
|
|
839
|
+
gcloud: [servingAs(D("1"), D("6"))],
|
|
840
|
+
meta: { ...META, imageDigests: { admin: D("9"), gateway: D("8") } },
|
|
841
|
+
manifests: { "0.10.45": manifestOf("0.10.45", D("1"), D("2")) },
|
|
842
|
+
});
|
|
843
|
+
expect((await runUpgraderJob(deps)).status).toBe("needs_cloud_shell");
|
|
844
|
+
expect(q.end()?.reason).toContain("The admin serves v0.10.45's image, the gateway does not");
|
|
845
|
+
expect(written).toEqual([]);
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
it("admin and gateway of two different signed releases are refused, naming both", async () => {
|
|
849
|
+
// Each image is genuinely signed, but no ONE release names both: a mixed
|
|
850
|
+
// deployment no release vouches for.
|
|
851
|
+
const { deps, q, written } = setup({
|
|
852
|
+
gcloud: [
|
|
853
|
+
servingAs(D("9"), D("2")),
|
|
854
|
+
releaseTags("admin", { "0.10.44": D("9") }),
|
|
855
|
+
releaseTags("gateway", { "0.10.45": D("2") }),
|
|
856
|
+
],
|
|
857
|
+
meta: {
|
|
858
|
+
...META,
|
|
859
|
+
imageDigests: { admin: D("1"), gateway: D("2") },
|
|
860
|
+
releaseVersion: "0.10.45",
|
|
861
|
+
},
|
|
862
|
+
manifests: {
|
|
863
|
+
"0.10.44": manifestOf("0.10.44", D("9"), D("8")),
|
|
864
|
+
"0.10.45": manifestOf("0.10.45", D("1"), D("2")),
|
|
865
|
+
},
|
|
866
|
+
});
|
|
867
|
+
expect((await runUpgraderJob(deps)).status).toBe("needs_cloud_shell");
|
|
868
|
+
const end = q.end();
|
|
869
|
+
expect(end?.codes).toEqual(["install-state-drift"]);
|
|
870
|
+
expect(end?.reason).toContain("The gateway serves v0.10.45's image, the admin does not");
|
|
871
|
+
expect(end?.reason).toContain("the admin serves v0.10.44's image, the gateway does not");
|
|
872
|
+
expect(written).toEqual([]);
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
it("an older signed release serving is recorded as what it is, and the target judged from it", async () => {
|
|
876
|
+
// install.json says 0.10.45, but someone put 0.10.44 back: the heal must
|
|
877
|
+
// record the OLDER version, never keep claiming the newer one.
|
|
878
|
+
const { deps, q, written } = setup({
|
|
879
|
+
gcloud: [servingAs(D("9"), D("8")), releaseTags("admin", { "0.10.44": D("9") })],
|
|
880
|
+
manifests: {
|
|
881
|
+
"0.10.44": manifestOf("0.10.44", D("9"), D("8")),
|
|
882
|
+
"0.10.45": manifestOf("0.10.45", D("1"), D("2")),
|
|
883
|
+
},
|
|
884
|
+
});
|
|
885
|
+
expect((await runUpgraderJob(deps)).status).toBe("succeeded");
|
|
886
|
+
expect(written[0]).toMatchObject({
|
|
887
|
+
releaseVersion: "0.10.44",
|
|
888
|
+
imageDigests: { admin: D("9"), gateway: D("8") },
|
|
889
|
+
});
|
|
890
|
+
expect(q.events.some((e) => e.message.startsWith("0.10.44 → 0.11.0"))).toBe(true);
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
it("a release whose manifest does not verify is never recorded", async () => {
|
|
894
|
+
const { deps, q, written } = setup({
|
|
895
|
+
meta: { ...META, imageDigests: { admin: D("9"), gateway: D("8") } },
|
|
896
|
+
manifests: {
|
|
897
|
+
"0.10.45": { ok: false, error: { code: "signature", message: "cert identity mismatch" } },
|
|
898
|
+
},
|
|
899
|
+
});
|
|
900
|
+
expect((await runUpgraderJob(deps)).status).toBe("needs_cloud_shell");
|
|
901
|
+
expect(q.end()?.reason).toContain("could not be verified: v0.10.45 (cert identity mismatch)");
|
|
902
|
+
expect(written).toEqual([]);
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
it("an install.json without digests is refreshed the same way", async () => {
|
|
906
|
+
const { imageDigests: _none, ...bare } = META;
|
|
907
|
+
const { deps, written } = setup({
|
|
908
|
+
meta: bare,
|
|
909
|
+
manifests: { "0.10.45": manifestOf("0.10.45", D("1"), D("2")) },
|
|
910
|
+
});
|
|
911
|
+
expect((await runUpgraderJob(deps)).status).toBe("succeeded");
|
|
912
|
+
expect(written[0]).toMatchObject({ imageDigests: { admin: D("1"), gateway: D("2") } });
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
it("a refresh that cannot be written stops before anything changes", async () => {
|
|
916
|
+
const { deps, order, q } = setup({
|
|
917
|
+
meta: { ...META, imageDigests: { admin: D("9"), gateway: D("2") } },
|
|
918
|
+
manifests: { "0.10.45": manifestOf("0.10.45", D("1"), D("2")) },
|
|
919
|
+
});
|
|
920
|
+
const r = await runUpgraderJob({
|
|
921
|
+
...deps,
|
|
922
|
+
writeInstall: () => {
|
|
923
|
+
throw new Error("403 on the ops bucket");
|
|
924
|
+
},
|
|
925
|
+
});
|
|
926
|
+
expect(r).toMatchObject({ exitCode: 1, status: "failed" });
|
|
927
|
+
expect(q.end()?.codes).toEqual(["install-state-write"]);
|
|
928
|
+
expect(q.end()?.reason).toContain("403 on the ops bucket");
|
|
929
|
+
expect(order.some((l) => /backups create|services update|^migrate/.test(l))).toBe(false);
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
it("a failed install.json write after the roll fails the run instead of ending succeeded", async () => {
|
|
933
|
+
const { deps, order, q } = setup();
|
|
368
934
|
const r = await runUpgraderJob({
|
|
369
935
|
...deps,
|
|
370
936
|
writeInstall: () => {
|
|
371
937
|
throw new Error("403 on the ops bucket");
|
|
372
938
|
},
|
|
373
939
|
});
|
|
374
|
-
expect(r
|
|
375
|
-
expect(q.
|
|
940
|
+
expect(r).toMatchObject({ exitCode: 1, status: "failed" });
|
|
941
|
+
expect(q.end()?.codes).toEqual(["record"]);
|
|
942
|
+
expect(q.end()?.reason).toContain("0.11.0 serves");
|
|
943
|
+
expect(q.events.some((e) => e.phase === "record" && e.level === "error")).toBe(true);
|
|
944
|
+
// The new release keeps serving: no rollback, no self-update.
|
|
945
|
+
expect(order.some((l) => /--to-revisions/.test(l))).toBe(false);
|
|
946
|
+
expect(order.some((l) => /jobs update caelo-production-upgrader/.test(l))).toBe(false);
|
|
376
947
|
});
|
|
377
948
|
|
|
378
949
|
it("a failed backup stops before migrations", async () => {
|