@caelo-cms/provisioning 0.10.49 → 0.10.50
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 +4 -0
- package/caddy/Caddyfile.staging +4 -0
- package/dist/agent-db.js +4 -0
- package/dist/caddy-redirects.d.ts +15 -0
- package/dist/caddy-redirects.d.ts.map +1 -0
- package/dist/caddy-redirects.js +65 -0
- package/dist/caddy-redirects.js.map +1 -0
- package/dist/caddy.d.ts +22 -2
- package/dist/caddy.d.ts.map +1 -1
- package/dist/caddy.js +66 -17
- package/dist/caddy.js.map +1 -1
- package/dist/cli-routing.d.ts.map +1 -1
- package/dist/cli-routing.js +7 -0
- package/dist/cli-routing.js.map +1 -1
- package/dist/cli.js +248 -254
- package/dist/cli.js.map +1 -1
- package/dist/compose.d.ts +54 -2
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +86 -32
- package/dist/compose.js.map +1 -1
- package/dist/gcp-names.d.ts +12 -0
- package/dist/gcp-names.d.ts.map +1 -1
- package/dist/gcp-names.js +21 -0
- package/dist/gcp-names.js.map +1 -1
- package/dist/gcp-url-map.d.ts +27 -0
- package/dist/gcp-url-map.d.ts.map +1 -0
- package/dist/gcp-url-map.js +62 -0
- package/dist/gcp-url-map.js.map +1 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/infra-contract.d.ts +20 -1
- package/dist/infra-contract.d.ts.map +1 -1
- package/dist/infra-contract.js +39 -1
- package/dist/infra-contract.js.map +1 -1
- package/dist/public-role-isolation.d.ts +19 -0
- package/dist/public-role-isolation.d.ts.map +1 -0
- package/dist/public-role-isolation.js +69 -0
- package/dist/public-role-isolation.js.map +1 -0
- package/dist/release-contract.d.ts.map +1 -1
- package/dist/release-contract.js +3 -0
- package/dist/release-contract.js.map +1 -1
- package/dist/release-manifest.d.ts.map +1 -1
- package/dist/release-manifest.js +2 -1
- package/dist/release-manifest.js.map +1 -1
- package/dist/self-hosted-backup.d.ts +86 -0
- package/dist/self-hosted-backup.d.ts.map +1 -0
- package/dist/self-hosted-backup.js +350 -0
- package/dist/self-hosted-backup.js.map +1 -0
- package/dist/self-hosted-release.d.ts +12 -3
- package/dist/self-hosted-release.d.ts.map +1 -1
- package/dist/self-hosted-release.js +6 -4
- package/dist/self-hosted-release.js.map +1 -1
- package/dist/self-hosted-start.d.ts +50 -0
- package/dist/self-hosted-start.d.ts.map +1 -0
- package/dist/self-hosted-start.js +59 -0
- package/dist/self-hosted-start.js.map +1 -0
- package/dist/self-hosted-upgrade.d.ts +30 -0
- package/dist/self-hosted-upgrade.d.ts.map +1 -1
- package/dist/self-hosted-upgrade.js +29 -7
- package/dist/self-hosted-upgrade.js.map +1 -1
- package/dist/tar-entries.d.ts +14 -0
- package/dist/tar-entries.d.ts.map +1 -0
- package/dist/tar-entries.js +104 -0
- package/dist/tar-entries.js.map +1 -0
- package/dist/upgrade/converge.d.ts +2 -2
- package/dist/upgrade/converge.d.ts.map +1 -1
- package/dist/upgrade/converge.js +44 -2
- package/dist/upgrade/converge.js.map +1 -1
- package/dist/upgrader-assess.d.ts.map +1 -1
- package/dist/upgrader-assess.js +2 -1
- package/dist/upgrader-assess.js.map +1 -1
- package/dist/url-map-redirects-access.d.ts +15 -0
- package/dist/url-map-redirects-access.d.ts.map +1 -0
- package/dist/url-map-redirects-access.js +81 -0
- package/dist/url-map-redirects-access.js.map +1 -0
- package/dist/wizard.d.ts.map +1 -1
- package/dist/wizard.js +3 -1
- package/dist/wizard.js.map +1 -1
- package/dist/wizards/gcp.d.ts.map +1 -1
- package/dist/wizards/gcp.js +45 -0
- package/dist/wizards/gcp.js.map +1 -1
- package/package.json +2 -2
- package/src/admin-memory.test.ts +2 -2
- package/src/caddy-redirects.docker.test.ts +255 -0
- package/src/caddy-redirects.test.ts +115 -0
- package/src/caddy-redirects.ts +68 -0
- package/src/caddy-validate.test.ts +119 -0
- package/src/caddy.test.ts +108 -0
- package/src/caddy.ts +89 -20
- package/src/cli-routing.test.ts +12 -0
- package/src/cli-routing.ts +7 -0
- package/src/cli.ts +299 -287
- package/src/compose.ts +106 -34
- package/src/gcp-names.ts +23 -0
- package/src/gcp-url-map.test.ts +150 -0
- package/src/gcp-url-map.ts +81 -0
- package/src/index.test.ts +18 -146
- package/src/index.ts +0 -9
- package/src/infra-contract.test.ts +39 -0
- package/src/infra-contract.ts +42 -1
- package/src/public-role-isolation.test.ts +89 -0
- package/src/public-role-isolation.ts +83 -0
- package/src/release-contract.ts +3 -0
- package/src/release-manifest.test.ts +12 -0
- package/src/release-manifest.ts +2 -1
- package/src/self-hosted-backup.test.ts +344 -0
- package/src/self-hosted-backup.ts +480 -0
- package/src/self-hosted-compose.test.ts +88 -4
- package/src/self-hosted-release.ts +22 -7
- package/src/self-hosted-start.test.ts +68 -0
- package/src/self-hosted-start.ts +123 -0
- package/src/self-hosted-upgrade.ts +35 -8
- package/src/stack-contract.test.ts +2 -2
- package/src/tar-entries.test.ts +52 -0
- package/src/tar-entries.ts +116 -0
- package/src/upgrade/converge.ts +51 -4
- package/src/upgrade/upgrade.test.ts +78 -1
- package/src/upgrader-assess.test.ts +22 -0
- package/src/upgrader-assess.ts +2 -1
- package/src/url-map-redirects-access.test.ts +88 -0
- package/src/url-map-redirects-access.ts +89 -0
- package/src/wizard.ts +3 -1
- package/src/wizards/gcp.ts +48 -0
- package/stacks/aws/index.ts +34 -2
- package/stacks/azure/index.ts +13 -0
- package/stacks/gcp/index.ts +24 -17
- package/stacks/gcp-firebase/index.ts +7 -0
- package/stacks/self-hosted/Pulumi.yaml +2 -2
- package/stacks/self-hosted/README.md +3 -3
- package/stacks/self-hosted/index.ts +45 -22
- package/dist/redirects-emit.d.ts +0 -52
- package/dist/redirects-emit.d.ts.map +0 -1
- package/dist/redirects-emit.js +0 -111
- package/dist/redirects-emit.js.map +0 -1
- package/src/redirects-emit.ts +0 -175
package/src/cli.ts
CHANGED
|
@@ -9,10 +9,14 @@
|
|
|
9
9
|
* init [--domain D --owner-email E] — first-time setup; generates
|
|
10
10
|
* secrets, resolves + cosign-verifies
|
|
11
11
|
* the release images, writes .caelo/,
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* starts the stack (migrations
|
|
13
|
+
* included), health-checks it and
|
|
14
|
+
* prints the bootstrap token URL.
|
|
15
|
+
* `--provider self-hosted` with no
|
|
16
|
+
* sub-command runs this. `--dev` runs
|
|
17
|
+
* a Caelo checkout (contributors).
|
|
18
|
+
* up — re-emits from the existing config
|
|
19
|
+
* and converges the running stack.
|
|
16
20
|
* migrate — applies DB migrations with the
|
|
17
21
|
* admin image the install runs.
|
|
18
22
|
* upgrade [--version X] — verified, backed-up, health-checked
|
|
@@ -22,9 +26,10 @@
|
|
|
22
26
|
* the Owner starts in the admin.
|
|
23
27
|
* regenerate-caddy — re-emits Caddyfile from the
|
|
24
28
|
* domains table; runs `caddy reload`.
|
|
25
|
-
* backup --to <path>
|
|
26
|
-
*
|
|
27
|
-
* restore --from <path>
|
|
29
|
+
* backup [--to <path>] — pg_dump of both databases + the
|
|
30
|
+
* site + media files → one .tar.gz.
|
|
31
|
+
* restore --from <path> --yes — replaces databases + files with
|
|
32
|
+
* a backup, restarts, health-checks.
|
|
28
33
|
* status — prints container health + cert
|
|
29
34
|
* expiry per domain.
|
|
30
35
|
*
|
|
@@ -43,14 +48,28 @@ import {
|
|
|
43
48
|
import type { AgentCliContext } from "./agent/command.js";
|
|
44
49
|
import type { AgentConfig } from "./agent/config.js";
|
|
45
50
|
import { generateBootstrapToken } from "./bootstrap-token.js";
|
|
46
|
-
import {
|
|
51
|
+
import {
|
|
52
|
+
type CaddyDomainSpec,
|
|
53
|
+
type CaddyTlsMode,
|
|
54
|
+
generateCaddyfile,
|
|
55
|
+
selfHostedAdminHost,
|
|
56
|
+
selfHostedSeedDomains,
|
|
57
|
+
} from "./caddy.js";
|
|
47
58
|
import { redactCredentials } from "./checkpoint-secrets.js";
|
|
48
59
|
import { initDelegatesToWizard, resolveCliRoute } from "./cli-routing.js";
|
|
49
60
|
import {
|
|
50
61
|
BOOTSTRAP_DIR,
|
|
62
|
+
COMPOSE_ADMIN_UPSTREAM,
|
|
63
|
+
COMPOSE_GATEWAY_UPSTREAM,
|
|
51
64
|
type ComposeImages,
|
|
65
|
+
composeProjectName,
|
|
52
66
|
generateDockerCompose,
|
|
67
|
+
isPublishablePort,
|
|
68
|
+
MEDIA_DIR,
|
|
69
|
+
type PublishedPorts,
|
|
53
70
|
type RolePasswords,
|
|
71
|
+
SITE_OUTPUT_ROOT,
|
|
72
|
+
selfHostedSiteUrl,
|
|
54
73
|
} from "./compose.js";
|
|
55
74
|
import { selfCommand } from "./self-command.js";
|
|
56
75
|
import {
|
|
@@ -76,8 +95,6 @@ interface CaeloConfig {
|
|
|
76
95
|
stagingDomain?: string;
|
|
77
96
|
ownerEmail: string;
|
|
78
97
|
postgresPassword: string;
|
|
79
|
-
minioRootUser: string;
|
|
80
|
-
minioRootPassword: string;
|
|
81
98
|
/**
|
|
82
99
|
* P18 — project KEK (32 hex bytes). Auto-generated on `cms-provision init`,
|
|
83
100
|
* persisted in `.caelo/config.json`, mounted into the admin + gateway
|
|
@@ -111,6 +128,24 @@ interface CaeloConfig {
|
|
|
111
128
|
* its database password and the loopback port Postgres is published on.
|
|
112
129
|
*/
|
|
113
130
|
agent?: AgentConfig;
|
|
131
|
+
/**
|
|
132
|
+
* #707 — how Caddy gets certificates (`init --tls internal` for a LAN or
|
|
133
|
+
* evaluation install without public DNS). Optional so older config.json
|
|
134
|
+
* files load; loadConfig() back-fills `acme`, what they always ran.
|
|
135
|
+
*/
|
|
136
|
+
tls?: CaddyTlsMode;
|
|
137
|
+
/**
|
|
138
|
+
* #707 — the host ports Caddy publishes (`init --http-port/--https-port`).
|
|
139
|
+
* Optional so older config.json files load; loadConfig() back-fills 80/443.
|
|
140
|
+
*/
|
|
141
|
+
ports?: PublishedPorts;
|
|
142
|
+
/**
|
|
143
|
+
* #707 — the Compose project (`name:` in the Compose file). Optional so
|
|
144
|
+
* older config.json files load; loadConfig() back-fills `caelo`, the
|
|
145
|
+
* project name Compose derived from `.caelo/` for them (their volumes
|
|
146
|
+
* carry it).
|
|
147
|
+
*/
|
|
148
|
+
composeProject?: string;
|
|
114
149
|
}
|
|
115
150
|
|
|
116
151
|
const REPO_ROOT = resolve(import.meta.dir, "../../..");
|
|
@@ -189,7 +224,17 @@ function randomSecret(bytes = 32): string {
|
|
|
189
224
|
|
|
190
225
|
function loadConfig(): CaeloConfig | null {
|
|
191
226
|
if (!existsSync(CONFIG_PATH)) return null;
|
|
192
|
-
|
|
227
|
+
const cfg = JSON.parse(readFileSync(CONFIG_PATH, "utf8")) as CaeloConfig;
|
|
228
|
+
// #707 — settings added later, back-filled (and stored) with what every
|
|
229
|
+
// install ran before they existed, so `upgrade` / `agent` render the same
|
|
230
|
+
// Compose file as before.
|
|
231
|
+
if (!cfg.tls || !cfg.ports || !cfg.composeProject) {
|
|
232
|
+
cfg.tls ??= "acme";
|
|
233
|
+
cfg.ports ??= { http: 80, https: 443 };
|
|
234
|
+
cfg.composeProject ??= "caelo";
|
|
235
|
+
saveConfig(cfg);
|
|
236
|
+
}
|
|
237
|
+
return cfg;
|
|
193
238
|
}
|
|
194
239
|
|
|
195
240
|
function saveConfig(c: CaeloConfig): void {
|
|
@@ -220,22 +265,24 @@ function emitConfig(cfg: CaeloConfig, extraDomains: CaddyDomainSpec[] = []): voi
|
|
|
220
265
|
cfg.stagingDomain = defaultStagingHost(cfg.domain);
|
|
221
266
|
saveConfig(cfg);
|
|
222
267
|
}
|
|
268
|
+
if (!cfg.tls) throw new Error("config has no tls — it is back-filled on load; this is a bug");
|
|
223
269
|
emitComposeFiles(cfg);
|
|
224
|
-
// Seed the install with the
|
|
225
|
-
//
|
|
270
|
+
// Seed the install with the admin host, the primary domain and the
|
|
271
|
+
// staging host. Extra domains added via /settings/domains are appended by
|
|
226
272
|
// regenerate-caddy when it queries the live domains table.
|
|
227
|
-
const baseDomains: CaddyDomainSpec[] = [
|
|
228
|
-
{ hostname: cfg.domain, kind: "admin", env: "production" },
|
|
229
|
-
{ hostname: cfg.domain, kind: "public", env: "production" },
|
|
230
|
-
{ hostname: cfg.stagingDomain, kind: "public", env: "staging" },
|
|
231
|
-
];
|
|
232
273
|
const caddy = generateCaddyfile({
|
|
233
274
|
ownerEmail: cfg.ownerEmail,
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
275
|
+
tls: cfg.tls,
|
|
276
|
+
publicSiteRoot: `${SITE_OUTPUT_ROOT}/output/production/current`,
|
|
277
|
+
stagingSiteRoot: `${SITE_OUTPUT_ROOT}/output/staging/current`,
|
|
278
|
+
// Caddy runs in its own container: the Compose service names, never
|
|
279
|
+
// localhost (#707).
|
|
280
|
+
adminUpstream: COMPOSE_ADMIN_UPSTREAM,
|
|
281
|
+
gatewayUpstream: COMPOSE_GATEWAY_UPSTREAM,
|
|
282
|
+
domains: dedupeDomains([
|
|
283
|
+
...selfHostedSeedDomains(cfg.domain, cfg.stagingDomain),
|
|
284
|
+
...extraDomains,
|
|
285
|
+
]),
|
|
239
286
|
});
|
|
240
287
|
writeFileSync(CADDYFILE_PATH, caddy);
|
|
241
288
|
}
|
|
@@ -254,6 +301,9 @@ function emitComposeFiles(cfg: CaeloConfig): void {
|
|
|
254
301
|
selfHostedBootstrapScript({ agent: activeAgent(cfg) !== undefined }),
|
|
255
302
|
{ mode: 0o755 },
|
|
256
303
|
);
|
|
304
|
+
// The bind-mounted content dirs, created by the CLI so Docker does not
|
|
305
|
+
// create them root-owned on a Linux host.
|
|
306
|
+
for (const dir of ["output", MEDIA_DIR]) mkdirSync(resolve(CAELO_DIR, dir), { recursive: true });
|
|
257
307
|
}
|
|
258
308
|
|
|
259
309
|
/** The Compose file for `cfg` running `images` (secrets come from config.json). */
|
|
@@ -263,17 +313,22 @@ function renderCompose(cfg: CaeloConfig, images: ComposeImages): string {
|
|
|
263
313
|
throw new Error("config has no rolePasswords — run `cms-provision up`");
|
|
264
314
|
}
|
|
265
315
|
if (!cfg.caeloSecretKek) throw new Error("config has no caeloSecretKek — run `cms-provision up`");
|
|
316
|
+
if (!cfg.ports || !cfg.composeProject) {
|
|
317
|
+
throw new Error(
|
|
318
|
+
"config has no ports / composeProject — they are back-filled on load; this is a bug",
|
|
319
|
+
);
|
|
320
|
+
}
|
|
266
321
|
return generateDockerCompose({
|
|
267
322
|
domain: cfg.domain,
|
|
268
323
|
postgresPassword: cfg.postgresPassword,
|
|
269
324
|
rolePasswords: cfg.rolePasswords,
|
|
270
|
-
minioRootUser: cfg.minioRootUser,
|
|
271
|
-
minioRootPassword: cfg.minioRootPassword,
|
|
272
325
|
caeloSecretKek: cfg.caeloSecretKek,
|
|
273
326
|
anthropicApiKey: cfg.anthropicApiKey,
|
|
274
327
|
resendApiKey: cfg.resendApiKey,
|
|
275
328
|
diskSize: "20Gi",
|
|
276
329
|
images,
|
|
330
|
+
ports: cfg.ports,
|
|
331
|
+
project: cfg.composeProject,
|
|
277
332
|
...(activeAgent(cfg) ? { agent: activeAgent(cfg) } : {}),
|
|
278
333
|
});
|
|
279
334
|
}
|
|
@@ -347,21 +402,85 @@ function installImages(cfg: CaeloConfig): ComposeImages {
|
|
|
347
402
|
}
|
|
348
403
|
|
|
349
404
|
function dedupeDomains(list: CaddyDomainSpec[]): CaddyDomainSpec[] {
|
|
350
|
-
//
|
|
351
|
-
// first occurrence
|
|
352
|
-
// collision (which keeps admin routing intact even if someone adds
|
|
353
|
-
//
|
|
405
|
+
// One vhost per hostname (Caddy refuses two site blocks for one address):
|
|
406
|
+
// the first occurrence wins, so the seed wins over operator additions on
|
|
407
|
+
// a collision (which keeps admin routing intact even if someone adds the
|
|
408
|
+
// admin host via the UI as a public domain by mistake).
|
|
354
409
|
const seen = new Set<string>();
|
|
355
410
|
const out: CaddyDomainSpec[] = [];
|
|
356
411
|
for (const d of list) {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
seen.add(key);
|
|
412
|
+
if (seen.has(d.hostname)) continue;
|
|
413
|
+
seen.add(d.hostname);
|
|
360
414
|
out.push(d);
|
|
361
415
|
}
|
|
362
416
|
return out;
|
|
363
417
|
}
|
|
364
418
|
|
|
419
|
+
/** `--http-port` / `--https-port` (default 80 / 443), or exit with a usage error. */
|
|
420
|
+
function portsArg(): PublishedPorts {
|
|
421
|
+
const parse = (name: string, fallback: number): number => {
|
|
422
|
+
const raw = arg(name);
|
|
423
|
+
if (raw === undefined) return fallback;
|
|
424
|
+
const port = Number(raw);
|
|
425
|
+
if (!isPublishablePort(port)) {
|
|
426
|
+
console.error(`--${name}: ${JSON.stringify(raw)} is not a TCP port`);
|
|
427
|
+
process.exit(2);
|
|
428
|
+
}
|
|
429
|
+
return port;
|
|
430
|
+
};
|
|
431
|
+
return { http: parse("http-port", 80), https: parse("https-port", 443) };
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** `--tls acme|internal` (default acme), or exit with a usage error. */
|
|
435
|
+
function tlsArg(): CaddyTlsMode {
|
|
436
|
+
const raw = arg("tls") ?? "acme";
|
|
437
|
+
if (raw !== "acme" && raw !== "internal") {
|
|
438
|
+
console.error(
|
|
439
|
+
`--tls: ${JSON.stringify(raw)} — choose acme (public DNS) or internal (local CA)`,
|
|
440
|
+
);
|
|
441
|
+
process.exit(2);
|
|
442
|
+
}
|
|
443
|
+
return raw;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** `https://<host>[:port]` for a host Caddy serves. */
|
|
447
|
+
function siteUrl(cfg: CaeloConfig, host: string): string {
|
|
448
|
+
if (!cfg.ports)
|
|
449
|
+
throw new Error("config has no ports — they are back-filled on load; this is a bug");
|
|
450
|
+
return selfHostedSiteUrl(host, cfg.ports.https);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Re-render the Caddyfile from config + the domains table and reload the
|
|
455
|
+
* running Caddy (regenerate-caddy, restore).
|
|
456
|
+
*/
|
|
457
|
+
async function refreshCaddy(
|
|
458
|
+
cfg: CaeloConfig,
|
|
459
|
+
): Promise<{ ok: true } | { ok: false; error: string }> {
|
|
460
|
+
emitConfig(cfg, await fetchExtraDomains());
|
|
461
|
+
const { reloadCaddy } = await import("./self-hosted-start.js");
|
|
462
|
+
return reloadCaddy(CAELO_DIR, bunHostRunner);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Pull, start Postgres, migrate, start everything, health-check
|
|
467
|
+
* (self-hosted-start.ts). Exits non-zero with the reason on failure; the
|
|
468
|
+
* operator re-runs `up`, which converges from wherever it stopped.
|
|
469
|
+
*/
|
|
470
|
+
async function startStack(): Promise<void> {
|
|
471
|
+
const { startSelfHosted } = await import("./self-hosted-start.js");
|
|
472
|
+
const r = await startSelfHosted({
|
|
473
|
+
caeloDir: CAELO_DIR,
|
|
474
|
+
runner: bunHostRunner,
|
|
475
|
+
log: (m) => console.log(`==> ${m}`),
|
|
476
|
+
});
|
|
477
|
+
if (!r.ok) {
|
|
478
|
+
console.error(`\nThe install did not come up (${r.stage}): ${r.error}`);
|
|
479
|
+
console.error(`Fix the cause and run \`${selfCommand("up")}\` — it continues from here.`);
|
|
480
|
+
process.exit(1);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
365
484
|
async function init(): Promise<void> {
|
|
366
485
|
const domain = arg("domain");
|
|
367
486
|
const ownerEmail = arg("owner-email");
|
|
@@ -382,7 +501,7 @@ async function init(): Promise<void> {
|
|
|
382
501
|
}
|
|
383
502
|
if (!domain || !ownerEmail) {
|
|
384
503
|
console.error(
|
|
385
|
-
"Usage: cms-provision init [--provider self-hosted] --domain example.com [--staging-domain staging.example.com] --owner-email me@example.com [--version x.y.z] [--skip-verify] [--dev]\n" +
|
|
504
|
+
"Usage: cms-provision init [--provider self-hosted] --domain example.com [--staging-domain staging.example.com] --owner-email me@example.com [--version x.y.z] [--skip-verify] [--dev] [--tls acme|internal] [--http-port 80] [--https-port 443]\n" +
|
|
386
505
|
"Cloud providers (gcp | gcp-firebase | aws | azure) run the wizard: cms-provision --provider <name>",
|
|
387
506
|
);
|
|
388
507
|
process.exit(2);
|
|
@@ -391,6 +510,12 @@ async function init(): Promise<void> {
|
|
|
391
510
|
console.error(`config already exists at ${CONFIG_PATH}; run \`cms-provision up\` to re-deploy`);
|
|
392
511
|
process.exit(2);
|
|
393
512
|
}
|
|
513
|
+
const tls = tlsArg();
|
|
514
|
+
const ports = portsArg();
|
|
515
|
+
if (ports.http === ports.https) {
|
|
516
|
+
console.error("--http-port and --https-port must differ");
|
|
517
|
+
process.exit(2);
|
|
518
|
+
}
|
|
394
519
|
const stagingArg = arg("staging-domain");
|
|
395
520
|
const stagingProblem = stagingArg
|
|
396
521
|
? stagingHostProblem(stagingArg, {
|
|
@@ -439,12 +564,13 @@ async function init(): Promise<void> {
|
|
|
439
564
|
rolePasswords: newRolePasswords(),
|
|
440
565
|
// A fresh volume: bootstrap.sh creates the roles with these on first start.
|
|
441
566
|
rolesConverged: true,
|
|
442
|
-
minioRootUser: "caelo",
|
|
443
|
-
minioRootPassword: randomSecret(32),
|
|
444
567
|
caeloSecretKek: randomSecret(32),
|
|
445
568
|
anthropicApiKey: process.env.ANTHROPIC_API_KEY,
|
|
446
569
|
resendApiKey: process.env.RESEND_API_KEY,
|
|
447
570
|
imageMode: dev ? "dev" : "release",
|
|
571
|
+
tls,
|
|
572
|
+
ports,
|
|
573
|
+
composeProject: composeProjectName(domain),
|
|
448
574
|
};
|
|
449
575
|
saveConfig(cfg);
|
|
450
576
|
if (images.mode === "release") {
|
|
@@ -486,21 +612,36 @@ async function init(): Promise<void> {
|
|
|
486
612
|
console.log(`Wrote ${COMPOSE_PATH}`);
|
|
487
613
|
console.log(`Wrote ${CADDYFILE_PATH}`);
|
|
488
614
|
console.log(`Wrote ${PENDING_TOKEN_PATH} (admin will insert on first boot)`);
|
|
489
|
-
console.log("");
|
|
490
615
|
console.log(
|
|
491
616
|
images.mode === "release"
|
|
492
617
|
? `Images: Caelo ${version}, pinned by digest (${images.admin.split("@")[1]?.slice(0, 19)}…)`
|
|
493
618
|
: "Images: DEV MODE — admin + gateway run this checkout (contributors only).",
|
|
494
619
|
);
|
|
620
|
+
// Printed BEFORE the start: the admin consumes pending-token.json on its
|
|
621
|
+
// first boot, so a start that fails half-way and is resumed with `up`
|
|
622
|
+
// would otherwise leave the operator without the link.
|
|
623
|
+
const adminHost = selfHostedAdminHost(domain);
|
|
624
|
+
const setupUrl = `${siteUrl(cfg, adminHost)}/setup?token=${tok.token}`;
|
|
495
625
|
console.log("");
|
|
496
|
-
console.log(
|
|
497
|
-
console.log(`
|
|
498
|
-
console.log(` ${selfCommand("migrate")} # applies the database migrations`);
|
|
499
|
-
console.log(` docker compose -f ${COMPOSE_PATH} up -d`);
|
|
500
|
-
console.log(" # Wait ~30s for Caddy + ACME");
|
|
626
|
+
console.log(`Owner setup link (save it; valid until ${tok.expiresAt}):`);
|
|
627
|
+
console.log(` ${setupUrl}`);
|
|
501
628
|
console.log("");
|
|
502
|
-
|
|
503
|
-
|
|
629
|
+
// #707 / §11.C — one command ends with a serving install, not with steps
|
|
630
|
+
// to copy.
|
|
631
|
+
await startStack();
|
|
632
|
+
console.log("");
|
|
633
|
+
console.log("Caelo is running. Point these DNS records at this host (A/AAAA):");
|
|
634
|
+
for (const host of [adminHost, domain, cfg.stagingDomain ?? defaultStagingHost(domain)]) {
|
|
635
|
+
console.log(` ${host}`);
|
|
636
|
+
}
|
|
637
|
+
console.log(
|
|
638
|
+
tls === "acme"
|
|
639
|
+
? "Caddy fetches the certificates once they resolve (ports 80 + 443 must be reachable)."
|
|
640
|
+
: "TLS: Caddy's local CA (--tls internal) — browsers warn until its root is trusted.",
|
|
641
|
+
);
|
|
642
|
+
console.log("");
|
|
643
|
+
console.log("Then create the Owner account:");
|
|
644
|
+
console.log(` ${setupUrl}`);
|
|
504
645
|
console.log(` (token expires ${tok.expiresAt})`);
|
|
505
646
|
if (agentSetup) {
|
|
506
647
|
console.log("");
|
|
@@ -515,6 +656,15 @@ async function up(): Promise<void> {
|
|
|
515
656
|
console.error("no config — run `cms-provision init` first");
|
|
516
657
|
process.exit(2);
|
|
517
658
|
}
|
|
659
|
+
// Postgres runs on the CURRENT Compose file first, so the roles can be
|
|
660
|
+
// converged and the operator-added domains read before anything is
|
|
661
|
+
// re-emitted (a failure here surfaces through the convergence below).
|
|
662
|
+
if (existsSync(COMPOSE_PATH)) {
|
|
663
|
+
await bunHostRunner.run(
|
|
664
|
+
["docker", "compose", "-f", COMPOSE_PATH, "up", "-d", "--wait", "postgres"],
|
|
665
|
+
{ inherit: true },
|
|
666
|
+
);
|
|
667
|
+
}
|
|
518
668
|
// #613 — the database roles first, then the Compose URLs that use them.
|
|
519
669
|
if (!cfg.rolePasswords) {
|
|
520
670
|
cfg.rolePasswords = newRolePasswords();
|
|
@@ -538,9 +688,10 @@ async function up(): Promise<void> {
|
|
|
538
688
|
cfg.rolesConverged = true;
|
|
539
689
|
saveConfig(cfg);
|
|
540
690
|
}
|
|
541
|
-
emitConfig(cfg, await
|
|
691
|
+
emitConfig(cfg, await fetchExtraDomains());
|
|
542
692
|
console.log(`Re-emitted ${COMPOSE_PATH} + ${CADDYFILE_PATH}`);
|
|
543
|
-
|
|
693
|
+
await startStack();
|
|
694
|
+
console.log(`\nCaelo is running: ${siteUrl(cfg, selfHostedAdminHost(cfg.domain))}`);
|
|
544
695
|
}
|
|
545
696
|
|
|
546
697
|
/** Whether the install's Postgres container answers (with the CURRENT Compose file). */
|
|
@@ -590,71 +741,63 @@ async function superuserPsql(script: string): Promise<{ exitCode: number; stderr
|
|
|
590
741
|
return { exitCode: await proc.exited, stderr };
|
|
591
742
|
}
|
|
592
743
|
|
|
593
|
-
interface DomainRow {
|
|
594
|
-
hostname: string;
|
|
595
|
-
kind: "admin" | "public";
|
|
596
|
-
env: "production" | "staging";
|
|
597
|
-
}
|
|
598
|
-
|
|
599
744
|
/**
|
|
600
|
-
*
|
|
601
|
-
* Caddyfile
|
|
602
|
-
* the
|
|
603
|
-
*
|
|
745
|
+
* Operator-added domains (Settings → Domains) from `cms_admin.domains`, so
|
|
746
|
+
* the regenerated Caddyfile keeps serving them. Uses `docker compose exec`
|
|
747
|
+
* so the host needs no psql. A stopped database yields the seed vhosts only
|
|
748
|
+
* (with a warning — nothing can be read); a running database whose query
|
|
749
|
+
* fails is an error, never a silent drop of the operator's domains.
|
|
604
750
|
*/
|
|
605
|
-
async function
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
"SELECT hostname, kind, env FROM domains WHERE removed_at IS NULL ORDER BY hostname";
|
|
610
|
-
const proc = Bun.spawn(
|
|
611
|
-
[
|
|
612
|
-
"docker",
|
|
613
|
-
"compose",
|
|
614
|
-
"-f",
|
|
615
|
-
composeFile,
|
|
616
|
-
"exec",
|
|
617
|
-
"-T",
|
|
618
|
-
"postgres",
|
|
619
|
-
"psql",
|
|
620
|
-
"-U",
|
|
621
|
-
"caelo",
|
|
622
|
-
"-d",
|
|
623
|
-
"caelo",
|
|
624
|
-
"-At",
|
|
625
|
-
"-F",
|
|
626
|
-
"|",
|
|
627
|
-
"-c",
|
|
628
|
-
sql,
|
|
629
|
-
],
|
|
630
|
-
{ stdout: "pipe", stderr: "pipe", env: { ...process.env, PGPASSWORD: cfg.postgresPassword } },
|
|
751
|
+
async function fetchExtraDomains(): Promise<CaddyDomainSpec[]> {
|
|
752
|
+
if (!(await postgresIsUp())) {
|
|
753
|
+
console.warn(
|
|
754
|
+
"Postgres is not running: the Caddyfile gets the seed hosts only (domains added in the admin are re-read on the next `up`).",
|
|
631
755
|
);
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
756
|
+
return [];
|
|
757
|
+
}
|
|
758
|
+
// The superuser bypasses RLS; the table exists once the migrations ran.
|
|
759
|
+
const sql =
|
|
760
|
+
"SELECT hostname, kind FROM domains WHERE kind IN ('admin', 'public') ORDER BY hostname";
|
|
761
|
+
const r = await bunHostRunner.run([
|
|
762
|
+
"docker",
|
|
763
|
+
"compose",
|
|
764
|
+
"-f",
|
|
765
|
+
COMPOSE_PATH,
|
|
766
|
+
"exec",
|
|
767
|
+
"-T",
|
|
768
|
+
"postgres",
|
|
769
|
+
"psql",
|
|
770
|
+
"-U",
|
|
771
|
+
"caelo",
|
|
772
|
+
"-d",
|
|
773
|
+
"cms_admin",
|
|
774
|
+
"-v",
|
|
775
|
+
"ON_ERROR_STOP=1",
|
|
776
|
+
"-At",
|
|
777
|
+
"-F",
|
|
778
|
+
"|",
|
|
779
|
+
"-c",
|
|
780
|
+
sql,
|
|
781
|
+
]);
|
|
782
|
+
if (r.exitCode !== 0) {
|
|
783
|
+
// Before the first migration there is no domains table — and no domains.
|
|
784
|
+
if (/relation "domains" does not exist|database "cms_admin" does not exist/.test(r.stderr)) {
|
|
636
785
|
return [];
|
|
637
786
|
}
|
|
638
|
-
|
|
639
|
-
for (const line of stdout.split("\n")) {
|
|
640
|
-
if (!line.trim()) continue;
|
|
641
|
-
const [hostname, kind, env] = line.split("|");
|
|
642
|
-
if (!hostname || !kind || !env) continue;
|
|
643
|
-
rows.push({
|
|
644
|
-
hostname,
|
|
645
|
-
kind: kind as DomainRow["kind"],
|
|
646
|
-
env: env as DomainRow["env"],
|
|
647
|
-
});
|
|
648
|
-
}
|
|
649
|
-
return rows.map<CaddyDomainSpec>((r) => ({
|
|
650
|
-
hostname: r.hostname,
|
|
651
|
-
kind: r.kind,
|
|
652
|
-
env: r.env,
|
|
653
|
-
}));
|
|
654
|
-
} catch (e) {
|
|
655
|
-
console.warn("regenerate-caddy: domains lookup threw (DB unreachable?); using seed only", e);
|
|
656
|
-
return [];
|
|
787
|
+
throw new Error(`could not read the domains table: ${r.stderr.trim()}`);
|
|
657
788
|
}
|
|
789
|
+
return parseDomainRows(r.stdout);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/** `hostname|kind` rows from psql -At → vhosts (domains has no env column: they are live hosts). */
|
|
793
|
+
function parseDomainRows(stdout: string): CaddyDomainSpec[] {
|
|
794
|
+
const rows: CaddyDomainSpec[] = [];
|
|
795
|
+
for (const line of stdout.split("\n")) {
|
|
796
|
+
const [hostname, kind] = line.trim().split("|");
|
|
797
|
+
if (!hostname || (kind !== "admin" && kind !== "public")) continue;
|
|
798
|
+
rows.push({ hostname, kind, env: "production" });
|
|
799
|
+
}
|
|
800
|
+
return rows;
|
|
658
801
|
}
|
|
659
802
|
|
|
660
803
|
async function regenerateCaddy(): Promise<void> {
|
|
@@ -664,32 +807,12 @@ async function regenerateCaddy(): Promise<void> {
|
|
|
664
807
|
console.error("no config — run `cms-provision init` first");
|
|
665
808
|
process.exit(2);
|
|
666
809
|
}
|
|
667
|
-
const
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
// any new domains automatically via ACME.
|
|
672
|
-
const proc = Bun.spawn(
|
|
673
|
-
[
|
|
674
|
-
"docker",
|
|
675
|
-
"compose",
|
|
676
|
-
"-f",
|
|
677
|
-
COMPOSE_PATH,
|
|
678
|
-
"exec",
|
|
679
|
-
"caddy",
|
|
680
|
-
"caddy",
|
|
681
|
-
"reload",
|
|
682
|
-
"--config",
|
|
683
|
-
"/etc/caddy/Caddyfile",
|
|
684
|
-
],
|
|
685
|
-
{ stdout: "inherit", stderr: "inherit" },
|
|
686
|
-
);
|
|
687
|
-
await proc.exited;
|
|
688
|
-
if (proc.exitCode !== 0) {
|
|
689
|
-
console.warn(
|
|
690
|
-
"caddy reload exited non-zero — Caddyfile written, run reload manually once the container is up.",
|
|
691
|
-
);
|
|
810
|
+
const r = await refreshCaddy(cfg);
|
|
811
|
+
if (!r.ok) {
|
|
812
|
+
console.error(`${CADDYFILE_PATH} was re-written, but ${r.error}`);
|
|
813
|
+
process.exit(1);
|
|
692
814
|
}
|
|
815
|
+
console.log(`Re-emitted ${CADDYFILE_PATH} and reloaded Caddy.`);
|
|
693
816
|
}
|
|
694
817
|
|
|
695
818
|
async function status(): Promise<void> {
|
|
@@ -713,188 +836,77 @@ async function status(): Promise<void> {
|
|
|
713
836
|
console.log("Cert status: visit /settings/domains in the admin");
|
|
714
837
|
}
|
|
715
838
|
|
|
839
|
+
/** `backup [--to <file>]` for a self-hosted install (self-hosted-backup.ts). */
|
|
716
840
|
async function backup(): Promise<void> {
|
|
717
841
|
requireSelfHosted("backup");
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
console.error("no config");
|
|
842
|
+
if (!loadConfig()) {
|
|
843
|
+
console.error("no config — run `cms-provision init` first");
|
|
721
844
|
process.exit(2);
|
|
722
845
|
}
|
|
723
|
-
const to =
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
// 1. pgBackRest full backup inside the sidecar container.
|
|
727
|
-
console.log("Step 1/3: pgBackRest full backup …");
|
|
728
|
-
const pgb = Bun.spawn(
|
|
729
|
-
[
|
|
730
|
-
"docker",
|
|
731
|
-
"compose",
|
|
732
|
-
"-f",
|
|
733
|
-
COMPOSE_PATH,
|
|
734
|
-
"exec",
|
|
735
|
-
"-T",
|
|
736
|
-
"pgbackrest",
|
|
737
|
-
"pgbackrest",
|
|
738
|
-
"--type=full",
|
|
739
|
-
"--stanza=caelo",
|
|
740
|
-
"backup",
|
|
741
|
-
],
|
|
742
|
-
{ stdout: "inherit", stderr: "inherit" },
|
|
846
|
+
const to = resolve(
|
|
847
|
+
arg("to") ?? `caelo-backup-${new Date().toISOString().replace(/[:.]/g, "-")}.tar.gz`,
|
|
743
848
|
);
|
|
744
|
-
await
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
[
|
|
754
|
-
"docker",
|
|
755
|
-
"compose",
|
|
756
|
-
"-f",
|
|
757
|
-
COMPOSE_PATH,
|
|
758
|
-
"exec",
|
|
759
|
-
"-T",
|
|
760
|
-
"minio",
|
|
761
|
-
"mc",
|
|
762
|
-
"mirror",
|
|
763
|
-
"--overwrite",
|
|
764
|
-
"/data",
|
|
765
|
-
"/tmp/minio-backup",
|
|
766
|
-
],
|
|
767
|
-
{ stdout: "inherit", stderr: "inherit" },
|
|
768
|
-
);
|
|
769
|
-
await mc.exited;
|
|
770
|
-
if (mc.exitCode !== 0) {
|
|
771
|
-
console.error("MinIO mirror failed; aborting");
|
|
772
|
-
process.exit(mc.exitCode ?? 1);
|
|
849
|
+
const { backupSelfHosted } = await import("./self-hosted-backup.js");
|
|
850
|
+
const r = await backupSelfHosted(to, readRelease(CAELO_DIR)?.version ?? CAELO_VERSION, {
|
|
851
|
+
caeloDir: CAELO_DIR,
|
|
852
|
+
runner: bunHostRunner,
|
|
853
|
+
log: (m) => console.log(`==> ${m}`),
|
|
854
|
+
});
|
|
855
|
+
if (!r.ok) {
|
|
856
|
+
console.error(`Backup failed: ${r.error}`);
|
|
857
|
+
process.exit(1);
|
|
773
858
|
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
const tar = Bun.spawn(
|
|
778
|
-
[
|
|
779
|
-
"docker",
|
|
780
|
-
"run",
|
|
781
|
-
"--rm",
|
|
782
|
-
"-v",
|
|
783
|
-
"caelo_caelo-pg-backups:/pgb:ro",
|
|
784
|
-
"-v",
|
|
785
|
-
"caelo_caelo-minio:/minio:ro",
|
|
786
|
-
"-v",
|
|
787
|
-
`${resolve(to, "..")}:/out`,
|
|
788
|
-
"alpine:3",
|
|
789
|
-
"sh",
|
|
790
|
-
"-c",
|
|
791
|
-
`apk add --no-cache zstd tar >/dev/null && tar --zstd -cf /out/${to.split("/").pop()} -C /pgb . -C /minio .`,
|
|
792
|
-
],
|
|
793
|
-
{ stdout: "inherit", stderr: "inherit" },
|
|
859
|
+
console.log(`Backup complete: ${r.path}`);
|
|
860
|
+
console.log(
|
|
861
|
+
`It holds the databases, the published site and the media — not ${CONFIG_PATH} (the secrets); keep a copy of that separately.`,
|
|
794
862
|
);
|
|
795
|
-
await tar.exited;
|
|
796
|
-
if (tar.exitCode !== 0) {
|
|
797
|
-
console.error("tar archive failed");
|
|
798
|
-
process.exit(tar.exitCode ?? 1);
|
|
799
|
-
}
|
|
800
|
-
console.log(`Backup complete: ${to}`);
|
|
801
863
|
}
|
|
802
864
|
|
|
865
|
+
/** `restore --from <file> --yes` for a self-hosted install (self-hosted-backup.ts). */
|
|
803
866
|
async function restore(): Promise<void> {
|
|
804
867
|
requireSelfHosted("restore");
|
|
805
868
|
const from = arg("from");
|
|
806
869
|
if (!from) {
|
|
807
|
-
console.error("Usage: cms-provision restore --from <
|
|
870
|
+
console.error("Usage: cms-provision restore --from <backup.tar.gz> --yes");
|
|
808
871
|
process.exit(2);
|
|
809
872
|
}
|
|
810
873
|
if (!process.argv.includes("--yes")) {
|
|
811
874
|
console.error(
|
|
812
|
-
`DESTRUCTIVE:
|
|
875
|
+
`DESTRUCTIVE: replaces both databases, the published site and the media with ${from}. Re-run with --yes to confirm.`,
|
|
813
876
|
);
|
|
814
877
|
process.exit(1);
|
|
815
878
|
}
|
|
816
879
|
const cfg = loadConfig();
|
|
817
|
-
if (!cfg) {
|
|
818
|
-
console.error("no config");
|
|
880
|
+
if (!cfg?.rolePasswords) {
|
|
881
|
+
console.error("no config with role passwords — run `cms-provision init` / `up` first");
|
|
819
882
|
process.exit(2);
|
|
820
883
|
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
const tarFile = from.split("/").pop();
|
|
844
|
-
const ex = Bun.spawn(
|
|
845
|
-
[
|
|
846
|
-
"docker",
|
|
847
|
-
"run",
|
|
848
|
-
"--rm",
|
|
849
|
-
"-v",
|
|
850
|
-
"caelo_caelo-pg-backups:/pgb",
|
|
851
|
-
"-v",
|
|
852
|
-
"caelo_caelo-minio:/minio",
|
|
853
|
-
"-v",
|
|
854
|
-
`${tarDir}:/in:ro`,
|
|
855
|
-
"alpine:3",
|
|
856
|
-
"sh",
|
|
857
|
-
"-c",
|
|
858
|
-
`apk add --no-cache zstd tar >/dev/null && rm -rf /pgb/* /minio/* && tar --zstd -xf /in/${tarFile} -C /pgb && tar --zstd -xf /in/${tarFile} -C /minio`,
|
|
859
|
-
],
|
|
860
|
-
{ stdout: "inherit", stderr: "inherit" },
|
|
861
|
-
);
|
|
862
|
-
await ex.exited;
|
|
863
|
-
if (ex.exitCode !== 0) {
|
|
864
|
-
console.error("extract failed");
|
|
865
|
-
process.exit(ex.exitCode ?? 1);
|
|
884
|
+
const passwords = cfg.rolePasswords;
|
|
885
|
+
const { restoreSelfHosted } = await import("./self-hosted-backup.js");
|
|
886
|
+
const r = await restoreSelfHosted(resolve(from), {
|
|
887
|
+
caeloDir: CAELO_DIR,
|
|
888
|
+
runner: bunHostRunner,
|
|
889
|
+
log: (m) => console.log(`==> ${m}`),
|
|
890
|
+
convergeRoles: () =>
|
|
891
|
+
convergeSelfHostedRoles(passwords, {
|
|
892
|
+
probe: postgresIsUp,
|
|
893
|
+
psql: superuserPsql,
|
|
894
|
+
...(activeAgent(cfg) ? { agentPassword: activeAgent(cfg)?.dbPassword } : {}),
|
|
895
|
+
}),
|
|
896
|
+
refreshCaddy: () => refreshCaddy(cfg),
|
|
897
|
+
});
|
|
898
|
+
if (!r.ok) {
|
|
899
|
+
console.error(`Restore failed: ${r.error}`);
|
|
900
|
+
if (r.installChanged) {
|
|
901
|
+
console.error(
|
|
902
|
+
`The install is partly restored. Fix the cause and run the same restore again — it starts over from the archive.`,
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
process.exit(1);
|
|
866
906
|
}
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
console.log("Step 3/4: pgBackRest restore …");
|
|
870
|
-
const pgr = Bun.spawn(
|
|
871
|
-
[
|
|
872
|
-
"docker",
|
|
873
|
-
"compose",
|
|
874
|
-
"-f",
|
|
875
|
-
COMPOSE_PATH,
|
|
876
|
-
"exec",
|
|
877
|
-
"-T",
|
|
878
|
-
"pgbackrest",
|
|
879
|
-
"pgbackrest",
|
|
880
|
-
"--stanza=caelo",
|
|
881
|
-
"restore",
|
|
882
|
-
],
|
|
883
|
-
{ stdout: "inherit", stderr: "inherit" },
|
|
907
|
+
console.log(
|
|
908
|
+
`Restore complete (backup of Caelo ${r.manifest.caeloVersion}, taken ${r.manifest.createdAt}).`,
|
|
884
909
|
);
|
|
885
|
-
await pgr.exited;
|
|
886
|
-
if (pgr.exitCode !== 0) {
|
|
887
|
-
console.error("pgBackRest restore failed");
|
|
888
|
-
process.exit(pgr.exitCode ?? 1);
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
// 4. Bring services back up.
|
|
892
|
-
console.log("Step 4/4: restarting services …");
|
|
893
|
-
await Bun.spawn(["docker", "compose", "-f", COMPOSE_PATH, "up", "-d"], {
|
|
894
|
-
stdout: "inherit",
|
|
895
|
-
stderr: "inherit",
|
|
896
|
-
}).exited;
|
|
897
|
-
console.log("Restore complete.");
|
|
898
910
|
}
|
|
899
911
|
|
|
900
912
|
/**
|
|
@@ -1099,7 +1111,7 @@ const handlers: Record<string, () => Promise<void>> = {
|
|
|
1099
1111
|
// back-compat scripts that bypassed the dispatcher.
|
|
1100
1112
|
status: lifecycleStatus,
|
|
1101
1113
|
"status-self-hosted": status,
|
|
1102
|
-
backup: lifecycleBackup,
|
|
1114
|
+
backup: async () => (selfHostedInstallPresent() ? backup() : lifecycleBackup()),
|
|
1103
1115
|
"backup-self-hosted": backup,
|
|
1104
1116
|
restore,
|
|
1105
1117
|
upgrade: lifecycleUpgrade,
|