@caelo-cms/provisioning 0.11.12 → 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 -22
- package/dist/admin-domain.d.ts.map +1 -1
- package/dist/admin-domain.js +4 -58
- 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 +22 -1
- package/dist/infra-contract.js.map +1 -1
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +5 -29
- 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/scheduler-setup.d.ts +5 -0
- package/dist/scheduler-setup.d.ts.map +1 -1
- package/dist/scheduler-setup.js +7 -0
- package/dist/scheduler-setup.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 -91
- package/src/admin-domain.ts +4 -64
- 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 +5 -3
- package/src/infra-contract.ts +22 -1
- package/src/lifecycle.ts +6 -40
- 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/scheduler-setup.test.ts +6 -0
- package/src/scheduler-setup.ts +7 -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
|
@@ -91,7 +91,7 @@ describe("ensureAuditJob", () => {
|
|
|
91
91
|
for (const c of bucketGrants) expect(flag(c, "condition")).toBe("None");
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
-
it("grants the job only results-create,
|
|
94
|
+
it("grants the job only results-create, read of the staging bucket and logs", async () => {
|
|
95
95
|
const { run, calls } = fakeGcloud();
|
|
96
96
|
const r = await ensureAuditJob(TARGET, { run, sleep: async () => {} });
|
|
97
97
|
expect(r.ok).toBe(true);
|
|
@@ -102,10 +102,6 @@ describe("ensureAuditJob", () => {
|
|
|
102
102
|
"storage buckets add-iam-policy-binding gs://acme-caelo-production-staging",
|
|
103
103
|
"roles/storage.objectViewer",
|
|
104
104
|
],
|
|
105
|
-
[
|
|
106
|
-
"storage buckets add-iam-policy-binding gs://acme-caelo-production-static",
|
|
107
|
-
"roles/storage.objectViewer",
|
|
108
|
-
],
|
|
109
105
|
["projects add-iam-policy-binding acme", "roles/logging.logWriter"],
|
|
110
106
|
]);
|
|
111
107
|
for (const g of job) {
|
|
@@ -151,7 +147,7 @@ describe("ensureAuditJob", () => {
|
|
|
151
147
|
expect(flag(deploy, "max-retries")).toBe("0");
|
|
152
148
|
expect(flag(deploy, "parallelism")).toBe("15");
|
|
153
149
|
expect(flag(deploy, "set-env-vars")).toBe(
|
|
154
|
-
`CAELO_PROVIDER=gcp,CAELO_AUDIT_RESULTS_BUCKET=${RESULTS},CAELO_STAGING_BUCKET=acme-caelo-production-staging
|
|
150
|
+
`CAELO_PROVIDER=gcp,CAELO_AUDIT_RESULTS_BUCKET=${RESULTS},CAELO_STAGING_BUCKET=acme-caelo-production-staging`,
|
|
155
151
|
);
|
|
156
152
|
const line = deploy.join(" ");
|
|
157
153
|
for (const banned of ["--network", "--subnet", "--vpc", "--set-secrets", "--update-secrets"]) {
|
|
@@ -206,16 +202,20 @@ describe("ensureAuditJob", () => {
|
|
|
206
202
|
expect(deployOf(calls)).toBeUndefined();
|
|
207
203
|
});
|
|
208
204
|
|
|
209
|
-
it("on gcp-firebase reads
|
|
205
|
+
it("#1330 — on gcp-firebase reads the staging bucket too: no preview channel any more", async () => {
|
|
210
206
|
const { run, calls } = fakeGcloud();
|
|
211
207
|
await ensureAuditJob({ ...TARGET, provider: "gcp-firebase" }, { run, sleep: async () => {} });
|
|
212
208
|
const job = grants(calls).filter((g) => g.member === AUDIT);
|
|
213
|
-
expect(job.map((g) => g.role)).toEqual([
|
|
214
|
-
"roles/storage.objectCreator",
|
|
215
|
-
|
|
209
|
+
expect(job.map((g) => [g.where, g.role])).toEqual([
|
|
210
|
+
[`storage buckets add-iam-policy-binding gs://${RESULTS}`, "roles/storage.objectCreator"],
|
|
211
|
+
[
|
|
212
|
+
"storage buckets add-iam-policy-binding gs://acme-caelo-production-staging",
|
|
213
|
+
"roles/storage.objectViewer",
|
|
214
|
+
],
|
|
215
|
+
["projects add-iam-policy-binding acme", "roles/logging.logWriter"],
|
|
216
216
|
]);
|
|
217
217
|
expect(flag(deployOf(calls), "set-env-vars")).toBe(
|
|
218
|
-
`CAELO_PROVIDER=gcp-firebase,CAELO_AUDIT_RESULTS_BUCKET=${RESULTS}`,
|
|
218
|
+
`CAELO_PROVIDER=gcp-firebase,CAELO_AUDIT_RESULTS_BUCKET=${RESULTS},CAELO_STAGING_BUCKET=acme-caelo-production-staging`,
|
|
219
219
|
);
|
|
220
220
|
});
|
|
221
221
|
|
package/src/audit-job-setup.ts
CHANGED
|
@@ -22,10 +22,12 @@
|
|
|
22
22
|
*
|
|
23
23
|
* - `roles/storage.objectCreator` on the audit results bucket — create
|
|
24
24
|
* only: it can neither read results back nor overwrite one;
|
|
25
|
-
* -
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
25
|
+
* - `roles/storage.objectViewer` on the staging bucket: it serves the
|
|
26
|
+
* staged build to its own browser from the served build slot, which
|
|
27
|
+
* holds the complete build (#1330, admin-core quality/staged-origin.ts),
|
|
28
|
+
* so Lighthouse never goes through the staging gateway (#875). Both
|
|
29
|
+
* providers since #1330 (gcp-firebase had no bucket and audited the
|
|
30
|
+
* preview channel before); no read on the static bucket any more;
|
|
29
31
|
* - `roles/logging.logWriter`.
|
|
30
32
|
*
|
|
31
33
|
* Nothing else: no Secret Manager, no Cloud SQL, and no VPC egress, so the
|
|
@@ -231,18 +233,16 @@ export async function ensureAuditJob(
|
|
|
231
233
|
}
|
|
232
234
|
done.push(`bucket ${resultsUrl} (results deleted after 1 day)`);
|
|
233
235
|
|
|
234
|
-
// 3. Bucket grants: the job creates results and
|
|
236
|
+
// 3. Bucket grants: the job creates results and reads the staged build;
|
|
235
237
|
// the admin reads results.
|
|
236
238
|
const bucketGrants: { bucket: string; member: string; role: string; why: string }[] = [
|
|
237
239
|
{ bucket: results, member, role: "roles/storage.objectCreator", why: "tasks write results" },
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
}))
|
|
245
|
-
: []),
|
|
240
|
+
{
|
|
241
|
+
bucket: gcpBucketName(projectId, env, "staging"),
|
|
242
|
+
member,
|
|
243
|
+
role: "roles/storage.objectViewer",
|
|
244
|
+
why: "tasks serve the staged build to their browser",
|
|
245
|
+
},
|
|
246
246
|
{
|
|
247
247
|
bucket: results,
|
|
248
248
|
member: runSaMember,
|
|
@@ -295,12 +295,7 @@ export async function ensureAuditJob(
|
|
|
295
295
|
const envVars = [
|
|
296
296
|
`CAELO_PROVIDER=${provider}`,
|
|
297
297
|
`CAELO_AUDIT_RESULTS_BUCKET=${results}`,
|
|
298
|
-
|
|
299
|
-
? [
|
|
300
|
-
`CAELO_STAGING_BUCKET=${gcpBucketName(projectId, env, "staging")}`,
|
|
301
|
-
`CAELO_STATIC_BUCKET=${gcpBucketName(projectId, env, "static")}`,
|
|
302
|
-
]
|
|
303
|
-
: []),
|
|
298
|
+
`CAELO_STAGING_BUCKET=${gcpBucketName(projectId, env, "staging")}`,
|
|
304
299
|
];
|
|
305
300
|
const deploy = await run([
|
|
306
301
|
"run",
|
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
* imported;
|
|
10
10
|
* - a container started like the Compose service (same Caddyfile shape,
|
|
11
11
|
* same `/srv/caelo` mount, same reload-loop command and snippet glob)
|
|
12
|
-
* answers redirected paths on the live
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* answers redirected paths on the live host with the planned status +
|
|
13
|
+
* Location — query kept, case-sensitive, the live page wins, 410 for
|
|
14
|
+
* gone — and serves nothing of output/staging on the staging host (#1330:
|
|
15
|
+
* that is the staging gateway's);
|
|
15
16
|
* - the reload loop picks up a deploy's new snippet, a snippet Caddy
|
|
16
17
|
* rejects leaves the previous redirects live, and the next good snippet
|
|
17
18
|
* is picked up again.
|
|
@@ -147,7 +148,7 @@ describe.skipIf(!dockerUp)(`self-hosted redirects through ${CADDY_IMAGE}`, () =>
|
|
|
147
148
|
ownerEmail: "owner@example.test",
|
|
148
149
|
tls: "internal",
|
|
149
150
|
publicSiteRoot: `${SITE_OUTPUT_ROOT}/output/production/current`,
|
|
150
|
-
|
|
151
|
+
stagingGatewayUpstream: "caelo-staging-gateway:8091",
|
|
151
152
|
adminUpstream: COMPOSE_ADMIN_UPSTREAM,
|
|
152
153
|
gatewayUpstream: COMPOSE_GATEWAY_UPSTREAM,
|
|
153
154
|
mcpAuthProxyUpstream: COMPOSE_MCP_AUTH_PROXY_UPSTREAM,
|
|
@@ -246,7 +247,11 @@ describe.skipIf(!dockerUp)(`self-hosted redirects through ${CADDY_IMAGE}`, () =>
|
|
|
246
247
|
expectRedirect("/hop1", 301, "/blog/");
|
|
247
248
|
expectRedirect("/Case", 301, "/about/");
|
|
248
249
|
expectRedirect("/q?dropped=1", 301, "/about?ref=old");
|
|
249
|
-
|
|
250
|
+
// #1330 — Caddy serves no staged file and answers no staging redirect:
|
|
251
|
+
// the staging host goes to the staging gateway (not running here, so
|
|
252
|
+
// Caddy answers 502), whatever lies in output/staging.
|
|
253
|
+
expect(probe(STAGING, "/staged-old").status).toBe(502);
|
|
254
|
+
expect(probe(STAGING, "/index.html").status).toBe(502);
|
|
250
255
|
expect(probe(LIVE, "/gone").status).toBe(410);
|
|
251
256
|
// Exact, case-sensitive: /case is not /Case.
|
|
252
257
|
expect(probe(LIVE, "/case").status).toBe(404);
|
|
@@ -24,7 +24,7 @@ const caddyfile = generateCaddyfile({
|
|
|
24
24
|
ownerEmail: "owner@example.com",
|
|
25
25
|
tls: "acme",
|
|
26
26
|
publicSiteRoot: "/srv/caelo/output/production/current",
|
|
27
|
-
|
|
27
|
+
stagingGatewayUpstream: "caelo-staging-gateway:8091",
|
|
28
28
|
adminUpstream: COMPOSE_ADMIN_UPSTREAM,
|
|
29
29
|
gatewayUpstream: COMPOSE_GATEWAY_UPSTREAM,
|
|
30
30
|
mcpAuthProxyUpstream: COMPOSE_MCP_AUTH_PROXY_UPSTREAM,
|
|
@@ -41,13 +41,13 @@ function block(host: string): string {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
describe("Caddyfile — redirects (#587)", () => {
|
|
44
|
-
it("
|
|
44
|
+
it("the live vhost imports the redirect snippet of the build it serves", () => {
|
|
45
45
|
expect(block("example.com")).toContain(
|
|
46
46
|
" root * /srv/caelo/output/production/current\n import /srv/caelo/output/production/current/_redirects.caddy*\n",
|
|
47
47
|
);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
);
|
|
48
|
+
// #1330 — staging's redirects are the staging gateway's (from the
|
|
49
|
+
// staging projection), not Caddy's.
|
|
50
|
+
expect(block("staging.example.com")).not.toContain("_redirects.caddy");
|
|
51
51
|
expect(block("admin.example.com")).not.toContain("_redirects.caddy");
|
|
52
52
|
});
|
|
53
53
|
|
|
@@ -67,6 +67,8 @@ describe("Compose — Caddy picks up new redirects (#587)", () => {
|
|
|
67
67
|
caeloSecretKek: "0".repeat(64),
|
|
68
68
|
dbTlsServerCertSha256: "abababababababababababababababababababababababababababababababab",
|
|
69
69
|
internalSecret: "i".repeat(48),
|
|
70
|
+
stagingHost: "staging.example.com",
|
|
71
|
+
stagingGateSecret: "g".repeat(64),
|
|
70
72
|
diskSize: "20Gi",
|
|
71
73
|
ports: { http: 80, https: 443 },
|
|
72
74
|
project: "caelo-example-com",
|
|
@@ -64,7 +64,7 @@ const base: CaddyfileSpec = {
|
|
|
64
64
|
ownerEmail: "owner@example.com",
|
|
65
65
|
tls: "acme",
|
|
66
66
|
publicSiteRoot: "/srv/caelo/output/production/current",
|
|
67
|
-
|
|
67
|
+
stagingGatewayUpstream: "caelo-staging-gateway:8091",
|
|
68
68
|
adminUpstream: COMPOSE_ADMIN_UPSTREAM,
|
|
69
69
|
gatewayUpstream: COMPOSE_GATEWAY_UPSTREAM,
|
|
70
70
|
mcpAuthProxyUpstream: COMPOSE_MCP_AUTH_PROXY_UPSTREAM,
|
package/src/caddy.test.ts
CHANGED
|
@@ -20,7 +20,7 @@ const spec: CaddyfileSpec = {
|
|
|
20
20
|
ownerEmail: "owner@example.com",
|
|
21
21
|
tls: "acme",
|
|
22
22
|
publicSiteRoot: "/srv/caelo/output/production/current",
|
|
23
|
-
|
|
23
|
+
stagingGatewayUpstream: "caelo-staging-gateway:8091",
|
|
24
24
|
adminUpstream: COMPOSE_ADMIN_UPSTREAM,
|
|
25
25
|
gatewayUpstream: COMPOSE_GATEWAY_UPSTREAM,
|
|
26
26
|
mcpAuthProxyUpstream: COMPOSE_MCP_AUTH_PROXY_UPSTREAM,
|
|
@@ -96,9 +96,12 @@ describe("Caddyfile upstreams (#707)", () => {
|
|
|
96
96
|
expect(out).not.toContain("localhost");
|
|
97
97
|
expect(out).not.toContain("127.0.0.1");
|
|
98
98
|
expect(block(out, "admin.example.com")).toContain("reverse_proxy caelo-admin:5173");
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
expect(block(out, "example.com")).toContain(
|
|
100
|
+
"handle /api/* {\n reverse_proxy caelo-gateway:8090\n",
|
|
101
|
+
);
|
|
102
|
+
// #1330 — the staging host goes to the staging gateway, every path.
|
|
103
|
+
expect(block(out, "staging.example.com")).toContain("reverse_proxy caelo-staging-gateway:8091");
|
|
104
|
+
expect(block(out, "staging.example.com")).not.toContain("caelo-gateway:8090");
|
|
102
105
|
});
|
|
103
106
|
|
|
104
107
|
it("does not route /admin/* on the public site (the admin routes from / on its own host)", () => {
|
|
@@ -123,12 +126,15 @@ describe("Caddyfile static site (#566)", () => {
|
|
|
123
126
|
);
|
|
124
127
|
});
|
|
125
128
|
|
|
126
|
-
it("serves production
|
|
129
|
+
it("serves production from its root; staging only through the staging gateway, noindex", () => {
|
|
127
130
|
const out = generateCaddyfile(spec);
|
|
128
131
|
expect(block(out, "example.com")).toContain("root * /srv/caelo/output/production/current");
|
|
129
132
|
expect(block(out, "example.com")).not.toContain("X-Robots-Tag");
|
|
130
133
|
const staging = block(out, "staging.example.com");
|
|
131
|
-
|
|
134
|
+
// #1330 — no second path to the staged files: Caddy serves none of them.
|
|
135
|
+
expect(staging).not.toContain("root *");
|
|
136
|
+
expect(staging).not.toContain("file_server");
|
|
137
|
+
expect(staging).not.toContain("output/staging");
|
|
132
138
|
expect(staging).toContain('header X-Robots-Tag "noindex"');
|
|
133
139
|
});
|
|
134
140
|
});
|
package/src/caddy.ts
CHANGED
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
* # MCP-Auth-Proxy on its own host, never the site's origin)
|
|
16
16
|
* # public → /api/* → <gatewayUpstream>; everything else is the
|
|
17
17
|
* # static site (try_files + file_server, 404 → /404.html)
|
|
18
|
+
* # staging → reverse_proxy <stagingGatewayUpstream> for every path
|
|
19
|
+
* # (#1330: the staging gateway serves the staged build
|
|
20
|
+
* # behind the staging gate, and staging's own /api)
|
|
18
21
|
* }
|
|
19
22
|
*
|
|
20
23
|
* Upstreams are `host:port` as seen FROM CADDY. In the Compose install
|
|
@@ -27,7 +30,10 @@
|
|
|
27
30
|
* run in CI) — issue #566 shipped a `try_files` nested inside
|
|
28
31
|
* `file_server { }`, which Caddy 2.11 refuses, so no vhost came up.
|
|
29
32
|
*
|
|
30
|
-
* Staging vhosts force `X-Robots-Tag: noindex`.
|
|
33
|
+
* Staging vhosts force `X-Robots-Tag: noindex`. Caddy serves no staging
|
|
34
|
+
* file itself (#1330): the staging gateway mounts the staged builds
|
|
35
|
+
* read-only, and Caddy has no other path to them, so nothing reaches a
|
|
36
|
+
* staged page without passing the gate.
|
|
31
37
|
*
|
|
32
38
|
* Public vhosts import the redirect snippet of the build they serve
|
|
33
39
|
* (`_redirects.caddy`, #587); the Caddy container reloads itself when a
|
|
@@ -91,7 +97,8 @@ export interface CaddyfileSpec {
|
|
|
91
97
|
readonly ownerEmail: string;
|
|
92
98
|
readonly tls: CaddyTlsMode;
|
|
93
99
|
readonly publicSiteRoot: string; // absolute path inside the Caddy container
|
|
94
|
-
|
|
100
|
+
/** #1330 — `host:port` of the staging gateway as Caddy reaches it (Compose: `caelo-staging-gateway:8091`). */
|
|
101
|
+
readonly stagingGatewayUpstream: string;
|
|
95
102
|
/** `host:port` of the admin as Caddy reaches it (Compose: `caelo-admin:5173`). */
|
|
96
103
|
readonly adminUpstream: string;
|
|
97
104
|
/** `host:port` of the API gateway as Caddy reaches it (Compose: `caelo-gateway:8090`). */
|
|
@@ -173,7 +180,7 @@ function assertSafeCaddyToken(label: string, value: string, kind: "address" | "t
|
|
|
173
180
|
export function generateCaddyfile(spec: CaddyfileSpec): string {
|
|
174
181
|
assertSafeCaddyToken("ownerEmail", spec.ownerEmail, "token");
|
|
175
182
|
assertSafeCaddyToken("publicSiteRoot", spec.publicSiteRoot, "token");
|
|
176
|
-
assertSafeCaddyToken("
|
|
183
|
+
assertSafeCaddyToken("stagingGatewayUpstream", spec.stagingGatewayUpstream, "token");
|
|
177
184
|
assertSafeCaddyToken("adminUpstream", spec.adminUpstream, "token");
|
|
178
185
|
assertSafeCaddyToken("gatewayUpstream", spec.gatewayUpstream, "token");
|
|
179
186
|
assertSafeCaddyToken("mcpAuthProxyUpstream", spec.mcpAuthProxyUpstream, "token");
|
|
@@ -232,12 +239,24 @@ function vhost(d: CaddyDomainSpec, spec: CaddyfileSpec): string {
|
|
|
232
239
|
header X-Content-Type-Options "${NOSNIFF}"
|
|
233
240
|
reverse_proxy ${spec.mcpAuthProxyUpstream}
|
|
234
241
|
}
|
|
242
|
+
`;
|
|
243
|
+
}
|
|
244
|
+
if (d.env === "staging") {
|
|
245
|
+
// #1330 — every path to the staging gateway: it runs the gate first and
|
|
246
|
+
// serves the staged build and staging's /api itself. The #891 security
|
|
247
|
+
// headers stay on as a second layer over the gateway's own.
|
|
248
|
+
return `${d.hostname} {${noindex}
|
|
249
|
+
header X-Content-Type-Options "${NOSNIFF}"
|
|
250
|
+
@svg path *.svg
|
|
251
|
+
header @svg Content-Security-Policy "${SVG_DOCUMENT_CSP}"
|
|
252
|
+
reverse_proxy ${spec.stagingGatewayUpstream}
|
|
253
|
+
}
|
|
235
254
|
`;
|
|
236
255
|
}
|
|
237
256
|
// public — API routes go to the gateway, the rest is the static site.
|
|
238
257
|
// `try_files` is its own directive (a rewrite), never a file_server
|
|
239
258
|
// subdirective (#566).
|
|
240
|
-
const root =
|
|
259
|
+
const root = spec.publicSiteRoot;
|
|
241
260
|
// #587 — the redirects table of the build this vhost serves.
|
|
242
261
|
return `${d.hostname} {${noindex}
|
|
243
262
|
root * ${root}
|
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
|
|