@caelo-cms/provisioning 0.10.48 → 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 +5 -1
- package/caddy/Caddyfile.staging +5 -1
- package/dist/agent-db.js +102 -58
- 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 +2 -41
- package/dist/release-manifest.d.ts.map +1 -1
- package/dist/release-manifest.js +20 -122
- 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/upgrade/release-images.d.ts +14 -2
- package/dist/upgrade/release-images.d.ts.map +1 -1
- package/dist/upgrade/release-images.js +4 -2
- package/dist/upgrade/release-images.js.map +1 -1
- package/dist/upgrader-assess.d.ts +4 -1
- package/dist/upgrader-assess.d.ts.map +1 -1
- package/dist/upgrader-assess.js +8 -1
- package/dist/upgrader-assess.js.map +1 -1
- package/dist/upgrader-job.d.ts +3 -1
- package/dist/upgrader-job.d.ts.map +1 -1
- package/dist/upgrader-job.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/agent/run.test.ts +55 -0
- 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 +20 -1
- package/src/release-manifest.ts +39 -158
- 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/release-images.ts +4 -2
- package/src/upgrade/upgrade.test.ts +78 -1
- package/src/upgrader-assess.test.ts +64 -1
- package/src/upgrader-assess.ts +20 -2
- package/src/upgrader-job.test.ts +3 -0
- package/src/upgrader-job.ts +3 -0
- 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/dist/cli.js
CHANGED
|
@@ -7,10 +7,14 @@
|
|
|
7
7
|
* init [--domain D --owner-email E] — first-time setup; generates
|
|
8
8
|
* secrets, resolves + cosign-verifies
|
|
9
9
|
* the release images, writes .caelo/,
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* starts the stack (migrations
|
|
11
|
+
* included), health-checks it and
|
|
12
|
+
* prints the bootstrap token URL.
|
|
13
|
+
* `--provider self-hosted` with no
|
|
14
|
+
* sub-command runs this. `--dev` runs
|
|
15
|
+
* a Caelo checkout (contributors).
|
|
16
|
+
* up — re-emits from the existing config
|
|
17
|
+
* and converges the running stack.
|
|
14
18
|
* migrate — applies DB migrations with the
|
|
15
19
|
* admin image the install runs.
|
|
16
20
|
* upgrade [--version X] — verified, backed-up, health-checked
|
|
@@ -20,9 +24,10 @@
|
|
|
20
24
|
* the Owner starts in the admin.
|
|
21
25
|
* regenerate-caddy — re-emits Caddyfile from the
|
|
22
26
|
* domains table; runs `caddy reload`.
|
|
23
|
-
* backup --to <path>
|
|
24
|
-
*
|
|
25
|
-
* restore --from <path>
|
|
27
|
+
* backup [--to <path>] — pg_dump of both databases + the
|
|
28
|
+
* site + media files → one .tar.gz.
|
|
29
|
+
* restore --from <path> --yes — replaces databases + files with
|
|
30
|
+
* a backup, restarts, health-checks.
|
|
26
31
|
* status — prints container health + cert
|
|
27
32
|
* expiry per domain.
|
|
28
33
|
*
|
|
@@ -33,10 +38,10 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
33
38
|
import { resolve } from "node:path";
|
|
34
39
|
import { CAELO_VERSION, defaultStagingHost, normalizeStagingHost, stagingHostProblem, } from "@caelo-cms/shared";
|
|
35
40
|
import { generateBootstrapToken } from "./bootstrap-token.js";
|
|
36
|
-
import { generateCaddyfile } from "./caddy.js";
|
|
41
|
+
import { generateCaddyfile, selfHostedAdminHost, selfHostedSeedDomains, } from "./caddy.js";
|
|
37
42
|
import { redactCredentials } from "./checkpoint-secrets.js";
|
|
38
43
|
import { initDelegatesToWizard, resolveCliRoute } from "./cli-routing.js";
|
|
39
|
-
import { BOOTSTRAP_DIR, generateDockerCompose, } from "./compose.js";
|
|
44
|
+
import { BOOTSTRAP_DIR, COMPOSE_ADMIN_UPSTREAM, COMPOSE_GATEWAY_UPSTREAM, composeProjectName, generateDockerCompose, isPublishablePort, MEDIA_DIR, SITE_OUTPUT_ROOT, selfHostedSiteUrl, } from "./compose.js";
|
|
40
45
|
import { selfCommand } from "./self-command.js";
|
|
41
46
|
import { bunHostRunner, readRelease, recordRelease, releaseImages, resolveVerifiedRelease, } from "./self-hosted-release.js";
|
|
42
47
|
import { convergeSelfHostedRoles, decideUp, selfHostedBootstrapScript, } from "./self-hosted-roles.js";
|
|
@@ -111,7 +116,17 @@ function randomSecret(bytes = 32) {
|
|
|
111
116
|
function loadConfig() {
|
|
112
117
|
if (!existsSync(CONFIG_PATH))
|
|
113
118
|
return null;
|
|
114
|
-
|
|
119
|
+
const cfg = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
|
|
120
|
+
// #707 — settings added later, back-filled (and stored) with what every
|
|
121
|
+
// install ran before they existed, so `upgrade` / `agent` render the same
|
|
122
|
+
// Compose file as before.
|
|
123
|
+
if (!cfg.tls || !cfg.ports || !cfg.composeProject) {
|
|
124
|
+
cfg.tls ??= "acme";
|
|
125
|
+
cfg.ports ??= { http: 80, https: 443 };
|
|
126
|
+
cfg.composeProject ??= "caelo";
|
|
127
|
+
saveConfig(cfg);
|
|
128
|
+
}
|
|
129
|
+
return cfg;
|
|
115
130
|
}
|
|
116
131
|
function saveConfig(c) {
|
|
117
132
|
if (!existsSync(CAELO_DIR))
|
|
@@ -140,22 +155,25 @@ function emitConfig(cfg, extraDomains = []) {
|
|
|
140
155
|
cfg.stagingDomain = defaultStagingHost(cfg.domain);
|
|
141
156
|
saveConfig(cfg);
|
|
142
157
|
}
|
|
158
|
+
if (!cfg.tls)
|
|
159
|
+
throw new Error("config has no tls — it is back-filled on load; this is a bug");
|
|
143
160
|
emitComposeFiles(cfg);
|
|
144
|
-
// Seed the install with the
|
|
145
|
-
//
|
|
161
|
+
// Seed the install with the admin host, the primary domain and the
|
|
162
|
+
// staging host. Extra domains added via /settings/domains are appended by
|
|
146
163
|
// regenerate-caddy when it queries the live domains table.
|
|
147
|
-
const baseDomains = [
|
|
148
|
-
{ hostname: cfg.domain, kind: "admin", env: "production" },
|
|
149
|
-
{ hostname: cfg.domain, kind: "public", env: "production" },
|
|
150
|
-
{ hostname: cfg.stagingDomain, kind: "public", env: "staging" },
|
|
151
|
-
];
|
|
152
164
|
const caddy = generateCaddyfile({
|
|
153
165
|
ownerEmail: cfg.ownerEmail,
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
166
|
+
tls: cfg.tls,
|
|
167
|
+
publicSiteRoot: `${SITE_OUTPUT_ROOT}/output/production/current`,
|
|
168
|
+
stagingSiteRoot: `${SITE_OUTPUT_ROOT}/output/staging/current`,
|
|
169
|
+
// Caddy runs in its own container: the Compose service names, never
|
|
170
|
+
// localhost (#707).
|
|
171
|
+
adminUpstream: COMPOSE_ADMIN_UPSTREAM,
|
|
172
|
+
gatewayUpstream: COMPOSE_GATEWAY_UPSTREAM,
|
|
173
|
+
domains: dedupeDomains([
|
|
174
|
+
...selfHostedSeedDomains(cfg.domain, cfg.stagingDomain),
|
|
175
|
+
...extraDomains,
|
|
176
|
+
]),
|
|
159
177
|
});
|
|
160
178
|
writeFileSync(CADDYFILE_PATH, caddy);
|
|
161
179
|
}
|
|
@@ -169,6 +187,10 @@ function emitComposeFiles(cfg) {
|
|
|
169
187
|
// No secrets (it reads the passwords from the container's environment),
|
|
170
188
|
// and the postgres user inside the container must be able to read it.
|
|
171
189
|
writeFileSync(BOOTSTRAP_SCRIPT_PATH, selfHostedBootstrapScript({ agent: activeAgent(cfg) !== undefined }), { mode: 0o755 });
|
|
190
|
+
// The bind-mounted content dirs, created by the CLI so Docker does not
|
|
191
|
+
// create them root-owned on a Linux host.
|
|
192
|
+
for (const dir of ["output", MEDIA_DIR])
|
|
193
|
+
mkdirSync(resolve(CAELO_DIR, dir), { recursive: true });
|
|
172
194
|
}
|
|
173
195
|
/** The Compose file for `cfg` running `images` (secrets come from config.json). */
|
|
174
196
|
function renderCompose(cfg, images) {
|
|
@@ -178,17 +200,20 @@ function renderCompose(cfg, images) {
|
|
|
178
200
|
}
|
|
179
201
|
if (!cfg.caeloSecretKek)
|
|
180
202
|
throw new Error("config has no caeloSecretKek — run `cms-provision up`");
|
|
203
|
+
if (!cfg.ports || !cfg.composeProject) {
|
|
204
|
+
throw new Error("config has no ports / composeProject — they are back-filled on load; this is a bug");
|
|
205
|
+
}
|
|
181
206
|
return generateDockerCompose({
|
|
182
207
|
domain: cfg.domain,
|
|
183
208
|
postgresPassword: cfg.postgresPassword,
|
|
184
209
|
rolePasswords: cfg.rolePasswords,
|
|
185
|
-
minioRootUser: cfg.minioRootUser,
|
|
186
|
-
minioRootPassword: cfg.minioRootPassword,
|
|
187
210
|
caeloSecretKek: cfg.caeloSecretKek,
|
|
188
211
|
anthropicApiKey: cfg.anthropicApiKey,
|
|
189
212
|
resendApiKey: cfg.resendApiKey,
|
|
190
213
|
diskSize: "20Gi",
|
|
191
214
|
images,
|
|
215
|
+
ports: cfg.ports,
|
|
216
|
+
project: cfg.composeProject,
|
|
192
217
|
...(activeAgent(cfg) ? { agent: activeAgent(cfg) } : {}),
|
|
193
218
|
});
|
|
194
219
|
}
|
|
@@ -258,21 +283,77 @@ function installImages(cfg) {
|
|
|
258
283
|
return releaseImages(release);
|
|
259
284
|
}
|
|
260
285
|
function dedupeDomains(list) {
|
|
261
|
-
//
|
|
262
|
-
// first occurrence
|
|
263
|
-
// collision (which keeps admin routing intact even if someone adds
|
|
264
|
-
//
|
|
286
|
+
// One vhost per hostname (Caddy refuses two site blocks for one address):
|
|
287
|
+
// the first occurrence wins, so the seed wins over operator additions on
|
|
288
|
+
// a collision (which keeps admin routing intact even if someone adds the
|
|
289
|
+
// admin host via the UI as a public domain by mistake).
|
|
265
290
|
const seen = new Set();
|
|
266
291
|
const out = [];
|
|
267
292
|
for (const d of list) {
|
|
268
|
-
|
|
269
|
-
if (seen.has(key))
|
|
293
|
+
if (seen.has(d.hostname))
|
|
270
294
|
continue;
|
|
271
|
-
seen.add(
|
|
295
|
+
seen.add(d.hostname);
|
|
272
296
|
out.push(d);
|
|
273
297
|
}
|
|
274
298
|
return out;
|
|
275
299
|
}
|
|
300
|
+
/** `--http-port` / `--https-port` (default 80 / 443), or exit with a usage error. */
|
|
301
|
+
function portsArg() {
|
|
302
|
+
const parse = (name, fallback) => {
|
|
303
|
+
const raw = arg(name);
|
|
304
|
+
if (raw === undefined)
|
|
305
|
+
return fallback;
|
|
306
|
+
const port = Number(raw);
|
|
307
|
+
if (!isPublishablePort(port)) {
|
|
308
|
+
console.error(`--${name}: ${JSON.stringify(raw)} is not a TCP port`);
|
|
309
|
+
process.exit(2);
|
|
310
|
+
}
|
|
311
|
+
return port;
|
|
312
|
+
};
|
|
313
|
+
return { http: parse("http-port", 80), https: parse("https-port", 443) };
|
|
314
|
+
}
|
|
315
|
+
/** `--tls acme|internal` (default acme), or exit with a usage error. */
|
|
316
|
+
function tlsArg() {
|
|
317
|
+
const raw = arg("tls") ?? "acme";
|
|
318
|
+
if (raw !== "acme" && raw !== "internal") {
|
|
319
|
+
console.error(`--tls: ${JSON.stringify(raw)} — choose acme (public DNS) or internal (local CA)`);
|
|
320
|
+
process.exit(2);
|
|
321
|
+
}
|
|
322
|
+
return raw;
|
|
323
|
+
}
|
|
324
|
+
/** `https://<host>[:port]` for a host Caddy serves. */
|
|
325
|
+
function siteUrl(cfg, host) {
|
|
326
|
+
if (!cfg.ports)
|
|
327
|
+
throw new Error("config has no ports — they are back-filled on load; this is a bug");
|
|
328
|
+
return selfHostedSiteUrl(host, cfg.ports.https);
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Re-render the Caddyfile from config + the domains table and reload the
|
|
332
|
+
* running Caddy (regenerate-caddy, restore).
|
|
333
|
+
*/
|
|
334
|
+
async function refreshCaddy(cfg) {
|
|
335
|
+
emitConfig(cfg, await fetchExtraDomains());
|
|
336
|
+
const { reloadCaddy } = await import("./self-hosted-start.js");
|
|
337
|
+
return reloadCaddy(CAELO_DIR, bunHostRunner);
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Pull, start Postgres, migrate, start everything, health-check
|
|
341
|
+
* (self-hosted-start.ts). Exits non-zero with the reason on failure; the
|
|
342
|
+
* operator re-runs `up`, which converges from wherever it stopped.
|
|
343
|
+
*/
|
|
344
|
+
async function startStack() {
|
|
345
|
+
const { startSelfHosted } = await import("./self-hosted-start.js");
|
|
346
|
+
const r = await startSelfHosted({
|
|
347
|
+
caeloDir: CAELO_DIR,
|
|
348
|
+
runner: bunHostRunner,
|
|
349
|
+
log: (m) => console.log(`==> ${m}`),
|
|
350
|
+
});
|
|
351
|
+
if (!r.ok) {
|
|
352
|
+
console.error(`\nThe install did not come up (${r.stage}): ${r.error}`);
|
|
353
|
+
console.error(`Fix the cause and run \`${selfCommand("up")}\` — it continues from here.`);
|
|
354
|
+
process.exit(1);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
276
357
|
async function init() {
|
|
277
358
|
const domain = arg("domain");
|
|
278
359
|
const ownerEmail = arg("owner-email");
|
|
@@ -290,7 +371,7 @@ async function init() {
|
|
|
290
371
|
return;
|
|
291
372
|
}
|
|
292
373
|
if (!domain || !ownerEmail) {
|
|
293
|
-
console.error("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" +
|
|
374
|
+
console.error("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" +
|
|
294
375
|
"Cloud providers (gcp | gcp-firebase | aws | azure) run the wizard: cms-provision --provider <name>");
|
|
295
376
|
process.exit(2);
|
|
296
377
|
}
|
|
@@ -298,6 +379,12 @@ async function init() {
|
|
|
298
379
|
console.error(`config already exists at ${CONFIG_PATH}; run \`cms-provision up\` to re-deploy`);
|
|
299
380
|
process.exit(2);
|
|
300
381
|
}
|
|
382
|
+
const tls = tlsArg();
|
|
383
|
+
const ports = portsArg();
|
|
384
|
+
if (ports.http === ports.https) {
|
|
385
|
+
console.error("--http-port and --https-port must differ");
|
|
386
|
+
process.exit(2);
|
|
387
|
+
}
|
|
301
388
|
const stagingArg = arg("staging-domain");
|
|
302
389
|
const stagingProblem = stagingArg
|
|
303
390
|
? stagingHostProblem(stagingArg, {
|
|
@@ -345,12 +432,13 @@ async function init() {
|
|
|
345
432
|
rolePasswords: newRolePasswords(),
|
|
346
433
|
// A fresh volume: bootstrap.sh creates the roles with these on first start.
|
|
347
434
|
rolesConverged: true,
|
|
348
|
-
minioRootUser: "caelo",
|
|
349
|
-
minioRootPassword: randomSecret(32),
|
|
350
435
|
caeloSecretKek: randomSecret(32),
|
|
351
436
|
anthropicApiKey: process.env.ANTHROPIC_API_KEY,
|
|
352
437
|
resendApiKey: process.env.RESEND_API_KEY,
|
|
353
438
|
imageMode: dev ? "dev" : "release",
|
|
439
|
+
tls,
|
|
440
|
+
ports,
|
|
441
|
+
composeProject: composeProjectName(domain),
|
|
354
442
|
};
|
|
355
443
|
saveConfig(cfg);
|
|
356
444
|
if (images.mode === "release") {
|
|
@@ -385,19 +473,32 @@ async function init() {
|
|
|
385
473
|
console.log(`Wrote ${COMPOSE_PATH}`);
|
|
386
474
|
console.log(`Wrote ${CADDYFILE_PATH}`);
|
|
387
475
|
console.log(`Wrote ${PENDING_TOKEN_PATH} (admin will insert on first boot)`);
|
|
388
|
-
console.log("");
|
|
389
476
|
console.log(images.mode === "release"
|
|
390
477
|
? `Images: Caelo ${version}, pinned by digest (${images.admin.split("@")[1]?.slice(0, 19)}…)`
|
|
391
478
|
: "Images: DEV MODE — admin + gateway run this checkout (contributors only).");
|
|
479
|
+
// Printed BEFORE the start: the admin consumes pending-token.json on its
|
|
480
|
+
// first boot, so a start that fails half-way and is resumed with `up`
|
|
481
|
+
// would otherwise leave the operator without the link.
|
|
482
|
+
const adminHost = selfHostedAdminHost(domain);
|
|
483
|
+
const setupUrl = `${siteUrl(cfg, adminHost)}/setup?token=${tok.token}`;
|
|
484
|
+
console.log("");
|
|
485
|
+
console.log(`Owner setup link (save it; valid until ${tok.expiresAt}):`);
|
|
486
|
+
console.log(` ${setupUrl}`);
|
|
392
487
|
console.log("");
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
console.log(` docker compose -f ${COMPOSE_PATH} up -d`);
|
|
397
|
-
console.log(" # Wait ~30s for Caddy + ACME");
|
|
488
|
+
// #707 / §11.C — one command ends with a serving install, not with steps
|
|
489
|
+
// to copy.
|
|
490
|
+
await startStack();
|
|
398
491
|
console.log("");
|
|
399
|
-
console.log("
|
|
400
|
-
|
|
492
|
+
console.log("Caelo is running. Point these DNS records at this host (A/AAAA):");
|
|
493
|
+
for (const host of [adminHost, domain, cfg.stagingDomain ?? defaultStagingHost(domain)]) {
|
|
494
|
+
console.log(` ${host}`);
|
|
495
|
+
}
|
|
496
|
+
console.log(tls === "acme"
|
|
497
|
+
? "Caddy fetches the certificates once they resolve (ports 80 + 443 must be reachable)."
|
|
498
|
+
: "TLS: Caddy's local CA (--tls internal) — browsers warn until its root is trusted.");
|
|
499
|
+
console.log("");
|
|
500
|
+
console.log("Then create the Owner account:");
|
|
501
|
+
console.log(` ${setupUrl}`);
|
|
401
502
|
console.log(` (token expires ${tok.expiresAt})`);
|
|
402
503
|
if (agentSetup) {
|
|
403
504
|
console.log("");
|
|
@@ -411,6 +512,12 @@ async function up() {
|
|
|
411
512
|
console.error("no config — run `cms-provision init` first");
|
|
412
513
|
process.exit(2);
|
|
413
514
|
}
|
|
515
|
+
// Postgres runs on the CURRENT Compose file first, so the roles can be
|
|
516
|
+
// converged and the operator-added domains read before anything is
|
|
517
|
+
// re-emitted (a failure here surfaces through the convergence below).
|
|
518
|
+
if (existsSync(COMPOSE_PATH)) {
|
|
519
|
+
await bunHostRunner.run(["docker", "compose", "-f", COMPOSE_PATH, "up", "-d", "--wait", "postgres"], { inherit: true });
|
|
520
|
+
}
|
|
414
521
|
// #613 — the database roles first, then the Compose URLs that use them.
|
|
415
522
|
if (!cfg.rolePasswords) {
|
|
416
523
|
cfg.rolePasswords = newRolePasswords();
|
|
@@ -432,9 +539,10 @@ async function up() {
|
|
|
432
539
|
cfg.rolesConverged = true;
|
|
433
540
|
saveConfig(cfg);
|
|
434
541
|
}
|
|
435
|
-
emitConfig(cfg, await
|
|
542
|
+
emitConfig(cfg, await fetchExtraDomains());
|
|
436
543
|
console.log(`Re-emitted ${COMPOSE_PATH} + ${CADDYFILE_PATH}`);
|
|
437
|
-
|
|
544
|
+
await startStack();
|
|
545
|
+
console.log(`\nCaelo is running: ${siteUrl(cfg, selfHostedAdminHost(cfg.domain))}`);
|
|
438
546
|
}
|
|
439
547
|
/** Whether the install's Postgres container answers (with the CURRENT Compose file). */
|
|
440
548
|
async function postgresIsUp() {
|
|
@@ -477,63 +585,59 @@ async function superuserPsql(script) {
|
|
|
477
585
|
return { exitCode: await proc.exited, stderr };
|
|
478
586
|
}
|
|
479
587
|
/**
|
|
480
|
-
*
|
|
481
|
-
* Caddyfile
|
|
482
|
-
* the
|
|
483
|
-
*
|
|
588
|
+
* Operator-added domains (Settings → Domains) from `cms_admin.domains`, so
|
|
589
|
+
* the regenerated Caddyfile keeps serving them. Uses `docker compose exec`
|
|
590
|
+
* so the host needs no psql. A stopped database yields the seed vhosts only
|
|
591
|
+
* (with a warning — nothing can be read); a running database whose query
|
|
592
|
+
* fails is an error, never a silent drop of the operator's domains.
|
|
484
593
|
*/
|
|
485
|
-
async function
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
594
|
+
async function fetchExtraDomains() {
|
|
595
|
+
if (!(await postgresIsUp())) {
|
|
596
|
+
console.warn("Postgres is not running: the Caddyfile gets the seed hosts only (domains added in the admin are re-read on the next `up`).");
|
|
597
|
+
return [];
|
|
598
|
+
}
|
|
599
|
+
// The superuser bypasses RLS; the table exists once the migrations ran.
|
|
600
|
+
const sql = "SELECT hostname, kind FROM domains WHERE kind IN ('admin', 'public') ORDER BY hostname";
|
|
601
|
+
const r = await bunHostRunner.run([
|
|
602
|
+
"docker",
|
|
603
|
+
"compose",
|
|
604
|
+
"-f",
|
|
605
|
+
COMPOSE_PATH,
|
|
606
|
+
"exec",
|
|
607
|
+
"-T",
|
|
608
|
+
"postgres",
|
|
609
|
+
"psql",
|
|
610
|
+
"-U",
|
|
611
|
+
"caelo",
|
|
612
|
+
"-d",
|
|
613
|
+
"cms_admin",
|
|
614
|
+
"-v",
|
|
615
|
+
"ON_ERROR_STOP=1",
|
|
616
|
+
"-At",
|
|
617
|
+
"-F",
|
|
618
|
+
"|",
|
|
619
|
+
"-c",
|
|
620
|
+
sql,
|
|
621
|
+
]);
|
|
622
|
+
if (r.exitCode !== 0) {
|
|
623
|
+
// Before the first migration there is no domains table — and no domains.
|
|
624
|
+
if (/relation "domains" does not exist|database "cms_admin" does not exist/.test(r.stderr)) {
|
|
512
625
|
return [];
|
|
513
626
|
}
|
|
514
|
-
|
|
515
|
-
for (const line of stdout.split("\n")) {
|
|
516
|
-
if (!line.trim())
|
|
517
|
-
continue;
|
|
518
|
-
const [hostname, kind, env] = line.split("|");
|
|
519
|
-
if (!hostname || !kind || !env)
|
|
520
|
-
continue;
|
|
521
|
-
rows.push({
|
|
522
|
-
hostname,
|
|
523
|
-
kind: kind,
|
|
524
|
-
env: env,
|
|
525
|
-
});
|
|
526
|
-
}
|
|
527
|
-
return rows.map((r) => ({
|
|
528
|
-
hostname: r.hostname,
|
|
529
|
-
kind: r.kind,
|
|
530
|
-
env: r.env,
|
|
531
|
-
}));
|
|
627
|
+
throw new Error(`could not read the domains table: ${r.stderr.trim()}`);
|
|
532
628
|
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
629
|
+
return parseDomainRows(r.stdout);
|
|
630
|
+
}
|
|
631
|
+
/** `hostname|kind` rows from psql -At → vhosts (domains has no env column: they are live hosts). */
|
|
632
|
+
function parseDomainRows(stdout) {
|
|
633
|
+
const rows = [];
|
|
634
|
+
for (const line of stdout.split("\n")) {
|
|
635
|
+
const [hostname, kind] = line.trim().split("|");
|
|
636
|
+
if (!hostname || (kind !== "admin" && kind !== "public"))
|
|
637
|
+
continue;
|
|
638
|
+
rows.push({ hostname, kind, env: "production" });
|
|
536
639
|
}
|
|
640
|
+
return rows;
|
|
537
641
|
}
|
|
538
642
|
async function regenerateCaddy() {
|
|
539
643
|
requireSelfHosted("regenerate-caddy");
|
|
@@ -542,27 +646,12 @@ async function regenerateCaddy() {
|
|
|
542
646
|
console.error("no config — run `cms-provision init` first");
|
|
543
647
|
process.exit(2);
|
|
544
648
|
}
|
|
545
|
-
const
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
// any new domains automatically via ACME.
|
|
550
|
-
const proc = Bun.spawn([
|
|
551
|
-
"docker",
|
|
552
|
-
"compose",
|
|
553
|
-
"-f",
|
|
554
|
-
COMPOSE_PATH,
|
|
555
|
-
"exec",
|
|
556
|
-
"caddy",
|
|
557
|
-
"caddy",
|
|
558
|
-
"reload",
|
|
559
|
-
"--config",
|
|
560
|
-
"/etc/caddy/Caddyfile",
|
|
561
|
-
], { stdout: "inherit", stderr: "inherit" });
|
|
562
|
-
await proc.exited;
|
|
563
|
-
if (proc.exitCode !== 0) {
|
|
564
|
-
console.warn("caddy reload exited non-zero — Caddyfile written, run reload manually once the container is up.");
|
|
649
|
+
const r = await refreshCaddy(cfg);
|
|
650
|
+
if (!r.ok) {
|
|
651
|
+
console.error(`${CADDYFILE_PATH} was re-written, but ${r.error}`);
|
|
652
|
+
process.exit(1);
|
|
565
653
|
}
|
|
654
|
+
console.log(`Re-emitted ${CADDYFILE_PATH} and reloaded Caddy.`);
|
|
566
655
|
}
|
|
567
656
|
async function status() {
|
|
568
657
|
const provider = loadProvider();
|
|
@@ -584,160 +673,65 @@ async function status() {
|
|
|
584
673
|
console.log("Container health: run `docker compose -f .caelo/docker-compose.yml ps`");
|
|
585
674
|
console.log("Cert status: visit /settings/domains in the admin");
|
|
586
675
|
}
|
|
676
|
+
/** `backup [--to <file>]` for a self-hosted install (self-hosted-backup.ts). */
|
|
587
677
|
async function backup() {
|
|
588
678
|
requireSelfHosted("backup");
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
console.error("no config");
|
|
679
|
+
if (!loadConfig()) {
|
|
680
|
+
console.error("no config — run `cms-provision init` first");
|
|
592
681
|
process.exit(2);
|
|
593
682
|
}
|
|
594
|
-
const to = arg("to") ??
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
"--type=full",
|
|
608
|
-
"--stanza=caelo",
|
|
609
|
-
"backup",
|
|
610
|
-
], { stdout: "inherit", stderr: "inherit" });
|
|
611
|
-
await pgb.exited;
|
|
612
|
-
if (pgb.exitCode !== 0) {
|
|
613
|
-
console.error("pgBackRest backup failed; aborting");
|
|
614
|
-
process.exit(pgb.exitCode ?? 1);
|
|
615
|
-
}
|
|
616
|
-
// 2. MinIO mirror to a tmp inside the minio container.
|
|
617
|
-
console.log("Step 2/3: mirroring MinIO bucket …");
|
|
618
|
-
const mc = Bun.spawn([
|
|
619
|
-
"docker",
|
|
620
|
-
"compose",
|
|
621
|
-
"-f",
|
|
622
|
-
COMPOSE_PATH,
|
|
623
|
-
"exec",
|
|
624
|
-
"-T",
|
|
625
|
-
"minio",
|
|
626
|
-
"mc",
|
|
627
|
-
"mirror",
|
|
628
|
-
"--overwrite",
|
|
629
|
-
"/data",
|
|
630
|
-
"/tmp/minio-backup",
|
|
631
|
-
], { stdout: "inherit", stderr: "inherit" });
|
|
632
|
-
await mc.exited;
|
|
633
|
-
if (mc.exitCode !== 0) {
|
|
634
|
-
console.error("MinIO mirror failed; aborting");
|
|
635
|
-
process.exit(mc.exitCode ?? 1);
|
|
636
|
-
}
|
|
637
|
-
// 3. tar+zstd from named volumes onto the host.
|
|
638
|
-
console.log(`Step 3/3: archiving → ${to}`);
|
|
639
|
-
const tar = Bun.spawn([
|
|
640
|
-
"docker",
|
|
641
|
-
"run",
|
|
642
|
-
"--rm",
|
|
643
|
-
"-v",
|
|
644
|
-
"caelo_caelo-pg-backups:/pgb:ro",
|
|
645
|
-
"-v",
|
|
646
|
-
"caelo_caelo-minio:/minio:ro",
|
|
647
|
-
"-v",
|
|
648
|
-
`${resolve(to, "..")}:/out`,
|
|
649
|
-
"alpine:3",
|
|
650
|
-
"sh",
|
|
651
|
-
"-c",
|
|
652
|
-
`apk add --no-cache zstd tar >/dev/null && tar --zstd -cf /out/${to.split("/").pop()} -C /pgb . -C /minio .`,
|
|
653
|
-
], { stdout: "inherit", stderr: "inherit" });
|
|
654
|
-
await tar.exited;
|
|
655
|
-
if (tar.exitCode !== 0) {
|
|
656
|
-
console.error("tar archive failed");
|
|
657
|
-
process.exit(tar.exitCode ?? 1);
|
|
658
|
-
}
|
|
659
|
-
console.log(`Backup complete: ${to}`);
|
|
683
|
+
const to = resolve(arg("to") ?? `caelo-backup-${new Date().toISOString().replace(/[:.]/g, "-")}.tar.gz`);
|
|
684
|
+
const { backupSelfHosted } = await import("./self-hosted-backup.js");
|
|
685
|
+
const r = await backupSelfHosted(to, readRelease(CAELO_DIR)?.version ?? CAELO_VERSION, {
|
|
686
|
+
caeloDir: CAELO_DIR,
|
|
687
|
+
runner: bunHostRunner,
|
|
688
|
+
log: (m) => console.log(`==> ${m}`),
|
|
689
|
+
});
|
|
690
|
+
if (!r.ok) {
|
|
691
|
+
console.error(`Backup failed: ${r.error}`);
|
|
692
|
+
process.exit(1);
|
|
693
|
+
}
|
|
694
|
+
console.log(`Backup complete: ${r.path}`);
|
|
695
|
+
console.log(`It holds the databases, the published site and the media — not ${CONFIG_PATH} (the secrets); keep a copy of that separately.`);
|
|
660
696
|
}
|
|
697
|
+
/** `restore --from <file> --yes` for a self-hosted install (self-hosted-backup.ts). */
|
|
661
698
|
async function restore() {
|
|
662
699
|
requireSelfHosted("restore");
|
|
663
700
|
const from = arg("from");
|
|
664
701
|
if (!from) {
|
|
665
|
-
console.error("Usage: cms-provision restore --from <
|
|
702
|
+
console.error("Usage: cms-provision restore --from <backup.tar.gz> --yes");
|
|
666
703
|
process.exit(2);
|
|
667
704
|
}
|
|
668
705
|
if (!process.argv.includes("--yes")) {
|
|
669
|
-
console.error(`DESTRUCTIVE:
|
|
706
|
+
console.error(`DESTRUCTIVE: replaces both databases, the published site and the media with ${from}. Re-run with --yes to confirm.`);
|
|
670
707
|
process.exit(1);
|
|
671
708
|
}
|
|
672
709
|
const cfg = loadConfig();
|
|
673
|
-
if (!cfg) {
|
|
674
|
-
console.error("no config");
|
|
710
|
+
if (!cfg?.rolePasswords) {
|
|
711
|
+
console.error("no config with role passwords — run `cms-provision init` / `up` first");
|
|
675
712
|
process.exit(2);
|
|
676
713
|
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
"--rm",
|
|
699
|
-
"-v",
|
|
700
|
-
"caelo_caelo-pg-backups:/pgb",
|
|
701
|
-
"-v",
|
|
702
|
-
"caelo_caelo-minio:/minio",
|
|
703
|
-
"-v",
|
|
704
|
-
`${tarDir}:/in:ro`,
|
|
705
|
-
"alpine:3",
|
|
706
|
-
"sh",
|
|
707
|
-
"-c",
|
|
708
|
-
`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`,
|
|
709
|
-
], { stdout: "inherit", stderr: "inherit" });
|
|
710
|
-
await ex.exited;
|
|
711
|
-
if (ex.exitCode !== 0) {
|
|
712
|
-
console.error("extract failed");
|
|
713
|
-
process.exit(ex.exitCode ?? 1);
|
|
714
|
-
}
|
|
715
|
-
// 3. pgBackRest restore.
|
|
716
|
-
console.log("Step 3/4: pgBackRest restore …");
|
|
717
|
-
const pgr = Bun.spawn([
|
|
718
|
-
"docker",
|
|
719
|
-
"compose",
|
|
720
|
-
"-f",
|
|
721
|
-
COMPOSE_PATH,
|
|
722
|
-
"exec",
|
|
723
|
-
"-T",
|
|
724
|
-
"pgbackrest",
|
|
725
|
-
"pgbackrest",
|
|
726
|
-
"--stanza=caelo",
|
|
727
|
-
"restore",
|
|
728
|
-
], { stdout: "inherit", stderr: "inherit" });
|
|
729
|
-
await pgr.exited;
|
|
730
|
-
if (pgr.exitCode !== 0) {
|
|
731
|
-
console.error("pgBackRest restore failed");
|
|
732
|
-
process.exit(pgr.exitCode ?? 1);
|
|
733
|
-
}
|
|
734
|
-
// 4. Bring services back up.
|
|
735
|
-
console.log("Step 4/4: restarting services …");
|
|
736
|
-
await Bun.spawn(["docker", "compose", "-f", COMPOSE_PATH, "up", "-d"], {
|
|
737
|
-
stdout: "inherit",
|
|
738
|
-
stderr: "inherit",
|
|
739
|
-
}).exited;
|
|
740
|
-
console.log("Restore complete.");
|
|
714
|
+
const passwords = cfg.rolePasswords;
|
|
715
|
+
const { restoreSelfHosted } = await import("./self-hosted-backup.js");
|
|
716
|
+
const r = await restoreSelfHosted(resolve(from), {
|
|
717
|
+
caeloDir: CAELO_DIR,
|
|
718
|
+
runner: bunHostRunner,
|
|
719
|
+
log: (m) => console.log(`==> ${m}`),
|
|
720
|
+
convergeRoles: () => convergeSelfHostedRoles(passwords, {
|
|
721
|
+
probe: postgresIsUp,
|
|
722
|
+
psql: superuserPsql,
|
|
723
|
+
...(activeAgent(cfg) ? { agentPassword: activeAgent(cfg)?.dbPassword } : {}),
|
|
724
|
+
}),
|
|
725
|
+
refreshCaddy: () => refreshCaddy(cfg),
|
|
726
|
+
});
|
|
727
|
+
if (!r.ok) {
|
|
728
|
+
console.error(`Restore failed: ${r.error}`);
|
|
729
|
+
if (r.installChanged) {
|
|
730
|
+
console.error(`The install is partly restored. Fix the cause and run the same restore again — it starts over from the archive.`);
|
|
731
|
+
}
|
|
732
|
+
process.exit(1);
|
|
733
|
+
}
|
|
734
|
+
console.log(`Restore complete (backup of Caelo ${r.manifest.caeloVersion}, taken ${r.manifest.createdAt}).`);
|
|
741
735
|
}
|
|
742
736
|
/**
|
|
743
737
|
* #696 — `migrate`: apply the database migrations with the admin image the
|
|
@@ -922,7 +916,7 @@ const handlers = {
|
|
|
922
916
|
// back-compat scripts that bypassed the dispatcher.
|
|
923
917
|
status: lifecycleStatus,
|
|
924
918
|
"status-self-hosted": status,
|
|
925
|
-
backup: lifecycleBackup,
|
|
919
|
+
backup: async () => (selfHostedInstallPresent() ? backup() : lifecycleBackup()),
|
|
926
920
|
"backup-self-hosted": backup,
|
|
927
921
|
restore,
|
|
928
922
|
upgrade: lifecycleUpgrade,
|