@caelo-cms/provisioning 0.10.47 → 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.
Files changed (315) hide show
  1. package/dist/agent/apply.d.ts +45 -0
  2. package/dist/agent/apply.d.ts.map +1 -0
  3. package/dist/agent/apply.js +83 -0
  4. package/dist/agent/apply.js.map +1 -0
  5. package/dist/agent/cli-provenance.d.ts +42 -0
  6. package/dist/agent/cli-provenance.d.ts.map +1 -0
  7. package/dist/agent/cli-provenance.js +138 -0
  8. package/dist/agent/cli-provenance.js.map +1 -0
  9. package/dist/agent/command.d.ts +79 -0
  10. package/dist/agent/command.d.ts.map +1 -0
  11. package/dist/agent/command.js +454 -0
  12. package/dist/agent/command.js.map +1 -0
  13. package/dist/agent/config.d.ts +30 -0
  14. package/dist/agent/config.d.ts.map +1 -0
  15. package/dist/agent/config.js +60 -0
  16. package/dist/agent/config.js.map +1 -0
  17. package/dist/agent/lock.d.ts +13 -0
  18. package/dist/agent/lock.d.ts.map +1 -0
  19. package/dist/agent/lock.js +100 -0
  20. package/dist/agent/lock.js.map +1 -0
  21. package/dist/agent/queue.d.ts +35 -0
  22. package/dist/agent/queue.d.ts.map +1 -0
  23. package/dist/agent/queue.js +50 -0
  24. package/dist/agent/queue.js.map +1 -0
  25. package/dist/agent/run.d.ts +97 -0
  26. package/dist/agent/run.d.ts.map +1 -0
  27. package/dist/agent/run.js +198 -0
  28. package/dist/agent/run.js.map +1 -0
  29. package/dist/agent/systemd.d.ts +55 -0
  30. package/dist/agent/systemd.d.ts.map +1 -0
  31. package/dist/agent/systemd.js +171 -0
  32. package/dist/agent/systemd.js.map +1 -0
  33. package/dist/agent/units.d.ts +44 -0
  34. package/dist/agent/units.d.ts.map +1 -0
  35. package/dist/agent/units.js +127 -0
  36. package/dist/agent/units.js.map +1 -0
  37. package/dist/agent-db.js +18362 -0
  38. package/dist/cli.js +119 -16
  39. package/dist/cli.js.map +1 -1
  40. package/dist/cloudsql-iam-user.d.ts +39 -0
  41. package/dist/cloudsql-iam-user.d.ts.map +1 -0
  42. package/dist/cloudsql-iam-user.js +119 -0
  43. package/dist/cloudsql-iam-user.js.map +1 -0
  44. package/dist/compose.d.ts +16 -0
  45. package/dist/compose.d.ts.map +1 -1
  46. package/dist/compose.js +26 -1
  47. package/dist/compose.js.map +1 -1
  48. package/dist/dns/index.d.ts +2 -0
  49. package/dist/dns/index.d.ts.map +1 -1
  50. package/dist/dns/index.js +3 -2
  51. package/dist/dns/index.js.map +1 -1
  52. package/dist/dns/manual.d.ts +5 -0
  53. package/dist/dns/manual.d.ts.map +1 -1
  54. package/dist/dns/manual.js +28 -12
  55. package/dist/dns/manual.js.map +1 -1
  56. package/dist/gcloud.d.ts +2 -0
  57. package/dist/gcloud.d.ts.map +1 -1
  58. package/dist/gcloud.js +1 -1
  59. package/dist/gcloud.js.map +1 -1
  60. package/dist/gcp-names.d.ts +15 -0
  61. package/dist/gcp-names.d.ts.map +1 -1
  62. package/dist/gcp-names.js +23 -0
  63. package/dist/gcp-names.js.map +1 -1
  64. package/dist/index.d.ts +6 -0
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +4 -0
  67. package/dist/index.js.map +1 -1
  68. package/dist/infra-contract.d.ts +1 -1
  69. package/dist/infra-contract.js +7 -1
  70. package/dist/infra-contract.js.map +1 -1
  71. package/dist/install-job-runner.d.ts +13 -0
  72. package/dist/install-job-runner.d.ts.map +1 -0
  73. package/dist/install-job-runner.js +155 -0
  74. package/dist/install-job-runner.js.map +1 -0
  75. package/dist/install-job.d.ts +210 -0
  76. package/dist/install-job.d.ts.map +1 -0
  77. package/dist/install-job.js +449 -0
  78. package/dist/install-job.js.map +1 -0
  79. package/dist/install-state.d.ts +10 -1
  80. package/dist/install-state.d.ts.map +1 -1
  81. package/dist/install-state.js +10 -2
  82. package/dist/install-state.js.map +1 -1
  83. package/dist/lifecycle.d.ts.map +1 -1
  84. package/dist/lifecycle.js +19 -4
  85. package/dist/lifecycle.js.map +1 -1
  86. package/dist/migration-runner.d.ts +5 -0
  87. package/dist/migration-runner.d.ts.map +1 -1
  88. package/dist/migration-runner.js +1 -1
  89. package/dist/migration-runner.js.map +1 -1
  90. package/dist/operator-access.d.ts +14 -8
  91. package/dist/operator-access.d.ts.map +1 -1
  92. package/dist/operator-access.js +16 -102
  93. package/dist/operator-access.js.map +1 -1
  94. package/dist/run-mode.d.ts +27 -0
  95. package/dist/run-mode.d.ts.map +1 -0
  96. package/dist/run-mode.js +22 -0
  97. package/dist/run-mode.js.map +1 -0
  98. package/dist/runtime-secrets.d.ts +19 -0
  99. package/dist/runtime-secrets.d.ts.map +1 -1
  100. package/dist/runtime-secrets.js +44 -0
  101. package/dist/runtime-secrets.js.map +1 -1
  102. package/dist/self-hosted-roles.d.ts +22 -1
  103. package/dist/self-hosted-roles.d.ts.map +1 -1
  104. package/dist/self-hosted-roles.js +81 -12
  105. package/dist/self-hosted-roles.js.map +1 -1
  106. package/dist/self-hosted-upgrade.d.ts +17 -0
  107. package/dist/self-hosted-upgrade.d.ts.map +1 -1
  108. package/dist/self-hosted-upgrade.js +16 -1
  109. package/dist/self-hosted-upgrade.js.map +1 -1
  110. package/dist/stack-contract.d.ts +6 -0
  111. package/dist/stack-contract.d.ts.map +1 -1
  112. package/dist/stack-contract.js +10 -1
  113. package/dist/stack-contract.js.map +1 -1
  114. package/dist/upgrade/converge.d.ts +6 -0
  115. package/dist/upgrade/converge.d.ts.map +1 -1
  116. package/dist/upgrade/converge.js +59 -2
  117. package/dist/upgrade/converge.js.map +1 -1
  118. package/dist/upgrade/gcp-lookup.d.ts +6 -0
  119. package/dist/upgrade/gcp-lookup.d.ts.map +1 -1
  120. package/dist/upgrade/gcp-lookup.js +25 -0
  121. package/dist/upgrade/gcp-lookup.js.map +1 -1
  122. package/dist/upgrade/release-images.d.ts +12 -0
  123. package/dist/upgrade/release-images.d.ts.map +1 -1
  124. package/dist/upgrade/release-images.js +31 -0
  125. package/dist/upgrade/release-images.js.map +1 -1
  126. package/dist/upgrade/upgrade.d.ts +5 -0
  127. package/dist/upgrade/upgrade.d.ts.map +1 -1
  128. package/dist/upgrade/upgrade.js +28 -1
  129. package/dist/upgrade/upgrade.js.map +1 -1
  130. package/dist/upgrader-assess.d.ts +82 -0
  131. package/dist/upgrader-assess.d.ts.map +1 -0
  132. package/dist/upgrader-assess.js +116 -0
  133. package/dist/upgrader-assess.js.map +1 -0
  134. package/dist/upgrader-gcp.d.ts +17 -0
  135. package/dist/upgrader-gcp.d.ts.map +1 -0
  136. package/dist/upgrader-gcp.js +87 -0
  137. package/dist/upgrader-gcp.js.map +1 -0
  138. package/dist/upgrader-job.d.ts +131 -0
  139. package/dist/upgrader-job.d.ts.map +1 -0
  140. package/dist/upgrader-job.js +259 -0
  141. package/dist/upgrader-job.js.map +1 -0
  142. package/dist/upgrader-reporter.d.ts +35 -0
  143. package/dist/upgrader-reporter.d.ts.map +1 -0
  144. package/dist/upgrader-reporter.js +79 -0
  145. package/dist/upgrader-reporter.js.map +1 -0
  146. package/dist/upgrader-setup.d.ts +59 -0
  147. package/dist/upgrader-setup.d.ts.map +1 -0
  148. package/dist/upgrader-setup.js +308 -0
  149. package/dist/upgrader-setup.js.map +1 -0
  150. package/dist/wizard.d.ts +6 -0
  151. package/dist/wizard.d.ts.map +1 -1
  152. package/dist/wizard.js +32 -4
  153. package/dist/wizard.js.map +1 -1
  154. package/dist/wizards/gcp-firebase.d.ts +3 -0
  155. package/dist/wizards/gcp-firebase.d.ts.map +1 -1
  156. package/dist/wizards/gcp-firebase.js +1 -0
  157. package/dist/wizards/gcp-firebase.js.map +1 -1
  158. package/dist/wizards/gcp-install-job.d.ts +74 -0
  159. package/dist/wizards/gcp-install-job.d.ts.map +1 -0
  160. package/dist/wizards/gcp-install-job.js +398 -0
  161. package/dist/wizards/gcp-install-job.js.map +1 -0
  162. package/dist/wizards/gcp-prompts.d.ts +3 -0
  163. package/dist/wizards/gcp-prompts.d.ts.map +1 -0
  164. package/dist/wizards/gcp-prompts.js +25 -0
  165. package/dist/wizards/gcp-prompts.js.map +1 -0
  166. package/dist/wizards/gcp-pulumi.d.ts +4 -2
  167. package/dist/wizards/gcp-pulumi.d.ts.map +1 -1
  168. package/dist/wizards/gcp-pulumi.js +1 -1
  169. package/dist/wizards/gcp-pulumi.js.map +1 -1
  170. package/dist/wizards/gcp.d.ts +40 -0
  171. package/dist/wizards/gcp.d.ts.map +1 -1
  172. package/dist/wizards/gcp.js +223 -59
  173. package/dist/wizards/gcp.js.map +1 -1
  174. package/package.json +6 -4
  175. package/src/adapter.ts +97 -0
  176. package/src/adc-impersonation.ts +176 -0
  177. package/src/admin-domain.test.ts +254 -0
  178. package/src/admin-domain.ts +245 -0
  179. package/src/admin-memory.test.ts +151 -0
  180. package/src/agent/apply.test.ts +185 -0
  181. package/src/agent/apply.ts +124 -0
  182. package/src/agent/cli-provenance.test.ts +159 -0
  183. package/src/agent/cli-provenance.ts +191 -0
  184. package/src/agent/command.test.ts +77 -0
  185. package/src/agent/command.ts +564 -0
  186. package/src/agent/config.ts +86 -0
  187. package/src/agent/lock-config.test.ts +97 -0
  188. package/src/agent/lock.ts +110 -0
  189. package/src/agent/queue.test.ts +10 -0
  190. package/src/agent/queue.ts +91 -0
  191. package/src/agent/run.test.ts +382 -0
  192. package/src/agent/run.ts +295 -0
  193. package/src/agent/systemd.ts +209 -0
  194. package/src/agent/units.test.ts +223 -0
  195. package/src/agent/units.ts +163 -0
  196. package/src/azure-front-door.test.ts +119 -0
  197. package/src/azure-front-door.ts +56 -0
  198. package/src/bootstrap-token.ts +20 -0
  199. package/src/caddy.ts +112 -0
  200. package/src/cdn-copy.ts +91 -0
  201. package/src/cert-wait.test.ts +194 -0
  202. package/src/cert-wait.ts +163 -0
  203. package/src/checkpoint-secrets.test.ts +380 -0
  204. package/src/checkpoint-secrets.ts +283 -0
  205. package/src/cli-routing.test.ts +45 -0
  206. package/src/cli-routing.ts +43 -0
  207. package/src/cli.ts +1491 -0
  208. package/src/cloudsql-iam-user.ts +147 -0
  209. package/src/compose.ts +334 -0
  210. package/src/default-firewall.test.ts +411 -0
  211. package/src/default-firewall.ts +342 -0
  212. package/src/deployed-release.test.ts +231 -0
  213. package/src/deployed-release.ts +220 -0
  214. package/src/dns/cloudflare.ts +182 -0
  215. package/src/dns/groups.test.ts +76 -0
  216. package/src/dns/groups.ts +86 -0
  217. package/src/dns/index.ts +56 -0
  218. package/src/dns/manual.ts +130 -0
  219. package/src/dns/types.ts +27 -0
  220. package/src/gateway-credentials.test.ts +180 -0
  221. package/src/gateway-credentials.ts +144 -0
  222. package/src/gcloud-components.test.ts +76 -0
  223. package/src/gcloud-components.ts +85 -0
  224. package/src/gcloud-retry.ts +41 -0
  225. package/src/gcloud-runner.test.ts +61 -0
  226. package/src/gcloud.test.ts +99 -0
  227. package/src/gcloud.ts +322 -0
  228. package/src/gcp-names.ts +135 -0
  229. package/src/index.test.ts +332 -0
  230. package/src/index.ts +114 -0
  231. package/src/infra-contract.ts +32 -0
  232. package/src/install-job-runner.ts +166 -0
  233. package/src/install-job.test.ts +566 -0
  234. package/src/install-job.ts +542 -0
  235. package/src/install-state.test.ts +239 -0
  236. package/src/install-state.ts +583 -0
  237. package/src/internal-token.ts +35 -0
  238. package/src/lifecycle.ts +900 -0
  239. package/src/mcp-iap-stacks.test.ts +58 -0
  240. package/src/mcp-iap.test.ts +149 -0
  241. package/src/mcp-iap.ts +131 -0
  242. package/src/media-volume.test.ts +296 -0
  243. package/src/migration-runner.ts +365 -0
  244. package/src/operator-access-stacks.test.ts +55 -0
  245. package/src/operator-access.test.ts +332 -0
  246. package/src/operator-access.ts +554 -0
  247. package/src/ops-backend.test.ts +320 -0
  248. package/src/ops-backend.ts +414 -0
  249. package/src/provisioner-identity.test.ts +482 -0
  250. package/src/provisioner-identity.ts +407 -0
  251. package/src/redirects-emit.ts +175 -0
  252. package/src/region-data.ts +428 -0
  253. package/src/regions.test.ts +277 -0
  254. package/src/regions.ts +370 -0
  255. package/src/release-contract.ts +57 -0
  256. package/src/release-manifest.test.ts +293 -0
  257. package/src/release-manifest.ts +430 -0
  258. package/src/run-mode.ts +40 -0
  259. package/src/runtime-secrets.test.ts +627 -0
  260. package/src/runtime-secrets.ts +754 -0
  261. package/src/self-command.test.ts +14 -0
  262. package/src/self-command.ts +21 -0
  263. package/src/self-hosted-compose.test.ts +177 -0
  264. package/src/self-hosted-release.ts +240 -0
  265. package/src/self-hosted-roles.integration.test.ts +161 -0
  266. package/src/self-hosted-roles.test.ts +124 -0
  267. package/src/self-hosted-roles.ts +203 -0
  268. package/src/self-hosted-upgrade.test.ts +394 -0
  269. package/src/self-hosted-upgrade.ts +548 -0
  270. package/src/stack-contract-parity.test.ts +199 -0
  271. package/src/stack-contract.test.ts +391 -0
  272. package/src/stack-contract.ts +888 -0
  273. package/src/stack-converge-secrets.test.ts +421 -0
  274. package/src/stack-converge.test.ts +833 -0
  275. package/src/stack-converge.ts +909 -0
  276. package/src/staging-admin.test.ts +246 -0
  277. package/src/staging-admin.ts +324 -0
  278. package/src/state-attach.test.ts +211 -0
  279. package/src/state-attach.ts +132 -0
  280. package/src/state-backend.test.ts +145 -0
  281. package/src/state-backend.ts +262 -0
  282. package/src/state-command.ts +293 -0
  283. package/src/state-migrate.test.ts +479 -0
  284. package/src/state-migrate.ts +434 -0
  285. package/src/state-pulumi.ts +289 -0
  286. package/src/state-store.test.ts +212 -0
  287. package/src/state-store.ts +243 -0
  288. package/src/upgrade/app-roll.ts +486 -0
  289. package/src/upgrade/backup.ts +109 -0
  290. package/src/upgrade/converge.ts +437 -0
  291. package/src/upgrade/gcp-lookup.ts +167 -0
  292. package/src/upgrade/health.test.ts +279 -0
  293. package/src/upgrade/health.ts +305 -0
  294. package/src/upgrade/release-images.test.ts +119 -0
  295. package/src/upgrade/release-images.ts +391 -0
  296. package/src/upgrade/reporter.ts +46 -0
  297. package/src/upgrade/upgrade.test.ts +626 -0
  298. package/src/upgrade/upgrade.ts +249 -0
  299. package/src/upgrader-assess.test.ts +265 -0
  300. package/src/upgrader-assess.ts +233 -0
  301. package/src/upgrader-gcp.ts +123 -0
  302. package/src/upgrader-job.test.ts +408 -0
  303. package/src/upgrader-job.ts +499 -0
  304. package/src/upgrader-reporter.test.ts +46 -0
  305. package/src/upgrader-reporter.ts +105 -0
  306. package/src/upgrader-setup.test.ts +169 -0
  307. package/src/upgrader-setup.ts +355 -0
  308. package/src/wizard.ts +425 -0
  309. package/src/wizards/gcp-cost.ts +188 -0
  310. package/src/wizards/gcp-firebase.ts +72 -0
  311. package/src/wizards/gcp-install-job.ts +506 -0
  312. package/src/wizards/gcp-prompts.ts +26 -0
  313. package/src/wizards/gcp-pulumi.test.ts +246 -0
  314. package/src/wizards/gcp-pulumi.ts +516 -0
  315. package/src/wizards/gcp.ts +1935 -0
