@caelo-cms/provisioning 0.10.47 → 0.10.49
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/caddy/Caddyfile.production +1 -1
- package/caddy/Caddyfile.staging +1 -1
- package/dist/agent/apply.d.ts +45 -0
- package/dist/agent/apply.d.ts.map +1 -0
- package/dist/agent/apply.js +83 -0
- package/dist/agent/apply.js.map +1 -0
- package/dist/agent/cli-provenance.d.ts +42 -0
- package/dist/agent/cli-provenance.d.ts.map +1 -0
- package/dist/agent/cli-provenance.js +138 -0
- package/dist/agent/cli-provenance.js.map +1 -0
- package/dist/agent/command.d.ts +79 -0
- package/dist/agent/command.d.ts.map +1 -0
- package/dist/agent/command.js +454 -0
- package/dist/agent/command.js.map +1 -0
- package/dist/agent/config.d.ts +30 -0
- package/dist/agent/config.d.ts.map +1 -0
- package/dist/agent/config.js +60 -0
- package/dist/agent/config.js.map +1 -0
- package/dist/agent/lock.d.ts +13 -0
- package/dist/agent/lock.d.ts.map +1 -0
- package/dist/agent/lock.js +100 -0
- package/dist/agent/lock.js.map +1 -0
- package/dist/agent/queue.d.ts +35 -0
- package/dist/agent/queue.d.ts.map +1 -0
- package/dist/agent/queue.js +50 -0
- package/dist/agent/queue.js.map +1 -0
- package/dist/agent/run.d.ts +97 -0
- package/dist/agent/run.d.ts.map +1 -0
- package/dist/agent/run.js +198 -0
- package/dist/agent/run.js.map +1 -0
- package/dist/agent/systemd.d.ts +55 -0
- package/dist/agent/systemd.d.ts.map +1 -0
- package/dist/agent/systemd.js +171 -0
- package/dist/agent/systemd.js.map +1 -0
- package/dist/agent/units.d.ts +44 -0
- package/dist/agent/units.d.ts.map +1 -0
- package/dist/agent/units.js +127 -0
- package/dist/agent/units.js.map +1 -0
- package/dist/agent-db.js +18402 -0
- package/dist/cli.js +119 -16
- package/dist/cli.js.map +1 -1
- package/dist/cloudsql-iam-user.d.ts +39 -0
- package/dist/cloudsql-iam-user.d.ts.map +1 -0
- package/dist/cloudsql-iam-user.js +119 -0
- package/dist/cloudsql-iam-user.js.map +1 -0
- package/dist/compose.d.ts +16 -0
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +26 -1
- package/dist/compose.js.map +1 -1
- package/dist/dns/index.d.ts +2 -0
- package/dist/dns/index.d.ts.map +1 -1
- package/dist/dns/index.js +3 -2
- package/dist/dns/index.js.map +1 -1
- package/dist/dns/manual.d.ts +5 -0
- package/dist/dns/manual.d.ts.map +1 -1
- package/dist/dns/manual.js +28 -12
- package/dist/dns/manual.js.map +1 -1
- package/dist/gcloud.d.ts +2 -0
- package/dist/gcloud.d.ts.map +1 -1
- package/dist/gcloud.js +1 -1
- package/dist/gcloud.js.map +1 -1
- package/dist/gcp-names.d.ts +15 -0
- package/dist/gcp-names.d.ts.map +1 -1
- package/dist/gcp-names.js +23 -0
- package/dist/gcp-names.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/infra-contract.d.ts +1 -1
- package/dist/infra-contract.js +7 -1
- package/dist/infra-contract.js.map +1 -1
- package/dist/install-job-runner.d.ts +13 -0
- package/dist/install-job-runner.d.ts.map +1 -0
- package/dist/install-job-runner.js +155 -0
- package/dist/install-job-runner.js.map +1 -0
- package/dist/install-job.d.ts +210 -0
- package/dist/install-job.d.ts.map +1 -0
- package/dist/install-job.js +449 -0
- package/dist/install-job.js.map +1 -0
- package/dist/install-state.d.ts +10 -1
- package/dist/install-state.d.ts.map +1 -1
- package/dist/install-state.js +10 -2
- package/dist/install-state.js.map +1 -1
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +19 -4
- package/dist/lifecycle.js.map +1 -1
- package/dist/migration-runner.d.ts +5 -0
- package/dist/migration-runner.d.ts.map +1 -1
- package/dist/migration-runner.js +1 -1
- package/dist/migration-runner.js.map +1 -1
- package/dist/operator-access.d.ts +14 -8
- package/dist/operator-access.d.ts.map +1 -1
- package/dist/operator-access.js +16 -102
- package/dist/operator-access.js.map +1 -1
- package/dist/release-manifest.d.ts +2 -41
- package/dist/release-manifest.d.ts.map +1 -1
- package/dist/release-manifest.js +18 -121
- package/dist/release-manifest.js.map +1 -1
- package/dist/run-mode.d.ts +27 -0
- package/dist/run-mode.d.ts.map +1 -0
- package/dist/run-mode.js +22 -0
- package/dist/run-mode.js.map +1 -0
- package/dist/runtime-secrets.d.ts +19 -0
- package/dist/runtime-secrets.d.ts.map +1 -1
- package/dist/runtime-secrets.js +44 -0
- package/dist/runtime-secrets.js.map +1 -1
- package/dist/self-hosted-roles.d.ts +22 -1
- package/dist/self-hosted-roles.d.ts.map +1 -1
- package/dist/self-hosted-roles.js +81 -12
- package/dist/self-hosted-roles.js.map +1 -1
- package/dist/self-hosted-upgrade.d.ts +17 -0
- package/dist/self-hosted-upgrade.d.ts.map +1 -1
- package/dist/self-hosted-upgrade.js +16 -1
- package/dist/self-hosted-upgrade.js.map +1 -1
- package/dist/stack-contract.d.ts +6 -0
- package/dist/stack-contract.d.ts.map +1 -1
- package/dist/stack-contract.js +10 -1
- package/dist/stack-contract.js.map +1 -1
- package/dist/upgrade/converge.d.ts +6 -0
- package/dist/upgrade/converge.d.ts.map +1 -1
- package/dist/upgrade/converge.js +59 -2
- package/dist/upgrade/converge.js.map +1 -1
- package/dist/upgrade/gcp-lookup.d.ts +6 -0
- package/dist/upgrade/gcp-lookup.d.ts.map +1 -1
- package/dist/upgrade/gcp-lookup.js +25 -0
- package/dist/upgrade/gcp-lookup.js.map +1 -1
- package/dist/upgrade/release-images.d.ts +26 -2
- package/dist/upgrade/release-images.d.ts.map +1 -1
- package/dist/upgrade/release-images.js +35 -2
- package/dist/upgrade/release-images.js.map +1 -1
- package/dist/upgrade/upgrade.d.ts +5 -0
- package/dist/upgrade/upgrade.d.ts.map +1 -1
- package/dist/upgrade/upgrade.js +28 -1
- package/dist/upgrade/upgrade.js.map +1 -1
- package/dist/upgrader-assess.d.ts +85 -0
- package/dist/upgrader-assess.d.ts.map +1 -0
- package/dist/upgrader-assess.js +122 -0
- package/dist/upgrader-assess.js.map +1 -0
- package/dist/upgrader-gcp.d.ts +17 -0
- package/dist/upgrader-gcp.d.ts.map +1 -0
- package/dist/upgrader-gcp.js +87 -0
- package/dist/upgrader-gcp.js.map +1 -0
- package/dist/upgrader-job.d.ts +133 -0
- package/dist/upgrader-job.d.ts.map +1 -0
- package/dist/upgrader-job.js +259 -0
- package/dist/upgrader-job.js.map +1 -0
- package/dist/upgrader-reporter.d.ts +35 -0
- package/dist/upgrader-reporter.d.ts.map +1 -0
- package/dist/upgrader-reporter.js +79 -0
- package/dist/upgrader-reporter.js.map +1 -0
- package/dist/upgrader-setup.d.ts +59 -0
- package/dist/upgrader-setup.d.ts.map +1 -0
- package/dist/upgrader-setup.js +308 -0
- package/dist/upgrader-setup.js.map +1 -0
- package/dist/wizard.d.ts +6 -0
- package/dist/wizard.d.ts.map +1 -1
- package/dist/wizard.js +32 -4
- package/dist/wizard.js.map +1 -1
- package/dist/wizards/gcp-firebase.d.ts +3 -0
- package/dist/wizards/gcp-firebase.d.ts.map +1 -1
- package/dist/wizards/gcp-firebase.js +1 -0
- package/dist/wizards/gcp-firebase.js.map +1 -1
- package/dist/wizards/gcp-install-job.d.ts +74 -0
- package/dist/wizards/gcp-install-job.d.ts.map +1 -0
- package/dist/wizards/gcp-install-job.js +398 -0
- package/dist/wizards/gcp-install-job.js.map +1 -0
- package/dist/wizards/gcp-prompts.d.ts +3 -0
- package/dist/wizards/gcp-prompts.d.ts.map +1 -0
- package/dist/wizards/gcp-prompts.js +25 -0
- package/dist/wizards/gcp-prompts.js.map +1 -0
- package/dist/wizards/gcp-pulumi.d.ts +4 -2
- package/dist/wizards/gcp-pulumi.d.ts.map +1 -1
- package/dist/wizards/gcp-pulumi.js +1 -1
- package/dist/wizards/gcp-pulumi.js.map +1 -1
- package/dist/wizards/gcp.d.ts +40 -0
- package/dist/wizards/gcp.d.ts.map +1 -1
- package/dist/wizards/gcp.js +223 -59
- package/dist/wizards/gcp.js.map +1 -1
- package/package.json +6 -4
- package/src/adapter.ts +97 -0
- package/src/adc-impersonation.ts +176 -0
- package/src/admin-domain.test.ts +254 -0
- package/src/admin-domain.ts +245 -0
- package/src/admin-memory.test.ts +151 -0
- package/src/agent/apply.test.ts +185 -0
- package/src/agent/apply.ts +124 -0
- package/src/agent/cli-provenance.test.ts +159 -0
- package/src/agent/cli-provenance.ts +191 -0
- package/src/agent/command.test.ts +77 -0
- package/src/agent/command.ts +564 -0
- package/src/agent/config.ts +86 -0
- package/src/agent/lock-config.test.ts +97 -0
- package/src/agent/lock.ts +110 -0
- package/src/agent/queue.test.ts +10 -0
- package/src/agent/queue.ts +91 -0
- package/src/agent/run.test.ts +437 -0
- package/src/agent/run.ts +295 -0
- package/src/agent/systemd.ts +209 -0
- package/src/agent/units.test.ts +223 -0
- package/src/agent/units.ts +163 -0
- package/src/azure-front-door.test.ts +119 -0
- package/src/azure-front-door.ts +56 -0
- package/src/bootstrap-token.ts +20 -0
- package/src/caddy.ts +112 -0
- package/src/cdn-copy.ts +91 -0
- package/src/cert-wait.test.ts +194 -0
- package/src/cert-wait.ts +163 -0
- package/src/checkpoint-secrets.test.ts +380 -0
- package/src/checkpoint-secrets.ts +283 -0
- package/src/cli-routing.test.ts +45 -0
- package/src/cli-routing.ts +43 -0
- package/src/cli.ts +1491 -0
- package/src/cloudsql-iam-user.ts +147 -0
- package/src/compose.ts +334 -0
- package/src/default-firewall.test.ts +411 -0
- package/src/default-firewall.ts +342 -0
- package/src/deployed-release.test.ts +231 -0
- package/src/deployed-release.ts +220 -0
- package/src/dns/cloudflare.ts +182 -0
- package/src/dns/groups.test.ts +76 -0
- package/src/dns/groups.ts +86 -0
- package/src/dns/index.ts +56 -0
- package/src/dns/manual.ts +130 -0
- package/src/dns/types.ts +27 -0
- package/src/gateway-credentials.test.ts +180 -0
- package/src/gateway-credentials.ts +144 -0
- package/src/gcloud-components.test.ts +76 -0
- package/src/gcloud-components.ts +85 -0
- package/src/gcloud-retry.ts +41 -0
- package/src/gcloud-runner.test.ts +61 -0
- package/src/gcloud.test.ts +99 -0
- package/src/gcloud.ts +322 -0
- package/src/gcp-names.ts +135 -0
- package/src/index.test.ts +332 -0
- package/src/index.ts +114 -0
- package/src/infra-contract.ts +32 -0
- package/src/install-job-runner.ts +166 -0
- package/src/install-job.test.ts +566 -0
- package/src/install-job.ts +542 -0
- package/src/install-state.test.ts +239 -0
- package/src/install-state.ts +583 -0
- package/src/internal-token.ts +35 -0
- package/src/lifecycle.ts +900 -0
- package/src/mcp-iap-stacks.test.ts +58 -0
- package/src/mcp-iap.test.ts +149 -0
- package/src/mcp-iap.ts +131 -0
- package/src/media-volume.test.ts +296 -0
- package/src/migration-runner.ts +365 -0
- package/src/operator-access-stacks.test.ts +55 -0
- package/src/operator-access.test.ts +332 -0
- package/src/operator-access.ts +554 -0
- package/src/ops-backend.test.ts +320 -0
- package/src/ops-backend.ts +414 -0
- package/src/provisioner-identity.test.ts +482 -0
- package/src/provisioner-identity.ts +407 -0
- package/src/redirects-emit.ts +175 -0
- package/src/region-data.ts +428 -0
- package/src/regions.test.ts +277 -0
- package/src/regions.ts +370 -0
- package/src/release-contract.ts +57 -0
- package/src/release-manifest.test.ts +300 -0
- package/src/release-manifest.ts +310 -0
- package/src/run-mode.ts +40 -0
- package/src/runtime-secrets.test.ts +627 -0
- package/src/runtime-secrets.ts +754 -0
- package/src/self-command.test.ts +14 -0
- package/src/self-command.ts +21 -0
- package/src/self-hosted-compose.test.ts +177 -0
- package/src/self-hosted-release.ts +240 -0
- package/src/self-hosted-roles.integration.test.ts +161 -0
- package/src/self-hosted-roles.test.ts +124 -0
- package/src/self-hosted-roles.ts +203 -0
- package/src/self-hosted-upgrade.test.ts +394 -0
- package/src/self-hosted-upgrade.ts +548 -0
- package/src/stack-contract-parity.test.ts +199 -0
- package/src/stack-contract.test.ts +391 -0
- package/src/stack-contract.ts +888 -0
- package/src/stack-converge-secrets.test.ts +421 -0
- package/src/stack-converge.test.ts +833 -0
- package/src/stack-converge.ts +909 -0
- package/src/staging-admin.test.ts +246 -0
- package/src/staging-admin.ts +324 -0
- package/src/state-attach.test.ts +211 -0
- package/src/state-attach.ts +132 -0
- package/src/state-backend.test.ts +145 -0
- package/src/state-backend.ts +262 -0
- package/src/state-command.ts +293 -0
- package/src/state-migrate.test.ts +479 -0
- package/src/state-migrate.ts +434 -0
- package/src/state-pulumi.ts +289 -0
- package/src/state-store.test.ts +212 -0
- package/src/state-store.ts +243 -0
- package/src/upgrade/app-roll.ts +486 -0
- package/src/upgrade/backup.ts +109 -0
- package/src/upgrade/converge.ts +437 -0
- package/src/upgrade/gcp-lookup.ts +167 -0
- package/src/upgrade/health.test.ts +279 -0
- package/src/upgrade/health.ts +305 -0
- package/src/upgrade/release-images.test.ts +119 -0
- package/src/upgrade/release-images.ts +393 -0
- package/src/upgrade/reporter.ts +46 -0
- package/src/upgrade/upgrade.test.ts +626 -0
- package/src/upgrade/upgrade.ts +249 -0
- package/src/upgrader-assess.test.ts +306 -0
- package/src/upgrader-assess.ts +250 -0
- package/src/upgrader-gcp.ts +123 -0
- package/src/upgrader-job.test.ts +411 -0
- package/src/upgrader-job.ts +502 -0
- package/src/upgrader-reporter.test.ts +46 -0
- package/src/upgrader-reporter.ts +105 -0
- package/src/upgrader-setup.test.ts +169 -0
- package/src/upgrader-setup.ts +355 -0
- package/src/wizard.ts +425 -0
- package/src/wizards/gcp-cost.ts +188 -0
- package/src/wizards/gcp-firebase.ts +72 -0
- package/src/wizards/gcp-install-job.ts +506 -0
- package/src/wizards/gcp-prompts.ts +26 -0
- package/src/wizards/gcp-pulumi.test.ts +246 -0
- package/src/wizards/gcp-pulumi.ts +516 -0
- package/src/wizards/gcp.ts +1935 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The upgrader job (upgrader-job.ts) against fakes: an in-memory request
|
|
5
|
+
* queue, a fake gcloud for a healthy gcp-firebase install, a fake signed
|
|
6
|
+
* manifest and health probe. Checks the order (backup before migrations,
|
|
7
|
+
* self-update last), what lands on the timeline, and how each failure ends
|
|
8
|
+
* the request.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, expect, it } from "bun:test";
|
|
12
|
+
import type { StoredUpdatePolicy } from "@caelo-cms/shared";
|
|
13
|
+
import type { GcloudResult } from "./gcloud.js";
|
|
14
|
+
import type { GcloudRunner } from "./gcloud-retry.js";
|
|
15
|
+
import type { InstallMetadata } from "./install-state.js";
|
|
16
|
+
import type { ReleaseManifest, ReleaseManifestResult } from "./release-manifest.js";
|
|
17
|
+
import type { HealthOutcome, HealthProbe } from "./upgrade/health.js";
|
|
18
|
+
import {
|
|
19
|
+
type ClaimedUpdate,
|
|
20
|
+
runUpgraderJob,
|
|
21
|
+
type UpdateEnd,
|
|
22
|
+
type UpdateFacts,
|
|
23
|
+
type UpdateQueue,
|
|
24
|
+
type UpgraderJobDeps,
|
|
25
|
+
} from "./upgrader-job.js";
|
|
26
|
+
|
|
27
|
+
const ok = (stdout = ""): GcloudResult => ({ ok: true, stdout, stderr: "", exitCode: 0 });
|
|
28
|
+
const fail = (stderr: string): GcloudResult => ({ ok: false, stdout: "", stderr, exitCode: 1 });
|
|
29
|
+
const D = (c: string) => `sha256:${c.repeat(64)}`;
|
|
30
|
+
|
|
31
|
+
const ADMIN = "caelo-production-admin-aaa";
|
|
32
|
+
const GATEWAY = "caelo-production-gateway-bbb";
|
|
33
|
+
|
|
34
|
+
const MANIFEST: ReleaseManifest = {
|
|
35
|
+
manifestVersion: 1,
|
|
36
|
+
version: "0.11.0",
|
|
37
|
+
channel: "stable",
|
|
38
|
+
digests: { admin: D("a"), gateway: D("b"), ops: D("c") },
|
|
39
|
+
minFromVersion: "0.10.0",
|
|
40
|
+
infraContractVersion: 2,
|
|
41
|
+
migrations: { contract: false, lastContractVersion: null, head: "0267_platform_updates" },
|
|
42
|
+
security: true,
|
|
43
|
+
advisories: ["GHSA-abcd-efgh-ijkm"],
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const POLICY: StoredUpdatePolicy = {
|
|
47
|
+
mode: "notify",
|
|
48
|
+
autoSecurity: false,
|
|
49
|
+
autoPatch: false,
|
|
50
|
+
autoMinor: false,
|
|
51
|
+
channel: "stable",
|
|
52
|
+
windowStart: null,
|
|
53
|
+
windowEnd: null,
|
|
54
|
+
windowTimezone: "UTC",
|
|
55
|
+
soakHours: 24,
|
|
56
|
+
securitySoakHours: 2,
|
|
57
|
+
paused: false,
|
|
58
|
+
pausedReason: null,
|
|
59
|
+
pausedAt: null,
|
|
60
|
+
pausedUntil: null,
|
|
61
|
+
pausedByOwner: false,
|
|
62
|
+
updatedAt: "2026-10-13T00:00:00Z",
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const META: InstallMetadata = {
|
|
66
|
+
installId: "gcp-firebase-acme",
|
|
67
|
+
provider: "gcp-firebase",
|
|
68
|
+
projectId: "acme",
|
|
69
|
+
domain: "acme.com",
|
|
70
|
+
ownerEmail: "owner@acme.com",
|
|
71
|
+
region: "europe-west1",
|
|
72
|
+
createdAt: "2026-01-01T00:00:00Z",
|
|
73
|
+
imageDigests: { admin: D("1"), gateway: D("2") },
|
|
74
|
+
releaseVersion: "0.10.45",
|
|
75
|
+
infraContractVersion: 2,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const serviceJson = (image: string, env: unknown[]) =>
|
|
79
|
+
JSON.stringify({ spec: { template: { spec: { containers: [{ image, env }] } } } });
|
|
80
|
+
const secretRef = (name: string, secret: string) => ({
|
|
81
|
+
name,
|
|
82
|
+
valueFrom: { secretKeyRef: { name: `caelo-production-${secret}`, key: "latest" } },
|
|
83
|
+
});
|
|
84
|
+
const ADMIN_JSON = serviceJson("registry/admin@sha256:old", [
|
|
85
|
+
{ name: "CAELO_FIREBASE_SITE", value: "caelo-production-site-abc123" },
|
|
86
|
+
{
|
|
87
|
+
name: "ADMIN_DATABASE_URL",
|
|
88
|
+
value: "postgresql://admin_role@10.20.0.3:5432/cms_admin?sslmode=require",
|
|
89
|
+
},
|
|
90
|
+
secretRef("ADMIN_DATABASE_PASSWORD", "postgres-password"),
|
|
91
|
+
]);
|
|
92
|
+
const GATEWAY_JSON = serviceJson("registry/gateway@sha256:old", []);
|
|
93
|
+
|
|
94
|
+
type Answer = GcloudResult | ((line: string) => GcloudResult);
|
|
95
|
+
|
|
96
|
+
function fakeGcloud(events: string[], overrides: [RegExp, Answer][] = []) {
|
|
97
|
+
const rules: [RegExp, Answer][] = [
|
|
98
|
+
...overrides,
|
|
99
|
+
[/^run services list .*caelo-production-admin/, ok(`${ADMIN}\n`)],
|
|
100
|
+
[/^run services list .*caelo-production-gateway/, ok(`${GATEWAY}\n`)],
|
|
101
|
+
[/describe (\S+) .*revisionName/, (l) => ok(`${l.split(" ")[3]}-00001-old\n`)],
|
|
102
|
+
[new RegExp(`describe ${ADMIN} .*--format=json`), ok(ADMIN_JSON)],
|
|
103
|
+
[new RegExp(`describe ${GATEWAY} .*--format=json`), ok(GATEWAY_JSON)],
|
|
104
|
+
[/describe (\S+) .*status\.url/, (l) => ok(`https://${l.split(" ")[3]}-xyz.a.run.app\n`)],
|
|
105
|
+
[
|
|
106
|
+
/^run revisions describe (\S+)/,
|
|
107
|
+
(l) => ok(`registry/x@${l.includes("-admin-") ? D("1") : D("2")}\n`),
|
|
108
|
+
],
|
|
109
|
+
[/^sql instances list/, ok("caelo-production-pg-abc\n")],
|
|
110
|
+
[/^sql backups list/, ok("1760000000000\n")],
|
|
111
|
+
];
|
|
112
|
+
const run: GcloudRunner = async (args) => {
|
|
113
|
+
const line = args.join(" ");
|
|
114
|
+
events.push(`gcloud ${line}`);
|
|
115
|
+
const rule = rules.find(([re]) => re.test(line));
|
|
116
|
+
if (!rule) return ok("{}");
|
|
117
|
+
return typeof rule[1] === "function" ? rule[1](line) : rule[1];
|
|
118
|
+
};
|
|
119
|
+
return run;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** An in-memory queue holding one pending request. */
|
|
123
|
+
function memoryQueue(request: ClaimedUpdate | null) {
|
|
124
|
+
const events: { phase: string; level: string; message: string }[] = [];
|
|
125
|
+
const facts: UpdateFacts[] = [];
|
|
126
|
+
let end: UpdateEnd | null = null;
|
|
127
|
+
const queue: UpdateQueue = {
|
|
128
|
+
claim: async () => request,
|
|
129
|
+
event: async (_id, phase, level, message) => {
|
|
130
|
+
events.push({ phase, level, message });
|
|
131
|
+
},
|
|
132
|
+
record: async (_id, f) => {
|
|
133
|
+
facts.push(f);
|
|
134
|
+
},
|
|
135
|
+
finish: async (_id, e) => {
|
|
136
|
+
end = e;
|
|
137
|
+
return true;
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
return { queue, events, facts, end: () => end };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function healthProbe(order: string[], deep: HealthOutcome): HealthProbe {
|
|
144
|
+
return {
|
|
145
|
+
baseline: async () => {
|
|
146
|
+
order.push("health baseline");
|
|
147
|
+
return { ok: true, detail: "reachable" };
|
|
148
|
+
},
|
|
149
|
+
deep: async () => {
|
|
150
|
+
order.push("health deep");
|
|
151
|
+
return deep;
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function setup(
|
|
157
|
+
opts: {
|
|
158
|
+
request?: Partial<ClaimedUpdate>;
|
|
159
|
+
manifest?: ReleaseManifestResult;
|
|
160
|
+
meta?: InstallMetadata | null;
|
|
161
|
+
gcloud?: [RegExp, Answer][];
|
|
162
|
+
deep?: HealthOutcome;
|
|
163
|
+
migrate?: { ok: boolean; error?: string };
|
|
164
|
+
locked?: boolean;
|
|
165
|
+
} = {},
|
|
166
|
+
) {
|
|
167
|
+
const order: string[] = [];
|
|
168
|
+
const request: ClaimedUpdate = {
|
|
169
|
+
id: "req-1",
|
|
170
|
+
targetVersion: "0.11.0",
|
|
171
|
+
trigger: "manual",
|
|
172
|
+
policy: POLICY,
|
|
173
|
+
...opts.request,
|
|
174
|
+
};
|
|
175
|
+
const q = memoryQueue(request);
|
|
176
|
+
const written: InstallMetadata[] = [];
|
|
177
|
+
const deps: UpgraderJobDeps = {
|
|
178
|
+
queue: q.queue,
|
|
179
|
+
run: fakeGcloud(order, opts.gcloud),
|
|
180
|
+
sleep: async () => {},
|
|
181
|
+
projectId: "acme",
|
|
182
|
+
region: "europe-west1",
|
|
183
|
+
env: "production",
|
|
184
|
+
execution: "caelo-production-upgrader-x1",
|
|
185
|
+
fetchManifest: async (v) => {
|
|
186
|
+
order.push(`manifest ${v}`);
|
|
187
|
+
return opts.manifest ?? { ok: true, manifest: MANIFEST };
|
|
188
|
+
},
|
|
189
|
+
lockState: () => {
|
|
190
|
+
order.push("lock");
|
|
191
|
+
if (opts.locked) throw new Error("held by caelo installer");
|
|
192
|
+
return () => order.push("unlock");
|
|
193
|
+
},
|
|
194
|
+
readInstall: () => (opts.meta === undefined ? META : opts.meta),
|
|
195
|
+
writeInstall: (m) => {
|
|
196
|
+
written.push(m);
|
|
197
|
+
},
|
|
198
|
+
migrate: async (imageRef) => {
|
|
199
|
+
order.push(`migrate ${imageRef}`);
|
|
200
|
+
return opts.migrate ?? { ok: true };
|
|
201
|
+
},
|
|
202
|
+
healthProbe: async () => ({
|
|
203
|
+
ok: true,
|
|
204
|
+
probe: healthProbe(order, opts.deep ?? { ok: true, detail: "fine" }),
|
|
205
|
+
}),
|
|
206
|
+
now: () => new Date("2026-10-13T12:00:00Z"),
|
|
207
|
+
log: () => {},
|
|
208
|
+
};
|
|
209
|
+
return { deps, order, q, written };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const step = (order: string[], re: RegExp) => order.findIndex((l) => re.test(l));
|
|
213
|
+
|
|
214
|
+
describe("runUpgraderJob", () => {
|
|
215
|
+
it("does nothing without a pending request", async () => {
|
|
216
|
+
const { deps } = setup();
|
|
217
|
+
const r = await runUpgraderJob({ ...deps, queue: memoryQueue(null).queue });
|
|
218
|
+
expect(r).toEqual({ exitCode: 0, requestId: null, status: null });
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("happy path: verify, backup, migrate, roll, health, record, self-update — in that order", async () => {
|
|
222
|
+
const { deps, order, q, written } = setup();
|
|
223
|
+
const r = await runUpgraderJob(deps);
|
|
224
|
+
expect(r).toEqual({ exitCode: 0, requestId: "req-1", status: "succeeded" });
|
|
225
|
+
const manifest = step(order, /^manifest 0\.11\.0/);
|
|
226
|
+
const backup = step(order, /^gcloud sql backups create/);
|
|
227
|
+
const migrate = step(order, /^migrate .*admin@sha256:a{64}/);
|
|
228
|
+
const roll = step(order, /^gcloud run services update caelo-production-admin-aaa/);
|
|
229
|
+
const deep = step(order, /^health deep/);
|
|
230
|
+
const self = step(
|
|
231
|
+
order,
|
|
232
|
+
/^gcloud run jobs update caelo-production-upgrader .*ops@sha256:c{64}/,
|
|
233
|
+
);
|
|
234
|
+
expect([manifest, backup, migrate, roll, deep, self].every((i) => i >= 0)).toBe(true);
|
|
235
|
+
expect(
|
|
236
|
+
manifest < backup && backup < migrate && migrate < roll && roll < deep && deep < self,
|
|
237
|
+
).toBe(true);
|
|
238
|
+
// Images come from the signed manifest, pinned by digest, from the fixed mirror.
|
|
239
|
+
expect(
|
|
240
|
+
order.some((l) =>
|
|
241
|
+
l.includes(`europe-west1-docker.pkg.dev/caelo-website/caelo-cms-images/admin@${D("a")}`),
|
|
242
|
+
),
|
|
243
|
+
).toBe(true);
|
|
244
|
+
expect(written.at(-1)).toMatchObject({
|
|
245
|
+
releaseVersion: "0.11.0",
|
|
246
|
+
imageDigests: { admin: D("a"), gateway: D("b") },
|
|
247
|
+
});
|
|
248
|
+
expect(q.facts).toContainEqual(
|
|
249
|
+
expect.objectContaining({ manifestSecurity: true, opsDigest: D("c") }),
|
|
250
|
+
);
|
|
251
|
+
expect(q.facts).toContainEqual({ backupId: "1760000000000" });
|
|
252
|
+
expect(q.end()).toEqual({
|
|
253
|
+
status: "succeeded",
|
|
254
|
+
reason: null,
|
|
255
|
+
codes: [],
|
|
256
|
+
pauseAutoUpdates: false,
|
|
257
|
+
});
|
|
258
|
+
const phases = [...new Set(q.events.map((e) => e.phase))];
|
|
259
|
+
expect(phases).toEqual([
|
|
260
|
+
"start",
|
|
261
|
+
"verify",
|
|
262
|
+
"assess",
|
|
263
|
+
"backup",
|
|
264
|
+
"health-baseline",
|
|
265
|
+
"migrations",
|
|
266
|
+
"roll",
|
|
267
|
+
"health",
|
|
268
|
+
"record",
|
|
269
|
+
"self-update",
|
|
270
|
+
"finish",
|
|
271
|
+
]);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("holds the install's state lock for the whole run, and stops when another run holds it", async () => {
|
|
275
|
+
const ok = setup();
|
|
276
|
+
await runUpgraderJob(ok.deps);
|
|
277
|
+
expect(ok.order[0]).toBe("lock");
|
|
278
|
+
expect(ok.order.indexOf("unlock")).toBeGreaterThan(
|
|
279
|
+
step(ok.order, /jobs update caelo-production-upgrader/),
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
const busy = setup({ locked: true });
|
|
283
|
+
const r = await runUpgraderJob(busy.deps);
|
|
284
|
+
expect(r.status).toBe("failed");
|
|
285
|
+
expect(busy.q.end()?.codes).toEqual(["state-locked"]);
|
|
286
|
+
expect(busy.order.some((l) => l.startsWith("manifest") || l.startsWith("gcloud"))).toBe(false);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("retries recording the end, and fails the run when it cannot", async () => {
|
|
290
|
+
const { deps, q } = setup();
|
|
291
|
+
let calls = 0;
|
|
292
|
+
const flaky: UpdateQueue = {
|
|
293
|
+
...q.queue,
|
|
294
|
+
finish: async (id, end) => {
|
|
295
|
+
calls += 1;
|
|
296
|
+
if (calls < 3) throw new Error("connection reset");
|
|
297
|
+
return q.queue.finish(id, end);
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
expect(await runUpgraderJob({ ...deps, queue: flaky })).toMatchObject({
|
|
301
|
+
exitCode: 0,
|
|
302
|
+
status: "succeeded",
|
|
303
|
+
});
|
|
304
|
+
expect(calls).toBe(3);
|
|
305
|
+
|
|
306
|
+
const dead: UpdateQueue = {
|
|
307
|
+
...q.queue,
|
|
308
|
+
finish: async () => {
|
|
309
|
+
throw new Error("database gone");
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
expect((await runUpgraderJob({ ...deps, queue: dead })).exitCode).toBe(1);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("an infrastructure change goes to Cloud Shell before anything changes", async () => {
|
|
316
|
+
const { deps, order, q } = setup({ meta: { ...META, infraContractVersion: 1 } });
|
|
317
|
+
const r = await runUpgraderJob(deps);
|
|
318
|
+
expect(r.status).toBe("needs_cloud_shell");
|
|
319
|
+
expect(q.end()?.codes).toEqual(["infra-contract"]);
|
|
320
|
+
expect(order.some((l) => /backups create|services update|^migrate/.test(l))).toBe(false);
|
|
321
|
+
expect(q.facts[0]).toMatchObject({ infraContractRequired: 2, infraContractInstalled: 1 });
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("a bad signature is refused and changes nothing", async () => {
|
|
325
|
+
const { deps, order, q } = setup({
|
|
326
|
+
manifest: { ok: false, error: { code: "signature", message: "cert identity mismatch" } },
|
|
327
|
+
});
|
|
328
|
+
expect((await runUpgraderJob(deps)).status).toBe("refused");
|
|
329
|
+
expect(q.end()?.codes).toEqual(["signature"]);
|
|
330
|
+
// Only read-only lookups ran.
|
|
331
|
+
expect(order.filter((l) => l.startsWith("gcloud") && !/ (list|describe) /.test(l))).toEqual([]);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("install state outside the install's own cloud needs Cloud Shell", async () => {
|
|
335
|
+
const { deps, q } = setup({ meta: null });
|
|
336
|
+
expect((await runUpgraderJob(deps)).status).toBe("needs_cloud_shell");
|
|
337
|
+
expect(q.end()?.codes).toEqual(["install-state"]);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it("refuses when the serving release is not the recorded one (a lost install.json write)", async () => {
|
|
341
|
+
const { deps, order, q } = setup({
|
|
342
|
+
meta: { ...META, imageDigests: { admin: D("9"), gateway: D("2") } },
|
|
343
|
+
});
|
|
344
|
+
expect((await runUpgraderJob(deps)).status).toBe("needs_cloud_shell");
|
|
345
|
+
expect(q.end()?.codes).toEqual(["install-state-drift"]);
|
|
346
|
+
expect(order.some((l) => l.startsWith("manifest"))).toBe(false);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it("a failed install.json write still ends succeeded, and the next run catches the drift", async () => {
|
|
350
|
+
const { deps, q } = setup();
|
|
351
|
+
const r = await runUpgraderJob({
|
|
352
|
+
...deps,
|
|
353
|
+
writeInstall: () => {
|
|
354
|
+
throw new Error("403 on the ops bucket");
|
|
355
|
+
},
|
|
356
|
+
});
|
|
357
|
+
expect(r.status).toBe("succeeded");
|
|
358
|
+
expect(q.events.some((e) => e.phase === "record" && e.level === "warn")).toBe(true);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("a failed backup stops before migrations", async () => {
|
|
362
|
+
const { deps, order, q } = setup({ gcloud: [[/^sql backups create/, fail("quota")]] });
|
|
363
|
+
const r = await runUpgraderJob(deps);
|
|
364
|
+
expect(r).toMatchObject({ exitCode: 1, status: "failed" });
|
|
365
|
+
expect(q.end()?.codes).toEqual(["backup"]);
|
|
366
|
+
expect(order.some((l) => l.startsWith("migrate"))).toBe(false);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it("failed migrations: failed, previous release keeps serving, backup named", async () => {
|
|
370
|
+
const { deps, order, q } = setup({ migrate: { ok: false, error: "boom" } });
|
|
371
|
+
const r = await runUpgraderJob(deps);
|
|
372
|
+
expect(r.status).toBe("failed");
|
|
373
|
+
expect(q.end()?.reason).toContain("1760000000000");
|
|
374
|
+
expect(order.some((l) => /services update caelo-production-admin/.test(l))).toBe(false);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it("an unhealthy release is rolled back; an automatic one pauses auto updates", async () => {
|
|
378
|
+
const { deps, order, q } = setup({
|
|
379
|
+
deep: { ok: false, error: "version mismatch" },
|
|
380
|
+
request: { trigger: "auto", policy: { ...POLICY, mode: "auto", autoSecurity: true } },
|
|
381
|
+
});
|
|
382
|
+
const r = await runUpgraderJob(deps);
|
|
383
|
+
expect(r).toMatchObject({ exitCode: 1, status: "rolled_back" });
|
|
384
|
+
expect(q.end()).toMatchObject({ status: "rolled_back", pauseAutoUpdates: true });
|
|
385
|
+
expect(
|
|
386
|
+
order.some((l) => /update-traffic caelo-production-admin-aaa .*--to-revisions/.test(l)),
|
|
387
|
+
).toBe(true);
|
|
388
|
+
expect(order.some((l) => /jobs update caelo-production-upgrader/.test(l))).toBe(false);
|
|
389
|
+
expect(q.events.some((e) => e.phase === "rollback")).toBe(true);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it("a failed self-update only warns", async () => {
|
|
393
|
+
const { deps, q } = setup({
|
|
394
|
+
gcloud: [[/^run jobs update caelo-production-upgrader/, fail("denied")]],
|
|
395
|
+
});
|
|
396
|
+
expect((await runUpgraderJob(deps)).status).toBe("succeeded");
|
|
397
|
+
expect(q.events.some((e) => e.phase === "self-update" && e.level === "warn")).toBe(true);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("an unexpected throw ends the request as failed", async () => {
|
|
401
|
+
const { deps, q } = setup();
|
|
402
|
+
const r = await runUpgraderJob({
|
|
403
|
+
...deps,
|
|
404
|
+
fetchManifest: async () => {
|
|
405
|
+
throw new Error("kaboom");
|
|
406
|
+
},
|
|
407
|
+
});
|
|
408
|
+
expect(r.status).toBe("failed");
|
|
409
|
+
expect(q.end()?.reason).toContain("kaboom");
|
|
410
|
+
});
|
|
411
|
+
});
|