@caelo-cms/provisioning 0.1.0

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.
@@ -0,0 +1,117 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ /**
4
+ * Caelo self-hosted Pulumi entry. Composes three resources:
5
+ * 1. `caeloConfigFile` — the generated docker-compose.yml + Caddyfile,
6
+ * written via local.Command on `pulumi up` and torn down on
7
+ * `pulumi destroy`.
8
+ * 2. `caeloBootstrapToken` — a single-use 24h owner-bootstrap token
9
+ * written to `<caeloDir>/pending-token.json`. Captured as a Pulumi
10
+ * output so operators can read `pulumi stack output bootstrapUrl`
11
+ * after `pulumi up`.
12
+ * 3. `caeloDockerCompose` — runs `docker compose up -d` and tears down
13
+ * with `docker compose down -v` on destroy.
14
+ *
15
+ * Why thin: Caelo's self-hosted stack is fundamentally a single Compose
16
+ * project. Wrapping it in real Pulumi resources buys lifecycle tracking
17
+ * + the same CLI shape we'll use for GCP / AWS / Azure in P15, without
18
+ * forcing operators to learn a separate provisioning DSL. The
19
+ * cms-provision CLI stays as the dev-iteration path; Pulumi owns the
20
+ * production install.
21
+ */
22
+
23
+ import { resolve } from "node:path";
24
+ import { local } from "@pulumi/command";
25
+ import * as pulumi from "@pulumi/pulumi";
26
+ import { generateBootstrapToken } from "../../src/bootstrap-token.js";
27
+ import { generateCaddyfile } from "../../src/caddy.js";
28
+ import { generateDockerCompose } from "../../src/compose.js";
29
+
30
+ const cfg = new pulumi.Config();
31
+ const domain = cfg.require("domain");
32
+ const ownerEmail = cfg.require("ownerEmail");
33
+ const caeloDir = cfg.get("caeloDir") ?? "./.caelo";
34
+
35
+ const composePath = resolve(caeloDir, "docker-compose.yml");
36
+ const caddyPath = resolve(caeloDir, "Caddyfile");
37
+ const tokenPath = resolve(caeloDir, "pending-token.json");
38
+
39
+ // Mint secrets at preview time. Pulumi's secret-handling encrypts these
40
+ // in the state file; `pulumi stack output --show-secrets postgresPassword`
41
+ // is the operator's recovery path.
42
+ const postgresPassword = pulumi.secret(randomHex(32));
43
+ const minioRootUser = "caelo";
44
+ const minioRootPassword = pulumi.secret(randomHex(32));
45
+
46
+ const composeYaml = pulumi.all([postgresPassword, minioRootPassword]).apply(([pgPw, minioPw]) =>
47
+ generateDockerCompose({
48
+ domain,
49
+ postgresPassword: pgPw,
50
+ minioRootUser,
51
+ minioRootPassword: minioPw,
52
+ anthropicApiKey: process.env.ANTHROPIC_API_KEY,
53
+ resendApiKey: process.env.RESEND_API_KEY,
54
+ diskSize: "20Gi",
55
+ }),
56
+ );
57
+
58
+ const caddyConf = generateCaddyfile({
59
+ ownerEmail,
60
+ publicSiteRoot: "/srv/caelo/output/production/current",
61
+ stagingSiteRoot: "/srv/caelo/output/staging/current",
62
+ adminPort: 5173,
63
+ gatewayPort: 8090,
64
+ domains: [
65
+ { hostname: domain, kind: "admin", env: "production" as const },
66
+ { hostname: domain, kind: "public", env: "production" as const },
67
+ { hostname: `staging.${domain}`, kind: "public", env: "staging" as const },
68
+ ],
69
+ });
70
+
71
+ // Mint the owner bootstrap token at preview time. Captured below as a
72
+ // Pulumi output so the URL surfaces on `pulumi up`.
73
+ const tokenInfo = generateBootstrapToken();
74
+
75
+ // Stage all three files via a single local.Command so a destroy
76
+ // removes them transactionally.
77
+ const writeFiles = new local.Command("caeloConfigFile", {
78
+ create: pulumi.interpolate`mkdir -p ${caeloDir} && cat > ${composePath} <<'EOF'
79
+ ${composeYaml}
80
+ EOF
81
+ cat > ${caddyPath} <<'EOF'
82
+ ${caddyConf}
83
+ EOF
84
+ cat > ${tokenPath} <<'EOF'
85
+ ${JSON.stringify(tokenInfo, null, 2)}
86
+ EOF
87
+ `,
88
+ delete: pulumi.interpolate`rm -f ${composePath} ${caddyPath} ${tokenPath}`,
89
+ triggers: [composeYaml, caddyConf],
90
+ });
91
+
92
+ // Bring the stack up. Triggers on the file content so a `pulumi up`
93
+ // after a config change re-runs `up -d` (which is idempotent).
94
+ const composeUp = new local.Command(
95
+ "caeloDockerCompose",
96
+ {
97
+ create: pulumi.interpolate`docker compose -f ${composePath} up -d`,
98
+ delete: pulumi.interpolate`docker compose -f ${composePath} down -v`,
99
+ triggers: [composeYaml],
100
+ },
101
+ { dependsOn: [writeFiles] },
102
+ );
103
+
104
+ export const composeFilePath = composePath;
105
+ export const caddyFilePath = caddyPath;
106
+ export const bootstrapUrl = pulumi.interpolate`https://${domain}/setup?token=${tokenInfo.token}`;
107
+ export const bootstrapTokenExpiresAt = tokenInfo.expiresAt;
108
+ export { minioRootPassword, postgresPassword };
109
+
110
+ // Re-export so the destroy summary shows operators what they'll lose.
111
+ export const composeRunOutput = composeUp.stdout;
112
+
113
+ function randomHex(bytes: number): string {
114
+ const buf = new Uint8Array(bytes);
115
+ crypto.getRandomValues(buf);
116
+ return [...buf].map((b) => b.toString(16).padStart(2, "0")).join("");
117
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "composite": true,
5
+ "rootDir": "./src",
6
+ "outDir": "./dist",
7
+ "noEmit": false
8
+ },
9
+ "include": ["src/**/*"],
10
+ "references": [
11
+ {
12
+ "path": "../shared"
13
+ }
14
+ ],
15
+ "exclude": ["**/*.test.ts", "**/dist", "**/node_modules"]
16
+ }