@@ -0,0 +1,86 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ /**
4
+ * The host agent's settings in `.caelo/config.json` (#696 Phase 4c): its
5
+ * database password and the loopback port Postgres is published on for it.
6
+ * Generated once and kept, so re-running `agent install`, `up` or `upgrade`
7
+ * converges onto the same values.
8
+ */
9
+
10
+ import { isAgentDbPort } from "../compose.js";
11
+ import { AGENT_DB_ROLE } from "../self-hosted-roles.js";
12
+ import type { AgentDbConnection } from "./queue.js";
13
+
14
+ /** `config.json` → `agent`. */
15
+ export interface AgentConfig {
16
+ readonly dbPort: number;
17
+ readonly dbPassword: string;
18
+ /**
19
+ * Set by `agent uninstall`: `init` / `upgrade` then leave the agent off
20
+ * until `agent install` is run again.
21
+ */
22
+ readonly disabled?: boolean;
23
+ }
24
+
25
+ /** The first port tried for the agent's Postgres; the next free one is taken. */
26
+ export const AGENT_DB_PORT_DEFAULT = 15432;
27
+ const PORT_CANDIDATES = 50;
28
+
29
+ /** Whether nothing listens on `127.0.0.1:port` (a quick bind test). */
30
+ export function loopbackPortFree(port: number): boolean {
31
+ try {
32
+ const server = Bun.listen({
33
+ hostname: "127.0.0.1",
34
+ port,
35
+ socket: { data() {} },
36
+ });
37
+ server.stop(true);
38
+ return true;
39
+ } catch {
40
+ return false;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * The agent's settings: `existing` unchanged (a running install keeps its
46
+ * port and password), else a fresh password and the first free port from
47
+ * {@link AGENT_DB_PORT_DEFAULT} (or `requestedPort`).
48
+ *
49
+ * @throws when `requestedPort` is not usable or no candidate port is free
50
+ */
51
+ export function agentConfigFor(
52
+ existing: AgentConfig | undefined,
53
+ opts: {
54
+ readonly requestedPort?: number;
55
+ readonly randomSecret: () => string;
56
+ readonly portFree?: (port: number) => boolean;
57
+ },
58
+ ): AgentConfig {
59
+ const free = opts.portFree ?? loopbackPortFree;
60
+ if (existing && (opts.requestedPort === undefined || opts.requestedPort === existing.dbPort)) {
61
+ const { disabled: _disabled, ...kept } = existing;
62
+ return kept;
63
+ }
64
+ if (opts.requestedPort !== undefined) {
65
+ if (!isAgentDbPort(opts.requestedPort)) {
66
+ throw new Error(`--port ${opts.requestedPort} is not a TCP port between 1024 and 65535.`);
67
+ }
68
+ if (!free(opts.requestedPort)) {
69
+ throw new Error(
70
+ `Port ${opts.requestedPort} on 127.0.0.1 is in use; pick another with --port.`,
71
+ );
72
+ }
73
+ return { dbPort: opts.requestedPort, dbPassword: existing?.dbPassword ?? opts.randomSecret() };
74
+ }
75
+ for (let port = AGENT_DB_PORT_DEFAULT; port < AGENT_DB_PORT_DEFAULT + PORT_CANDIDATES; port++) {
76
+ if (free(port)) return { dbPort: port, dbPassword: opts.randomSecret() };
77
+ }
78
+ throw new Error(
79
+ `No free port on 127.0.0.1 between ${AGENT_DB_PORT_DEFAULT} and ${AGENT_DB_PORT_DEFAULT + PORT_CANDIDATES - 1}; pass --port.`,
80
+ );
81
+ }
82
+
83
+ /** The agent's database login. */
84
+ export function agentDbConnection(agent: AgentConfig): AgentDbConnection {
85
+ return { user: AGENT_DB_ROLE, password: agent.dbPassword, port: agent.dbPort };
86
+ }
@@ -0,0 +1,97 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ /** The install's upgrade lock (agent/lock.ts) and the agent's settings (agent/config.ts). */
4
+
5
+ import { afterEach, beforeEach, describe, expect, it } from "bun:test";
6
+ import { existsSync, mkdtempSync, readdirSync, rmSync, utimesSync, writeFileSync } from "node:fs";
7
+ import { tmpdir } from "node:os";
8
+ import { join } from "node:path";
9
+ import { AGENT_DB_PORT_DEFAULT, agentConfigFor, agentDbConnection } from "./config.js";
10
+ import { acquireUpgradeLock, upgradeLockPath } from "./lock.js";
11
+
12
+ describe("acquireUpgradeLock", () => {
13
+ let dir: string;
14
+ beforeEach(() => {
15
+ dir = mkdtempSync(join(tmpdir(), "caelo-lock-"));
16
+ });
17
+ afterEach(() => rmSync(dir, { recursive: true, force: true }));
18
+
19
+ it("lets one holder in and names it to the next", () => {
20
+ const release = acquireUpgradeLock(dir, "the update agent", { pid: 101, alive: () => true });
21
+ expect(() =>
22
+ acquireUpgradeLock(dir, "`cms-provision upgrade`", { pid: 102, alive: () => true }),
23
+ ).toThrow(/the update agent is running on this install \(pid 101/);
24
+ release();
25
+ release();
26
+ expect(existsSync(upgradeLockPath(dir))).toBe(false);
27
+ acquireUpgradeLock(dir, "`cms-provision upgrade`", { pid: 102, alive: () => true })();
28
+ });
29
+
30
+ it("takes over a lock whose process is gone", () => {
31
+ acquireUpgradeLock(dir, "a crashed agent", { pid: 999_999 });
32
+ const release = acquireUpgradeLock(dir, "the update agent", {
33
+ pid: 5,
34
+ alive: (pid) => pid !== 999_999,
35
+ });
36
+ expect(typeof release).toBe("function");
37
+ });
38
+
39
+ it("treats an unreadable lock file as held until it is older than any run", () => {
40
+ writeFileSync(upgradeLockPath(dir), "{not json");
41
+ expect(() => acquireUpgradeLock(dir, "the update agent")).toThrow(/unreadable/);
42
+ const old = new Date(Date.now() - 3 * 60 * 60 * 1000);
43
+ utimesSync(upgradeLockPath(dir), old, old);
44
+ expect(() => acquireUpgradeLock(dir, "the update agent")()).not.toThrow();
45
+ });
46
+
47
+ it("leaves no temp files behind", () => {
48
+ acquireUpgradeLock(dir, "the update agent")();
49
+ expect(readdirSync(dir)).toEqual([]);
50
+ });
51
+ });
52
+
53
+ describe("agentConfigFor", () => {
54
+ const secret = () => "s".repeat(64);
55
+
56
+ it("keeps an existing agent's port and password (and re-enables it)", () => {
57
+ const existing = { dbPort: 15433, dbPassword: "p", disabled: true };
58
+ expect(agentConfigFor(existing, { randomSecret: secret, portFree: () => false })).toEqual({
59
+ dbPort: 15433,
60
+ dbPassword: "p",
61
+ });
62
+ });
63
+
64
+ it("takes the first free loopback port from the default", () => {
65
+ const busy = new Set([AGENT_DB_PORT_DEFAULT, AGENT_DB_PORT_DEFAULT + 1]);
66
+ expect(
67
+ agentConfigFor(undefined, { randomSecret: secret, portFree: (p) => !busy.has(p) }),
68
+ ).toEqual({ dbPort: AGENT_DB_PORT_DEFAULT + 2, dbPassword: secret() });
69
+ });
70
+
71
+ it("checks an explicit port and keeps the password when moving", () => {
72
+ expect(() =>
73
+ agentConfigFor(undefined, { requestedPort: 80, randomSecret: secret, portFree: () => true }),
74
+ ).toThrow(/between 1024 and 65535/);
75
+ expect(() =>
76
+ agentConfigFor(undefined, {
77
+ requestedPort: 20000,
78
+ randomSecret: secret,
79
+ portFree: () => false,
80
+ }),
81
+ ).toThrow(/in use/);
82
+ expect(
83
+ agentConfigFor(
84
+ { dbPort: 15432, dbPassword: "keep" },
85
+ { requestedPort: 20000, randomSecret: secret, portFree: () => true },
86
+ ),
87
+ ).toEqual({ dbPort: 20000, dbPassword: "keep" });
88
+ });
89
+
90
+ it("logs in as caelo_agent", () => {
91
+ expect(agentDbConnection({ dbPort: 15432, dbPassword: "pw" })).toEqual({
92
+ user: "caelo_agent",
93
+ password: "pw",
94
+ port: 15432,
95
+ });
96
+ });
97
+ });
@@ -0,0 +1,110 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ /**
4
+ * One upgrade at a time per self-hosted install (#696 Phase 4c): the host
5
+ * agent and a hand-run `cms-provision upgrade` both take
6
+ * `.caelo/upgrade.lock` for the whole run — two concurrent rolls would
7
+ * overwrite each other's Compose file and restore the wrong one on rollback.
8
+ *
9
+ * The file holds the holder's pid; a lock whose process is gone (a crash, a
10
+ * reboot) is taken over rather than blocking updates forever.
11
+ *
12
+ * The record is written to a private temp file first and then hard-linked
13
+ * into place, so the lock file never exists half-written: a reader that
14
+ * finds it can always trust its pid. (An unreadable file is only taken over
15
+ * once it is older than any run could be.)
16
+ */
17
+
18
+ import { linkSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
19
+ import { join } from "node:path";
20
+
21
+ /** `.caelo/upgrade.lock`. */
22
+ export function upgradeLockPath(caeloDir: string): string {
23
+ return join(caeloDir, "upgrade.lock");
24
+ }
25
+
26
+ interface LockRecord {
27
+ readonly pid: number;
28
+ readonly holder: string;
29
+ readonly at: string;
30
+ }
31
+
32
+ /** An unreadable lock file younger than this still counts as held (longer than any run). */
33
+ const UNREADABLE_LOCK_MAX_AGE_MS = 2 * 60 * 60 * 1000;
34
+
35
+ /** The lock file's modification time (0 when it vanished meanwhile). */
36
+ function lockMtime(path: string): number {
37
+ try {
38
+ return statSync(path).mtimeMs;
39
+ } catch {
40
+ return 0;
41
+ }
42
+ }
43
+
44
+ /** Whether process `pid` exists (signal 0 checks without signalling). */
45
+ function processAlive(pid: number): boolean {
46
+ try {
47
+ process.kill(pid, 0);
48
+ return true;
49
+ } catch (e) {
50
+ // EPERM: it exists, it is just someone else's.
51
+ return (e as NodeJS.ErrnoException).code === "EPERM";
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Take the install's upgrade lock for `holder`.
57
+ *
58
+ * @returns the release function (idempotent)
59
+ * @throws when a live process holds it, naming that holder
60
+ */
61
+ export function acquireUpgradeLock(
62
+ caeloDir: string,
63
+ holder: string,
64
+ deps: { readonly pid?: number; readonly alive?: (pid: number) => boolean } = {},
65
+ ): () => void {
66
+ const path = upgradeLockPath(caeloDir);
67
+ const alive = deps.alive ?? processAlive;
68
+ const record: LockRecord = {
69
+ pid: deps.pid ?? process.pid,
70
+ holder,
71
+ at: new Date().toISOString(),
72
+ };
73
+ const temp = `${path}.${record.pid}.${crypto.randomUUID()}`;
74
+ writeFileSync(temp, JSON.stringify(record), { mode: 0o600, flag: "wx" });
75
+ try {
76
+ for (let attempt = 0; attempt < 2; attempt++) {
77
+ try {
78
+ // Atomic: fails with EEXIST when someone holds the lock.
79
+ linkSync(temp, path);
80
+ let released = false;
81
+ return () => {
82
+ if (released) return;
83
+ released = true;
84
+ rmSync(path, { force: true });
85
+ };
86
+ } catch (e) {
87
+ if ((e as NodeJS.ErrnoException).code !== "EEXIST") throw e;
88
+ }
89
+ let held: Partial<LockRecord> | null = null;
90
+ try {
91
+ held = JSON.parse(readFileSync(path, "utf8")) as Partial<LockRecord>;
92
+ } catch {
93
+ // Corrupt (never half-written, see above): take it over only when old.
94
+ }
95
+ if (held === null) {
96
+ if (Date.now() - lockMtime(path) < UNREADABLE_LOCK_MAX_AGE_MS) {
97
+ throw new Error(`another upgrade holds ${path} (its record is unreadable).`);
98
+ }
99
+ } else if (typeof held.pid === "number" && alive(held.pid)) {
100
+ throw new Error(
101
+ `${held.holder ?? "another upgrade"} is running on this install (pid ${held.pid}, since ${held.at ?? "?"}).`,
102
+ );
103
+ }
104
+ rmSync(path, { force: true });
105
+ }
106
+ } finally {
107
+ rmSync(temp, { force: true });
108
+ }
109
+ throw new Error(`could not take ${path}; another process keeps creating it.`);
110
+ }
@@ -0,0 +1,10 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ import { describe, expect, it } from "bun:test";
4
+ import { loadAgentQueueOpener } from "./queue.js";
5
+
6
+ describe("loadAgentQueueOpener", () => {
7
+ it("finds the agent's database module (the bundle, or the workspace package)", async () => {
8
+ expect(typeof (await loadAgentQueueOpener())).toBe("function");
9
+ });
10
+ });
@@ -0,0 +1,91 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ /**
4
+ * The self-hosted host agent's database side (#696 Phase 4c): the same
5
+ * {@link UpdateQueue} the Google Cloud upgrader job drives, plus the
6
+ * heartbeat the admin reads to decide whether to offer "Install update".
7
+ *
8
+ * The queue is implemented in `@caelo-cms/platform-updater` (the job ops,
9
+ * through the Query API — never raw SQL), which the published CLI cannot
10
+ * depend on: it is private, and it depends on this package. So the build
11
+ * bundles its `agent-db` entry into `dist/agent-db.js` (package.json
12
+ * `build`), and {@link loadAgentQueueOpener} imports that file; a source
13
+ * checkout imports the workspace package instead.
14
+ */
15
+
16
+ import { existsSync } from "node:fs";
17
+ import { fileURLToPath } from "node:url";
18
+ import type { UpdateQueue } from "../upgrader-job.js";
19
+
20
+ /** One check-in of the agent (`platform_update_agent`). */
21
+ export interface AgentHeartbeat {
22
+ /** The Caelo release of the agent's own code. */
23
+ readonly agentVersion: string;
24
+ /** The host it runs on. */
25
+ readonly host: string;
26
+ /** Why an install would fail on this host right now; null when none. */
27
+ readonly problem: string | null;
28
+ }
29
+
30
+ /** The upgrader's queue plus the agent's heartbeat. */
31
+ export interface AgentQueue extends UpdateQueue {
32
+ heartbeat(beat: AgentHeartbeat): Promise<void>;
33
+ }
34
+
35
+ /** Where the agent's database login reaches Postgres (loopback only). */
36
+ export interface AgentDbConnection {
37
+ readonly user: string;
38
+ readonly password: string;
39
+ readonly port: number;
40
+ }
41
+
42
+ /** An open queue and the pools behind it. */
43
+ export interface OpenedAgentQueue {
44
+ readonly queue: AgentQueue;
45
+ close(): Promise<void>;
46
+ }
47
+
48
+ /** Opens the queue over the agent's login (`@caelo-cms/platform-updater` agent-db). */
49
+ export type OpenAgentQueue = (conn: AgentDbConnection) => Promise<OpenedAgentQueue>;
50
+
51
+ /**
52
+ * Where the bundle is, relative to this module: next to the compiled CLI
53
+ * (`dist/agent/queue.js` → `dist/agent-db.js`), or — when Bun loads the
54
+ * published package's `src/` through its `bun` export condition — in the
55
+ * package's `dist/`.
56
+ */
57
+ const BUNDLED_AGENT_DB = ["../agent-db.js", "../../dist/agent-db.js"] as const;
58
+
59
+ /**
60
+ * The workspace package a source checkout loads instead. Held in a variable
61
+ * so TypeScript does not resolve it: provisioning cannot declare a
62
+ * dependency on the package that depends on it.
63
+ */
64
+ const WORKSPACE_AGENT_DB = "@caelo-cms/platform-updater/agent-db";
65
+
66
+ /**
67
+ * Load the function that opens the agent's queue: the bundle shipped in the
68
+ * published package, else (a source checkout) the workspace package.
69
+ *
70
+ * @throws when neither is present — a build that skipped the bundle
71
+ */
72
+ export async function loadAgentQueueOpener(): Promise<OpenAgentQueue> {
73
+ const bundled = BUNDLED_AGENT_DB.map((rel) => new URL(rel, import.meta.url)).find((url) =>
74
+ existsSync(fileURLToPath(url)),
75
+ );
76
+ const specifier = bundled?.href ?? WORKSPACE_AGENT_DB;
77
+ let mod: { openAgentQueue?: OpenAgentQueue };
78
+ try {
79
+ mod = (await import(specifier)) as { openAgentQueue?: OpenAgentQueue };
80
+ } catch (e) {
81
+ throw new Error(
82
+ `the update agent's database module is missing (${specifier}: ${e instanceof Error ? e.message : String(e)}). This @caelo-cms/provisioning build is incomplete — reinstall it.`,
83
+ );
84
+ }
85
+ if (typeof mod.openAgentQueue !== "function") {
86
+ throw new Error(
87
+ `${specifier} does not export openAgentQueue — reinstall @caelo-cms/provisioning.`,
88
+ );
89
+ }
90
+ return mod.openAgentQueue;
91
+ }