@caelo-cms/provisioning 0.10.47 → 0.10.49
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 +1 -1
- package/caddy/Caddyfile.staging +1 -1
- package/dist/agent/apply.d.ts +45 -0
- package/dist/agent/apply.d.ts.map +1 -0
- package/dist/agent/apply.js +83 -0
- package/dist/agent/apply.js.map +1 -0
- package/dist/agent/cli-provenance.d.ts +42 -0
- package/dist/agent/cli-provenance.d.ts.map +1 -0
- package/dist/agent/cli-provenance.js +138 -0
- package/dist/agent/cli-provenance.js.map +1 -0
- package/dist/agent/command.d.ts +79 -0
- package/dist/agent/command.d.ts.map +1 -0
- package/dist/agent/command.js +454 -0
- package/dist/agent/command.js.map +1 -0
- package/dist/agent/config.d.ts +30 -0
- package/dist/agent/config.d.ts.map +1 -0
- package/dist/agent/config.js +60 -0
- package/dist/agent/config.js.map +1 -0
- package/dist/agent/lock.d.ts +13 -0
- package/dist/agent/lock.d.ts.map +1 -0
- package/dist/agent/lock.js +100 -0
- package/dist/agent/lock.js.map +1 -0
- package/dist/agent/queue.d.ts +35 -0
- package/dist/agent/queue.d.ts.map +1 -0
- package/dist/agent/queue.js +50 -0
- package/dist/agent/queue.js.map +1 -0
- package/dist/agent/run.d.ts +97 -0
- package/dist/agent/run.d.ts.map +1 -0
- package/dist/agent/run.js +198 -0
- package/dist/agent/run.js.map +1 -0
- package/dist/agent/systemd.d.ts +55 -0
- package/dist/agent/systemd.d.ts.map +1 -0
- package/dist/agent/systemd.js +171 -0
- package/dist/agent/systemd.js.map +1 -0
- package/dist/agent/units.d.ts +44 -0
- package/dist/agent/units.d.ts.map +1 -0
- package/dist/agent/units.js +127 -0
- package/dist/agent/units.js.map +1 -0
- package/dist/agent-db.js +18402 -0
- package/dist/cli.js +119 -16
- package/dist/cli.js.map +1 -1
- package/dist/cloudsql-iam-user.d.ts +39 -0
- package/dist/cloudsql-iam-user.d.ts.map +1 -0
- package/dist/cloudsql-iam-user.js +119 -0
- package/dist/cloudsql-iam-user.js.map +1 -0
- package/dist/compose.d.ts +16 -0
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +26 -1
- package/dist/compose.js.map +1 -1
- package/dist/dns/index.d.ts +2 -0
- package/dist/dns/index.d.ts.map +1 -1
- package/dist/dns/index.js +3 -2
- package/dist/dns/index.js.map +1 -1
- package/dist/dns/manual.d.ts +5 -0
- package/dist/dns/manual.d.ts.map +1 -1
- package/dist/dns/manual.js +28 -12
- package/dist/dns/manual.js.map +1 -1
- package/dist/gcloud.d.ts +2 -0
- package/dist/gcloud.d.ts.map +1 -1
- package/dist/gcloud.js +1 -1
- package/dist/gcloud.js.map +1 -1
- package/dist/gcp-names.d.ts +15 -0
- package/dist/gcp-names.d.ts.map +1 -1
- package/dist/gcp-names.js +23 -0
- package/dist/gcp-names.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/infra-contract.d.ts +1 -1
- package/dist/infra-contract.js +7 -1
- package/dist/infra-contract.js.map +1 -1
- package/dist/install-job-runner.d.ts +13 -0
- package/dist/install-job-runner.d.ts.map +1 -0
- package/dist/install-job-runner.js +155 -0
- package/dist/install-job-runner.js.map +1 -0
- package/dist/install-job.d.ts +210 -0
- package/dist/install-job.d.ts.map +1 -0
- package/dist/install-job.js +449 -0
- package/dist/install-job.js.map +1 -0
- package/dist/install-state.d.ts +10 -1
- package/dist/install-state.d.ts.map +1 -1
- package/dist/install-state.js +10 -2
- package/dist/install-state.js.map +1 -1
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +19 -4
- package/dist/lifecycle.js.map +1 -1
- package/dist/migration-runner.d.ts +5 -0
- package/dist/migration-runner.d.ts.map +1 -1
- package/dist/migration-runner.js +1 -1
- package/dist/migration-runner.js.map +1 -1
- package/dist/operator-access.d.ts +14 -8
- package/dist/operator-access.d.ts.map +1 -1
- package/dist/operator-access.js +16 -102
- package/dist/operator-access.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 +18 -121
- package/dist/release-manifest.js.map +1 -1
- package/dist/run-mode.d.ts +27 -0
- package/dist/run-mode.d.ts.map +1 -0
- package/dist/run-mode.js +22 -0
- package/dist/run-mode.js.map +1 -0
- package/dist/runtime-secrets.d.ts +19 -0
- package/dist/runtime-secrets.d.ts.map +1 -1
- package/dist/runtime-secrets.js +44 -0
- package/dist/runtime-secrets.js.map +1 -1
- package/dist/self-hosted-roles.d.ts +22 -1
- package/dist/self-hosted-roles.d.ts.map +1 -1
- package/dist/self-hosted-roles.js +81 -12
- package/dist/self-hosted-roles.js.map +1 -1
- package/dist/self-hosted-upgrade.d.ts +17 -0
- package/dist/self-hosted-upgrade.d.ts.map +1 -1
- package/dist/self-hosted-upgrade.js +16 -1
- package/dist/self-hosted-upgrade.js.map +1 -1
- package/dist/stack-contract.d.ts +6 -0
- package/dist/stack-contract.d.ts.map +1 -1
- package/dist/stack-contract.js +10 -1
- package/dist/stack-contract.js.map +1 -1
- package/dist/upgrade/converge.d.ts +6 -0
- package/dist/upgrade/converge.d.ts.map +1 -1
- package/dist/upgrade/converge.js +59 -2
- package/dist/upgrade/converge.js.map +1 -1
- package/dist/upgrade/gcp-lookup.d.ts +6 -0
- package/dist/upgrade/gcp-lookup.d.ts.map +1 -1
- package/dist/upgrade/gcp-lookup.js +25 -0
- package/dist/upgrade/gcp-lookup.js.map +1 -1
- package/dist/upgrade/release-images.d.ts +26 -2
- package/dist/upgrade/release-images.d.ts.map +1 -1
- package/dist/upgrade/release-images.js +35 -2
- package/dist/upgrade/release-images.js.map +1 -1
- package/dist/upgrade/upgrade.d.ts +5 -0
- package/dist/upgrade/upgrade.d.ts.map +1 -1
- package/dist/upgrade/upgrade.js +28 -1
- package/dist/upgrade/upgrade.js.map +1 -1
- package/dist/upgrader-assess.d.ts +85 -0
- package/dist/upgrader-assess.d.ts.map +1 -0
- package/dist/upgrader-assess.js +122 -0
- package/dist/upgrader-assess.js.map +1 -0
- package/dist/upgrader-gcp.d.ts +17 -0
- package/dist/upgrader-gcp.d.ts.map +1 -0
- package/dist/upgrader-gcp.js +87 -0
- package/dist/upgrader-gcp.js.map +1 -0
- package/dist/upgrader-job.d.ts +133 -0
- package/dist/upgrader-job.d.ts.map +1 -0
- package/dist/upgrader-job.js +259 -0
- package/dist/upgrader-job.js.map +1 -0
- package/dist/upgrader-reporter.d.ts +35 -0
- package/dist/upgrader-reporter.d.ts.map +1 -0
- package/dist/upgrader-reporter.js +79 -0
- package/dist/upgrader-reporter.js.map +1 -0
- package/dist/upgrader-setup.d.ts +59 -0
- package/dist/upgrader-setup.d.ts.map +1 -0
- package/dist/upgrader-setup.js +308 -0
- package/dist/upgrader-setup.js.map +1 -0
- package/dist/wizard.d.ts +6 -0
- package/dist/wizard.d.ts.map +1 -1
- package/dist/wizard.js +32 -4
- package/dist/wizard.js.map +1 -1
- package/dist/wizards/gcp-firebase.d.ts +3 -0
- package/dist/wizards/gcp-firebase.d.ts.map +1 -1
- package/dist/wizards/gcp-firebase.js +1 -0
- package/dist/wizards/gcp-firebase.js.map +1 -1
- package/dist/wizards/gcp-install-job.d.ts +74 -0
- package/dist/wizards/gcp-install-job.d.ts.map +1 -0
- package/dist/wizards/gcp-install-job.js +398 -0
- package/dist/wizards/gcp-install-job.js.map +1 -0
- package/dist/wizards/gcp-prompts.d.ts +3 -0
- package/dist/wizards/gcp-prompts.d.ts.map +1 -0
- package/dist/wizards/gcp-prompts.js +25 -0
- package/dist/wizards/gcp-prompts.js.map +1 -0
- package/dist/wizards/gcp-pulumi.d.ts +4 -2
- package/dist/wizards/gcp-pulumi.d.ts.map +1 -1
- package/dist/wizards/gcp-pulumi.js +1 -1
- package/dist/wizards/gcp-pulumi.js.map +1 -1
- package/dist/wizards/gcp.d.ts +40 -0
- package/dist/wizards/gcp.d.ts.map +1 -1
- package/dist/wizards/gcp.js +223 -59
- package/dist/wizards/gcp.js.map +1 -1
- package/package.json +6 -4
- package/src/adapter.ts +97 -0
- package/src/adc-impersonation.ts +176 -0
- package/src/admin-domain.test.ts +254 -0
- package/src/admin-domain.ts +245 -0
- package/src/admin-memory.test.ts +151 -0
- package/src/agent/apply.test.ts +185 -0
- package/src/agent/apply.ts +124 -0
- package/src/agent/cli-provenance.test.ts +159 -0
- package/src/agent/cli-provenance.ts +191 -0
- package/src/agent/command.test.ts +77 -0
- package/src/agent/command.ts +564 -0
- package/src/agent/config.ts +86 -0
- package/src/agent/lock-config.test.ts +97 -0
- package/src/agent/lock.ts +110 -0
- package/src/agent/queue.test.ts +10 -0
- package/src/agent/queue.ts +91 -0
- package/src/agent/run.test.ts +437 -0
- package/src/agent/run.ts +295 -0
- package/src/agent/systemd.ts +209 -0
- package/src/agent/units.test.ts +223 -0
- package/src/agent/units.ts +163 -0
- package/src/azure-front-door.test.ts +119 -0
- package/src/azure-front-door.ts +56 -0
- package/src/bootstrap-token.ts +20 -0
- package/src/caddy.ts +112 -0
- package/src/cdn-copy.ts +91 -0
- package/src/cert-wait.test.ts +194 -0
- package/src/cert-wait.ts +163 -0
- package/src/checkpoint-secrets.test.ts +380 -0
- package/src/checkpoint-secrets.ts +283 -0
- package/src/cli-routing.test.ts +45 -0
- package/src/cli-routing.ts +43 -0
- package/src/cli.ts +1491 -0
- package/src/cloudsql-iam-user.ts +147 -0
- package/src/compose.ts +334 -0
- package/src/default-firewall.test.ts +411 -0
- package/src/default-firewall.ts +342 -0
- package/src/deployed-release.test.ts +231 -0
- package/src/deployed-release.ts +220 -0
- package/src/dns/cloudflare.ts +182 -0
- package/src/dns/groups.test.ts +76 -0
- package/src/dns/groups.ts +86 -0
- package/src/dns/index.ts +56 -0
- package/src/dns/manual.ts +130 -0
- package/src/dns/types.ts +27 -0
- package/src/gateway-credentials.test.ts +180 -0
- package/src/gateway-credentials.ts +144 -0
- package/src/gcloud-components.test.ts +76 -0
- package/src/gcloud-components.ts +85 -0
- package/src/gcloud-retry.ts +41 -0
- package/src/gcloud-runner.test.ts +61 -0
- package/src/gcloud.test.ts +99 -0
- package/src/gcloud.ts +322 -0
- package/src/gcp-names.ts +135 -0
- package/src/index.test.ts +332 -0
- package/src/index.ts +114 -0
- package/src/infra-contract.ts +32 -0
- package/src/install-job-runner.ts +166 -0
- package/src/install-job.test.ts +566 -0
- package/src/install-job.ts +542 -0
- package/src/install-state.test.ts +239 -0
- package/src/install-state.ts +583 -0
- package/src/internal-token.ts +35 -0
- package/src/lifecycle.ts +900 -0
- package/src/mcp-iap-stacks.test.ts +58 -0
- package/src/mcp-iap.test.ts +149 -0
- package/src/mcp-iap.ts +131 -0
- package/src/media-volume.test.ts +296 -0
- package/src/migration-runner.ts +365 -0
- package/src/operator-access-stacks.test.ts +55 -0
- package/src/operator-access.test.ts +332 -0
- package/src/operator-access.ts +554 -0
- package/src/ops-backend.test.ts +320 -0
- package/src/ops-backend.ts +414 -0
- package/src/provisioner-identity.test.ts +482 -0
- package/src/provisioner-identity.ts +407 -0
- package/src/redirects-emit.ts +175 -0
- package/src/region-data.ts +428 -0
- package/src/regions.test.ts +277 -0
- package/src/regions.ts +370 -0
- package/src/release-contract.ts +57 -0
- package/src/release-manifest.test.ts +300 -0
- package/src/release-manifest.ts +310 -0
- package/src/run-mode.ts +40 -0
- package/src/runtime-secrets.test.ts +627 -0
- package/src/runtime-secrets.ts +754 -0
- package/src/self-command.test.ts +14 -0
- package/src/self-command.ts +21 -0
- package/src/self-hosted-compose.test.ts +177 -0
- package/src/self-hosted-release.ts +240 -0
- package/src/self-hosted-roles.integration.test.ts +161 -0
- package/src/self-hosted-roles.test.ts +124 -0
- package/src/self-hosted-roles.ts +203 -0
- package/src/self-hosted-upgrade.test.ts +394 -0
- package/src/self-hosted-upgrade.ts +548 -0
- package/src/stack-contract-parity.test.ts +199 -0
- package/src/stack-contract.test.ts +391 -0
- package/src/stack-contract.ts +888 -0
- package/src/stack-converge-secrets.test.ts +421 -0
- package/src/stack-converge.test.ts +833 -0
- package/src/stack-converge.ts +909 -0
- package/src/staging-admin.test.ts +246 -0
- package/src/staging-admin.ts +324 -0
- package/src/state-attach.test.ts +211 -0
- package/src/state-attach.ts +132 -0
- package/src/state-backend.test.ts +145 -0
- package/src/state-backend.ts +262 -0
- package/src/state-command.ts +293 -0
- package/src/state-migrate.test.ts +479 -0
- package/src/state-migrate.ts +434 -0
- package/src/state-pulumi.ts +289 -0
- package/src/state-store.test.ts +212 -0
- package/src/state-store.ts +243 -0
- package/src/upgrade/app-roll.ts +486 -0
- package/src/upgrade/backup.ts +109 -0
- package/src/upgrade/converge.ts +437 -0
- package/src/upgrade/gcp-lookup.ts +167 -0
- package/src/upgrade/health.test.ts +279 -0
- package/src/upgrade/health.ts +305 -0
- package/src/upgrade/release-images.test.ts +119 -0
- package/src/upgrade/release-images.ts +393 -0
- package/src/upgrade/reporter.ts +46 -0
- package/src/upgrade/upgrade.test.ts +626 -0
- package/src/upgrade/upgrade.ts +249 -0
- package/src/upgrader-assess.test.ts +306 -0
- package/src/upgrader-assess.ts +250 -0
- package/src/upgrader-gcp.ts +123 -0
- package/src/upgrader-job.test.ts +411 -0
- package/src/upgrader-job.ts +502 -0
- package/src/upgrader-reporter.test.ts +46 -0
- package/src/upgrader-reporter.ts +105 -0
- package/src/upgrader-setup.test.ts +169 -0
- package/src/upgrader-setup.ts +355 -0
- package/src/wizard.ts +425 -0
- package/src/wizards/gcp-cost.ts +188 -0
- package/src/wizards/gcp-firebase.ts +72 -0
- package/src/wizards/gcp-install-job.ts +506 -0
- package/src/wizards/gcp-prompts.ts +26 -0
- package/src/wizards/gcp-pulumi.test.ts +246 -0
- package/src/wizards/gcp-pulumi.ts +516 -0
- package/src/wizards/gcp.ts +1935 -0
package/src/caddy.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* P14 — Caddyfile generator.
|
|
5
|
+
*
|
|
6
|
+
* Reads the `domains` table + the deploy targets and emits a
|
|
7
|
+
* deterministic Caddyfile string. The CLI's `regenerate-caddy`
|
|
8
|
+
* sub-command writes this to `/etc/caddy/Caddyfile` and runs
|
|
9
|
+
* `caddy reload`.
|
|
10
|
+
*
|
|
11
|
+
* Per-vhost shape:
|
|
12
|
+
* <hostname> {
|
|
13
|
+
* # admin → reverse_proxy localhost:5173
|
|
14
|
+
* # public → root + try_files (static), with /api/* → gateway
|
|
15
|
+
* tls <ownerEmail>
|
|
16
|
+
* }
|
|
17
|
+
*
|
|
18
|
+
* Staging vhosts force `X-Robots-Tag: noindex`.
|
|
19
|
+
*
|
|
20
|
+
* Public vhosts mark content-hashed build outputs (fonts, plugin
|
|
21
|
+
* bundles — `CONTENT_HASHED_PATH_PATTERN` in @caelo-cms/shared)
|
|
22
|
+
* `immutable` for a year. Everything else keeps Caddy's file_server
|
|
23
|
+
* default (no Cache-Control; ETag + Last-Modified for revalidation),
|
|
24
|
+
* so pages and slug-addressed media are never pinned in caches. The
|
|
25
|
+
* matcher also requires the file to exist (`file`), so a 404 for a
|
|
26
|
+
* hashed URL (an old page requesting an asset the new build dropped)
|
|
27
|
+
* is never cached for a year.
|
|
28
|
+
*
|
|
29
|
+
* `.ico` files (theme favicons) are served as `image/x-icon`, the one
|
|
30
|
+
* icon MIME the media library stores and `<link rel="icon" type>`
|
|
31
|
+
* declares. file_server only fills Content-Type when no earlier handler
|
|
32
|
+
* set it, so the `header` line wins over the host's mime.types name.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { CONTENT_HASHED_PATH_PATTERN, IMMUTABLE_CACHE_CONTROL } from "@caelo-cms/shared";
|
|
36
|
+
|
|
37
|
+
export interface CaddyDomainSpec {
|
|
38
|
+
readonly hostname: string;
|
|
39
|
+
readonly kind: "admin" | "public";
|
|
40
|
+
readonly env: "production" | "staging";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface CaddyfileSpec {
|
|
44
|
+
readonly ownerEmail: string;
|
|
45
|
+
readonly publicSiteRoot: string; // absolute path on disk
|
|
46
|
+
readonly stagingSiteRoot: string;
|
|
47
|
+
readonly adminPort: number;
|
|
48
|
+
readonly gatewayPort: number;
|
|
49
|
+
readonly domains: ReadonlyArray<CaddyDomainSpec>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function generateCaddyfile(spec: CaddyfileSpec): string {
|
|
53
|
+
const blocks: string[] = [];
|
|
54
|
+
|
|
55
|
+
// Global options.
|
|
56
|
+
blocks.push(`{
|
|
57
|
+
email ${spec.ownerEmail}
|
|
58
|
+
}
|
|
59
|
+
`);
|
|
60
|
+
|
|
61
|
+
for (const d of spec.domains) {
|
|
62
|
+
blocks.push(vhost(d, spec));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Localhost dev fallback when no domains are configured (so a fresh
|
|
66
|
+
// `cms-provision` produces a working Caddyfile even pre-DNS).
|
|
67
|
+
if (spec.domains.length === 0) {
|
|
68
|
+
blocks.push(`# No domains configured yet — cms-provision regenerate-caddy
|
|
69
|
+
# will overwrite this file when you add one at /settings/domains.
|
|
70
|
+
:8081 {
|
|
71
|
+
reverse_proxy localhost:${spec.adminPort}
|
|
72
|
+
}
|
|
73
|
+
`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return blocks.join("\n");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function vhost(d: CaddyDomainSpec, spec: CaddyfileSpec): string {
|
|
80
|
+
const noindex = d.env === "staging" ? `\n header X-Robots-Tag "noindex"` : "";
|
|
81
|
+
if (d.kind === "admin") {
|
|
82
|
+
return `${d.hostname} {${noindex}
|
|
83
|
+
reverse_proxy localhost:${spec.adminPort}
|
|
84
|
+
}
|
|
85
|
+
`;
|
|
86
|
+
}
|
|
87
|
+
// public — API routes go to the gateway,
|
|
88
|
+
// the rest serves static files. Locale variants serve from a
|
|
89
|
+
const root = d.env === "staging" ? spec.stagingSiteRoot : spec.publicSiteRoot;
|
|
90
|
+
return `${d.hostname} {${noindex}
|
|
91
|
+
root * ${root}
|
|
92
|
+
@content_hashed {
|
|
93
|
+
path_regexp content_hashed ${CONTENT_HASHED_PATH_PATTERN}
|
|
94
|
+
file
|
|
95
|
+
}
|
|
96
|
+
header @content_hashed Cache-Control "${IMMUTABLE_CACHE_CONTROL}"
|
|
97
|
+
@ico path *.ico
|
|
98
|
+
header @ico Content-Type "image/x-icon"
|
|
99
|
+
handle /api/* {
|
|
100
|
+
reverse_proxy localhost:${spec.gatewayPort}
|
|
101
|
+
}
|
|
102
|
+
handle /admin/* {
|
|
103
|
+
reverse_proxy localhost:${spec.adminPort}
|
|
104
|
+
}
|
|
105
|
+
handle {
|
|
106
|
+
file_server {
|
|
107
|
+
try_files {path} {path}/index.html /index.html
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
`;
|
|
112
|
+
}
|
package/src/cdn-copy.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* P15 — per-provider CDN-copy adapter contract.
|
|
5
|
+
*
|
|
6
|
+
* P7 ships the `media_assets.usage_count` threshold + the `media.set_cdn`
|
|
7
|
+
* op that flips a per-asset boolean. P15 wires the actual edge-pin
|
|
8
|
+
* operation per provider:
|
|
9
|
+
* - AWS → CloudFront cache invalidation + warmer request.
|
|
10
|
+
* - GCP → set Cache-Control metadata on the GCS object so Cloud CDN
|
|
11
|
+
* respects the long TTL.
|
|
12
|
+
* - Azure → Front Door purge + warm endpoint.
|
|
13
|
+
*
|
|
14
|
+
* The redeploy-orchestrator's `cdnCopyTick()` (currently a no-op for
|
|
15
|
+
* cloud installs) drains the per-asset CDN-pin queue using the adapter
|
|
16
|
+
* registered for the active provider.
|
|
17
|
+
*
|
|
18
|
+
* Each provider's stack publishes a `CdnCopyAdapter` instance via its
|
|
19
|
+
* Pulumi outputs (the actual implementation is dynamically imported by
|
|
20
|
+
* the orchestrator at runtime so `@caelo-cms/provisioning` doesn't pull in
|
|
21
|
+
* `@aws-sdk/...`/`@google-cloud/...`/`@azure/...` as static deps). The
|
|
22
|
+
* interface defined here is the only thing both sides agree on.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export interface CdnCopyAdapter {
|
|
26
|
+
/**
|
|
27
|
+
* Mark the asset at `assetKey` (object key in primary storage) as
|
|
28
|
+
* CDN-cached. Returns the public CDN URL — admin uses this as the
|
|
29
|
+
* canonical URL for the asset thereafter.
|
|
30
|
+
*/
|
|
31
|
+
pin(assetKey: string): Promise<string>;
|
|
32
|
+
/** Reverse — drop from CDN edge cache. */
|
|
33
|
+
unpin(assetKey: string): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* No-op adapter for self-hosted installs (Caddy serves directly from
|
|
38
|
+
* disk; no separate CDN tier). Returns the input asset key as a relative
|
|
39
|
+
* URL so callers don't have to special-case the no-CDN case.
|
|
40
|
+
*/
|
|
41
|
+
export const selfHostedCdnCopy: CdnCopyAdapter = {
|
|
42
|
+
async pin(assetKey) {
|
|
43
|
+
return `/media/${assetKey}`;
|
|
44
|
+
},
|
|
45
|
+
async unpin() {
|
|
46
|
+
// no-op
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Resolves the right adapter per `process.env.CAELO_PROVIDER`. Lazy
|
|
52
|
+
* dynamic-imports the per-provider implementation so a self-hosted
|
|
53
|
+
* install never loads the cloud SDKs.
|
|
54
|
+
*
|
|
55
|
+
* Returns `selfHostedCdnCopy` ONLY for `provider==='self-hosted'` or an
|
|
56
|
+
* unset env (treated as self-hosted). For `gcp`/`aws`/`azure`, the
|
|
57
|
+
* matching `cdn-copy-<gcs|aws|azure>.ts` file MUST exist in this
|
|
58
|
+
* directory — those land with the per-provider PRs. Per CLAUDE.md §2
|
|
59
|
+
* "no fallbacks pre-1.0" we deliberately do NOT silently degrade to
|
|
60
|
+
* the self-hosted no-op when the file is missing: a missing module is
|
|
61
|
+
* a deploy bug + the orchestrator tick should crash loudly so the
|
|
62
|
+
* operator notices instead of silently dropping CDN-pin requests.
|
|
63
|
+
*/
|
|
64
|
+
export async function loadCdnCopyAdapter(provider?: string): Promise<CdnCopyAdapter> {
|
|
65
|
+
switch (provider) {
|
|
66
|
+
case "gcp":
|
|
67
|
+
// biome-ignore lint/suspicious/noExplicitAny: opt-in dynamic import
|
|
68
|
+
return ((await import("./cdn-copy-gcs.js" as string)) as any).gcsCloudCdnPin;
|
|
69
|
+
case "gcp-firebase":
|
|
70
|
+
// v0.3.0 — Firebase Hosting manages CDN caching natively (each
|
|
71
|
+
// deploy creates an immutable version + the CDN's freshness is
|
|
72
|
+
// dictated by Firebase Hosting headers). There's no per-asset
|
|
73
|
+
// "pin to edge" API; this adapter is effectively a no-op that
|
|
74
|
+
// returns the same shape as self-hosted.
|
|
75
|
+
return selfHostedCdnCopy;
|
|
76
|
+
case "aws":
|
|
77
|
+
// biome-ignore lint/suspicious/noExplicitAny: opt-in dynamic import
|
|
78
|
+
return ((await import("./cdn-copy-aws.js" as string)) as any).s3CloudfrontPrewarm;
|
|
79
|
+
case "azure":
|
|
80
|
+
// biome-ignore lint/suspicious/noExplicitAny: opt-in dynamic import
|
|
81
|
+
return ((await import("./cdn-copy-azure.js" as string)) as any).azureBlobCdnPrefetch;
|
|
82
|
+
case undefined:
|
|
83
|
+
case "":
|
|
84
|
+
case "self-hosted":
|
|
85
|
+
return selfHostedCdnCopy;
|
|
86
|
+
default:
|
|
87
|
+
throw new Error(
|
|
88
|
+
`loadCdnCopyAdapter: unknown CAELO_PROVIDER='${provider}'. Expected one of: self-hosted | gcp | gcp-firebase | aws | azure.`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "bun:test";
|
|
4
|
+
import { type CertProbe, domainMappingProbe, lbCertProbe, waitForCert } from "./cert-wait.js";
|
|
5
|
+
import type { GcloudResult } from "./gcloud.js";
|
|
6
|
+
|
|
7
|
+
const ok = (stdout = ""): GcloudResult => ({ ok: true, stdout, stderr: "", exitCode: 0 });
|
|
8
|
+
const fail = (stderr: string): GcloudResult => ({ ok: false, stdout: "", stderr, exitCode: 1 });
|
|
9
|
+
|
|
10
|
+
/** A fake clock the injected sleep advances. */
|
|
11
|
+
function clock() {
|
|
12
|
+
let t = 0;
|
|
13
|
+
return {
|
|
14
|
+
now: () => t,
|
|
15
|
+
sleep: async (ms: number) => {
|
|
16
|
+
t += ms;
|
|
17
|
+
},
|
|
18
|
+
elapsed: () => t,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function probes(...seq: CertProbe[]) {
|
|
23
|
+
let i = 0;
|
|
24
|
+
return async () => seq[Math.min(i++, seq.length - 1)] as CertProbe;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("waitForCert", () => {
|
|
28
|
+
it("returns once the probe reports ready, reporting each new detail once", async () => {
|
|
29
|
+
const c = clock();
|
|
30
|
+
const updates: string[] = [];
|
|
31
|
+
const r = await waitForCert({
|
|
32
|
+
probe: probes(
|
|
33
|
+
{ state: "pending", detail: "waiting for DNS" },
|
|
34
|
+
{ state: "pending", detail: "waiting for DNS" },
|
|
35
|
+
{ state: "pending", detail: "issuing" },
|
|
36
|
+
{ state: "ready" },
|
|
37
|
+
),
|
|
38
|
+
timeoutMs: 60_000,
|
|
39
|
+
intervalMs: 10_000,
|
|
40
|
+
onUpdate: (d) => updates.push(d),
|
|
41
|
+
...c,
|
|
42
|
+
});
|
|
43
|
+
expect(r).toEqual({ ok: true });
|
|
44
|
+
expect(updates).toEqual(["waiting for DNS", "issuing"]);
|
|
45
|
+
expect(c.elapsed()).toBe(30_000);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("times out with the last detail instead of waiting forever", async () => {
|
|
49
|
+
const c = clock();
|
|
50
|
+
const r = await waitForCert({
|
|
51
|
+
probe: probes({ state: "pending", detail: "CertificateProvisioned: DNS not set" }),
|
|
52
|
+
timeoutMs: 25_000,
|
|
53
|
+
intervalMs: 10_000,
|
|
54
|
+
...c,
|
|
55
|
+
});
|
|
56
|
+
expect(r).toEqual({
|
|
57
|
+
ok: false,
|
|
58
|
+
reason: "timeout",
|
|
59
|
+
lastDetail: "CertificateProvisioned: DNS not set",
|
|
60
|
+
});
|
|
61
|
+
// Probed at 0, 10s, 20s, 25s (the deadline) — never slept past it.
|
|
62
|
+
expect(c.elapsed()).toBe(25_000);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("stops at once when gcloud fails, since waiting cannot fix that", async () => {
|
|
66
|
+
const c = clock();
|
|
67
|
+
const r = await waitForCert({
|
|
68
|
+
probe: probes({ state: "error", error: "PERMISSION_DENIED" }),
|
|
69
|
+
timeoutMs: 60_000,
|
|
70
|
+
intervalMs: 10_000,
|
|
71
|
+
...c,
|
|
72
|
+
});
|
|
73
|
+
expect(r).toEqual({ ok: false, reason: "error", error: "PERMISSION_DENIED" });
|
|
74
|
+
expect(c.elapsed()).toBe(0);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const target = { hostname: "admin.example.com", region: "europe-west1", projectId: "p" };
|
|
79
|
+
|
|
80
|
+
describe("domainMappingProbe", () => {
|
|
81
|
+
it("is ready when the mapping's Ready condition is True", async () => {
|
|
82
|
+
const calls: string[][] = [];
|
|
83
|
+
const run = async (args: string[]) => {
|
|
84
|
+
calls.push(args);
|
|
85
|
+
return ok(JSON.stringify({ status: { conditions: [{ type: "Ready", status: "True" }] } }));
|
|
86
|
+
};
|
|
87
|
+
expect(await domainMappingProbe(run, target)).toEqual({ state: "ready" });
|
|
88
|
+
expect(calls[0]).toEqual([
|
|
89
|
+
"beta",
|
|
90
|
+
"run",
|
|
91
|
+
"domain-mappings",
|
|
92
|
+
"describe",
|
|
93
|
+
"--domain=admin.example.com",
|
|
94
|
+
"--region=europe-west1",
|
|
95
|
+
"--project=p",
|
|
96
|
+
"--format=json",
|
|
97
|
+
]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("reports the certificate condition's message while pending", async () => {
|
|
101
|
+
const run = async () =>
|
|
102
|
+
ok(
|
|
103
|
+
JSON.stringify({
|
|
104
|
+
status: {
|
|
105
|
+
conditions: [
|
|
106
|
+
{ type: "Ready", status: "Unknown", message: "not ready" },
|
|
107
|
+
{
|
|
108
|
+
type: "CertificateProvisioned",
|
|
109
|
+
status: "Unknown",
|
|
110
|
+
message: "Certificate issuance pending. Add the DNS record.",
|
|
111
|
+
},
|
|
112
|
+
{ type: "DomainRoutable", status: "True" },
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
expect(await domainMappingProbe(run, target)).toEqual({
|
|
118
|
+
state: "pending",
|
|
119
|
+
detail: "CertificateProvisioned: Certificate issuance pending. Add the DNS record.",
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("is pending (not ready) before Google reports any condition", async () => {
|
|
124
|
+
const run = async () => ok(JSON.stringify({ status: {} }));
|
|
125
|
+
expect((await domainMappingProbe(run, target)).state).toBe("pending");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("surfaces a gcloud failure as an error", async () => {
|
|
129
|
+
const run = async () => fail("ERROR: (gcloud) You do not currently have an active account");
|
|
130
|
+
const r = await domainMappingProbe(run, target);
|
|
131
|
+
expect(r.state).toBe("error");
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("lbCertProbe", () => {
|
|
136
|
+
const list = (certs: unknown[]) => async () => ok(JSON.stringify(certs));
|
|
137
|
+
const lb = { projectId: "p", hostname: "admin.example.com" };
|
|
138
|
+
|
|
139
|
+
it("is ready when the admin's own certificate is ACTIVE although the main domain's is not", async () => {
|
|
140
|
+
const r = await lbCertProbe(
|
|
141
|
+
list([
|
|
142
|
+
{
|
|
143
|
+
name: "combined",
|
|
144
|
+
managed: {
|
|
145
|
+
status: "PROVISIONING",
|
|
146
|
+
domainStatus: { "example.com": "FAILED_NOT_VISIBLE", "admin.example.com": "ACTIVE" },
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: "admin-only",
|
|
151
|
+
managed: { status: "ACTIVE", domainStatus: { "admin.example.com": "ACTIVE" } },
|
|
152
|
+
},
|
|
153
|
+
]),
|
|
154
|
+
lb,
|
|
155
|
+
);
|
|
156
|
+
expect(r).toEqual({ state: "ready" });
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("does not count a domain that is ACTIVE on a certificate that is not served yet", async () => {
|
|
160
|
+
const r = await lbCertProbe(
|
|
161
|
+
list([
|
|
162
|
+
{
|
|
163
|
+
name: "combined",
|
|
164
|
+
managed: {
|
|
165
|
+
status: "PROVISIONING",
|
|
166
|
+
domainStatus: { "example.com": "FAILED_NOT_VISIBLE", "admin.example.com": "ACTIVE" },
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
]),
|
|
170
|
+
lb,
|
|
171
|
+
);
|
|
172
|
+
expect(r).toEqual({ state: "pending", detail: "combined: admin.example.com ACTIVE" });
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("is ready for installs that only have the combined certificate, once it is ACTIVE", async () => {
|
|
176
|
+
const r = await lbCertProbe(
|
|
177
|
+
list([
|
|
178
|
+
{
|
|
179
|
+
name: "combined",
|
|
180
|
+
managed: {
|
|
181
|
+
status: "ACTIVE",
|
|
182
|
+
domainStatus: { "example.com": "ACTIVE", "admin.example.com": "ACTIVE" },
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
]),
|
|
186
|
+
lb,
|
|
187
|
+
);
|
|
188
|
+
expect(r).toEqual({ state: "ready" });
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("is pending when no certificate lists the host", async () => {
|
|
192
|
+
expect((await lbCertProbe(list([]), lb)).state).toBe("pending");
|
|
193
|
+
});
|
|
194
|
+
});
|
package/src/cert-wait.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Waiting for a Google-managed TLS certificate (§11.C: the CLI polls certs
|
|
5
|
+
* itself instead of telling the operator to check back later).
|
|
6
|
+
*
|
|
7
|
+
* Two certificate sources, one wait loop:
|
|
8
|
+
* - the `gcp` load balancer's managed SSL certificates
|
|
9
|
+
* ({@link lbCertProbe}) — the installer waits for the admin host;
|
|
10
|
+
* - a Cloud Run domain mapping's certificate ({@link domainMappingProbe})
|
|
11
|
+
* — `admin-domain enable` / `staging-domain enable` on gcp-firebase.
|
|
12
|
+
*
|
|
13
|
+
* {@link waitForCert} is pure apart from its injected clock, sleep and
|
|
14
|
+
* probe, so the timeout and progress behaviour is unit-tested without
|
|
15
|
+
* gcloud or real time.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { GcloudResult } from "./gcloud.js";
|
|
19
|
+
|
|
20
|
+
type Run = (args: string[]) => Promise<GcloudResult>;
|
|
21
|
+
|
|
22
|
+
/** One look at a certificate. */
|
|
23
|
+
export type CertProbe =
|
|
24
|
+
| { readonly state: "ready" }
|
|
25
|
+
| { readonly state: "pending"; readonly detail: string }
|
|
26
|
+
/** gcloud itself failed (auth, permissions) — waiting will not fix it. */
|
|
27
|
+
| { readonly state: "error"; readonly error: string };
|
|
28
|
+
|
|
29
|
+
export type CertWaitResult =
|
|
30
|
+
| { readonly ok: true }
|
|
31
|
+
| { readonly ok: false; readonly reason: "timeout"; readonly lastDetail: string }
|
|
32
|
+
| { readonly ok: false; readonly reason: "error"; readonly error: string };
|
|
33
|
+
|
|
34
|
+
export interface CertWaitOpts {
|
|
35
|
+
readonly probe: () => Promise<CertProbe>;
|
|
36
|
+
readonly timeoutMs: number;
|
|
37
|
+
readonly intervalMs: number;
|
|
38
|
+
/** Called whenever the probe's detail text changes (spinner message). */
|
|
39
|
+
readonly onUpdate?: (detail: string) => void;
|
|
40
|
+
readonly sleep?: (ms: number) => Promise<void>;
|
|
41
|
+
readonly now?: () => number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Poll `probe` until the certificate is ready, gcloud fails, or `timeoutMs`
|
|
46
|
+
* passes. The probe runs once more at the deadline so a certificate that
|
|
47
|
+
* flips during the last interval is not reported as a timeout.
|
|
48
|
+
*/
|
|
49
|
+
export async function waitForCert(opts: CertWaitOpts): Promise<CertWaitResult> {
|
|
50
|
+
const now = opts.now ?? Date.now;
|
|
51
|
+
const sleep = opts.sleep ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)));
|
|
52
|
+
const deadline = now() + opts.timeoutMs;
|
|
53
|
+
let lastDetail = "";
|
|
54
|
+
for (;;) {
|
|
55
|
+
const p = await opts.probe();
|
|
56
|
+
if (p.state === "ready") return { ok: true };
|
|
57
|
+
if (p.state === "error") return { ok: false, reason: "error", error: p.error };
|
|
58
|
+
if (p.detail !== lastDetail) {
|
|
59
|
+
lastDetail = p.detail;
|
|
60
|
+
opts.onUpdate?.(p.detail);
|
|
61
|
+
}
|
|
62
|
+
const remaining = deadline - now();
|
|
63
|
+
if (remaining <= 0) return { ok: false, reason: "timeout", lastDetail };
|
|
64
|
+
await sleep(Math.min(opts.intervalMs, remaining));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface MappingCondition {
|
|
69
|
+
type?: string;
|
|
70
|
+
status?: string;
|
|
71
|
+
message?: string;
|
|
72
|
+
reason?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The certificate state of the Cloud Run domain mapping for `hostname`.
|
|
77
|
+
* Ready once the mapping's `Ready` condition is True (Google sets it after
|
|
78
|
+
* `CertificateProvisioned` and `DomainRoutable`); until then the detail is
|
|
79
|
+
* the first non-True condition's message, which says what Google waits for
|
|
80
|
+
* (usually the DNS record).
|
|
81
|
+
*/
|
|
82
|
+
export async function domainMappingProbe(
|
|
83
|
+
run: Run,
|
|
84
|
+
target: { readonly hostname: string; readonly region: string; readonly projectId: string },
|
|
85
|
+
): Promise<CertProbe> {
|
|
86
|
+
const r = await run([
|
|
87
|
+
"beta",
|
|
88
|
+
"run",
|
|
89
|
+
"domain-mappings",
|
|
90
|
+
"describe",
|
|
91
|
+
`--domain=${target.hostname}`,
|
|
92
|
+
`--region=${target.region}`,
|
|
93
|
+
`--project=${target.projectId}`,
|
|
94
|
+
"--format=json",
|
|
95
|
+
]);
|
|
96
|
+
if (!r.ok) return { state: "error", error: `describe the domain mapping: ${r.stderr.trim()}` };
|
|
97
|
+
const conditions =
|
|
98
|
+
(JSON.parse(r.stdout) as { status?: { conditions?: MappingCondition[] } }).status?.conditions ??
|
|
99
|
+
[];
|
|
100
|
+
const ready = conditions.find((c) => c.type === "Ready");
|
|
101
|
+
if (ready?.status === "True") return { state: "ready" };
|
|
102
|
+
const blocking =
|
|
103
|
+
conditions.find((c) => c.type === "CertificateProvisioned" && c.status !== "True") ??
|
|
104
|
+
conditions.find((c) => c.status !== "True");
|
|
105
|
+
const detail = blocking
|
|
106
|
+
? `${blocking.type}: ${blocking.message || blocking.reason || blocking.status || "pending"}`
|
|
107
|
+
: "Google has not reported the mapping's status yet";
|
|
108
|
+
return { state: "pending", detail };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
interface SslCertificateJson {
|
|
112
|
+
name?: string;
|
|
113
|
+
managed?: { status?: string; domainStatus?: Record<string, string> };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Whether the `gcp` load balancer has an ACTIVE managed certificate that
|
|
118
|
+
* covers `hostname`. Any such certificate counts: installs from before the
|
|
119
|
+
* admin got its own certificate serve the admin from the combined one.
|
|
120
|
+
* A certificate is only served once its overall status is ACTIVE, so a
|
|
121
|
+
* combined certificate whose other domain still fails does not count.
|
|
122
|
+
*/
|
|
123
|
+
export async function lbCertProbe(
|
|
124
|
+
run: Run,
|
|
125
|
+
target: { readonly projectId: string; readonly hostname: string },
|
|
126
|
+
): Promise<CertProbe> {
|
|
127
|
+
const r = await run([
|
|
128
|
+
"compute",
|
|
129
|
+
"ssl-certificates",
|
|
130
|
+
"list",
|
|
131
|
+
"--global",
|
|
132
|
+
`--project=${target.projectId}`,
|
|
133
|
+
"--format=json",
|
|
134
|
+
]);
|
|
135
|
+
if (!r.ok) return { state: "error", error: `list SSL certificates: ${r.stderr.trim()}` };
|
|
136
|
+
const covering = (JSON.parse(r.stdout) as SslCertificateJson[]).filter(
|
|
137
|
+
(c) => c.managed?.domainStatus?.[target.hostname] !== undefined,
|
|
138
|
+
);
|
|
139
|
+
if (
|
|
140
|
+
covering.some(
|
|
141
|
+
(c) =>
|
|
142
|
+
c.managed?.status === "ACTIVE" && c.managed.domainStatus?.[target.hostname] === "ACTIVE",
|
|
143
|
+
)
|
|
144
|
+
) {
|
|
145
|
+
return { state: "ready" };
|
|
146
|
+
}
|
|
147
|
+
if (covering.length === 0) {
|
|
148
|
+
return { state: "pending", detail: `no managed certificate lists ${target.hostname} yet` };
|
|
149
|
+
}
|
|
150
|
+
const detail = covering
|
|
151
|
+
.map(
|
|
152
|
+
(c) =>
|
|
153
|
+
`${c.name ?? "certificate"}: ${target.hostname} ${c.managed?.domainStatus?.[target.hostname]}`,
|
|
154
|
+
)
|
|
155
|
+
.join("; ");
|
|
156
|
+
return { state: "pending", detail };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* How long the CLI waits for a certificate before handing the operator the
|
|
161
|
+
* status command. Google documents up to 60 min after DNS resolves.
|
|
162
|
+
*/
|
|
163
|
+
export const CERT_WAIT_TIMEOUT_MS = 60 * 60 * 1000;
|