@caelo-cms/provisioning 0.10.46 → 0.10.48
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/adc-impersonation.d.ts.map +1 -1
- package/dist/adc-impersonation.js +23 -1
- package/dist/adc-impersonation.js.map +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 +18362 -0
- package/dist/cli.js +122 -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 +19 -1
- package/dist/install-state.d.ts.map +1 -1
- package/dist/install-state.js +31 -3
- package/dist/install-state.js.map +1 -1
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +24 -5
- 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/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/state-attach.d.ts +23 -0
- package/dist/state-attach.d.ts.map +1 -0
- package/dist/state-attach.js +112 -0
- package/dist/state-attach.js.map +1 -0
- package/dist/state-backend.d.ts +11 -0
- package/dist/state-backend.d.ts.map +1 -1
- package/dist/state-backend.js +20 -0
- package/dist/state-backend.js.map +1 -1
- package/dist/state-command.d.ts +20 -0
- package/dist/state-command.d.ts.map +1 -0
- package/dist/state-command.js +237 -0
- package/dist/state-command.js.map +1 -0
- package/dist/state-migrate.d.ts +121 -0
- package/dist/state-migrate.d.ts.map +1 -0
- package/dist/state-migrate.js +290 -0
- package/dist/state-migrate.js.map +1 -0
- package/dist/state-pulumi.d.ts +17 -0
- package/dist/state-pulumi.d.ts.map +1 -0
- package/dist/state-pulumi.js +218 -0
- package/dist/state-pulumi.js.map +1 -0
- package/dist/state-store.d.ts +27 -0
- package/dist/state-store.d.ts.map +1 -1
- package/dist/state-store.js +65 -0
- package/dist/state-store.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 +12 -0
- package/dist/upgrade/release-images.d.ts.map +1 -1
- package/dist/upgrade/release-images.js +31 -0
- 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 +82 -0
- package/dist/upgrader-assess.d.ts.map +1 -0
- package/dist/upgrader-assess.js +116 -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 +131 -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 +9 -2
- package/dist/wizards/gcp-pulumi.d.ts.map +1 -1
- package/dist/wizards/gcp-pulumi.js +8 -6
- 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 +291 -83
- 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 +382 -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 +293 -0
- package/src/release-manifest.ts +430 -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 +391 -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 +265 -0
- package/src/upgrader-assess.ts +233 -0
- package/src/upgrader-gcp.ts +123 -0
- package/src/upgrader-job.test.ts +408 -0
- package/src/upgrader-job.ts +499 -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,382 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* One poll of the self-hosted host agent (agent/run.ts) against fakes:
|
|
5
|
+
* heartbeat, claim, the refusal matrix (nothing of the target runs before
|
|
6
|
+
* its signed manifest passed), the hand-over to the target release's CLI,
|
|
7
|
+
* and what happens when that process records nothing.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, it } from "bun:test";
|
|
11
|
+
import type { StoredUpdatePolicy } from "@caelo-cms/shared";
|
|
12
|
+
import type { ReleaseManifest, ReleaseManifestResult } from "../release-manifest.js";
|
|
13
|
+
import type { SelfHostedRelease } from "../self-hosted-release.js";
|
|
14
|
+
import type { ClaimedUpdate, UpdateEnd, UpdateFacts } from "../upgrader-job.js";
|
|
15
|
+
import type { AgentHeartbeat, AgentQueue } from "./queue.js";
|
|
16
|
+
import { type AgentApplyRequest, type AgentRunDeps, runAgentOnce } from "./run.js";
|
|
17
|
+
|
|
18
|
+
const D = (c: string) => `sha256:${c.repeat(64)}`;
|
|
19
|
+
|
|
20
|
+
const MANIFEST: ReleaseManifest = {
|
|
21
|
+
manifestVersion: 1,
|
|
22
|
+
version: "0.11.0",
|
|
23
|
+
channel: "stable",
|
|
24
|
+
digests: { admin: D("a"), gateway: D("b"), ops: D("c") },
|
|
25
|
+
minFromVersion: "0.10.0",
|
|
26
|
+
// A raised cloud contract: irrelevant on self-hosted.
|
|
27
|
+
infraContractVersion: 7,
|
|
28
|
+
migrations: { contract: false, lastContractVersion: null, head: "0270_platform_update_agent" },
|
|
29
|
+
security: false,
|
|
30
|
+
advisories: [],
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const POLICY: StoredUpdatePolicy = {
|
|
34
|
+
mode: "notify",
|
|
35
|
+
autoSecurity: false,
|
|
36
|
+
autoPatch: false,
|
|
37
|
+
autoMinor: false,
|
|
38
|
+
channel: "stable",
|
|
39
|
+
windowStart: null,
|
|
40
|
+
windowEnd: null,
|
|
41
|
+
windowTimezone: "UTC",
|
|
42
|
+
soakHours: 24,
|
|
43
|
+
paused: false,
|
|
44
|
+
pausedReason: null,
|
|
45
|
+
pausedAt: null,
|
|
46
|
+
updatedAt: "2026-10-13T00:00:00Z",
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const REQUEST: ClaimedUpdate = {
|
|
50
|
+
id: "11111111-1111-4111-8111-111111111111",
|
|
51
|
+
targetVersion: "0.11.0",
|
|
52
|
+
trigger: "manual",
|
|
53
|
+
policy: POLICY,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const INSTALLED: SelfHostedRelease = {
|
|
57
|
+
version: "0.10.47",
|
|
58
|
+
images: {
|
|
59
|
+
admin: `ghcr.io/caelo-cms/admin@${D("1")}`,
|
|
60
|
+
gateway: `ghcr.io/caelo-cms/gateway@${D("2")}`,
|
|
61
|
+
},
|
|
62
|
+
installedAt: "2026-10-01T00:00:00Z",
|
|
63
|
+
history: [],
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
interface Memory {
|
|
67
|
+
queue: AgentQueue;
|
|
68
|
+
beats: AgentHeartbeat[];
|
|
69
|
+
claims: (string | null)[];
|
|
70
|
+
events: { phase: string; level: string; message: string }[];
|
|
71
|
+
facts: UpdateFacts[];
|
|
72
|
+
ends: UpdateEnd[];
|
|
73
|
+
/** Whether the request is still running (finish only ends a running one). */
|
|
74
|
+
running: boolean;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function memoryQueue(request: ClaimedUpdate | null, opts: { heartbeatFails?: boolean } = {}) {
|
|
78
|
+
const m: Memory = {
|
|
79
|
+
beats: [],
|
|
80
|
+
claims: [],
|
|
81
|
+
events: [],
|
|
82
|
+
facts: [],
|
|
83
|
+
ends: [],
|
|
84
|
+
running: false,
|
|
85
|
+
queue: undefined as unknown as AgentQueue,
|
|
86
|
+
};
|
|
87
|
+
m.queue = {
|
|
88
|
+
heartbeat: async (beat) => {
|
|
89
|
+
if (opts.heartbeatFails) throw new Error("password authentication failed");
|
|
90
|
+
m.beats.push(beat);
|
|
91
|
+
},
|
|
92
|
+
claim: async (execution) => {
|
|
93
|
+
m.claims.push(execution);
|
|
94
|
+
m.running = request !== null;
|
|
95
|
+
return request;
|
|
96
|
+
},
|
|
97
|
+
event: async (_id, phase, level, message) => {
|
|
98
|
+
m.events.push({ phase, level, message });
|
|
99
|
+
},
|
|
100
|
+
record: async (_id, facts) => {
|
|
101
|
+
m.facts.push(facts);
|
|
102
|
+
},
|
|
103
|
+
finish: async (_id, end) => {
|
|
104
|
+
if (!m.running) return false;
|
|
105
|
+
m.running = false;
|
|
106
|
+
m.ends.push(end);
|
|
107
|
+
return true;
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
return m;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function setup(over: Partial<AgentRunDeps> = {}, request: ClaimedUpdate | null = REQUEST) {
|
|
114
|
+
const q = memoryQueue(request);
|
|
115
|
+
const applied: AgentApplyRequest[] = [];
|
|
116
|
+
const locks = { taken: 0, released: 0 };
|
|
117
|
+
const deps: AgentRunDeps = {
|
|
118
|
+
queue: q.queue,
|
|
119
|
+
agentVersion: "0.10.47",
|
|
120
|
+
host: "box-1",
|
|
121
|
+
preflight: async () => null,
|
|
122
|
+
readInstalled: () => INSTALLED,
|
|
123
|
+
fetchManifest: async (): Promise<ReleaseManifestResult> => ({ ok: true, manifest: MANIFEST }),
|
|
124
|
+
lock: () => {
|
|
125
|
+
locks.taken += 1;
|
|
126
|
+
return () => {
|
|
127
|
+
locks.released += 1;
|
|
128
|
+
};
|
|
129
|
+
},
|
|
130
|
+
verifyTargetCli: async (version) => ({
|
|
131
|
+
ok: true,
|
|
132
|
+
dir: `/srv/.caelo/agent/cli/caelo-${version}`,
|
|
133
|
+
cliPath: `/srv/.caelo/agent/cli/caelo-${version}/node_modules/@caelo-cms/provisioning/dist/cli.js`,
|
|
134
|
+
}),
|
|
135
|
+
applyTarget: async (req) => {
|
|
136
|
+
applied.push(req);
|
|
137
|
+
// The target's CLI records the end itself.
|
|
138
|
+
await q.queue.finish(req.requestId, {
|
|
139
|
+
status: "succeeded",
|
|
140
|
+
reason: null,
|
|
141
|
+
codes: [],
|
|
142
|
+
pauseAutoUpdates: false,
|
|
143
|
+
});
|
|
144
|
+
return { exitCode: 0, output: "" };
|
|
145
|
+
},
|
|
146
|
+
now: () => new Date("2026-10-13T12:00:00Z"),
|
|
147
|
+
log: () => {},
|
|
148
|
+
sleep: async () => {},
|
|
149
|
+
...over,
|
|
150
|
+
};
|
|
151
|
+
return { deps, q, applied, locks };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
describe("runAgentOnce", () => {
|
|
155
|
+
it("checks in and does nothing more without a pending request", async () => {
|
|
156
|
+
const { deps, q, applied } = setup({ preflight: async () => "cosign is missing" }, null);
|
|
157
|
+
expect(await runAgentOnce(deps)).toEqual({ exitCode: 0, requestId: null, status: null });
|
|
158
|
+
expect(q.beats).toEqual([
|
|
159
|
+
{ agentVersion: "0.10.47", host: "box-1", problem: "cosign is missing" },
|
|
160
|
+
]);
|
|
161
|
+
expect(q.claims).toEqual(["agent@box-1"]);
|
|
162
|
+
expect(applied).toEqual([]);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("fails the poll (exit 1) when it cannot check in, and claims nothing", async () => {
|
|
166
|
+
const q = memoryQueue(REQUEST, { heartbeatFails: true });
|
|
167
|
+
const { deps } = setup({ queue: q.queue });
|
|
168
|
+
expect((await runAgentOnce(deps)).exitCode).toBe(1);
|
|
169
|
+
expect(q.claims).toEqual([]);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("verifies, decides, then hands the request to the target release's CLI", async () => {
|
|
173
|
+
const { deps, q, applied, locks } = setup();
|
|
174
|
+
const r = await runAgentOnce(deps);
|
|
175
|
+
expect(r).toEqual({ exitCode: 0, requestId: REQUEST.id, status: null });
|
|
176
|
+
expect(applied).toEqual([
|
|
177
|
+
{
|
|
178
|
+
requestId: REQUEST.id,
|
|
179
|
+
targetVersion: "0.11.0",
|
|
180
|
+
trigger: "manual",
|
|
181
|
+
digests: { admin: D("a"), gateway: D("b") },
|
|
182
|
+
cliPath:
|
|
183
|
+
"/srv/.caelo/agent/cli/caelo-0.11.0/node_modules/@caelo-cms/provisioning/dist/cli.js",
|
|
184
|
+
},
|
|
185
|
+
]);
|
|
186
|
+
expect(q.facts[0]).toMatchObject({ adminDigest: D("a"), manifestSecurity: false });
|
|
187
|
+
expect([...new Set(q.events.map((e) => e.phase))]).toEqual(["start", "verify", "assess"]);
|
|
188
|
+
expect(q.events.some((e) => /built by Caelo's release workflow/.test(e.message))).toBe(true);
|
|
189
|
+
expect(q.events.some((e) => /signed by Caelo's release workflow/.test(e.message))).toBe(true);
|
|
190
|
+
expect(q.ends).toEqual([
|
|
191
|
+
{ status: "succeeded", reason: null, codes: [], pauseAutoUpdates: false },
|
|
192
|
+
]);
|
|
193
|
+
expect(locks).toEqual({ taken: 1, released: 1 });
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
interface RefusalRow {
|
|
197
|
+
readonly name: string;
|
|
198
|
+
readonly over: Partial<AgentRunDeps>;
|
|
199
|
+
readonly request?: ClaimedUpdate;
|
|
200
|
+
/** `<status>/<codes>`. */
|
|
201
|
+
readonly expect: string;
|
|
202
|
+
readonly reason?: RegExp;
|
|
203
|
+
}
|
|
204
|
+
const refusals: readonly RefusalRow[] = [
|
|
205
|
+
{
|
|
206
|
+
name: "a host problem (preflight)",
|
|
207
|
+
over: { preflight: async () => "Docker is not reachable for the agent's user." },
|
|
208
|
+
expect: "failed/preflight",
|
|
209
|
+
reason: /Nothing was changed: Docker is not reachable/,
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
name: "another upgrade holds the lock",
|
|
213
|
+
over: {
|
|
214
|
+
lock: () => {
|
|
215
|
+
throw new Error("`cms-provision upgrade` is running on this install (pid 42).");
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
expect: "failed/state-locked",
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
name: "a malformed release.json",
|
|
222
|
+
over: {
|
|
223
|
+
readInstalled: () => {
|
|
224
|
+
throw new Error("release.json is malformed");
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
expect: "failed/install-state",
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
name: "a bad signature",
|
|
231
|
+
over: {
|
|
232
|
+
fetchManifest: async () => ({ ok: false, error: { code: "signature", message: "no" } }),
|
|
233
|
+
},
|
|
234
|
+
expect: "refused/signature",
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
name: "the registry is unreachable",
|
|
238
|
+
over: {
|
|
239
|
+
fetchManifest: async () => ({ ok: false, error: { code: "registry", message: "dns" } }),
|
|
240
|
+
},
|
|
241
|
+
expect: "failed/registry",
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
name: "a downgrade",
|
|
245
|
+
over: { readInstalled: () => ({ ...INSTALLED, version: "0.12.0" }) },
|
|
246
|
+
expect: "refused/not-newer",
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
name: "below minFromVersion",
|
|
250
|
+
over: {
|
|
251
|
+
fetchManifest: async () => ({
|
|
252
|
+
ok: true,
|
|
253
|
+
manifest: { ...MANIFEST, minFromVersion: "0.10.50" },
|
|
254
|
+
}),
|
|
255
|
+
},
|
|
256
|
+
expect: "refused/min-from-version",
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
name: "no recorded release (an install from before prebuilt images)",
|
|
260
|
+
over: { readInstalled: () => null },
|
|
261
|
+
expect: "needs_cloud_shell/install-version-unknown",
|
|
262
|
+
reason: /upgrade command on the server/,
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
name: "contract migrations on an automatic run",
|
|
266
|
+
over: {
|
|
267
|
+
fetchManifest: async () => ({
|
|
268
|
+
ok: true,
|
|
269
|
+
manifest: { ...MANIFEST, migrations: { ...MANIFEST.migrations, contract: true } },
|
|
270
|
+
}),
|
|
271
|
+
},
|
|
272
|
+
request: {
|
|
273
|
+
...REQUEST,
|
|
274
|
+
trigger: "auto",
|
|
275
|
+
policy: { ...POLICY, mode: "auto", autoMinor: true },
|
|
276
|
+
},
|
|
277
|
+
expect: "refused/contract-migrations",
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
name: "a target CLI whose npm provenance does not check out",
|
|
281
|
+
over: {
|
|
282
|
+
verifyTargetCli: async () => ({
|
|
283
|
+
ok: false,
|
|
284
|
+
error: { code: "provenance", message: "built by https://github.com/evil/caelo-cms" },
|
|
285
|
+
}),
|
|
286
|
+
},
|
|
287
|
+
expect: "refused/cli-provenance",
|
|
288
|
+
reason: /not the one Caelo's release workflow published/,
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
name: "npm missing on the server",
|
|
292
|
+
over: {
|
|
293
|
+
verifyTargetCli: async () => ({
|
|
294
|
+
ok: false,
|
|
295
|
+
error: { code: "npm-missing", message: "npm is not installed" },
|
|
296
|
+
}),
|
|
297
|
+
},
|
|
298
|
+
expect: "failed/cli-npm-missing",
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
name: "an automatic run the policy does not allow",
|
|
302
|
+
over: {},
|
|
303
|
+
request: { ...REQUEST, trigger: "auto" },
|
|
304
|
+
expect: "refused/policy",
|
|
305
|
+
},
|
|
306
|
+
];
|
|
307
|
+
|
|
308
|
+
it.each(refusals.map((r) => [r.name, r] as const))(
|
|
309
|
+
"stops before any target code runs: %s",
|
|
310
|
+
async (_name, row) => {
|
|
311
|
+
const { deps, q, applied, locks } = setup(row.over, row.request ?? REQUEST);
|
|
312
|
+
const r = await runAgentOnce(deps);
|
|
313
|
+
expect(applied).toEqual([]);
|
|
314
|
+
const end = q.ends[0];
|
|
315
|
+
expect(`${end?.status}/${end?.codes.join(",")}`).toBe(row.expect);
|
|
316
|
+
if (row.reason) expect(end?.reason ?? "").toMatch(row.reason);
|
|
317
|
+
expect(r.status).toBe(end?.status ?? null);
|
|
318
|
+
expect(r.exitCode).toBe(end?.status === "failed" ? 1 : 0);
|
|
319
|
+
// Whatever the lock did, it is not left held.
|
|
320
|
+
expect(locks.released).toBe(locks.taken);
|
|
321
|
+
expect(q.events.at(-1)?.phase).toBe("finish");
|
|
322
|
+
},
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
it("ends the request as failed with the output when the target's CLI recorded nothing", async () => {
|
|
326
|
+
const { deps, q } = setup({
|
|
327
|
+
applyTarget: async () => ({ exitCode: 1, output: "error: package not found" }),
|
|
328
|
+
});
|
|
329
|
+
const r = await runAgentOnce(deps);
|
|
330
|
+
expect(r).toEqual({ exitCode: 1, requestId: REQUEST.id, status: "failed" });
|
|
331
|
+
expect(q.ends[0]?.status).toBe("failed");
|
|
332
|
+
expect(q.ends[0]?.codes).toEqual(["apply"]);
|
|
333
|
+
expect(q.ends[0]?.reason).toContain("error: package not found");
|
|
334
|
+
expect(q.events.at(-1)).toMatchObject({ phase: "finish", level: "error" });
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it("leaves a rolled-back end the target's CLI recorded alone", async () => {
|
|
338
|
+
const { deps, q } = setup();
|
|
339
|
+
const rolled: AgentRunDeps = {
|
|
340
|
+
...deps,
|
|
341
|
+
applyTarget: async (req) => {
|
|
342
|
+
await q.queue.finish(req.requestId, {
|
|
343
|
+
status: "rolled_back",
|
|
344
|
+
reason: "unhealthy",
|
|
345
|
+
codes: ["roll-admin"],
|
|
346
|
+
pauseAutoUpdates: false,
|
|
347
|
+
});
|
|
348
|
+
return { exitCode: 1, output: "" };
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
const r = await runAgentOnce(rolled);
|
|
352
|
+
expect(r).toEqual({ exitCode: 1, requestId: REQUEST.id, status: null });
|
|
353
|
+
expect(q.ends.map((e) => e.status)).toEqual(["rolled_back"]);
|
|
354
|
+
expect(q.events.some((e) => e.phase === "finish")).toBe(false);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("keeps checking in while the target's CLI runs", async () => {
|
|
358
|
+
const { deps, q } = setup();
|
|
359
|
+
const slow: AgentRunDeps = {
|
|
360
|
+
...deps,
|
|
361
|
+
heartbeatEveryMs: 5,
|
|
362
|
+
applyTarget: async (req) => {
|
|
363
|
+
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
364
|
+
return deps.applyTarget(req);
|
|
365
|
+
},
|
|
366
|
+
};
|
|
367
|
+
await runAgentOnce(slow);
|
|
368
|
+
const during = q.beats.length;
|
|
369
|
+
expect(during).toBeGreaterThan(2);
|
|
370
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
371
|
+
expect(q.beats.length).toBe(during);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it("pauses automatic installs when an automatic run fails", async () => {
|
|
375
|
+
const { deps, q } = setup(
|
|
376
|
+
{ preflight: async () => "cosign is missing." },
|
|
377
|
+
{ ...REQUEST, trigger: "auto" },
|
|
378
|
+
);
|
|
379
|
+
await runAgentOnce(deps);
|
|
380
|
+
expect(q.ends[0]).toMatchObject({ status: "failed", pauseAutoUpdates: true });
|
|
381
|
+
});
|
|
382
|
+
});
|
package/src/agent/run.ts
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* One poll of the self-hosted host agent (#696 Phase 4c) — what the
|
|
5
|
+
* `caelo-agent-<domain>-<hash>` timer runs every minute:
|
|
6
|
+
*
|
|
7
|
+
* 1. preflight (Docker reachable, cosign present, a recorded release) and
|
|
8
|
+
* a heartbeat with the result, so Settings → Updates knows the agent is
|
|
9
|
+
* alive and what would stop an install before the Owner clicks;
|
|
10
|
+
* 2. claim the oldest pending request (ending a run that died mid-way) —
|
|
11
|
+
* the admin only ever writes the request row;
|
|
12
|
+
* 3. verify the target's signed release manifest (cosign, fixed registry
|
|
13
|
+
* and signer — nothing from the request but the version) and decide
|
|
14
|
+
* with the upgrader's own refusal matrix (upgrader-assess.ts): bad
|
|
15
|
+
* signature, downgrade, below minFromVersion, contract migrations or a
|
|
16
|
+
* policy refusal on an automatic run. Self-hosted has no separately
|
|
17
|
+
* converged infrastructure, so the infra-contract row never applies;
|
|
18
|
+
* 4. install the TARGET release's own CLI from npm and verify its registry
|
|
19
|
+
* signatures + provenance (agent/cli-provenance.ts, the checks of the
|
|
20
|
+
* Cloud Shell runner) — refused outright when they fail;
|
|
21
|
+
* 5. only then run that CLI's `agent apply` (agent/apply.ts), which
|
|
22
|
+
* performs the same backup → pull → migrate → roll → health → rollback
|
|
23
|
+
* as a hand-run `upgrade` and narrates it into the request's timeline.
|
|
24
|
+
* The target's CLI renders the target's Compose file, so a release that
|
|
25
|
+
* changes the stack needs no extra step. No code of the target runs
|
|
26
|
+
* before its manifest verified and passed the checks above;
|
|
27
|
+
* 6. if that process ended without recording an end state, end the
|
|
28
|
+
* request as failed with its output.
|
|
29
|
+
*
|
|
30
|
+
* The install's upgrade lock is held for the whole run, so a hand-run
|
|
31
|
+
* `upgrade` and the agent never roll at the same time.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { PLATFORM_UPDATE_AGENT_POLL_SECONDS, type PlatformUpdateTrigger } from "@caelo-cms/shared";
|
|
35
|
+
import type { ReleaseManifest, ReleaseManifestResult } from "../release-manifest.js";
|
|
36
|
+
import type { SelfHostedRelease } from "../self-hosted-release.js";
|
|
37
|
+
import { assessUpgradeRequest } from "../upgrader-assess.js";
|
|
38
|
+
import type { UpdateEnd, UpdateEndStatus } from "../upgrader-job.js";
|
|
39
|
+
import { recordUpdateEnd } from "../upgrader-job.js";
|
|
40
|
+
import { eventReporter, type JobLog } from "../upgrader-reporter.js";
|
|
41
|
+
import type { VerifiedCli } from "./cli-provenance.js";
|
|
42
|
+
import type { AgentQueue } from "./queue.js";
|
|
43
|
+
|
|
44
|
+
/** What the target release's `agent apply` is told (argv of the child). */
|
|
45
|
+
export interface AgentApplyRequest {
|
|
46
|
+
readonly requestId: string;
|
|
47
|
+
readonly targetVersion: string;
|
|
48
|
+
readonly trigger: PlatformUpdateTrigger;
|
|
49
|
+
/** From the verified manifest: the images the target must resolve to. */
|
|
50
|
+
readonly digests: { readonly admin: string; readonly gateway: string };
|
|
51
|
+
/** The target's CLI, installed and provenance-verified (agent/cli-provenance.ts). */
|
|
52
|
+
readonly cliPath: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** How the target's `agent apply` process ended. */
|
|
56
|
+
export interface AgentApplyOutcome {
|
|
57
|
+
readonly exitCode: number;
|
|
58
|
+
/** The tail of its output, for the timeline when it recorded nothing. */
|
|
59
|
+
readonly output: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface AgentRunDeps {
|
|
63
|
+
readonly queue: AgentQueue;
|
|
64
|
+
/** This agent's own release (CAELO_VERSION). */
|
|
65
|
+
readonly agentVersion: string;
|
|
66
|
+
readonly host: string;
|
|
67
|
+
/** Why an install would fail on this host now, or null. */
|
|
68
|
+
readonly preflight: () => Promise<string | null>;
|
|
69
|
+
/** `.caelo/release.json` (null when none is recorded; throws when malformed). */
|
|
70
|
+
readonly readInstalled: () => SelfHostedRelease | null;
|
|
71
|
+
/** fetchVerifiedReleaseManifest against the fixed registry. */
|
|
72
|
+
readonly fetchManifest: (version: string) => Promise<ReleaseManifestResult>;
|
|
73
|
+
/** Take the install's upgrade lock (agent/lock.ts); throws when held. */
|
|
74
|
+
readonly lock: () => () => void;
|
|
75
|
+
/**
|
|
76
|
+
* Install the target release's CLI and verify its npm signatures +
|
|
77
|
+
* provenance (agent/cli-provenance.ts). Nothing of it runs unless this
|
|
78
|
+
* passes.
|
|
79
|
+
*/
|
|
80
|
+
readonly verifyTargetCli: (version: string) => Promise<VerifiedCli>;
|
|
81
|
+
/** Run the target release's `agent apply` and wait for it. */
|
|
82
|
+
readonly applyTarget: (request: AgentApplyRequest) => Promise<AgentApplyOutcome>;
|
|
83
|
+
readonly now: () => Date;
|
|
84
|
+
readonly log: JobLog;
|
|
85
|
+
readonly sleep?: (ms: number) => Promise<void>;
|
|
86
|
+
/**
|
|
87
|
+
* How often the agent keeps checking in while the target's CLI runs (an
|
|
88
|
+
* update takes minutes; the admin would otherwise think the agent gone).
|
|
89
|
+
* Default: every poll interval.
|
|
90
|
+
*/
|
|
91
|
+
readonly heartbeatEveryMs?: number;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** What one poll did. */
|
|
95
|
+
export interface AgentRunResult {
|
|
96
|
+
readonly exitCode: number;
|
|
97
|
+
readonly requestId: string | null;
|
|
98
|
+
/** The end the agent itself recorded (null when the target's CLI did, or nothing ran). */
|
|
99
|
+
readonly status: UpdateEndStatus | null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** One poll. Never throws; see the file comment for the sequence. */
|
|
103
|
+
export async function runAgentOnce(deps: AgentRunDeps): Promise<AgentRunResult> {
|
|
104
|
+
const { queue, log } = deps;
|
|
105
|
+
const problem = await deps.preflight().catch((e: unknown) => `preflight failed: ${errorText(e)}`);
|
|
106
|
+
try {
|
|
107
|
+
await queue.heartbeat({ agentVersion: deps.agentVersion, host: deps.host, problem });
|
|
108
|
+
} catch (e) {
|
|
109
|
+
log({ severity: "ERROR", message: `could not check in with the database: ${errorText(e)}` });
|
|
110
|
+
return { exitCode: 1, requestId: null, status: null };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let claimed: Awaited<ReturnType<AgentQueue["claim"]>>;
|
|
114
|
+
try {
|
|
115
|
+
claimed = await queue.claim(`agent@${deps.host}`.slice(0, 200));
|
|
116
|
+
} catch (e) {
|
|
117
|
+
log({ severity: "ERROR", message: `could not read update requests: ${errorText(e)}` });
|
|
118
|
+
return { exitCode: 1, requestId: null, status: null };
|
|
119
|
+
}
|
|
120
|
+
if (!claimed) return { exitCode: 0, requestId: null, status: null };
|
|
121
|
+
const request = claimed;
|
|
122
|
+
const r = eventReporter(
|
|
123
|
+
(phase, level, message) => queue.event(request.id, phase, level, message),
|
|
124
|
+
log,
|
|
125
|
+
request.id,
|
|
126
|
+
);
|
|
127
|
+
let release: (() => void) | null = null;
|
|
128
|
+
const unlock = () => {
|
|
129
|
+
try {
|
|
130
|
+
release?.();
|
|
131
|
+
} catch (e) {
|
|
132
|
+
log({ severity: "WARNING", message: `could not release the upgrade lock: ${errorText(e)}` });
|
|
133
|
+
}
|
|
134
|
+
release = null;
|
|
135
|
+
};
|
|
136
|
+
const stopWith = async (
|
|
137
|
+
status: UpdateEndStatus,
|
|
138
|
+
codes: readonly string[],
|
|
139
|
+
reason: string,
|
|
140
|
+
): Promise<AgentRunResult> => {
|
|
141
|
+
r.phase("finish");
|
|
142
|
+
r.error(reason);
|
|
143
|
+
await r.flush();
|
|
144
|
+
unlock();
|
|
145
|
+
const end: UpdateEnd = {
|
|
146
|
+
status,
|
|
147
|
+
reason,
|
|
148
|
+
codes,
|
|
149
|
+
pauseAutoUpdates:
|
|
150
|
+
request.trigger === "auto" && (status === "failed" || status === "rolled_back"),
|
|
151
|
+
};
|
|
152
|
+
const recorded = await recordUpdateEnd(queue, request.id, end, log, deps.sleep);
|
|
153
|
+
const failed = status === "failed" || !recorded;
|
|
154
|
+
return { exitCode: failed ? 1 : 0, requestId: request.id, status };
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
r.phase("start");
|
|
159
|
+
r.info(
|
|
160
|
+
`The update agent on ${deps.host} (Caelo ${deps.agentVersion}) picked up the request to install Caelo ${request.targetVersion}.`,
|
|
161
|
+
);
|
|
162
|
+
if (problem) {
|
|
163
|
+
return await stopWith(
|
|
164
|
+
"failed",
|
|
165
|
+
["preflight"],
|
|
166
|
+
`Nothing was changed: ${problem} Fix this on the server, then try again.`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
release = deps.lock();
|
|
171
|
+
} catch (e) {
|
|
172
|
+
return await stopWith(
|
|
173
|
+
"failed",
|
|
174
|
+
["state-locked"],
|
|
175
|
+
`Nothing was changed: ${errorText(e)} Try again when it has finished.`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
r.phase("verify");
|
|
180
|
+
let installed: SelfHostedRelease | null;
|
|
181
|
+
try {
|
|
182
|
+
installed = deps.readInstalled();
|
|
183
|
+
} catch (e) {
|
|
184
|
+
return await stopWith("failed", ["install-state"], `Nothing was changed: ${errorText(e)}`);
|
|
185
|
+
}
|
|
186
|
+
const manifest = await deps.fetchManifest(request.targetVersion);
|
|
187
|
+
const decision = assessUpgradeRequest({
|
|
188
|
+
request,
|
|
189
|
+
manifest,
|
|
190
|
+
installed: { version: installed?.version ?? null, infraContractVersion: null },
|
|
191
|
+
policy: request.policy,
|
|
192
|
+
now: deps.now(),
|
|
193
|
+
manualPath: "server",
|
|
194
|
+
});
|
|
195
|
+
if (decision.manifest) {
|
|
196
|
+
r.success(
|
|
197
|
+
`Release information of ${decision.manifest.version} verified: signed by Caelo's release workflow.`,
|
|
198
|
+
);
|
|
199
|
+
await recordFacts(queue, request.id, decision.manifest);
|
|
200
|
+
}
|
|
201
|
+
r.phase("assess");
|
|
202
|
+
if (decision.kind === "stop") {
|
|
203
|
+
return await stopWith(decision.status, decision.codes, decision.reason);
|
|
204
|
+
}
|
|
205
|
+
for (const w of decision.warnings) r.warn(w);
|
|
206
|
+
const m = decision.manifest;
|
|
207
|
+
const step = r.step(
|
|
208
|
+
`Fetching the Caelo ${m.version} installer and verifying its npm provenance...`,
|
|
209
|
+
);
|
|
210
|
+
const cli = await deps.verifyTargetCli(m.version);
|
|
211
|
+
if (!cli.ok) {
|
|
212
|
+
step.failed(cli.error.message);
|
|
213
|
+
// A package that is not the release's is refused; a missing npm or a
|
|
214
|
+
// failed download is a failure to retry.
|
|
215
|
+
const refused = ["audit-signatures", "version", "provenance"].includes(cli.error.code);
|
|
216
|
+
return await stopWith(
|
|
217
|
+
refused ? "refused" : "failed",
|
|
218
|
+
[`cli-${cli.error.code}`],
|
|
219
|
+
`Nothing was changed: the installer of Caelo ${m.version} ${refused ? "is not the one Caelo's release workflow published" : "could not be fetched"} (${cli.error.message}).`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
step.done(
|
|
223
|
+
`The Caelo ${m.version} installer is signed and was built by Caelo's release workflow.`,
|
|
224
|
+
);
|
|
225
|
+
r.info(
|
|
226
|
+
`${installed?.version} → ${m.version}: handing over to the Caelo ${m.version} installer on this server.`,
|
|
227
|
+
);
|
|
228
|
+
await r.flush();
|
|
229
|
+
|
|
230
|
+
const beat = setInterval(
|
|
231
|
+
() => {
|
|
232
|
+
queue
|
|
233
|
+
.heartbeat({ agentVersion: deps.agentVersion, host: deps.host, problem: null })
|
|
234
|
+
.catch((e: unknown) => {
|
|
235
|
+
log({ severity: "WARNING", message: `could not check in: ${errorText(e)}` });
|
|
236
|
+
});
|
|
237
|
+
},
|
|
238
|
+
deps.heartbeatEveryMs ?? PLATFORM_UPDATE_AGENT_POLL_SECONDS * 1000,
|
|
239
|
+
);
|
|
240
|
+
let outcome: AgentApplyOutcome;
|
|
241
|
+
try {
|
|
242
|
+
outcome = await deps.applyTarget({
|
|
243
|
+
requestId: request.id,
|
|
244
|
+
targetVersion: m.version,
|
|
245
|
+
trigger: request.trigger,
|
|
246
|
+
digests: { admin: m.digests.admin, gateway: m.digests.gateway },
|
|
247
|
+
cliPath: cli.cliPath,
|
|
248
|
+
});
|
|
249
|
+
} finally {
|
|
250
|
+
clearInterval(beat);
|
|
251
|
+
}
|
|
252
|
+
unlock();
|
|
253
|
+
if (outcome.exitCode === 0) return { exitCode: 0, requestId: request.id, status: null };
|
|
254
|
+
// The target's CLI ends the request itself; only if it could not (it
|
|
255
|
+
// never started, or crashed) is the request still running here.
|
|
256
|
+
const reason = `The Caelo ${m.version} installer stopped (exit ${outcome.exitCode}) before it recorded how the update ended. If the site misbehaves, check \`docker compose ps\` on the server.${outcome.output ? `\n${outcome.output}` : ""}`;
|
|
257
|
+
const end: UpdateEnd = {
|
|
258
|
+
status: "failed",
|
|
259
|
+
reason: reason.slice(0, 4000),
|
|
260
|
+
codes: ["apply"],
|
|
261
|
+
pauseAutoUpdates: request.trigger === "auto",
|
|
262
|
+
};
|
|
263
|
+
let ended = false;
|
|
264
|
+
try {
|
|
265
|
+
ended = await queue.finish(request.id, end);
|
|
266
|
+
} catch (e) {
|
|
267
|
+
log({ severity: "ERROR", message: `could not end request ${request.id}: ${errorText(e)}` });
|
|
268
|
+
return { exitCode: 1, requestId: request.id, status: null };
|
|
269
|
+
}
|
|
270
|
+
if (!ended) return { exitCode: 1, requestId: request.id, status: null };
|
|
271
|
+
r.phase("finish");
|
|
272
|
+
r.error(reason);
|
|
273
|
+
await r.flush();
|
|
274
|
+
return { exitCode: 1, requestId: request.id, status: "failed" };
|
|
275
|
+
} catch (e) {
|
|
276
|
+
return await stopWith(
|
|
277
|
+
"failed",
|
|
278
|
+
["internal"],
|
|
279
|
+
`The update agent stopped unexpectedly: ${errorText(e)}. If the site misbehaves, check \`docker compose ps\` on the server.`,
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function recordFacts(queue: AgentQueue, requestId: string, m: ReleaseManifest) {
|
|
285
|
+
await queue.record(requestId, {
|
|
286
|
+
manifestSecurity: m.security,
|
|
287
|
+
contractMigrations: m.migrations.contract,
|
|
288
|
+
adminDigest: m.digests.admin,
|
|
289
|
+
gatewayDigest: m.digests.gateway,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function errorText(e: unknown): string {
|
|
294
|
+
return e instanceof Error ? e.message : String(e);
|
|
295
|
+
}
|