@caelo-cms/provisioning 0.10.53 → 0.10.57
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 +272 -128
- package/dist/audit-job-setup.d.ts +69 -0
- package/dist/audit-job-setup.d.ts.map +1 -0
- package/dist/audit-job-setup.js +317 -0
- package/dist/audit-job-setup.js.map +1 -0
- package/dist/caddy.d.ts +9 -0
- package/dist/caddy.d.ts.map +1 -1
- package/dist/caddy.js +38 -0
- package/dist/caddy.js.map +1 -1
- package/dist/cli-routing.js +1 -1
- package/dist/cli-routing.js.map +1 -1
- package/dist/cli.js +36 -9
- 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 +25 -1
- package/dist/gcp-names.d.ts.map +1 -1
- package/dist/gcp-names.js +36 -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 +28 -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.map +1 -1
- package/dist/operator-access.js +13 -3
- package/dist/operator-access.js.map +1 -1
- package/dist/private-file.d.ts +26 -0
- package/dist/private-file.d.ts.map +1 -0
- package/dist/private-file.js +49 -0
- package/dist/private-file.js.map +1 -0
- package/dist/release-contract.d.ts +3 -2
- package/dist/release-contract.d.ts.map +1 -1
- package/dist/release-contract.js +8 -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 +117 -0
- package/dist/remote-mcp.d.ts.map +1 -0
- package/dist/remote-mcp.js +379 -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 +175 -3
- package/dist/stack-contract.d.ts.map +1 -1
- package/dist/stack-contract.js +183 -3
- package/dist/stack-contract.js.map +1 -1
- package/dist/stack-converge.d.ts +38 -1
- package/dist/stack-converge.d.ts.map +1 -1
- package/dist/stack-converge.js +74 -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/tar-entries.d.ts.map +1 -1
- package/dist/tar-entries.js +84 -9
- package/dist/tar-entries.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 +88 -19
- package/dist/upgrade/app-roll.js.map +1 -1
- package/dist/upgrade/audit-job-roll.d.ts +38 -0
- package/dist/upgrade/audit-job-roll.d.ts.map +1 -0
- package/dist/upgrade/audit-job-roll.js +43 -0
- package/dist/upgrade/audit-job-roll.js.map +1 -0
- package/dist/upgrade/converge.d.ts.map +1 -1
- package/dist/upgrade/converge.js +141 -8
- 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 +132 -14
- package/dist/upgrader-setup.js.map +1 -1
- package/dist/wizards/gcp-cost.d.ts.map +1 -1
- package/dist/wizards/gcp-cost.js +14 -6
- 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 +7 -0
- package/dist/wizards/gcp-pulumi.js.map +1 -1
- package/dist/wizards/gcp.d.ts.map +1 -1
- package/dist/wizards/gcp.js +130 -4
- package/dist/wizards/gcp.js.map +1 -1
- package/package.json +4 -4
- package/src/agent/run.test.ts +15 -1
- package/src/audit-job-setup.test.ts +234 -0
- package/src/audit-job-setup.ts +360 -0
- package/src/caddy-redirects.docker.test.ts +19 -6
- package/src/caddy.test.ts +30 -0
- package/src/caddy.ts +43 -0
- package/src/cli-init-validation.test.ts +44 -0
- package/src/cli-routing.ts +1 -1
- package/src/cli.ts +40 -8
- 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 +43 -1
- package/src/gcp-url-map.test.ts +9 -1
- package/src/index.ts +8 -0
- package/src/infra-contract.test.ts +7 -4
- package/src/infra-contract.ts +28 -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 +14 -0
- package/src/operator-access.ts +13 -3
- package/src/private-file.test.ts +80 -0
- package/src/private-file.ts +50 -0
- package/src/release-contract.ts +8 -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 +517 -0
- package/src/remote-mcp.ts +460 -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 +34 -2
- package/src/stack-contract.ts +305 -3
- package/src/stack-converge-secrets.test.ts +3 -0
- package/src/stack-converge.test.ts +21 -5
- package/src/stack-converge.ts +101 -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/tar-entries.test.ts +149 -7
- package/src/tar-entries.ts +90 -9
- package/src/upgrade/app-roll.ts +135 -21
- package/src/upgrade/audit-job-roll.ts +68 -0
- package/src/upgrade/converge.ts +170 -7
- 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 +279 -5
- package/src/upgrade/upgrade.ts +120 -24
- package/src/upgrader-assess.test.ts +93 -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 +587 -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 +4 -3
- package/src/upgrader-setup.ts +137 -13
- package/src/wizards/gcp-cost.ts +15 -6
- package/src/wizards/gcp-pulumi.ts +7 -0
- package/src/wizards/gcp.ts +162 -4
- package/stacks/aws/index.ts +18 -0
- package/stacks/azure/index.ts +18 -0
- package/stacks/gcp/index.ts +59 -7
- package/stacks/gcp-firebase/index.ts +50 -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";
|
|
@@ -49,6 +86,7 @@ const POLICY: StoredUpdatePolicy = {
|
|
|
49
86
|
autoSecurity: false,
|
|
50
87
|
autoPatch: false,
|
|
51
88
|
autoMinor: false,
|
|
89
|
+
autoConverge: false,
|
|
52
90
|
channel: "stable",
|
|
53
91
|
windowStart: null,
|
|
54
92
|
windowEnd: null,
|
|
@@ -76,6 +114,11 @@ const META: InstallMetadata = {
|
|
|
76
114
|
infraContractVersion: 2,
|
|
77
115
|
};
|
|
78
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
|
+
|
|
79
122
|
const serviceJson = (image: string, env: unknown[]) =>
|
|
80
123
|
JSON.stringify({ spec: { template: { spec: { containers: [{ image, env }] } } } });
|
|
81
124
|
const secretRef = (name: string, secret: string) => ({
|
|
@@ -105,7 +148,8 @@ function fakeGcloud(events: string[], overrides: [RegExp, Answer][] = []) {
|
|
|
105
148
|
[/describe (\S+) .*status\.url/, (l) => ok(`https://${l.split(" ")[3]}-xyz.a.run.app\n`)],
|
|
106
149
|
[
|
|
107
150
|
/^run revisions describe (\S+)/,
|
|
108
|
-
(l) =>
|
|
151
|
+
(l) =>
|
|
152
|
+
ok(l.includes("-admin-") ? revisionJson(D("1"), P("1")) : revisionJson(D("2"), P("2"))),
|
|
109
153
|
],
|
|
110
154
|
[/^sql instances list/, ok("caelo-production-pg-abc\n")],
|
|
111
155
|
[/^sql backups list/, ok("1760000000000\n")],
|
|
@@ -158,12 +202,20 @@ function setup(
|
|
|
158
202
|
opts: {
|
|
159
203
|
request?: Partial<ClaimedUpdate>;
|
|
160
204
|
manifest?: ReleaseManifestResult;
|
|
205
|
+
/** Manifests of other versions than the target (the serving release's). */
|
|
206
|
+
manifests?: Record<string, ReleaseManifestResult>;
|
|
161
207
|
meta?: InstallMetadata | null;
|
|
162
208
|
gcloud?: [RegExp, Answer][];
|
|
163
209
|
deep?: HealthOutcome;
|
|
164
210
|
migrate?: { ok: boolean; error?: string };
|
|
165
211
|
locked?: boolean;
|
|
166
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;
|
|
167
219
|
} = {},
|
|
168
220
|
) {
|
|
169
221
|
const order: string[] = [];
|
|
@@ -184,14 +236,21 @@ function setup(
|
|
|
184
236
|
region: "europe-west1",
|
|
185
237
|
env: "production",
|
|
186
238
|
execution: "caelo-production-upgrader-x1",
|
|
239
|
+
iam: fakeIam(order, opts.iamWithheld ?? {}, opts.iamError),
|
|
187
240
|
fetchManifest: async (v) => {
|
|
188
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
|
+
}
|
|
189
247
|
return opts.manifest ?? { ok: true, manifest: MANIFEST };
|
|
190
248
|
},
|
|
191
|
-
lockState: () => {
|
|
249
|
+
lockState: (onBreak) => {
|
|
192
250
|
order.push("lock");
|
|
193
251
|
if (opts.locked) throw new StateLockHeldError("held by caelo installer");
|
|
194
252
|
if (opts.lockDenied) throw new Error("storage.objects.list denied");
|
|
253
|
+
if (opts.brokeStaleLock) onBreak(opts.brokeStaleLock);
|
|
195
254
|
return () => order.push("unlock");
|
|
196
255
|
},
|
|
197
256
|
readInstall: () => (opts.meta === undefined ? META : opts.meta),
|
|
@@ -212,6 +271,23 @@ function setup(
|
|
|
212
271
|
return { deps, order, q, written };
|
|
213
272
|
}
|
|
214
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
|
+
|
|
215
291
|
const step = (order: string[], re: RegExp) => order.findIndex((l) => re.test(l));
|
|
216
292
|
|
|
217
293
|
describe("runUpgraderJob", () => {
|
|
@@ -274,10 +350,12 @@ describe("runUpgraderJob", () => {
|
|
|
274
350
|
]);
|
|
275
351
|
});
|
|
276
352
|
|
|
277
|
-
it("
|
|
353
|
+
it("#815: decides first, locks right before the backup, holds the lock to the end", async () => {
|
|
278
354
|
const ok = setup();
|
|
279
355
|
await runUpgraderJob(ok.deps);
|
|
280
|
-
|
|
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/));
|
|
281
359
|
expect(ok.order.indexOf("unlock")).toBeGreaterThan(
|
|
282
360
|
step(ok.order, /jobs update caelo-production-upgrader/),
|
|
283
361
|
);
|
|
@@ -286,7 +364,83 @@ describe("runUpgraderJob", () => {
|
|
|
286
364
|
const r = await runUpgraderJob(busy.deps);
|
|
287
365
|
expect(r.status).toBe("failed");
|
|
288
366
|
expect(busy.q.end()?.codes).toEqual(["state-locked"]);
|
|
289
|
-
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
|
+
}
|
|
290
444
|
});
|
|
291
445
|
|
|
292
446
|
it("does not call a lock it cannot take 'busy': an access failure asks for Cloud Shell", async () => {
|
|
@@ -297,9 +451,61 @@ describe("runUpgraderJob", () => {
|
|
|
297
451
|
expect(end?.codes).toEqual(["state-access"]);
|
|
298
452
|
expect(end?.reason).not.toContain("another install operation");
|
|
299
453
|
expect(end?.reason).toContain("storage.objects.list denied");
|
|
300
|
-
expect(denied.order.some((l) =>
|
|
301
|
-
|
|
302
|
-
|
|
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();
|
|
303
509
|
});
|
|
304
510
|
|
|
305
511
|
it("retries recording the end, and fails the run when it cannot", async () => {
|
|
@@ -353,25 +559,391 @@ describe("runUpgraderJob", () => {
|
|
|
353
559
|
expect(q.end()?.codes).toEqual(["install-state"]);
|
|
354
560
|
});
|
|
355
561
|
|
|
356
|
-
it("
|
|
357
|
-
|
|
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({
|
|
358
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") })],
|
|
359
831
|
});
|
|
360
832
|
expect((await runUpgraderJob(deps)).status).toBe("needs_cloud_shell");
|
|
361
833
|
expect(q.end()?.codes).toEqual(["install-state-drift"]);
|
|
362
|
-
expect(
|
|
834
|
+
expect(written).toEqual([]);
|
|
363
835
|
});
|
|
364
836
|
|
|
365
|
-
it("
|
|
366
|
-
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();
|
|
367
934
|
const r = await runUpgraderJob({
|
|
368
935
|
...deps,
|
|
369
936
|
writeInstall: () => {
|
|
370
937
|
throw new Error("403 on the ops bucket");
|
|
371
938
|
},
|
|
372
939
|
});
|
|
373
|
-
expect(r
|
|
374
|
-
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);
|
|
375
947
|
});
|
|
376
948
|
|
|
377
949
|
it("a failed backup stops before migrations", async () => {
|