@caelo-cms/provisioning 0.11.13 → 0.11.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin-domain.d.ts +4 -46
- package/dist/admin-domain.d.ts.map +1 -1
- package/dist/admin-domain.js +4 -101
- package/dist/admin-domain.js.map +1 -1
- package/dist/agent-db.js +29 -5
- package/dist/audit-job-setup.d.ts.map +1 -1
- package/dist/audit-job-setup.js +14 -19
- package/dist/audit-job-setup.js.map +1 -1
- package/dist/caddy.d.ts +2 -1
- package/dist/caddy.d.ts.map +1 -1
- package/dist/caddy.js +21 -3
- package/dist/caddy.js.map +1 -1
- package/dist/cli.js +111 -62
- package/dist/cli.js.map +1 -1
- package/dist/compose.d.ts +22 -2
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +53 -3
- package/dist/compose.js.map +1 -1
- package/dist/converge-checkpoint.d.ts +1 -0
- package/dist/converge-checkpoint.d.ts.map +1 -1
- package/dist/converge-checkpoint.js +1 -0
- package/dist/converge-checkpoint.js.map +1 -1
- package/dist/deployed-release.d.ts.map +1 -1
- package/dist/deployed-release.js +40 -8
- package/dist/deployed-release.js.map +1 -1
- package/dist/gcp-names.d.ts +33 -14
- package/dist/gcp-names.d.ts.map +1 -1
- package/dist/gcp-names.js +43 -0
- package/dist/gcp-names.js.map +1 -1
- package/dist/gcp-url-map.d.ts +19 -4
- package/dist/gcp-url-map.d.ts.map +1 -1
- package/dist/gcp-url-map.js +33 -5
- package/dist/gcp-url-map.js.map +1 -1
- package/dist/infra-contract.d.ts +1 -1
- package/dist/infra-contract.d.ts.map +1 -1
- package/dist/infra-contract.js +18 -1
- package/dist/infra-contract.js.map +1 -1
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +5 -39
- package/dist/lifecycle.js.map +1 -1
- package/dist/mcp-auth-proxy-setup.d.ts +8 -2
- package/dist/mcp-auth-proxy-setup.d.ts.map +1 -1
- package/dist/mcp-auth-proxy-setup.js +4 -0
- package/dist/mcp-auth-proxy-setup.js.map +1 -1
- package/dist/release-manifest.d.ts +1 -1
- package/dist/release-manifest.d.ts.map +1 -1
- package/dist/release-manifest.js +2 -0
- package/dist/release-manifest.js.map +1 -1
- package/dist/remote-mcp.d.ts.map +1 -1
- package/dist/remote-mcp.js +11 -1
- package/dist/remote-mcp.js.map +1 -1
- package/dist/runtime-secrets.d.ts +10 -0
- package/dist/runtime-secrets.d.ts.map +1 -1
- package/dist/runtime-secrets.js +10 -1
- package/dist/runtime-secrets.js.map +1 -1
- package/dist/stack-contract.d.ts +61 -20
- package/dist/stack-contract.d.ts.map +1 -1
- package/dist/stack-contract.js +173 -35
- package/dist/stack-contract.js.map +1 -1
- package/dist/stack-converge.d.ts.map +1 -1
- package/dist/stack-converge.js +4 -2
- package/dist/stack-converge.js.map +1 -1
- package/dist/staging-admin.d.ts +2 -2
- package/dist/staging-admin.d.ts.map +1 -1
- package/dist/staging-gateway-setup.d.ts +140 -0
- package/dist/staging-gateway-setup.d.ts.map +1 -0
- package/dist/staging-gateway-setup.js +848 -0
- package/dist/staging-gateway-setup.js.map +1 -0
- package/dist/upgrade/app-roll.d.ts +4 -4
- package/dist/upgrade/app-roll.d.ts.map +1 -1
- package/dist/upgrade/app-roll.js +7 -5
- package/dist/upgrade/app-roll.js.map +1 -1
- package/dist/upgrade/converge.d.ts.map +1 -1
- package/dist/upgrade/converge.js +36 -27
- package/dist/upgrade/converge.js.map +1 -1
- package/dist/upgrade/image-forms.d.ts +3 -3
- package/dist/upgrade/image-forms.d.ts.map +1 -1
- package/dist/upgrade/image-forms.js +2 -2
- package/dist/upgrade/image-forms.js.map +1 -1
- package/dist/upgrade/release-images.d.ts +20 -0
- package/dist/upgrade/release-images.d.ts.map +1 -1
- package/dist/upgrade/release-images.js +28 -0
- package/dist/upgrade/release-images.js.map +1 -1
- package/dist/upgrade/upgrade.d.ts +3 -1
- package/dist/upgrade/upgrade.d.ts.map +1 -1
- package/dist/upgrade/upgrade.js +22 -1
- package/dist/upgrade/upgrade.js.map +1 -1
- package/dist/upgrader-job.d.ts.map +1 -1
- package/dist/upgrader-job.js +30 -5
- package/dist/upgrader-job.js.map +1 -1
- package/dist/upgrader-records.d.ts +2 -2
- package/dist/upgrader-records.d.ts.map +1 -1
- package/dist/upgrader-records.js +2 -2
- package/dist/upgrader-records.js.map +1 -1
- package/dist/wizard.d.ts.map +1 -1
- package/dist/wizard.js +3 -3
- package/dist/wizard.js.map +1 -1
- package/dist/wizards/gcp.d.ts.map +1 -1
- package/dist/wizards/gcp.js +48 -9
- package/dist/wizards/gcp.js.map +1 -1
- package/package.json +2 -2
- package/src/admin-domain.test.ts +0 -156
- package/src/admin-domain.ts +4 -117
- package/src/admin-memory.test.ts +2 -0
- package/src/audit-job-setup.test.ts +11 -11
- package/src/audit-job-setup.ts +14 -19
- package/src/caddy-redirects.docker.test.ts +10 -5
- package/src/caddy-redirects.test.ts +7 -5
- package/src/caddy-validate.test.ts +1 -1
- package/src/caddy.test.ts +12 -6
- package/src/caddy.ts +23 -4
- package/src/cli.ts +138 -77
- package/src/compose.ts +67 -2
- package/src/converge-checkpoint.ts +1 -0
- package/src/database-tls-builders.test.ts +3 -1
- package/src/deployed-release.test.ts +21 -0
- package/src/deployed-release.ts +42 -7
- package/src/gateway-env-isolation.test.ts +2 -0
- package/src/gcp-names.ts +53 -0
- package/src/gcp-url-map.test.ts +26 -2
- package/src/gcp-url-map.ts +36 -5
- package/src/index.test.ts +9 -3
- package/src/infra-contract.test.ts +4 -3
- package/src/infra-contract.ts +18 -1
- package/src/lifecycle.ts +8 -52
- package/src/mcp-auth-proxy-setup.test.ts +15 -6
- package/src/mcp-auth-proxy-setup.ts +14 -2
- package/src/release-manifest.ts +3 -1
- package/src/remote-mcp-stack-config.test.ts +1 -0
- package/src/remote-mcp.test.ts +5 -1
- package/src/remote-mcp.ts +10 -1
- package/src/runtime-secrets.test.ts +10 -1
- package/src/runtime-secrets.ts +21 -0
- package/src/self-hosted-backup.test.ts +6 -4
- package/src/self-hosted-compose.test.ts +2 -0
- package/src/stack-contract-parity.test.ts +7 -3
- package/src/stack-contract.test.ts +73 -13
- package/src/stack-contract.ts +195 -42
- package/src/stack-converge-secrets.test.ts +7 -2
- package/src/stack-converge.test.ts +37 -9
- package/src/stack-converge.ts +4 -1
- package/src/staging-admin.ts +2 -2
- package/src/staging-gateway-setup.test.ts +361 -0
- package/src/staging-gateway-setup.ts +1083 -0
- package/src/upgrade/app-roll.ts +7 -4
- package/src/upgrade/converge.ts +40 -26
- package/src/upgrade/image-forms.ts +7 -7
- package/src/upgrade/release-images.test.ts +28 -0
- package/src/upgrade/release-images.ts +44 -0
- package/src/upgrade/upgrade.test.ts +63 -11
- package/src/upgrade/upgrade.ts +29 -2
- package/src/upgrader-job.test.ts +23 -0
- package/src/upgrader-job.ts +45 -11
- package/src/upgrader-records.ts +7 -7
- package/src/wizard.ts +2 -3
- package/src/wizards/gcp.ts +73 -9
- package/stacks/gcp/index.ts +47 -6
- package/stacks/gcp-firebase/index.ts +44 -12
- package/stacks/self-hosted/index.ts +9 -2
package/src/cli.ts
CHANGED
|
@@ -64,6 +64,7 @@ import {
|
|
|
64
64
|
COMPOSE_ADMIN_UPSTREAM,
|
|
65
65
|
COMPOSE_GATEWAY_UPSTREAM,
|
|
66
66
|
COMPOSE_MCP_AUTH_PROXY_UPSTREAM,
|
|
67
|
+
COMPOSE_STAGING_GATEWAY_UPSTREAM,
|
|
67
68
|
type ComposeImages,
|
|
68
69
|
composeProjectName,
|
|
69
70
|
generateDockerCompose,
|
|
@@ -111,6 +112,14 @@ interface CaeloConfig {
|
|
|
111
112
|
* generated before P18 still load — emitConfig() back-fills if missing.
|
|
112
113
|
*/
|
|
113
114
|
caeloSecretKek?: string;
|
|
115
|
+
/**
|
|
116
|
+
* #650 / #1330 — the staging gate's key (32 hex bytes): signs "Open
|
|
117
|
+
* staging" links, derives the crawler credentials. Generated once and
|
|
118
|
+
* persisted here; never re-minted, or every crawler credential stops
|
|
119
|
+
* working. The admin gets it as CAELO_STAGING_GATE_SECRET and publishes it
|
|
120
|
+
* to the staging gateway through the staging projection.
|
|
121
|
+
*/
|
|
122
|
+
stagingGateSecret?: string;
|
|
114
123
|
/**
|
|
115
124
|
* #613 — one password per application role. Optional in the type so
|
|
116
125
|
* config.json files from before still load; `up` back-fills them and
|
|
@@ -319,7 +328,7 @@ function writeCaddyfile(cfg: CaeloConfig, extraDomains: CaddyDomainSpec[]): void
|
|
|
319
328
|
ownerEmail: cfg.ownerEmail,
|
|
320
329
|
tls: cfg.tls,
|
|
321
330
|
publicSiteRoot: `${SITE_OUTPUT_ROOT}/output/production/current`,
|
|
322
|
-
|
|
331
|
+
stagingGatewayUpstream: COMPOSE_STAGING_GATEWAY_UPSTREAM,
|
|
323
332
|
// Caddy runs in its own container: the Compose service names, never
|
|
324
333
|
// localhost (#707).
|
|
325
334
|
adminUpstream: COMPOSE_ADMIN_UPSTREAM,
|
|
@@ -359,6 +368,16 @@ function renderCompose(cfg: CaeloConfig, images: ComposeImages): string {
|
|
|
359
368
|
throw new Error("config has no rolePasswords — run `cms-provision up`");
|
|
360
369
|
}
|
|
361
370
|
if (!cfg.caeloSecretKek) throw new Error("config has no caeloSecretKek — run `cms-provision up`");
|
|
371
|
+
// #1330 — back-filled once for installs from before the staging gate
|
|
372
|
+
// came to self-hosted; persisted before anything uses it.
|
|
373
|
+
if (!cfg.stagingGateSecret) {
|
|
374
|
+
cfg.stagingGateSecret = randomSecret(32);
|
|
375
|
+
saveConfig(cfg);
|
|
376
|
+
}
|
|
377
|
+
if (!cfg.stagingDomain) {
|
|
378
|
+
cfg.stagingDomain = defaultStagingHost(cfg.domain);
|
|
379
|
+
saveConfig(cfg);
|
|
380
|
+
}
|
|
362
381
|
if (!cfg.ports || !cfg.composeProject) {
|
|
363
382
|
throw new Error(
|
|
364
383
|
"config has no ports / composeProject — they are back-filled on load; this is a bug",
|
|
@@ -369,6 +388,8 @@ function renderCompose(cfg: CaeloConfig, images: ComposeImages): string {
|
|
|
369
388
|
for (const line of ensureSelfHostedDbTls(CAELO_DIR)) console.log(` database TLS: ${line}`);
|
|
370
389
|
return generateDockerCompose({
|
|
371
390
|
domain: cfg.domain,
|
|
391
|
+
stagingHost: cfg.stagingDomain,
|
|
392
|
+
stagingGateSecret: cfg.stagingGateSecret,
|
|
372
393
|
postgresPassword: cfg.postgresPassword,
|
|
373
394
|
rolePasswords: cfg.rolePasswords,
|
|
374
395
|
caeloSecretKek: cfg.caeloSecretKek,
|
|
@@ -1420,7 +1441,7 @@ async function domainMappingCommand(kind: "admin" | "staging"): Promise<void> {
|
|
|
1420
1441
|
selectInstall,
|
|
1421
1442
|
writeMetadata,
|
|
1422
1443
|
} = await import("./install-state.js");
|
|
1423
|
-
const { enableAdminDomain
|
|
1444
|
+
const { enableAdminDomain } = await import("./admin-domain.js");
|
|
1424
1445
|
// Guessing among several installs could map the domain in the wrong project.
|
|
1425
1446
|
const selected = selectInstall(
|
|
1426
1447
|
listInstalls().filter((m) => m.projectId),
|
|
@@ -1448,13 +1469,10 @@ async function domainMappingCommand(kind: "admin" | "staging"): Promise<void> {
|
|
|
1448
1469
|
const requested = arg("staging-domain");
|
|
1449
1470
|
const { normalizeStagingHost, stagingHostProblem } = await import("@caelo-cms/shared");
|
|
1450
1471
|
let adminHost: string | null = null;
|
|
1451
|
-
|
|
1472
|
+
const google = googleAdminTarget(meta);
|
|
1473
|
+
if (!requested && google) {
|
|
1452
1474
|
const { inspectStagingAdmin } = await import("./staging-admin.js");
|
|
1453
|
-
const inspected = await inspectStagingAdmin(
|
|
1454
|
-
provider: "gcp-firebase",
|
|
1455
|
-
projectId: meta.projectId,
|
|
1456
|
-
region: meta.region,
|
|
1457
|
-
});
|
|
1475
|
+
const inspected = await inspectStagingAdmin(google);
|
|
1458
1476
|
if (!inspected.ok) {
|
|
1459
1477
|
console.error(
|
|
1460
1478
|
`${command}: could not read the admin's staging host: ${inspected.error}\n` +
|
|
@@ -1465,11 +1483,10 @@ async function domainMappingCommand(kind: "admin" | "staging"): Promise<void> {
|
|
|
1465
1483
|
adminHost = inspected.value.stagingHost;
|
|
1466
1484
|
}
|
|
1467
1485
|
const host = requested ?? adminHost ?? installStagingHost(meta);
|
|
1468
|
-
const problem = stagingHostProblem(
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
);
|
|
1486
|
+
const problem = stagingHostProblem(host, {
|
|
1487
|
+
liveHosts: [meta.domain, `www.${meta.domain}`],
|
|
1488
|
+
adminHosts: [`admin.${meta.domain}`],
|
|
1489
|
+
});
|
|
1473
1490
|
if (problem) {
|
|
1474
1491
|
console.error(
|
|
1475
1492
|
requested
|
|
@@ -1498,23 +1515,20 @@ async function domainMappingCommand(kind: "admin" | "staging"): Promise<void> {
|
|
|
1498
1515
|
);
|
|
1499
1516
|
process.exit(1);
|
|
1500
1517
|
}
|
|
1518
|
+
if (kind === "staging") {
|
|
1519
|
+
await serveStagingHost(meta, command, retry, { rotatePassword, showPassword });
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1501
1522
|
if (meta.provider !== "gcp-firebase") {
|
|
1502
1523
|
console.log(
|
|
1503
|
-
|
|
1504
|
-
?
|
|
1505
|
-
|
|
1506
|
-
: `admin-domain is for gcp-firebase installs; this one is ${meta.provider}.`
|
|
1507
|
-
: meta.provider === "self-hosted"
|
|
1508
|
-
? `Recorded staging host ${installStagingHost(meta)}; Caddy serves it after \`cms-provision regenerate-caddy\`.`
|
|
1509
|
-
: `staging-domain is for gcp-firebase installs; on ${meta.provider} the protected staging domain is not available yet (issue #650 follow-up).`,
|
|
1524
|
+
meta.provider === "gcp"
|
|
1525
|
+
? `Nothing to do: on gcp the load balancer already serves admin.${meta.domain}.`
|
|
1526
|
+
: `admin-domain is for gcp-firebase installs; this one is ${meta.provider}.`,
|
|
1510
1527
|
);
|
|
1511
1528
|
return;
|
|
1512
1529
|
}
|
|
1513
1530
|
const target = { projectId: meta.projectId, region: meta.region, domain: meta.domain };
|
|
1514
|
-
const r =
|
|
1515
|
-
kind === "admin"
|
|
1516
|
-
? await enableAdminDomain(target)
|
|
1517
|
-
: await enableStagingDomain({ ...target, stagingHost: installStagingHost(meta) });
|
|
1531
|
+
const r = await enableAdminDomain(target);
|
|
1518
1532
|
if (r.status === "failed") {
|
|
1519
1533
|
console.error(`${command}: ${r.error}`);
|
|
1520
1534
|
process.exit(1);
|
|
@@ -1527,56 +1541,7 @@ async function domainMappingCommand(kind: "admin" | "staging"): Promise<void> {
|
|
|
1527
1541
|
);
|
|
1528
1542
|
process.exit(3);
|
|
1529
1543
|
}
|
|
1530
|
-
|
|
1531
|
-
console.log(
|
|
1532
|
-
`${r.hostname} ${r.created ? "is now mapped" : "was already mapped"} to ${mappedTo}.`,
|
|
1533
|
-
);
|
|
1534
|
-
if (kind === "staging") {
|
|
1535
|
-
// #678 — the operator's decision reaches the admin here: the host is
|
|
1536
|
-
// set there, password protection is switched on the first time, and
|
|
1537
|
-
// the password is printed once (never stored by the CLI).
|
|
1538
|
-
const { applyStagingAdmin, describeStagingApply } = await import("./staging-admin.js");
|
|
1539
|
-
const applied = await applyStagingAdmin(
|
|
1540
|
-
{ provider: "gcp-firebase", projectId: meta.projectId, region: meta.region },
|
|
1541
|
-
{
|
|
1542
|
-
host: r.hostname,
|
|
1543
|
-
...(rotatePassword ? { rotateBasicAuth: true } : {}),
|
|
1544
|
-
...(showPassword ? { revealBasicAuth: true } : {}),
|
|
1545
|
-
},
|
|
1546
|
-
);
|
|
1547
|
-
if (!applied.ok) {
|
|
1548
|
-
console.error(
|
|
1549
|
-
`${command}: ${r.hostname} is mapped, but the admin could not be updated: ${applied.error}\n` +
|
|
1550
|
-
`Run \`${retry}\` again once that is fixed (it is idempotent).`,
|
|
1551
|
-
);
|
|
1552
|
-
process.exit(5);
|
|
1553
|
-
}
|
|
1554
|
-
// Prints the staging basic-auth password ON PURPOSE (#678): the operator
|
|
1555
|
-
// asked for it to be shown once; it is never written to disk or logs.
|
|
1556
|
-
console.log(describeStagingApply(applied.value).join("\n"));
|
|
1557
|
-
// #1058 — and the admin's CAELO_STAGING_HOST, so the provisioned value
|
|
1558
|
-
// agrees with install.json and the stored host (the converge job
|
|
1559
|
-
// refuses an install whose records and admin disagree). After the
|
|
1560
|
-
// provision call: the update starts a new admin revision.
|
|
1561
|
-
const { setAdminStagingHostEnv } = await import("./admin-domain.js");
|
|
1562
|
-
const env = await setAdminStagingHostEnv({
|
|
1563
|
-
projectId: meta.projectId,
|
|
1564
|
-
region: meta.region,
|
|
1565
|
-
stagingHost: r.hostname,
|
|
1566
|
-
});
|
|
1567
|
-
if (env.status === "failed") {
|
|
1568
|
-
console.error(
|
|
1569
|
-
`${command}: ${r.hostname} is mapped and stored in the admin, but its CAELO_STAGING_HOST could not be updated: ${env.error}\n` +
|
|
1570
|
-
`Run \`${retry}\` again once that is fixed (it is idempotent).`,
|
|
1571
|
-
);
|
|
1572
|
-
process.exit(5);
|
|
1573
|
-
}
|
|
1574
|
-
if (env.status === "set") {
|
|
1575
|
-
console.log(
|
|
1576
|
-
`The admin (${env.service}) now runs with CAELO_STAGING_HOST=${r.hostname}${env.previous ? ` (was ${env.previous})` : ""}.`,
|
|
1577
|
-
);
|
|
1578
|
-
}
|
|
1579
|
-
}
|
|
1544
|
+
console.log(`${r.hostname} ${r.created ? "is now mapped" : "was already mapped"} to the admin.`);
|
|
1580
1545
|
if (r.records.length === 0) {
|
|
1581
1546
|
console.log(
|
|
1582
1547
|
`Google has not published the DNS records yet; run \`cms-provision ${command} enable\` again in a minute.`,
|
|
@@ -1591,10 +1556,106 @@ async function domainMappingCommand(kind: "admin" | "staging"): Promise<void> {
|
|
|
1591
1556
|
region: meta.region,
|
|
1592
1557
|
});
|
|
1593
1558
|
if (!waited) process.exit(4);
|
|
1559
|
+
console.log(`Admin login: https://${r.hostname}/login`);
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
/** The admin-internal target of a Google Cloud install, or null for another provider. */
|
|
1563
|
+
function googleAdminTarget(
|
|
1564
|
+
meta: import("./install-state.js").InstallMetadata,
|
|
1565
|
+
): import("./staging-admin.js").StagingAdminTarget | null {
|
|
1566
|
+
if (!meta.projectId || !meta.region) return null;
|
|
1567
|
+
if (meta.provider === "gcp-firebase") {
|
|
1568
|
+
return { provider: "gcp-firebase", projectId: meta.projectId, region: meta.region };
|
|
1569
|
+
}
|
|
1570
|
+
if (meta.provider === "gcp") {
|
|
1571
|
+
return { provider: "gcp", projectId: meta.projectId, domain: meta.domain };
|
|
1572
|
+
}
|
|
1573
|
+
return null;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
/**
|
|
1577
|
+
* #1330 — `staging-domain enable` on a Google Cloud install: route the
|
|
1578
|
+
* staging host to the staging gateway (staging-gateway-setup.ts — the same
|
|
1579
|
+
* step every converge runs), tell the admin the host and switch on password
|
|
1580
|
+
* protection the first time (#678, printing the password once), keep the
|
|
1581
|
+
* admin's CAELO_STAGING_HOST in step (#1058), and print the DNS records.
|
|
1582
|
+
*/
|
|
1583
|
+
async function serveStagingHost(
|
|
1584
|
+
meta: import("./install-state.js").InstallMetadata,
|
|
1585
|
+
command: string,
|
|
1586
|
+
retry: string,
|
|
1587
|
+
opts: { readonly rotatePassword: boolean; readonly showPassword: boolean },
|
|
1588
|
+
): Promise<void> {
|
|
1589
|
+
const { installStagingHost } = await import("./install-state.js");
|
|
1590
|
+
const host = installStagingHost(meta);
|
|
1591
|
+
const google = googleAdminTarget(meta);
|
|
1592
|
+
if (!google || !meta.projectId || !meta.region) {
|
|
1593
|
+
console.log(
|
|
1594
|
+
meta.provider === "self-hosted"
|
|
1595
|
+
? `Recorded staging host ${host}; the staging gateway serves it behind Caddy after \`cms-provision regenerate-caddy\`.`
|
|
1596
|
+
: `On ${meta.provider} the protected staging site is not available yet: it needs the staging gateway this provider does not run yet (#749 / #750).`,
|
|
1597
|
+
);
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
1600
|
+
const { ensureStagingGatewayEdge } = await import("./staging-gateway-setup.js");
|
|
1601
|
+
const { GCP_STACK_ENV } = await import("./gcp-names.js");
|
|
1602
|
+
const edge = await ensureStagingGatewayEdge({
|
|
1603
|
+
provider: meta.provider as "gcp" | "gcp-firebase",
|
|
1604
|
+
projectId: meta.projectId,
|
|
1605
|
+
region: meta.region,
|
|
1606
|
+
env: GCP_STACK_ENV,
|
|
1607
|
+
domain: meta.domain,
|
|
1608
|
+
stagingHost: host,
|
|
1609
|
+
});
|
|
1610
|
+
for (const line of edge.done) console.log(` ${line}`);
|
|
1611
|
+
if (!edge.ok) {
|
|
1612
|
+
console.error(
|
|
1613
|
+
`${command}: ${host} could not be routed to the staging gateway: ${edge.error}\nRun \`${retry}\` again once that is fixed (it is idempotent); \`cms-provision upgrade\` sets up the staging gateway itself if it is missing.`,
|
|
1614
|
+
);
|
|
1615
|
+
process.exit(1);
|
|
1616
|
+
}
|
|
1617
|
+
const { applyStagingAdmin, describeStagingApply } = await import("./staging-admin.js");
|
|
1618
|
+
const applied = await applyStagingAdmin(google, {
|
|
1619
|
+
host,
|
|
1620
|
+
...(opts.rotatePassword ? { rotateBasicAuth: true } : {}),
|
|
1621
|
+
...(opts.showPassword ? { revealBasicAuth: true } : {}),
|
|
1622
|
+
});
|
|
1623
|
+
if (!applied.ok) {
|
|
1624
|
+
console.error(
|
|
1625
|
+
`${command}: ${host} is routed to the staging gateway, but the admin could not be updated: ${applied.error}\nRun \`${retry}\` again once that is fixed (it is idempotent).`,
|
|
1626
|
+
);
|
|
1627
|
+
process.exit(5);
|
|
1628
|
+
}
|
|
1629
|
+
// Prints the staging basic-auth password ON PURPOSE (#678): the operator
|
|
1630
|
+
// asked for it to be shown once; it is never written to disk or logs.
|
|
1631
|
+
console.log(describeStagingApply(applied.value).join("\n"));
|
|
1632
|
+
const { setAdminStagingHostEnv } = await import("./admin-domain.js");
|
|
1633
|
+
const env = await setAdminStagingHostEnv({
|
|
1634
|
+
projectId: meta.projectId,
|
|
1635
|
+
region: meta.region,
|
|
1636
|
+
stagingHost: host,
|
|
1637
|
+
});
|
|
1638
|
+
if (env.status === "failed") {
|
|
1639
|
+
console.error(
|
|
1640
|
+
`${command}: ${host} is routed and stored in the admin, but its CAELO_STAGING_HOST could not be updated: ${env.error}\nRun \`${retry}\` again once that is fixed (it is idempotent).`,
|
|
1641
|
+
);
|
|
1642
|
+
process.exit(5);
|
|
1643
|
+
}
|
|
1644
|
+
if (env.status === "set") {
|
|
1645
|
+
console.log(
|
|
1646
|
+
`The admin (${env.service}) now runs with CAELO_STAGING_HOST=${host}${env.previous ? ` (was ${env.previous})` : ""}.`,
|
|
1647
|
+
);
|
|
1648
|
+
}
|
|
1649
|
+
if (edge.dnsRecords.length > 0) {
|
|
1650
|
+
const width = Math.max(...edge.dnsRecords.map((d) => d.name.length));
|
|
1651
|
+
console.log(
|
|
1652
|
+
`\nSet these DNS records at your registrar (if you have not already; Settings → Updates shows them too):\n${edge.dnsRecords
|
|
1653
|
+
.map((d) => ` ${d.name.padEnd(width)} ${d.type.padEnd(5)} ${d.value}`)
|
|
1654
|
+
.join("\n")}`,
|
|
1655
|
+
);
|
|
1656
|
+
}
|
|
1594
1657
|
console.log(
|
|
1595
|
-
|
|
1596
|
-
? `Admin login: https://${r.hostname}/login`
|
|
1597
|
-
: `Protected staging site: https://${r.hostname}/`,
|
|
1658
|
+
`Protected staging site: https://${host}/ (its certificate is issued once the records resolve)`,
|
|
1598
1659
|
);
|
|
1599
1660
|
}
|
|
1600
1661
|
|
package/src/compose.ts
CHANGED
|
@@ -45,6 +45,8 @@ import {
|
|
|
45
45
|
publicSiteUrl,
|
|
46
46
|
REMOTE_MCP_MODE,
|
|
47
47
|
REMOTE_MCP_MODE_ENV,
|
|
48
|
+
STAGING_GATEWAY_TRUSTED_PROXY_HOPS,
|
|
49
|
+
STAGING_STORAGE_ENV,
|
|
48
50
|
TRUSTED_PROXY_HOPS_ENV,
|
|
49
51
|
} from "./stack-contract.js";
|
|
50
52
|
import { ghcrRepository, isImageDigest, type ReleaseService } from "./upgrade/release-images.js";
|
|
@@ -120,6 +122,18 @@ export const MEDIA_ROOT = "/data/media";
|
|
|
120
122
|
/** The gateway's HTTP port inside the Compose network. */
|
|
121
123
|
export const COMPOSE_GATEWAY_PORT = 8090;
|
|
122
124
|
|
|
125
|
+
/** #1330 — the staging gateway's HTTP port inside the Compose network. */
|
|
126
|
+
export const COMPOSE_STAGING_GATEWAY_PORT = 8091;
|
|
127
|
+
|
|
128
|
+
/** #1330 — the Compose service name of the staging gateway. */
|
|
129
|
+
export const COMPOSE_STAGING_GATEWAY_SERVICE = "caelo-staging-gateway";
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* #1330 — where the staging gateway sees the staged builds: the admin's
|
|
133
|
+
* `output/staging` (host `.caelo/output/output/staging`), mounted read-only.
|
|
134
|
+
*/
|
|
135
|
+
export const STAGING_GATEWAY_STORAGE_DIR = "/srv/staging";
|
|
136
|
+
|
|
123
137
|
/** #1328 — the MCP-Auth-Proxy's HTTP port inside the Compose network. */
|
|
124
138
|
export const COMPOSE_MCP_AUTH_PROXY_PORT = 8095;
|
|
125
139
|
|
|
@@ -130,12 +144,17 @@ export const COMPOSE_MCP_AUTH_PROXY_SERVICE = "caelo-mcp-auth-proxy";
|
|
|
130
144
|
* The Compose services that touch the databases — what `backup` and
|
|
131
145
|
* `restore` stop while they read or rewrite them. Workers run inside the
|
|
132
146
|
* admin (CLAUDE.md §11.B Tier 5); there is no orchestrator or runner
|
|
133
|
-
* service. The
|
|
147
|
+
* service. The staging gateway (#1330) holds the staging database's login.
|
|
148
|
+
* The MCP-Auth-Proxy (#1328) is deliberately not here: it holds no
|
|
134
149
|
* database login, so it keeps running, and a release from before it has no
|
|
135
150
|
* such service for `up` to start. Its health is checked wherever the
|
|
136
151
|
* install starts (self-hosted-start.ts) or rolls (self-hosted-upgrade.ts).
|
|
137
152
|
*/
|
|
138
|
-
export const COMPOSE_APP_SERVICES = [
|
|
153
|
+
export const COMPOSE_APP_SERVICES = [
|
|
154
|
+
"caelo-admin",
|
|
155
|
+
"caelo-gateway",
|
|
156
|
+
COMPOSE_STAGING_GATEWAY_SERVICE,
|
|
157
|
+
] as const;
|
|
139
158
|
|
|
140
159
|
/**
|
|
141
160
|
* Where Caddy reaches the admin and the gateway: their Compose service
|
|
@@ -144,6 +163,7 @@ export const COMPOSE_APP_SERVICES = ["caelo-admin", "caelo-gateway"] as const;
|
|
|
144
163
|
*/
|
|
145
164
|
export const COMPOSE_ADMIN_UPSTREAM = `caelo-admin:${COMPOSE_ADMIN_PORT}`;
|
|
146
165
|
export const COMPOSE_GATEWAY_UPSTREAM = `caelo-gateway:${COMPOSE_GATEWAY_PORT}`;
|
|
166
|
+
export const COMPOSE_STAGING_GATEWAY_UPSTREAM = `${COMPOSE_STAGING_GATEWAY_SERVICE}:${COMPOSE_STAGING_GATEWAY_PORT}`;
|
|
147
167
|
export const COMPOSE_MCP_AUTH_PROXY_UPSTREAM = `${COMPOSE_MCP_AUTH_PROXY_SERVICE}:${COMPOSE_MCP_AUTH_PROXY_PORT}`;
|
|
148
168
|
|
|
149
169
|
/**
|
|
@@ -226,6 +246,15 @@ export interface RolePasswords {
|
|
|
226
246
|
|
|
227
247
|
export interface ComposeSpec {
|
|
228
248
|
readonly domain: string;
|
|
249
|
+
/** #650 — the staging site's host; seeds the admin's staging host setting. */
|
|
250
|
+
readonly stagingHost: string;
|
|
251
|
+
/**
|
|
252
|
+
* #650 / #1330 — the staging gate's key (`.caelo/config.json`
|
|
253
|
+
* `stagingGateSecret`, generated once): signs "Open staging" links and
|
|
254
|
+
* derives the crawler credentials. Admin only; the admin publishes it to
|
|
255
|
+
* the staging gateway through the staging projection.
|
|
256
|
+
*/
|
|
257
|
+
readonly stagingGateSecret: string;
|
|
229
258
|
/** The Postgres superuser's; only the database container uses it. */
|
|
230
259
|
readonly postgresPassword: string; // generated by cms-provision; persisted in .caelo/secrets
|
|
231
260
|
readonly rolePasswords: RolePasswords;
|
|
@@ -378,6 +407,8 @@ ${postgresAgent(spec.agent)} volumes:
|
|
|
378
407
|
${adminService(spec, adminMemoryMiB, env)}
|
|
379
408
|
|
|
380
409
|
${gatewayService(spec)}
|
|
410
|
+
|
|
411
|
+
${stagingGatewayService(spec)}
|
|
381
412
|
${mcpAuthProxyService(spec.images)}
|
|
382
413
|
${schedulerService(spec)}
|
|
383
414
|
volumes:
|
|
@@ -475,6 +506,12 @@ ${serviceRuntime(
|
|
|
475
506
|
PUBLIC_STAGING_ADMIN_DATABASE_URL: "${dbUrl("admin_role", spec.rolePasswords.admin, "cms_public_staging")}"
|
|
476
507
|
CAELO_SECRET_KEK: "${escapeYaml(spec.caeloSecretKek)}"
|
|
477
508
|
CAELO_INTERNAL_SECRET: "${escapeYaml(spec.internalSecret)}"
|
|
509
|
+
# #650 / #1330 — the staging gate's key and the staging host the admin seeds.
|
|
510
|
+
CAELO_STAGING_GATE_SECRET: "${escapeYaml(spec.stagingGateSecret)}"
|
|
511
|
+
CAELO_STAGING_HOST: "${escapeYaml(spec.stagingHost)}"
|
|
512
|
+
# #1330 — the staging gateway on the Compose network: the Stage's
|
|
513
|
+
# serve check asks it whether it sees the build just written.
|
|
514
|
+
CAELO_STAGING_GATEWAY_URL: "http://${COMPOSE_STAGING_GATEWAY_UPSTREAM}"
|
|
478
515
|
CAELO_SITE_URL: "${escapeYaml(selfHostedSiteUrl(spec.domain, spec.ports.https))}"
|
|
479
516
|
PORT: "${COMPOSE_ADMIN_PORT}"
|
|
480
517
|
CAELO_OUTPUT_ROOT: "${SITE_OUTPUT_ROOT}"
|
|
@@ -505,6 +542,34 @@ ${poolEnvLines("gateway")}
|
|
|
505
542
|
NODE_ENV: production`;
|
|
506
543
|
}
|
|
507
544
|
|
|
545
|
+
/**
|
|
546
|
+
* #1330 — the staging gateway: the gateway's image with
|
|
547
|
+
* `CAELO_GATEWAY_ENVIRONMENT=staging`. It serves the staged builds, which it
|
|
548
|
+
* reads from the admin's `output/staging` mounted read-only (no other part
|
|
549
|
+
* of the output, nothing writable), behind the staging gate, and staging's
|
|
550
|
+
* own data: `public_staging_role` on `cms_public_staging` is its only
|
|
551
|
+
* login. Caddy sends every request for the staging host here.
|
|
552
|
+
*/
|
|
553
|
+
function stagingGatewayService(spec: ComposeSpec): string {
|
|
554
|
+
return ` ${COMPOSE_STAGING_GATEWAY_SERVICE}:
|
|
555
|
+
${serviceRuntime(spec.images, "gateway", "bun run apps/api-gateway/src/server.ts", [
|
|
556
|
+
DATABASE_CA_MOUNT,
|
|
557
|
+
`./output/output/staging:${STAGING_GATEWAY_STORAGE_DIR}:ro`,
|
|
558
|
+
])}
|
|
559
|
+
restart: unless-stopped
|
|
560
|
+
depends_on:
|
|
561
|
+
postgres: { condition: service_healthy }
|
|
562
|
+
environment:
|
|
563
|
+
PUBLIC_DATABASE_URL: "${dbUrl("public_staging_role", spec.rolePasswords.publicStaging, "cms_public_staging")}"
|
|
564
|
+
${GATEWAY_ENVIRONMENT_ENV}: "staging"
|
|
565
|
+
GATEWAY_PORT: "${COMPOSE_STAGING_GATEWAY_PORT}"
|
|
566
|
+
${STAGING_STORAGE_ENV}: "file://${STAGING_GATEWAY_STORAGE_DIR}"
|
|
567
|
+
# Caddy is the one proxy in front (client-ip.ts in the gateway).
|
|
568
|
+
${TRUSTED_PROXY_HOPS_ENV}: "${STAGING_GATEWAY_TRUSTED_PROXY_HOPS["self-hosted"]}"
|
|
569
|
+
${poolEnvLines("gateway")}
|
|
570
|
+
NODE_ENV: production`;
|
|
571
|
+
}
|
|
572
|
+
|
|
508
573
|
/**
|
|
509
574
|
* #1328 — the MCP-Auth-Proxy: remote MCP's sign-in broker, its own
|
|
510
575
|
* container so it never shares a process with visitor handlers. Remote MCP
|
|
@@ -49,6 +49,7 @@ export const ConvergeCheckpointSchema = InstallJobStatusSchema.extend({
|
|
|
49
49
|
admin: z.string().min(1),
|
|
50
50
|
gateway: z.string().min(1),
|
|
51
51
|
// #1328 — absent in a checkpoint an older release wrote.
|
|
52
|
+
"staging-gateway": z.string().min(1).optional(),
|
|
52
53
|
"mcp-auth-proxy": z.string().min(1).optional(),
|
|
53
54
|
})
|
|
54
55
|
.optional(),
|
|
@@ -32,6 +32,8 @@ describe("database URL builders (#1338)", () => {
|
|
|
32
32
|
caeloSecretKek: "0".repeat(64),
|
|
33
33
|
dbTlsServerCertSha256: "abababababababababababababababababababababababababababababababab",
|
|
34
34
|
internalSecret: "i".repeat(48),
|
|
35
|
+
stagingHost: "staging.example.com",
|
|
36
|
+
stagingGateSecret: "g".repeat(64),
|
|
35
37
|
diskSize: "20Gi",
|
|
36
38
|
images: {
|
|
37
39
|
mode: "release",
|
|
@@ -43,7 +45,7 @@ describe("database URL builders (#1338)", () => {
|
|
|
43
45
|
});
|
|
44
46
|
const urls = [...yaml.matchAll(/_DATABASE_URL: "([^"]+)"/g)].map((m) => m[1] as string);
|
|
45
47
|
// admin: cms_admin, cms_public, cms_public_staging (#1329); gateway: cms_public.
|
|
46
|
-
expect(urls).toHaveLength(
|
|
48
|
+
expect(urls).toHaveLength(5);
|
|
47
49
|
for (const url of urls) expect(databaseTlsProblem(url)).toBeNull();
|
|
48
50
|
});
|
|
49
51
|
|
|
@@ -111,6 +111,27 @@ describe("readDeployedImageDigests", () => {
|
|
|
111
111
|
if (!r.ok) expect(r.error).toContain("not pinned to a digest");
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
+
it("#1330 — accepts a staging gateway on the gateway's image, refuses one on another", async () => {
|
|
115
|
+
const answers = (staging: string) => ({
|
|
116
|
+
...firebaseLists(),
|
|
117
|
+
"metadata.name~^caelo-production-staging-gateway": [ok("caelo-production-staging-gateway\n")],
|
|
118
|
+
"describe caelo-production-admin-abc": [ok(`${REG}/admin@${A}`)],
|
|
119
|
+
"describe caelo-production-data-gateway": [ok(`${REG}/gateway@${B}`)],
|
|
120
|
+
"describe caelo-production-gateway-def": [ok(`${REG}/mcp-auth-proxy@${C}`)],
|
|
121
|
+
"describe caelo-production-staging-gateway": [ok(`${REG}/gateway@${staging}`)],
|
|
122
|
+
});
|
|
123
|
+
const same = fakeGcloud(answers(B));
|
|
124
|
+
expect(await readDeployedImageDigests({ ...FIREBASE, run: same.run })).toEqual({
|
|
125
|
+
ok: true,
|
|
126
|
+
digests: { admin: A, gateway: B, "mcp-auth-proxy": C },
|
|
127
|
+
});
|
|
128
|
+
expect(same.calls.some((c) => c.includes("caelo-production-staging-gateway"))).toBe(true);
|
|
129
|
+
const other = fakeGcloud(answers(A));
|
|
130
|
+
const r = await readDeployedImageDigests({ ...FIREBASE, run: other.run });
|
|
131
|
+
expect(r.ok).toBe(false);
|
|
132
|
+
if (!r.ok) expect(r.error).toContain("the staging gateway runs");
|
|
133
|
+
});
|
|
134
|
+
|
|
114
135
|
it("surfaces a gcloud failure", async () => {
|
|
115
136
|
const { run } = fakeGcloud({ "services list": [fail("PERMISSION_DENIED")] });
|
|
116
137
|
const r = await readDeployedImageDigests({ ...FIREBASE, run });
|
package/src/deployed-release.ts
CHANGED
|
@@ -21,7 +21,9 @@ import {
|
|
|
21
21
|
cloudRunServicePrefix,
|
|
22
22
|
type GcpProvider,
|
|
23
23
|
RETIRED_SERVICE_ENV,
|
|
24
|
+
SERVICE_IMAGE_NAMES,
|
|
24
25
|
SERVICE_SECRET_ENV,
|
|
26
|
+
type ServiceImageName,
|
|
25
27
|
} from "./stack-contract.js";
|
|
26
28
|
import { liveContainerEnv } from "./stack-converge.js";
|
|
27
29
|
|
|
@@ -35,7 +37,7 @@ async function findDeployedService(
|
|
|
35
37
|
run: GcloudRunner,
|
|
36
38
|
opts: { provider: GcpProvider; projectId: string; region: string },
|
|
37
39
|
slug: CloudRunSlug,
|
|
38
|
-
): Promise<{ ok: true; name: string } | { ok: false; error: string }> {
|
|
40
|
+
): Promise<{ ok: true; name: string } | { ok: false; error: string; missing: boolean }> {
|
|
39
41
|
const prefix = cloudRunServicePrefix(opts.provider, GCP_STACK_ENV, slug);
|
|
40
42
|
const list = await run([
|
|
41
43
|
"run",
|
|
@@ -46,7 +48,8 @@ async function findDeployedService(
|
|
|
46
48
|
`--filter=metadata.name~^${prefix}`,
|
|
47
49
|
"--format=value(metadata.name)",
|
|
48
50
|
]);
|
|
49
|
-
if (!list.ok)
|
|
51
|
+
if (!list.ok)
|
|
52
|
+
return { ok: false, error: `list Cloud Run services: ${list.stderr.trim()}`, missing: false };
|
|
50
53
|
const names = list.stdout
|
|
51
54
|
.split("\n")
|
|
52
55
|
.map((s) => s.trim())
|
|
@@ -58,6 +61,7 @@ async function findDeployedService(
|
|
|
58
61
|
names.length === 0
|
|
59
62
|
? `no ${prefix}* Cloud Run service: the install runs a release from before it (the ${slug} became its own service in #1328). Run \`upgrade\` first: it creates the service and moves the release over.`
|
|
60
63
|
: `expected one ${prefix}* Cloud Run service, found ${names.join(", ")}`,
|
|
64
|
+
missing: names.length === 0,
|
|
61
65
|
};
|
|
62
66
|
}
|
|
63
67
|
return { ok: true, name: names[0] as string };
|
|
@@ -86,7 +90,11 @@ export async function checkDeployedRuntimeEnv(opts: {
|
|
|
86
90
|
const run = opts.run ?? defaultGcloud;
|
|
87
91
|
for (const slug of CLOUD_RUN_SLUGS) {
|
|
88
92
|
const found = await findDeployedService(run, opts, slug);
|
|
89
|
-
|
|
93
|
+
// #1330 — the staging gateway is the CLI's (staging-gateway-setup.ts):
|
|
94
|
+
// an install from before it gets it after this `up`, so a missing one
|
|
95
|
+
// runs no release that could mismatch the env.
|
|
96
|
+
if (!found.ok && slug === "staging-gateway" && found.missing) continue;
|
|
97
|
+
if (!found.ok) return { ok: false, error: found.error };
|
|
90
98
|
const describe = await run([
|
|
91
99
|
"run",
|
|
92
100
|
"services",
|
|
@@ -131,8 +139,14 @@ export async function readDeployedImageDigests(opts: {
|
|
|
131
139
|
run?: GcloudRunner;
|
|
132
140
|
}): Promise<{ ok: true; digests: ImageDigests } | { ok: false; error: string }> {
|
|
133
141
|
const run = opts.run ?? defaultGcloud;
|
|
134
|
-
const digests: Record<
|
|
135
|
-
|
|
142
|
+
const digests: Record<ServiceImageName, string> = {
|
|
143
|
+
admin: "",
|
|
144
|
+
gateway: "",
|
|
145
|
+
"mcp-auth-proxy": "",
|
|
146
|
+
};
|
|
147
|
+
const digestOf = async (
|
|
148
|
+
slug: CloudRunSlug,
|
|
149
|
+
): Promise<{ ok: true; digest: string } | { ok: false; error: string; missing: boolean }> => {
|
|
136
150
|
const found = await findDeployedService(run, opts, slug);
|
|
137
151
|
if (!found.ok) return found;
|
|
138
152
|
const image = await run([
|
|
@@ -144,15 +158,36 @@ export async function readDeployedImageDigests(opts: {
|
|
|
144
158
|
`--project=${opts.projectId}`,
|
|
145
159
|
"--format=value(spec.template.spec.containers[0].image)",
|
|
146
160
|
]);
|
|
147
|
-
if (!image.ok)
|
|
161
|
+
if (!image.ok) {
|
|
162
|
+
return { ok: false, error: `describe ${found.name}: ${image.stderr.trim()}`, missing: false };
|
|
163
|
+
}
|
|
148
164
|
const digest = digestFromImageRef(image.stdout);
|
|
149
165
|
if (!digest) {
|
|
150
166
|
return {
|
|
151
167
|
ok: false,
|
|
152
168
|
error: `${found.name} runs '${image.stdout.trim()}', which is not pinned to a digest`,
|
|
169
|
+
missing: false,
|
|
153
170
|
};
|
|
154
171
|
}
|
|
155
|
-
|
|
172
|
+
return { ok: true, digest };
|
|
173
|
+
};
|
|
174
|
+
// One digest per release image.
|
|
175
|
+
for (const slug of SERVICE_IMAGE_NAMES) {
|
|
176
|
+
const d = await digestOf(slug);
|
|
177
|
+
if (!d.ok) return { ok: false, error: d.error };
|
|
178
|
+
digests[slug] = d.digest;
|
|
179
|
+
}
|
|
180
|
+
// #1330 — the staging gateway runs the gateway's image and rolls with it:
|
|
181
|
+
// one that runs another image means the services serve two releases, and
|
|
182
|
+
// the wizard must not record (or deploy) either as "the" release. An
|
|
183
|
+
// install from before it has none yet (the CLI creates it after `up`).
|
|
184
|
+
const staging = await digestOf("staging-gateway");
|
|
185
|
+
if (!staging.ok && !staging.missing) return { ok: false, error: staging.error };
|
|
186
|
+
if (staging.ok && staging.digest !== digests.gateway) {
|
|
187
|
+
return {
|
|
188
|
+
ok: false,
|
|
189
|
+
error: `the staging gateway runs ${staging.digest}, but the gateway runs ${digests.gateway}: the two must run the same release image. Run \`upgrade\` first — it rolls both onto one release — then re-run the installer.`,
|
|
190
|
+
};
|
|
156
191
|
}
|
|
157
192
|
return { ok: true, digests };
|
|
158
193
|
}
|
|
@@ -162,6 +162,8 @@ describe("self-hosted: the generated compose gateway service (#635)", () => {
|
|
|
162
162
|
caeloSecretKek: "0".repeat(64),
|
|
163
163
|
dbTlsServerCertSha256: "abababababababababababababababababababababababababababababababab",
|
|
164
164
|
internalSecret: "i".repeat(48),
|
|
165
|
+
stagingHost: "staging.example.com",
|
|
166
|
+
stagingGateSecret: "g".repeat(64),
|
|
165
167
|
diskSize: "20Gi",
|
|
166
168
|
images: {
|
|
167
169
|
mode: "release" as const,
|
package/src/gcp-names.ts
CHANGED
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* the CLI's dependencies.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import { createHash } from "node:crypto";
|
|
19
|
+
|
|
18
20
|
/** The Pulumi stack name the wizard provisions and `upgrade` operates on. */
|
|
19
21
|
export const GCP_STACK_ENV = "production";
|
|
20
22
|
|
|
@@ -87,6 +89,57 @@ export function mcpAuthProxyServiceAccountEmail(projectId: string, env: string):
|
|
|
87
89
|
return `${mcpAuthProxyServiceAccountId(env)}@${projectId}.iam.gserviceaccount.com`;
|
|
88
90
|
}
|
|
89
91
|
|
|
92
|
+
/**
|
|
93
|
+
* #1330 — account id of the service account the staging gateway runs as. It
|
|
94
|
+
* may read the staging database's login password and the staging bucket,
|
|
95
|
+
* and nothing else: no live data, no media, no gate key (the gate key
|
|
96
|
+
* reaches it through the staging projection). Shortened: account ids max
|
|
97
|
+
* out at 30 characters (`caelo-production-stg-gateway-sa` would be 31).
|
|
98
|
+
*/
|
|
99
|
+
export function stagingGatewayServiceAccountId(env: string): string {
|
|
100
|
+
return `${gcpNamePrefix(env)}-stg-gw-sa`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Email of the service account the staging gateway runs as. */
|
|
104
|
+
export function stagingGatewayServiceAccountEmail(projectId: string, env: string): string {
|
|
105
|
+
return `${stagingGatewayServiceAccountId(env)}@${projectId}.iam.gserviceaccount.com`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* #1330 — the `gcp` staging gateway's load-balancer resources, CLI-owned
|
|
110
|
+
* (staging-gateway-setup.ts): its serverless NEG, its backend service and
|
|
111
|
+
* the managed certificate for the staging host. None matches a prefix the
|
|
112
|
+
* other lookups use (`…-waf`, `…-admin-backend`, `…-gateway-backend`,
|
|
113
|
+
* `…-mcp-auth-proxy-*`).
|
|
114
|
+
*/
|
|
115
|
+
export function stagingGatewayEdgeNames(env: string): {
|
|
116
|
+
readonly neg: string;
|
|
117
|
+
readonly backend: string;
|
|
118
|
+
readonly cert: string;
|
|
119
|
+
readonly pathMatcher: string;
|
|
120
|
+
} {
|
|
121
|
+
const prefix = `${gcpNamePrefix(env)}-staging-gateway`;
|
|
122
|
+
return {
|
|
123
|
+
neg: `${prefix}-neg`,
|
|
124
|
+
backend: `${prefix}-backend`,
|
|
125
|
+
cert: `${gcpNamePrefix(env)}-staging-cert`,
|
|
126
|
+
pathMatcher: "staging",
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* #1330 — the second Firebase Hosting site of a `gcp-firebase` install,
|
|
132
|
+
* which serves the staging host through a `**` rewrite to the staging
|
|
133
|
+
* gateway (E13 a of #763). Derived from the live site's id (Hosting site ids
|
|
134
|
+
* are global, and the live one carries a random suffix): `caelo-<env>-stg-`
|
|
135
|
+
* plus six hex characters of its SHA-256, so the admin and the converge,
|
|
136
|
+
* which both know the live site id (`CAELO_FIREBASE_SITE`), name the same
|
|
137
|
+
* site without storing it anywhere.
|
|
138
|
+
*/
|
|
139
|
+
export function firebaseStagingSiteId(env: string, liveSiteId: string): string {
|
|
140
|
+
return `${gcpNamePrefix(env)}-stg-${createHash("sha256").update(liveSiteId).digest("hex").slice(0, 6)}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
90
143
|
/**
|
|
91
144
|
* Account id of the `gcp` stack's static-publisher service account. Account
|
|
92
145
|
* ids max out at 30 chars, so the env is shortened.
|