@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
|
@@ -23,7 +23,12 @@ import { restoreCommand, takeSqlBackup } from "./backup.js";
|
|
|
23
23
|
import { needsConverge, startConverge } from "./converge.js";
|
|
24
24
|
import type { HealthOutcome, HealthProbe } from "./health.js";
|
|
25
25
|
import type { UpgradeReporter } from "./reporter.js";
|
|
26
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
type GcpUpgradeDeps,
|
|
28
|
+
type GcpUpgradeTarget,
|
|
29
|
+
upgradeGcpInstall,
|
|
30
|
+
upgradeGcpInstallOutcome,
|
|
31
|
+
} from "./upgrade.js";
|
|
27
32
|
|
|
28
33
|
const ok = (stdout = ""): GcloudResult => ({ ok: true, stdout, stderr: "", exitCode: 0 });
|
|
29
34
|
const fail = (stderr: string): GcloudResult => ({ ok: false, stdout: "", stderr, exitCode: 1 });
|
|
@@ -460,6 +465,142 @@ describe("upgradeGcpInstall — converge, then app-roll", () => {
|
|
|
460
465
|
expect(calls.some((c) => c.includes("remove-iam-policy-binding"))).toBe(false);
|
|
461
466
|
});
|
|
462
467
|
|
|
468
|
+
it("#883 review: reports rolledBack structurally — true only when every pin back succeeded", async () => {
|
|
469
|
+
const deep = { ok: false as const, error: "cms_admin is missing 1 migration(s)" };
|
|
470
|
+
const back = upgradeDeps([], { deep });
|
|
471
|
+
expect(await upgradeGcpInstallOutcome(TARGET, back.deps)).toEqual({
|
|
472
|
+
ok: false,
|
|
473
|
+
rolledBack: true,
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
// The gateway's pin back fails: the previous release does NOT serve.
|
|
477
|
+
const stuck = upgradeDeps([], {
|
|
478
|
+
deep,
|
|
479
|
+
overrides: [[new RegExp(`update-traffic ${GATEWAY} .*--to-revisions`), fail("503")]],
|
|
480
|
+
});
|
|
481
|
+
expect(await upgradeGcpInstallOutcome(TARGET, stuck.deps)).toEqual({
|
|
482
|
+
ok: false,
|
|
483
|
+
rolledBack: false,
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
// Nothing rolled (migrations failed): no rollback, so not rolled back.
|
|
487
|
+
const early = upgradeDeps([]);
|
|
488
|
+
expect(
|
|
489
|
+
await upgradeGcpInstallOutcome(TARGET, {
|
|
490
|
+
...early.deps,
|
|
491
|
+
migrate: async () => ({ ok: false, error: "boom" }),
|
|
492
|
+
}),
|
|
493
|
+
).toEqual({ ok: false, rolledBack: false });
|
|
494
|
+
|
|
495
|
+
expect(await upgradeGcpInstallOutcome(TARGET, upgradeDeps([]).deps)).toEqual({ ok: true });
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it("sets up the converge job as the install's provisioner after the upgrader (#819)", async () => {
|
|
499
|
+
const events: string[] = [];
|
|
500
|
+
const { deps, lines } = upgradeDeps(events);
|
|
501
|
+
expect(await upgradeGcpInstall(TARGET, deps)).toBe(true);
|
|
502
|
+
const upgrader = indexOf(events, /^gcloud run jobs deploy caelo-production-upgrader /);
|
|
503
|
+
const converge = indexOf(events, /^gcloud run jobs deploy caelo-production-converge /);
|
|
504
|
+
expect(upgrader).toBeLessThan(converge);
|
|
505
|
+
expect(events[converge]).toContain(
|
|
506
|
+
"--service-account=caelo-provisioner@acme.iam.gserviceaccount.com",
|
|
507
|
+
);
|
|
508
|
+
expect(events[converge]).toContain(`--image=registry/ops@sha256:${"c".repeat(64)}`);
|
|
509
|
+
expect(lines.some((l) => l.startsWith("done: Converge job ready"))).toBe(true);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
it("a failed converge-job setup leaves the converge incomplete and CAELO_CONVERGE_JOB as it was", async () => {
|
|
513
|
+
const events: string[] = [];
|
|
514
|
+
const { deps, recorded, lines } = upgradeDeps(events, {
|
|
515
|
+
overrides: [[/^run jobs deploy caelo-production-converge /, fail("PERMISSION_DENIED")]],
|
|
516
|
+
});
|
|
517
|
+
expect(await upgradeGcpInstall(TARGET, deps)).toBe(true);
|
|
518
|
+
expect(recorded.contract).toEqual([]);
|
|
519
|
+
expect(lines.some((l) => l.includes("Converge job not set up"))).toBe(true);
|
|
520
|
+
expect(
|
|
521
|
+
events.some((e) => /services update caelo-production-admin.*CAELO_CONVERGE_JOB=/.test(e)),
|
|
522
|
+
).toBe(false);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it("#883 review: an install without caelo-provisioner still records the contract; its admin keeps the terminal path", async () => {
|
|
526
|
+
const events: string[] = [];
|
|
527
|
+
const { deps, recorded, lines } = upgradeDeps(events, {
|
|
528
|
+
overrides: [
|
|
529
|
+
[
|
|
530
|
+
/^iam service-accounts describe caelo-provisioner@/,
|
|
531
|
+
fail("ERROR: NOT_FOUND: Unknown service account"),
|
|
532
|
+
],
|
|
533
|
+
// A project without the default network's open rules: nothing
|
|
534
|
+
// else keeps this converge incomplete.
|
|
535
|
+
[/^compute firewall-rules list/, ok("[]")],
|
|
536
|
+
],
|
|
537
|
+
});
|
|
538
|
+
expect(await upgradeGcpInstall(TARGET, deps)).toBe(true);
|
|
539
|
+
// Not blocked forever: the converge is complete without the job.
|
|
540
|
+
expect(recorded.contract).toEqual([TARGET.infraContractVersion]);
|
|
541
|
+
expect(lines.some((l) => l.includes("no caelo-provisioner"))).toBe(true);
|
|
542
|
+
expect(lines.some((l) => l.includes("Converge job not set up"))).toBe(false);
|
|
543
|
+
// No job, so no CAELO_CONVERGE_JOB: the admin routes such releases to the terminal.
|
|
544
|
+
expect(events.some((e) => /^gcloud run jobs deploy caelo-production-converge /.test(e))).toBe(
|
|
545
|
+
false,
|
|
546
|
+
);
|
|
547
|
+
expect(
|
|
548
|
+
events.some((e) => /services update caelo-production-admin.*CAELO_CONVERGE_JOB=/.test(e)),
|
|
549
|
+
).toBe(false);
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
it("reports its backup and rollback targets before relying on them (#819 checkpoint)", async () => {
|
|
553
|
+
const events: string[] = [];
|
|
554
|
+
const { deps } = upgradeDeps(events);
|
|
555
|
+
const seen: string[] = [];
|
|
556
|
+
expect(
|
|
557
|
+
await upgradeGcpInstall(TARGET, {
|
|
558
|
+
...deps,
|
|
559
|
+
onCheckpoint: (c) => {
|
|
560
|
+
if (c.priorRevisions) events.push(`checkpoint revisions ${c.priorRevisions.admin}`);
|
|
561
|
+
if (c.backup) events.push(`checkpoint backup ${c.backup.backupId}`);
|
|
562
|
+
seen.push(Object.keys(c).join(","));
|
|
563
|
+
},
|
|
564
|
+
}),
|
|
565
|
+
).toBe(true);
|
|
566
|
+
expect(seen).toEqual(["priorRevisions", "backup"]);
|
|
567
|
+
const revisions = indexOf(events, /^checkpoint revisions caelo-production-admin-aaa-00001-old/);
|
|
568
|
+
const backup = indexOf(events, /^gcloud sql backups create/);
|
|
569
|
+
const saved = indexOf(events, new RegExp(`^checkpoint backup ${BACKUP_ID}`));
|
|
570
|
+
const migrate = indexOf(events, /^migrate /);
|
|
571
|
+
expect(revisions).toBeLessThan(backup);
|
|
572
|
+
expect(backup).toBeLessThan(saved);
|
|
573
|
+
expect(saved).toBeLessThan(migrate);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
it("a resumed run keeps the interrupted run's backup and rolls back onto what served before it (#819)", async () => {
|
|
577
|
+
const events: string[] = [];
|
|
578
|
+
const { deps, lines } = upgradeDeps(events, {
|
|
579
|
+
deep: { ok: false, error: "unhealthy" },
|
|
580
|
+
});
|
|
581
|
+
const before = { admin: `${ADMIN}-00000-before`, gateway: `${GATEWAY}-00000-before` };
|
|
582
|
+
expect(
|
|
583
|
+
await upgradeGcpInstall(TARGET, {
|
|
584
|
+
...deps,
|
|
585
|
+
resume: {
|
|
586
|
+
backup: { projectId: "acme", instance: SQL, backupId: "1759999999999" },
|
|
587
|
+
priorRevisions: before,
|
|
588
|
+
},
|
|
589
|
+
onCheckpoint: () => events.push("checkpoint"),
|
|
590
|
+
}),
|
|
591
|
+
).toBe(false);
|
|
592
|
+
expect(events.some((e) => /sql backups create/.test(e))).toBe(false);
|
|
593
|
+
expect(events).not.toContain("checkpoint");
|
|
594
|
+
expect(lines).toContain(
|
|
595
|
+
"info: Continuing an interrupted update: keeping its pre-upgrade backup 1759999999999.",
|
|
596
|
+
);
|
|
597
|
+
const rollbacks = events.filter((e) => e.includes("--to-revisions"));
|
|
598
|
+
expect(rollbacks).toEqual([
|
|
599
|
+
expect.stringContaining(`--to-revisions ${before.gateway}=100`),
|
|
600
|
+
expect.stringContaining(`--to-revisions ${before.admin}=100`),
|
|
601
|
+
]);
|
|
602
|
+
});
|
|
603
|
+
|
|
463
604
|
it("revokes public_role's Cloud SQL role memberships before the migrations audit them (#633)", async () => {
|
|
464
605
|
const events: string[] = [];
|
|
465
606
|
const { deps, lines } = upgradeDeps(events);
|
|
@@ -527,6 +668,65 @@ describe("upgradeGcpInstall — converge, then app-roll", () => {
|
|
|
527
668
|
expect(recorded.contract).toEqual([]);
|
|
528
669
|
});
|
|
529
670
|
|
|
671
|
+
it("#806 — the rolls carry remote MCP: identity env, the gateway's client file and timeout", async () => {
|
|
672
|
+
const events: string[] = [];
|
|
673
|
+
const { deps, calls } = upgradeDeps(events);
|
|
674
|
+
expect(await upgradeGcpInstall(TARGET, deps)).toBe(true);
|
|
675
|
+
const rolls = calls.filter((c) =>
|
|
676
|
+
/^run services update \S+ --region europe-west1 .*--image/.test(c),
|
|
677
|
+
);
|
|
678
|
+
const admin = rolls.find((c) => c.startsWith(`run services update ${ADMIN} `)) ?? "";
|
|
679
|
+
const gateway = rolls.find((c) => c.startsWith(`run services update ${GATEWAY} `)) ?? "";
|
|
680
|
+
expect(admin).toContain(
|
|
681
|
+
`CAELO_IAP_AUDIENCE=/projects/42/locations/europe-west1/services/${ADMIN}`,
|
|
682
|
+
);
|
|
683
|
+
expect(admin).toContain(`CAELO_MCP_PUBLIC_URL=https://${GATEWAY}-xyz.a.run.app`);
|
|
684
|
+
expect(gateway).toContain(`CAELO_ADMIN_URL=https://${ADMIN}-xyz.a.run.app`);
|
|
685
|
+
expect(gateway).toContain(`CAELO_MCP_PUBLIC_URL=https://${GATEWAY}-xyz.a.run.app`);
|
|
686
|
+
expect(gateway).toContain(
|
|
687
|
+
"--update-secrets=/secrets/operator-oauth-client/client.json=caelo-production-operator-oauth-client:latest",
|
|
688
|
+
);
|
|
689
|
+
expect(gateway).toContain("--timeout=3600");
|
|
690
|
+
expect(admin).not.toContain("--timeout");
|
|
691
|
+
// No extra revision: everything rides the image rolls.
|
|
692
|
+
expect(calls.filter((c) => /^run services update /.test(c))).toHaveLength(2);
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
it("#806 — on gcp, routes the discovery paths and adds the Cloud Armor rule before the roll", async () => {
|
|
696
|
+
const events: string[] = [];
|
|
697
|
+
const urlMap = {
|
|
698
|
+
name: "caelo-production-url-map-1",
|
|
699
|
+
hostRules: [{ hosts: ["acme.com"], pathMatcher: "public" }],
|
|
700
|
+
pathMatchers: [{ name: "public", pathRules: [{ paths: ["/api/*"], service: "gw" }] }],
|
|
701
|
+
};
|
|
702
|
+
const puts: string[] = [];
|
|
703
|
+
const { deps, lines } = upgradeDeps(events, {
|
|
704
|
+
overrides: [
|
|
705
|
+
[/^compute url-maps list/, ok(JSON.stringify([urlMap]))],
|
|
706
|
+
[/^compute security-policies list/, ok("caelo-production-waf-1\n")],
|
|
707
|
+
[/^compute security-policies describe/, ok(JSON.stringify({ rules: [] }))],
|
|
708
|
+
],
|
|
709
|
+
});
|
|
710
|
+
const recordingHttp: HttpFetch = async (url, init) => {
|
|
711
|
+
if (init.method === "PUT" && url.includes("/urlMaps/"))
|
|
712
|
+
puts.push(`${url} ${init.body ?? ""}`);
|
|
713
|
+
return http(url, init);
|
|
714
|
+
};
|
|
715
|
+
expect(await upgradeGcpInstall(GCP_TARGET, { ...deps, http: recordingHttp })).toBe(true);
|
|
716
|
+
expect(events).toContainEqual(
|
|
717
|
+
expect.stringMatching(
|
|
718
|
+
/^gcloud compute security-policies rules create 900 --security-policy=caelo-production-waf-1 /,
|
|
719
|
+
),
|
|
720
|
+
);
|
|
721
|
+
expect(puts).toHaveLength(1);
|
|
722
|
+
expect(puts[0]).toContain("/global/urlMaps/caelo-production-url-map-1");
|
|
723
|
+
expect(puts[0]).toContain("/.well-known/oauth-authorization-server/*");
|
|
724
|
+
expect(lines).toContain("done: Remote-MCP load-balancer rules ready");
|
|
725
|
+
expect(indexOf(events, /security-policies rules create/)).toBeLessThan(
|
|
726
|
+
indexOf(events, /^migrate /),
|
|
727
|
+
);
|
|
728
|
+
});
|
|
729
|
+
|
|
530
730
|
it("on gcp-firebase there is no URL map: no redirects role", async () => {
|
|
531
731
|
const events: string[] = [];
|
|
532
732
|
const { deps, calls } = upgradeDeps(events);
|
|
@@ -691,7 +891,7 @@ describe("app-roll and converge stay on their side", () => {
|
|
|
691
891
|
},
|
|
692
892
|
},
|
|
693
893
|
);
|
|
694
|
-
expect(result).toEqual({ ok: false, phase: "roll", migrated: true });
|
|
894
|
+
expect(result).toEqual({ ok: false, phase: "roll", migrated: true, rolledBack: true });
|
|
695
895
|
expect(calls).toContain(
|
|
696
896
|
`run services update-traffic ${ADMIN} --region europe-west1 --project acme --to-revisions ${ADMIN}-00001-old=100 --quiet`,
|
|
697
897
|
);
|
|
@@ -715,7 +915,7 @@ describe("app-roll and converge stay on their side", () => {
|
|
|
715
915
|
recordDigests: async () => {},
|
|
716
916
|
},
|
|
717
917
|
);
|
|
718
|
-
expect(result).toEqual({ ok: false, phase: "migrations", migrated: true });
|
|
918
|
+
expect(result).toEqual({ ok: false, phase: "migrations", migrated: true, rolledBack: false });
|
|
719
919
|
expect(calls.filter((c) => APP_ROLL_WRITE.test(c))).toEqual([]);
|
|
720
920
|
expect(
|
|
721
921
|
lines.some((l) => l.includes("contract-type") && l.includes(restoreCommand(BACKUP))),
|
package/src/upgrade/upgrade.ts
CHANGED
|
@@ -17,7 +17,14 @@
|
|
|
17
17
|
* install's infrastructure contract version
|
|
18
18
|
*
|
|
19
19
|
* Every side effect goes through {@link GcpUpgradeDeps}, so the sequence runs
|
|
20
|
-
* against a fake gcloud in tests. lifecycle.ts wires the real ones
|
|
20
|
+
* against a fake gcloud in tests. lifecycle.ts wires the real ones for the
|
|
21
|
+
* terminal; converge-job-gcp.ts wires them for the converge job (#819),
|
|
22
|
+
* which runs this same sequence from inside the install's project. The job
|
|
23
|
+
* may be killed mid-way and started again, so the sequence reports its two
|
|
24
|
+
* facts a resumed run must not recompute — the pre-upgrade backup and the
|
|
25
|
+
* revisions that served before ({@link UpgradeCheckpoint}) — and accepts
|
|
26
|
+
* them back: a resumed run neither backs up the half-migrated database
|
|
27
|
+
* again nor rolls back onto a revision the interrupted run created.
|
|
21
28
|
*/
|
|
22
29
|
|
|
23
30
|
import type { GcloudRunner, Sleep } from "../gcloud-retry.js";
|
|
@@ -31,7 +38,7 @@ import {
|
|
|
31
38
|
type RollSlug,
|
|
32
39
|
reportPlan,
|
|
33
40
|
} from "./app-roll.js";
|
|
34
|
-
import { backupFailureHelp, restoreCommand, takeSqlBackup } from "./backup.js";
|
|
41
|
+
import { backupFailureHelp, restoreCommand, type SqlBackup, takeSqlBackup } from "./backup.js";
|
|
35
42
|
import { startConverge } from "./converge.js";
|
|
36
43
|
import type { HealthProbe } from "./health.js";
|
|
37
44
|
import type { UpgradeReporter } from "./reporter.js";
|
|
@@ -51,6 +58,21 @@ export interface GcpUpgradeTarget {
|
|
|
51
58
|
readonly infraContractVersion: number;
|
|
52
59
|
readonly skipVerify: boolean;
|
|
53
60
|
readonly skipHealthCheck: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Whether the release's migrations are contract-type, from the signed
|
|
63
|
+
* release manifest (the converge job knows it; the terminal does not).
|
|
64
|
+
*/
|
|
65
|
+
readonly migrationsContract?: boolean | null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* What an upgrade run has fixed that a resumed run must reuse (#819): the
|
|
70
|
+
* backup taken before anything changed, and the revisions that served
|
|
71
|
+
* before — the rollback targets.
|
|
72
|
+
*/
|
|
73
|
+
export interface UpgradeCheckpoint {
|
|
74
|
+
readonly backup?: SqlBackup;
|
|
75
|
+
readonly priorRevisions?: Readonly<Record<RollSlug, string>>;
|
|
54
76
|
}
|
|
55
77
|
|
|
56
78
|
export interface GcpUpgradeDeps {
|
|
@@ -86,13 +108,43 @@ export interface GcpUpgradeDeps {
|
|
|
86
108
|
readonly provisionerIdentity?: () => Promise<boolean>;
|
|
87
109
|
/** Today, for the backup's description. */
|
|
88
110
|
readonly now?: () => Date;
|
|
111
|
+
/** #819 — what an interrupted run of this same upgrade already fixed. */
|
|
112
|
+
readonly resume?: UpgradeCheckpoint;
|
|
113
|
+
/** #819 — called once each checkpoint fact is known (before it is relied on). */
|
|
114
|
+
readonly onCheckpoint?: (checkpoint: UpgradeCheckpoint) => void;
|
|
89
115
|
}
|
|
90
116
|
|
|
117
|
+
/** How {@link upgradeGcpInstallOutcome} ended. */
|
|
118
|
+
export type GcpUpgradeOutcome =
|
|
119
|
+
| { readonly ok: true }
|
|
120
|
+
| {
|
|
121
|
+
readonly ok: false;
|
|
122
|
+
/**
|
|
123
|
+
* The roll failed AND every rolled service was pinned back to the
|
|
124
|
+
* revision that served before (app-roll.ts AppRollResult): the
|
|
125
|
+
* previous release serves again. Structural, never read from text.
|
|
126
|
+
*/
|
|
127
|
+
readonly rolledBack: boolean;
|
|
128
|
+
};
|
|
129
|
+
|
|
91
130
|
/** Upgrade one install; true on success. See the file comment for the order. */
|
|
92
131
|
export async function upgradeGcpInstall(
|
|
93
132
|
target: GcpUpgradeTarget,
|
|
94
133
|
deps: GcpUpgradeDeps,
|
|
95
134
|
): Promise<boolean> {
|
|
135
|
+
return (await upgradeGcpInstallOutcome(target, deps)).ok;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* {@link upgradeGcpInstall} with how a failure ended: whether the previous
|
|
140
|
+
* release serves again after a rollback (the converge job reports
|
|
141
|
+
* rolled_back only then, #819 review).
|
|
142
|
+
*/
|
|
143
|
+
export async function upgradeGcpInstallOutcome(
|
|
144
|
+
target: GcpUpgradeTarget,
|
|
145
|
+
deps: GcpUpgradeDeps,
|
|
146
|
+
): Promise<GcpUpgradeOutcome> {
|
|
147
|
+
const failed: GcpUpgradeOutcome = { ok: false, rolledBack: false };
|
|
96
148
|
const { reporter: r, run } = deps;
|
|
97
149
|
const { projectId } = target;
|
|
98
150
|
|
|
@@ -102,7 +154,7 @@ export async function upgradeGcpInstall(
|
|
|
102
154
|
if (!images.ok) {
|
|
103
155
|
sPre.failed("Couldn't resolve the release images");
|
|
104
156
|
r.error(images.error);
|
|
105
|
-
return
|
|
157
|
+
return failed;
|
|
106
158
|
}
|
|
107
159
|
const planned = await planAppRoll(
|
|
108
160
|
{
|
|
@@ -119,34 +171,46 @@ export async function upgradeGcpInstall(
|
|
|
119
171
|
if (!planned.ok) {
|
|
120
172
|
sPre.failed("Pre-flight failed");
|
|
121
173
|
r.error(planned.error);
|
|
122
|
-
return
|
|
174
|
+
return failed;
|
|
123
175
|
}
|
|
124
|
-
const plan = planned.plan;
|
|
176
|
+
const plan = withPriorRevisions(planned.plan, deps, r);
|
|
125
177
|
sPre.done(`Pre-flight ok — ${plan.services.length} services planned`);
|
|
126
178
|
reportPlan(plan, r);
|
|
127
179
|
|
|
128
180
|
// 2. P21 ship 4 — signatures against the Caelo release workflow's
|
|
129
181
|
// identity: refuses a compromised registry, a typosquat or a fork's image.
|
|
130
182
|
if (!target.skipVerify) {
|
|
131
|
-
if (!(await deps.verifyImages(plan.services))) return
|
|
183
|
+
if (!(await deps.verifyImages(plan.services))) return failed;
|
|
132
184
|
} else {
|
|
133
185
|
r.warn("--skip-verify set — image signatures NOT verified.");
|
|
134
186
|
}
|
|
135
187
|
|
|
136
188
|
// 3. The first change: a failure leaves the install exactly as it was.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
r.
|
|
147
|
-
|
|
189
|
+
// A resumed run keeps the interrupted run's backup: a new one could
|
|
190
|
+
// already hold the half-migrated schema.
|
|
191
|
+
let backup: { readonly ok: true; readonly backup: SqlBackup };
|
|
192
|
+
if (deps.resume?.backup) {
|
|
193
|
+
backup = { ok: true, backup: deps.resume.backup };
|
|
194
|
+
r.info(
|
|
195
|
+
`Continuing an interrupted update: keeping its pre-upgrade backup ${backup.backup.backupId}.`,
|
|
196
|
+
);
|
|
197
|
+
} else {
|
|
198
|
+
const sBak = r.step("Taking a pre-upgrade Cloud SQL backup...");
|
|
199
|
+
const taken = await takeSqlBackup(
|
|
200
|
+
run,
|
|
201
|
+
{ projectId, env: target.env },
|
|
202
|
+
`pre-upgrade to ${target.targetTag}`,
|
|
203
|
+
deps.now,
|
|
204
|
+
);
|
|
205
|
+
if (!taken.ok) {
|
|
206
|
+
sBak.failed(`Pre-upgrade backup failed: ${taken.error}`);
|
|
207
|
+
r.warn(backupFailureHelp({ projectId }));
|
|
208
|
+
return failed;
|
|
209
|
+
}
|
|
210
|
+
sBak.done(`Pre-upgrade backup ${taken.backup.backupId} of ${taken.backup.instance} taken`);
|
|
211
|
+
backup = taken;
|
|
212
|
+
deps.onCheckpoint?.({ backup: taken.backup });
|
|
148
213
|
}
|
|
149
|
-
sBak.done(`Pre-upgrade backup ${backup.backup.backupId} of ${backup.backup.instance} taken`);
|
|
150
214
|
|
|
151
215
|
// 4. Converge, the part before the roll. The upgrader job runs the ops
|
|
152
216
|
// image with rights over the whole app, so it is deployed only when its
|
|
@@ -160,7 +224,7 @@ export async function upgradeGcpInstall(
|
|
|
160
224
|
...(deps.http ? { http: deps.http } : {}),
|
|
161
225
|
...(deps.provisionerIdentity ? { provisionerIdentity: deps.provisionerIdentity } : {}),
|
|
162
226
|
});
|
|
163
|
-
if (!(await converge.beforeAppRoll())) return
|
|
227
|
+
if (!(await converge.beforeAppRoll())) return failed;
|
|
164
228
|
|
|
165
229
|
// 5. App-roll, with the converge steps that sit between its own.
|
|
166
230
|
let health: HealthProbe | null = null;
|
|
@@ -169,7 +233,7 @@ export async function upgradeGcpInstall(
|
|
|
169
233
|
if (!probe.ok) {
|
|
170
234
|
r.error(probe.error);
|
|
171
235
|
r.warn("No migrations ran and no traffic moved.");
|
|
172
|
-
return
|
|
236
|
+
return failed;
|
|
173
237
|
}
|
|
174
238
|
health = probe.probe;
|
|
175
239
|
}
|
|
@@ -178,8 +242,8 @@ export async function upgradeGcpInstall(
|
|
|
178
242
|
plan,
|
|
179
243
|
backup: backup.backup,
|
|
180
244
|
targetVersion: /^\d+\.\d+\.\d+/.test(target.targetTag) ? target.targetTag : null,
|
|
181
|
-
// Unknown until the signed release manifest names it
|
|
182
|
-
migrationsContract: null,
|
|
245
|
+
// Unknown on the terminal path until the signed release manifest names it.
|
|
246
|
+
migrationsContract: target.migrationsContract ?? null,
|
|
183
247
|
},
|
|
184
248
|
converge.hooks,
|
|
185
249
|
{
|
|
@@ -191,7 +255,7 @@ export async function upgradeGcpInstall(
|
|
|
191
255
|
recordDigests: deps.recordDigests,
|
|
192
256
|
},
|
|
193
257
|
);
|
|
194
|
-
if (!rolled.ok) return false;
|
|
258
|
+
if (!rolled.ok) return { ok: false, rolledBack: rolled.rolledBack };
|
|
195
259
|
r.success(`Upgrade to ${target.targetTag} complete (admin + gateway revisions serving).`);
|
|
196
260
|
r.info(
|
|
197
261
|
`Pre-upgrade backup: ${backup.backup.backupId} (restore: ${restoreCommand(backup.backup)})`,
|
|
@@ -212,7 +276,39 @@ export async function upgradeGcpInstall(
|
|
|
212
276
|
"Some converge steps did not succeed (see above); the install is not marked as converged, so in-product updates stay unavailable until a re-run of upgrade completes them.",
|
|
213
277
|
);
|
|
214
278
|
}
|
|
215
|
-
return true;
|
|
279
|
+
return { ok: true };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* The plan with the rollback targets an interrupted run recorded (#819): it
|
|
284
|
+
* may have rolled a service already, so the revision serving NOW would be
|
|
285
|
+
* the new release's. A fresh run records the planned ones instead.
|
|
286
|
+
*/
|
|
287
|
+
function withPriorRevisions(
|
|
288
|
+
plan: AppRollPlan,
|
|
289
|
+
deps: GcpUpgradeDeps,
|
|
290
|
+
r: UpgradeReporter,
|
|
291
|
+
): AppRollPlan {
|
|
292
|
+
const recorded = deps.resume?.priorRevisions;
|
|
293
|
+
if (!recorded) {
|
|
294
|
+
const prior = Object.fromEntries(plan.services.map((s) => [s.slug, s.priorRevision])) as Record<
|
|
295
|
+
RollSlug,
|
|
296
|
+
string
|
|
297
|
+
>;
|
|
298
|
+
deps.onCheckpoint?.({ priorRevisions: prior });
|
|
299
|
+
return plan;
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
...plan,
|
|
303
|
+
services: plan.services.map((s) => {
|
|
304
|
+
const before = recorded[s.slug];
|
|
305
|
+
if (before === s.priorRevision) return s;
|
|
306
|
+
r.info(
|
|
307
|
+
`Continuing an interrupted update: ${s.slug} goes back to ${before} (what served before it started) if the new release fails its checks.`,
|
|
308
|
+
);
|
|
309
|
+
return { ...s, priorRevision: before };
|
|
310
|
+
}),
|
|
311
|
+
};
|
|
216
312
|
}
|
|
217
313
|
|
|
218
314
|
/**
|
|
@@ -54,7 +54,11 @@ const verified = (m: Partial<ReleaseManifest> = {}): ReleaseManifestResult => ({
|
|
|
54
54
|
});
|
|
55
55
|
const broken = (
|
|
56
56
|
code: "registry" | "signature" | "schema" | "blob-digest" | "version-mismatch" | "artifact-shape",
|
|
57
|
-
|
|
57
|
+
transient?: true,
|
|
58
|
+
): ReleaseManifestResult => ({
|
|
59
|
+
ok: false,
|
|
60
|
+
error: { code, message: `${code} problem`, ...(transient ? { transient } : {}) },
|
|
61
|
+
});
|
|
58
62
|
|
|
59
63
|
interface Row {
|
|
60
64
|
readonly name: string;
|
|
@@ -73,9 +77,16 @@ const ROWS: readonly Row[] = [
|
|
|
73
77
|
{ name: "a signed, newer, app-only release", expect: "proceed" },
|
|
74
78
|
{
|
|
75
79
|
name: "registry unreachable",
|
|
76
|
-
manifest: broken("registry"),
|
|
80
|
+
manifest: broken("registry", true),
|
|
77
81
|
expect: "failed/registry",
|
|
78
|
-
reasonContains: "
|
|
82
|
+
reasonContains: "try again later",
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
// Regression #865: a 404 never heals, so it must not end retryable.
|
|
86
|
+
name: "no signed manifest for the target (404)",
|
|
87
|
+
manifest: broken("registry"),
|
|
88
|
+
expect: "refused/manifest-missing",
|
|
89
|
+
reasonContains: "has no signed release manifest",
|
|
79
90
|
},
|
|
80
91
|
{ name: "bad signature", manifest: broken("signature"), expect: "refused/signature" },
|
|
81
92
|
{ name: "tampered blob", manifest: broken("blob-digest"), expect: "refused/signature" },
|
|
@@ -95,6 +106,7 @@ const ROWS: readonly Row[] = [
|
|
|
95
106
|
name: "same version",
|
|
96
107
|
installed: { version: "0.11.0", infraContractVersion: 2 },
|
|
97
108
|
expect: "refused/not-newer",
|
|
109
|
+
reasonContains: "v0.11.0 already serves on this install",
|
|
98
110
|
},
|
|
99
111
|
{
|
|
100
112
|
name: "downgrade",
|
|
@@ -327,3 +339,80 @@ describe("assessUpgradeRequest — self-hosted (the host agent, #696 Phase 4c)",
|
|
|
327
339
|
).toBe("refused/contract-migrations");
|
|
328
340
|
});
|
|
329
341
|
});
|
|
342
|
+
|
|
343
|
+
describe("assessUpgradeRequest — the converge route (#819)", () => {
|
|
344
|
+
const assess = (row: {
|
|
345
|
+
manifest?: ReleaseManifestResult;
|
|
346
|
+
installed?: InstalledState;
|
|
347
|
+
trigger?: "manual" | "auto";
|
|
348
|
+
policy?: Partial<StoredUpdatePolicy>;
|
|
349
|
+
}) =>
|
|
350
|
+
assessUpgradeRequest({
|
|
351
|
+
request: { targetVersion: "0.11.0", trigger: row.trigger ?? "manual" },
|
|
352
|
+
manifest: row.manifest ?? verified({ infraContractVersion: 3 }),
|
|
353
|
+
installed: row.installed ?? INSTALLED,
|
|
354
|
+
policy: { ...POLICY, ...row.policy },
|
|
355
|
+
now: NOW,
|
|
356
|
+
route: "converge",
|
|
357
|
+
});
|
|
358
|
+
const code = (d: ReturnType<typeof assess>) =>
|
|
359
|
+
d.kind === "stop" ? `${d.status}/${d.codes.join(",")}` : d.kind;
|
|
360
|
+
|
|
361
|
+
it("installs a release that raises the infrastructure contract: that is its job", () => {
|
|
362
|
+
expect(code(assess({}))).toBe("proceed");
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it("the contract bump is no reason to warn about destructive migrations", () => {
|
|
366
|
+
const d = assess({});
|
|
367
|
+
expect(d.kind === "proceed" ? d.warnings : ["stopped"]).toEqual([]);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("converges automatically only with the Owner's autoConverge (#864), default off", () => {
|
|
371
|
+
expect(code(assess({ trigger: "auto" }))).toBe("refused/policy");
|
|
372
|
+
const off = assess({ trigger: "auto" });
|
|
373
|
+
expect(off.kind === "stop" ? off.reason : "").toContain("Settings → Updates");
|
|
374
|
+
expect(code(assess({ trigger: "auto", policy: { autoConverge: true } }))).toBe("proceed");
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it("autoConverge never widens the rest of the policy: kinds, mode and pause still decide", () => {
|
|
378
|
+
const on = { autoConverge: true };
|
|
379
|
+
// 0.10.45 → 0.11.0 is a minor release.
|
|
380
|
+
expect(code(assess({ trigger: "auto", policy: { ...on, autoMinor: false } }))).toBe(
|
|
381
|
+
"refused/policy",
|
|
382
|
+
);
|
|
383
|
+
expect(code(assess({ trigger: "auto", policy: { ...on, mode: "notify" } }))).toBe(
|
|
384
|
+
"refused/policy",
|
|
385
|
+
);
|
|
386
|
+
expect(code(assess({ trigger: "auto", policy: { ...on, paused: true } }))).toBe(
|
|
387
|
+
"refused/policy",
|
|
388
|
+
);
|
|
389
|
+
// The Owner's click needs no autoConverge.
|
|
390
|
+
expect(code(assess({ trigger: "manual" }))).toBe("proceed");
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it("keeps every other refusal: signature, downgrade, minFromVersion, unknown version", () => {
|
|
394
|
+
expect(code(assess({ manifest: broken("signature") }))).toBe("refused/signature");
|
|
395
|
+
expect(code(assess({ manifest: broken("blob-digest") }))).toBe("refused/signature");
|
|
396
|
+
expect(code(assess({ installed: { version: "0.11.0", infraContractVersion: 2 } }))).toBe(
|
|
397
|
+
"refused/not-newer",
|
|
398
|
+
);
|
|
399
|
+
expect(
|
|
400
|
+
code(assess({ manifest: verified({ infraContractVersion: 3, minFromVersion: "0.10.50" }) })),
|
|
401
|
+
).toBe("refused/min-from-version");
|
|
402
|
+
expect(code(assess({ installed: { version: null, infraContractVersion: 2 } }))).toBe(
|
|
403
|
+
"needs_cloud_shell/install-version-unknown",
|
|
404
|
+
);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it("the upgrader's own route still hands the contract bump on, naming the converge job first", () => {
|
|
408
|
+
const d = assessUpgradeRequest({
|
|
409
|
+
request: { targetVersion: "0.11.0", trigger: "manual" },
|
|
410
|
+
manifest: verified({ infraContractVersion: 3 }),
|
|
411
|
+
installed: INSTALLED,
|
|
412
|
+
policy: POLICY,
|
|
413
|
+
now: NOW,
|
|
414
|
+
});
|
|
415
|
+
expect(code(d)).toBe("needs_cloud_shell/infra-contract");
|
|
416
|
+
expect(d.kind === "stop" ? d.reason : "").toContain("converge job");
|
|
417
|
+
});
|
|
418
|
+
});
|