@meshxdata/fops 0.1.48 → 0.1.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/CHANGELOG.md +368 -0
  2. package/package.json +1 -1
  3. package/src/commands/lifecycle.js +30 -11
  4. package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-core.js +347 -6
  5. package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-data-bootstrap.js +421 -0
  6. package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-flux.js +5 -179
  7. package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-naming.js +14 -4
  8. package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-postgres.js +171 -4
  9. package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-storage.js +303 -8
  10. package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks.js +2 -0
  11. package/src/plugins/bundled/fops-plugin-azure/lib/azure-auth.js +1 -1
  12. package/src/plugins/bundled/fops-plugin-azure/lib/azure-fleet-swarm.js +936 -0
  13. package/src/plugins/bundled/fops-plugin-azure/lib/azure-fleet.js +10 -918
  14. package/src/plugins/bundled/fops-plugin-azure/lib/azure-helpers.js +5 -0
  15. package/src/plugins/bundled/fops-plugin-azure/lib/azure-keyvault-keys.js +413 -0
  16. package/src/plugins/bundled/fops-plugin-azure/lib/azure-keyvault.js +14 -399
  17. package/src/plugins/bundled/fops-plugin-azure/lib/azure-ops-config.js +754 -0
  18. package/src/plugins/bundled/fops-plugin-azure/lib/azure-ops-knock.js +527 -0
  19. package/src/plugins/bundled/fops-plugin-azure/lib/azure-ops-ssh.js +427 -0
  20. package/src/plugins/bundled/fops-plugin-azure/lib/azure-ops.js +99 -1686
  21. package/src/plugins/bundled/fops-plugin-azure/lib/azure-provision-health.js +279 -0
  22. package/src/plugins/bundled/fops-plugin-azure/lib/azure-provision-init.js +186 -0
  23. package/src/plugins/bundled/fops-plugin-azure/lib/azure-provision.js +66 -444
  24. package/src/plugins/bundled/fops-plugin-azure/lib/azure-results.js +11 -0
  25. package/src/plugins/bundled/fops-plugin-azure/lib/azure-vm-lifecycle.js +5 -540
  26. package/src/plugins/bundled/fops-plugin-azure/lib/azure-vm-terraform.js +544 -0
  27. package/src/plugins/bundled/fops-plugin-azure/lib/commands/infra-cmds.js +75 -3
  28. package/src/plugins/bundled/fops-plugin-azure/lib/commands/test-cmds.js +227 -11
  29. package/src/plugins/bundled/fops-plugin-azure/lib/commands/vm-cmds.js +2 -1
  30. package/src/plugins/bundled/fops-plugin-azure/lib/pytest-parse.js +21 -0
  31. package/src/plugins/bundled/fops-plugin-foundation/index.js +309 -44
@@ -0,0 +1,279 @@
1
+ /**
2
+ * Azure VM — Post-start health checks
3
+ * Extracted from azure-provision.js for maintainability
4
+ *
5
+ * Waits for Postgres, runs migrations check, grants admin, and verifies URL reachability.
6
+ */
7
+ import chalk from "chalk";
8
+ import { DIM, OK, WARN, banner, hint, sshCmd } from "./azure-helpers.js";
9
+
10
+ // ── TLS fetch helper (suppresses self-signed cert warnings) ──────────────────
11
+
12
+ let _tlsWarningSuppressed = false;
13
+ async function tlsFetch(url, opts = {}) {
14
+ if (!_tlsWarningSuppressed) {
15
+ _tlsWarningSuppressed = true;
16
+ const origEmit = process.emitWarning;
17
+ process.emitWarning = (warning, ...args) => {
18
+ if (typeof warning === "string" && warning.includes("NODE_TLS_REJECT_UNAUTHORIZED")) return;
19
+ return origEmit.call(process, warning, ...args);
20
+ };
21
+ }
22
+ const prev = process.env.NODE_TLS_REJECT_UNAUTHORIZED;
23
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
24
+ try {
25
+ return await fetch(url, opts);
26
+ } finally {
27
+ if (prev === undefined) delete process.env.NODE_TLS_REJECT_UNAUTHORIZED;
28
+ else process.env.NODE_TLS_REJECT_UNAUTHORIZED = prev;
29
+ }
30
+ }
31
+
32
+ // ── URL wait mode ────────────────────────────────────────────────────────────
33
+
34
+ export const URL_WAIT_MODE_HTTP_ANY = "http-any";
35
+ export const URL_WAIT_MODE_HTTP_2XX = "http-2xx";
36
+ const URL_WAIT_PROGRESS_INTERVAL_MS = 15000;
37
+
38
+ function normalizeUrlWaitMode(mode) {
39
+ const normalized = String(mode || URL_WAIT_MODE_HTTP_ANY).trim().toLowerCase();
40
+ if (normalized === URL_WAIT_MODE_HTTP_2XX) return URL_WAIT_MODE_HTTP_2XX;
41
+ return URL_WAIT_MODE_HTTP_ANY;
42
+ }
43
+
44
+ function isUrlReadyByMode(status, waitMode) {
45
+ if (waitMode === URL_WAIT_MODE_HTTP_2XX) {
46
+ return status >= 200 && status < 300;
47
+ }
48
+ // Any HTTP response means DNS + TLS + edge routing are reachable.
49
+ return Number.isInteger(status) && status >= 100 && status <= 599;
50
+ }
51
+
52
+ // ── Postgres wait constants ──────────────────────────────────────────────────
53
+
54
+ const POSTGRES_INITIAL_DELAY_MS = 45000; // give compose time to start containers after nohup fops up
55
+ const POSTGRES_POLL_INTERVAL_MS = 10000;
56
+ const POSTGRES_MAX_WAIT_MS = 600000; // 10 min for first boot / heavy stack or recovery
57
+ const POSTGRES_RECOVERY_HINT_INTERVAL_MS = 60000; // remind every 60s that we're waiting on recovery
58
+
59
+ // ── Post-start checks ────────────────────────────────────────────────────────
60
+
61
+ export async function postStartChecks(execa, ip, adminUser, { maxWait = POSTGRES_MAX_WAIT_MS, publicUrl, waitMode } = {}) {
62
+ banner("Post-start checks");
63
+
64
+ const ssh = (cmd, timeout = 30000) => sshCmd(execa, ip, adminUser, cmd, timeout);
65
+
66
+ hint("Waiting for Postgres…");
67
+ await new Promise((r) => setTimeout(r, POSTGRES_INITIAL_DELAY_MS));
68
+ const pgStart = Date.now();
69
+ let pgReady = false;
70
+ let lastRecoveryHintAt = -POSTGRES_RECOVERY_HINT_INTERVAL_MS; // so first recovery is reported immediately
71
+ let lastEventCheckAt = 0;
72
+ const EVENT_CHECK_INTERVAL_MS = 15000; // show docker events every 15s while waiting
73
+
74
+ while (Date.now() - pgStart < maxWait) {
75
+ const { exitCode, stdout, stderr } = await ssh(
76
+ "cd /opt/foundation-compose && sudo docker compose exec -T postgres psql -U foundation -d foundation -c 'SELECT 1' 2>&1",
77
+ 15000
78
+ );
79
+ if (exitCode === 0) { pgReady = true; break; }
80
+ const out = (stdout || "") + (stderr || "");
81
+ const inRecovery = /recovery|not yet accepting connections|Consistent recovery state has not been yet reached/i.test(out);
82
+ if (inRecovery && Date.now() - lastRecoveryHintAt >= POSTGRES_RECOVERY_HINT_INTERVAL_MS) {
83
+ console.log(WARN(" Postgres in recovery (database was not properly shut down). Waiting for it to accept connections…"));
84
+ lastRecoveryHintAt = Date.now();
85
+ }
86
+
87
+ // Show recent docker events while waiting
88
+ if (Date.now() - lastEventCheckAt >= EVENT_CHECK_INTERVAL_MS) {
89
+ const { stdout: events } = await ssh(
90
+ "docker events --since 30s --until 0s --format '{{.Type}}|{{.Action}}|{{.Actor.Attributes.name}}|{{.Actor.Attributes.image}}' 2>/dev/null | tail -8",
91
+ 10000
92
+ ).catch(() => ({ stdout: "" }));
93
+ if (events?.trim()) {
94
+ const eventIcons = { pull: "📥", start: "▶", create: "✦", die: "✗", kill: "⚡", stop: "■" };
95
+ const lines = events.trim().split("\n").map((line) => {
96
+ const [type, action, name, image] = line.split("|");
97
+ const icon = eventIcons[action] || "·";
98
+ const svc = name || image?.split("/").pop()?.split(":")[0] || "";
99
+ return `${icon} ${action.padEnd(7)} ${svc}`;
100
+ });
101
+ const elapsed = Math.round((Date.now() - pgStart) / 1000);
102
+ console.log(DIM(` [${elapsed}s] Docker activity:`));
103
+ for (const line of lines) console.log(DIM(` ${line}`));
104
+ }
105
+ lastEventCheckAt = Date.now();
106
+ }
107
+
108
+ await new Promise((r) => setTimeout(r, POSTGRES_POLL_INTERVAL_MS));
109
+ }
110
+ if (!pgReady) {
111
+ console.log(WARN(" ⚠ Postgres not ready after timeout (may still be in recovery after unclean shutdown)"));
112
+
113
+ const { stdout: composePs } = await ssh(
114
+ "cd /opt/foundation-compose && sudo docker compose ps -a --format '{{.Service}}\t{{.State}}\t{{.Status}}' 2>/dev/null | head -20"
115
+ );
116
+ const states = composePs?.trim()?.split("\n").map((line) => line.split("\t")[1]).filter(Boolean) ?? [];
117
+ const allCreated = states.length > 0 && states.every((s) => s === "created");
118
+ if (composePs?.trim()) {
119
+ hint("Container status:");
120
+ for (const line of composePs.trim().split("\n")) hint(` ${line}`);
121
+ if (allCreated) {
122
+ hint("Containers still in 'created' — stack may still be starting. Wait a few minutes then: fops azure deploy");
123
+ }
124
+ } else {
125
+ hint("No containers found — docker compose may not have started.");
126
+ }
127
+
128
+ const { stdout: pullErrors } = await ssh(
129
+ "grep -i -E 'error|denied|unauthorized|manifest unknown|pull access denied' /tmp/fops-up.log 2>/dev/null | tail -5"
130
+ );
131
+ if (pullErrors?.trim()) {
132
+ console.log(WARN(" Possible pull errors:"));
133
+ for (const line of pullErrors.trim().split("\n")) hint(` ${line}`);
134
+ }
135
+
136
+ const { stdout: ghcrCheck } = await ssh(
137
+ "sudo cat /root/.docker/config.json 2>/dev/null | grep -q ghcr.io && echo 'ok' || echo 'missing'"
138
+ );
139
+ if (ghcrCheck?.trim() !== "ok") {
140
+ console.log(WARN(" ⚠ GHCR credentials missing from /root/.docker/config.json"));
141
+ hint("Re-run: fops azure up (will re-configure credentials)");
142
+ }
143
+
144
+ const { stdout: upLogTail } = await ssh(
145
+ "tail -80 /tmp/fops-up.log 2>/dev/null || echo '(log not found or empty)'"
146
+ );
147
+ if (upLogTail?.trim()) {
148
+ console.log(WARN(" Last lines of /tmp/fops-up.log:"));
149
+ for (const line of upLogTail.trim().split("\n").slice(-40)) {
150
+ console.log(chalk.dim(` ${line}`));
151
+ }
152
+ }
153
+
154
+ hint(`\nDebug: ssh ${adminUser}@${ip} "tail -100 /tmp/fops-up.log"`);
155
+ hint("Retry: fops azure deploy\n");
156
+ return;
157
+ }
158
+ console.log(OK(" ✓ Postgres ready"));
159
+
160
+ hint("Waiting for backend migrations…");
161
+ const migStart = Date.now();
162
+ let migReady = false;
163
+ while (Date.now() - migStart < 120000) {
164
+ const { exitCode: migCheck } = await ssh(
165
+ `cd /opt/foundation-compose && sudo docker compose exec -T postgres psql -U foundation -d foundation -c "SELECT 1 FROM \\"user\\" LIMIT 1" >/dev/null 2>&1`
166
+ );
167
+ if (migCheck === 0) { migReady = true; break; }
168
+ await new Promise((r) => setTimeout(r, 5000));
169
+ }
170
+ if (!migReady) {
171
+ hint("Retrying migration check in 30s…");
172
+ await new Promise((r) => setTimeout(r, 30000));
173
+ const { exitCode: retryCheck } = await ssh(
174
+ `cd /opt/foundation-compose && sudo docker compose exec -T postgres psql -U foundation -d foundation -c "SELECT 1 FROM \\"user\\" LIMIT 1" >/dev/null 2>&1`
175
+ );
176
+ if (retryCheck === 0) migReady = true;
177
+ }
178
+ if (!migReady) {
179
+ console.log(WARN(" ⚠ Migrations not complete — skipping grant-admin (schema not ready)"));
180
+ hint("Run manually after stack is up: fops azure grant admin");
181
+ } else {
182
+ const operatorEmail = process.env.FOUNDATION_USERNAME || process.env.FOUNDATION_OPERATOR_EMAIL || process.env.QA_USERNAME || "compose@meshx.io";
183
+ hint(`Ensuring ${operatorEmail} user + Foundation Admin…`);
184
+ const ensureUserSql = [
185
+ // Ensure the default operator user exists
186
+ `INSERT INTO "user" (identifier, urn, username, first_name, last_name, email, is_system)`,
187
+ ` VALUES (gen_random_uuid(), 'urn:meshx:user:operator', $op, 'Foundation', 'Operator', $op, false)`.replace(/\$op/g, `'${operatorEmail.replace(/'/g, "''")}'`),
188
+ ` ON CONFLICT (username) DO NOTHING;`,
189
+ // Grant Foundation Admin to operator user
190
+ `INSERT INTO role_member (identifier, role_id)`,
191
+ ` SELECT u.identifier, r.id FROM "user" u CROSS JOIN "role" r`,
192
+ ` WHERE u.username = '${operatorEmail.replace(/'/g, "''")}' AND r.name = 'Foundation Admin'`,
193
+ ` ON CONFLICT (role_id, identifier) DO NOTHING;`,
194
+ // Grant Foundation Admin to all other non-system users too
195
+ `INSERT INTO role_member (identifier, role_id)`,
196
+ ` SELECT u.identifier, r.id FROM "user" u CROSS JOIN "role" r`,
197
+ ` WHERE NOT u.is_system`,
198
+ ` AND u.username NOT IN ('admin@meshx.io', 'scheduler@meshx.io', 'opa@meshx.io')`,
199
+ ` AND r.name = 'Foundation Admin'`,
200
+ ` ON CONFLICT (role_id, identifier) DO NOTHING;`,
201
+ ].join("\n");
202
+ const b64Grant = Buffer.from(ensureUserSql).toString("base64");
203
+ let grantCode = -1;
204
+ let grantOut = "";
205
+ for (let attempt = 0; attempt < 2; attempt++) {
206
+ if (attempt === 1) {
207
+ hint("Grant failed; retrying in 20s…");
208
+ await new Promise((r) => setTimeout(r, 20000));
209
+ }
210
+ const result = await ssh(
211
+ `cd /opt/foundation-compose && echo '${b64Grant}' | base64 -d | sudo docker compose exec -T postgres psql -U foundation -d foundation -v ON_ERROR_STOP=1 2>&1`
212
+ );
213
+ grantOut = result.stdout || "";
214
+ grantCode = result.exitCode;
215
+ if (grantCode === 0) break;
216
+ }
217
+ if (grantCode === 0) {
218
+ console.log(OK(` ✓ ${operatorEmail} — Foundation Admin granted`));
219
+ } else {
220
+ console.log(WARN(" ⚠ Admin grant failed — run manually: fops azure grant admin"));
221
+ if (grantOut?.trim()) hint(grantOut.trim());
222
+ }
223
+
224
+ const orgSql = `INSERT INTO user_organization (user_id, organization_id) SELECT u.id, o.id FROM "user" u CROSS JOIN organization o WHERE o.name = 'root' ON CONFLICT DO NOTHING;`;
225
+ const b64Org = Buffer.from(orgSql).toString("base64");
226
+ let orgCode = -1;
227
+ for (let attempt = 0; attempt < 2; attempt++) {
228
+ if (attempt === 1) await new Promise((r) => setTimeout(r, 15000));
229
+ const result = await ssh(
230
+ `cd /opt/foundation-compose && echo '${b64Org}' | base64 -d | sudo docker compose exec -T postgres psql -U foundation -d foundation -v ON_ERROR_STOP=1 2>&1`
231
+ );
232
+ orgCode = result.exitCode;
233
+ if (orgCode === 0) break;
234
+ }
235
+ if (orgCode === 0) {
236
+ console.log(OK(" ✓ Root organization membership ensured"));
237
+ }
238
+ }
239
+
240
+ // Wait for the public URL to become reachable (Traefik + DNS propagation)
241
+ if (publicUrl) {
242
+ const urlWaitMode = normalizeUrlWaitMode(waitMode);
243
+ hint(`Waiting for ${publicUrl} to respond (mode: ${urlWaitMode})…`);
244
+ const urlMaxWait = 300000; // 5 min — Traefik + DNS can be slow after start
245
+ const urlStart = Date.now();
246
+ let lastProgressAt = 0;
247
+ let lastProbe = "not probed yet";
248
+ let urlOk = false;
249
+ while (Date.now() - urlStart < urlMaxWait) {
250
+ try {
251
+ const r = await tlsFetch(publicUrl, { signal: AbortSignal.timeout(8000), redirect: "follow" });
252
+ lastProbe = `HTTP ${r.status}`;
253
+ if (isUrlReadyByMode(r.status, urlWaitMode)) {
254
+ urlOk = true;
255
+ break;
256
+ }
257
+ } catch (err) {
258
+ // DNS not ready or connection refused — retry
259
+ lastProbe = err?.name || err?.message || "network error";
260
+ }
261
+ const now = Date.now();
262
+ if (now - lastProgressAt >= URL_WAIT_PROGRESS_INTERVAL_MS) {
263
+ const elapsed = Math.round((now - urlStart) / 1000);
264
+ hint(`Still waiting for ${publicUrl} [${elapsed}s] — last probe: ${lastProbe}`);
265
+ lastProgressAt = now;
266
+ }
267
+ await new Promise(r => setTimeout(r, 5000));
268
+ }
269
+ if (urlOk) {
270
+ console.log(OK(` ✓ ${publicUrl} is reachable (${lastProbe})`));
271
+ } else {
272
+ console.log(WARN(` ⚠ ${publicUrl} not responding yet (${lastProbe}) — Traefik, DNS, or edge config may still be propagating`));
273
+ hint(`Try stricter mode if needed: fops azure up --wait-mode ${URL_WAIT_MODE_HTTP_2XX}`);
274
+ hint("Check: curl -sk " + publicUrl);
275
+ }
276
+ }
277
+
278
+ console.log("");
279
+ }
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Azure VM — Initial provisioning
3
+ * Extracted from azure-provision.js for maintainability
4
+ *
5
+ * Installs Docker, Node.js, fops CLI, clones foundation-compose repo on a fresh Ubuntu VM.
6
+ */
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+ import { OK, WARN, banner, hint, sshCmd } from "./azure-helpers.js";
10
+
11
+ // ═══════════════════════════════════════════════════════════════════════════
12
+ // Provision — install Docker, Node, fops, clone repo on a fresh Ubuntu VM
13
+ // ═══════════════════════════════════════════════════════════════════════════
14
+
15
+ export async function provisionVm(execa, ip, adminUser, { githubToken, branch = "main", fopsVersion = "latest" } = {}) {
16
+ banner("Provisioning VM");
17
+ const ssh = (cmd, timeout = 120000) => sshCmd(execa, ip, adminUser, cmd, timeout);
18
+
19
+ async function runScript(label, script, timeout = 180000) {
20
+ hint(`${label}…`);
21
+ const b64 = Buffer.from(script).toString("base64");
22
+ const { exitCode, stdout } = await ssh(
23
+ `echo '${b64}' | base64 -d | sudo bash -e 2>&1`, timeout,
24
+ );
25
+ if (exitCode !== 0) {
26
+ console.log(WARN(` ⚠ ${label} had issues`));
27
+ if (stdout?.trim()) hint(stdout.trim().split("\n").pop());
28
+ } else {
29
+ console.log(OK(` ✓ ${label}`));
30
+ }
31
+ return exitCode;
32
+ }
33
+
34
+ const waitAptLock = "while fuser /var/lib/dpkg/lock-frontend /var/lib/apt/lists/lock /var/cache/apt/archives/lock >/dev/null 2>&1; do echo 'Waiting for apt/dpkg lock…'; sleep 5; done";
35
+ await runScript("Waiting for cloud-init", [
36
+ "cloud-init status --wait 2>/dev/null || true",
37
+ "while fuser /var/lib/dpkg/lock-frontend /var/lib/apt/lists/lock /var/cache/apt/archives/lock >/dev/null 2>&1; do sleep 3; done",
38
+ ].join("\n"), 300000);
39
+
40
+ await runScript("Installing system packages", [
41
+ waitAptLock,
42
+ "export DEBIAN_FRONTEND=noninteractive",
43
+ "apt-get update -y -qq",
44
+ "apt-get install -y -qq apt-transport-https ca-certificates curl gnupg lsb-release jq git make unzip zsh software-properties-common python3-venv python3-pip",
45
+ ].join("\n"), 300000);
46
+
47
+ await runScript("Installing Docker", [
48
+ waitAptLock,
49
+ "export DEBIAN_FRONTEND=noninteractive",
50
+ "install -m 0755 -d /etc/apt/keyrings",
51
+ "curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg",
52
+ "chmod a+r /etc/apt/keyrings/docker.gpg",
53
+ `echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list`,
54
+ "apt-get update -qq",
55
+ "apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin",
56
+ "systemctl enable docker && systemctl start docker",
57
+ `usermod -aG docker ${adminUser}`,
58
+ ].join("\n"), 300000);
59
+
60
+ await runScript("Configuring br_netfilter for k3s DNS", [
61
+ "modprobe br_netfilter",
62
+ "echo br_netfilter > /etc/modules-load.d/br_netfilter.conf",
63
+ "sysctl -w net.bridge.bridge-nf-call-iptables=1",
64
+ "echo 'net.bridge.bridge-nf-call-iptables = 1' > /etc/sysctl.d/99-br-netfilter.conf",
65
+ ].join("\n"));
66
+
67
+ await runScript("Installing GitHub CLI", [
68
+ waitAptLock,
69
+ "set +e",
70
+ "curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg 2>/dev/null",
71
+ "chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg 2>/dev/null",
72
+ "echo \"deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main\" > /etc/apt/sources.list.d/github-cli.list",
73
+ "for _ in 1 2 3 4 5; do if apt-get update -qq && apt-get install -y -qq gh; then break; fi; echo 'Retrying in 10s…'; sleep 10; done",
74
+ "set -e",
75
+ "command -v gh >/dev/null 2>&1 || (echo 'gh not found after install attempts' && exit 1)",
76
+ ].join("\n"), 120000);
77
+
78
+ const fopsPkg = fopsVersion === "latest"
79
+ ? "@meshxdata/fops"
80
+ : `@meshxdata/fops@${fopsVersion}`;
81
+ await runScript("Installing Node.js + fops", [
82
+ waitAptLock,
83
+ `curl -fsSL https://deb.nodesource.com/setup_20.x | bash -`,
84
+ "apt-get install -y -qq nodejs",
85
+ `npm install -g ${fopsPkg}`,
86
+ `FOPS_DIR=$(npm root -g)/@meshxdata/fops`,
87
+ `if [ ! -d "$FOPS_DIR/node_modules/commander" ]; then`,
88
+ ` echo "Dependencies missing — installing inside package dir…"`,
89
+ ` cd "$FOPS_DIR" && npm install --omit=dev`,
90
+ `fi`,
91
+ `fops --version || { echo "fops binary not on PATH — linking manually"; ln -sf "$(npm root -g)/@meshxdata/fops/fops.mjs" /usr/local/bin/fops; fops --version; }`,
92
+ ].join("\n"), 300000);
93
+
94
+ if (githubToken) {
95
+ await runScript("Configuring GitHub credentials", [
96
+ `printf 'machine github.com login x-access-token password %s\\n' '${githubToken}' > /root/.netrc && chmod 600 /root/.netrc`,
97
+ `printf 'machine github.com login x-access-token password %s\\n' '${githubToken}' > /home/${adminUser}/.netrc && chmod 600 /home/${adminUser}/.netrc && chown ${adminUser}:${adminUser} /home/${adminUser}/.netrc`,
98
+ `echo '${githubToken}' | docker login ghcr.io -u x-access-token --password-stdin`,
99
+ `mkdir -p /home/${adminUser}/.docker && cp /root/.docker/config.json /home/${adminUser}/.docker/config.json 2>/dev/null && chown -R ${adminUser}:${adminUser} /home/${adminUser}/.docker || true`,
100
+ ].join("\n"));
101
+ }
102
+
103
+ await runScript("Cloning foundation-compose", [
104
+ "rm -rf /opt/foundation-compose",
105
+ `git clone --branch ${branch} --depth 1 https://github.com/meshxdata/foundation-compose.git /opt/foundation-compose`,
106
+ "mkdir -p /opt/foundation-compose/credentials",
107
+ "touch /opt/foundation-compose/credentials/kubeconfig.yaml",
108
+ `chown -R ${adminUser}:${adminUser} /opt/foundation-compose`,
109
+ ].join("\n"), 300000);
110
+
111
+ await runScript("Initializing submodules", [
112
+ "cd /opt/foundation-compose",
113
+ "git submodule update --init --recursive --depth 1",
114
+ `chown -R ${adminUser}:${adminUser} /opt/foundation-compose`,
115
+ ].join("\n"), 300000);
116
+
117
+ await runScript("Installing fops-api systemd service", [
118
+ `cat > /etc/systemd/system/fops-api.service <<'UNIT'`,
119
+ "[Unit]",
120
+ "Description=fops API server",
121
+ "After=network.target docker.service",
122
+ "Wants=docker.service",
123
+ "",
124
+ "[Service]",
125
+ "Type=simple",
126
+ `User=${adminUser}`,
127
+ "WorkingDirectory=/opt/foundation-compose",
128
+ "Environment=COMPOSE_ROOT=/opt/foundation-compose",
129
+ "EnvironmentFile=-/opt/foundation-compose/.env",
130
+ "ExecStart=/usr/bin/env fops serve --host 127.0.0.1 --port 4100",
131
+ "Restart=always",
132
+ "RestartSec=10",
133
+ "Environment=NODE_ENV=production",
134
+ "",
135
+ "[Install]",
136
+ "WantedBy=multi-user.target",
137
+ "UNIT",
138
+ "systemctl daemon-reload",
139
+ "systemctl enable --now fops-api",
140
+ ].join("\n"));
141
+
142
+ const cloudInitSrc = path.resolve(
143
+ import.meta.dirname, "..", "..", "..", "..", "..", "packer", "scripts", "cloud-init-foundation.sh",
144
+ );
145
+ let cloudInitScript = "";
146
+ try {
147
+ cloudInitScript = fs.readFileSync(cloudInitSrc, "utf8");
148
+ } catch {
149
+ hint("cloud-init-foundation.sh not found locally — skipping per-boot script");
150
+ }
151
+ if (cloudInitScript) {
152
+ const b64 = Buffer.from(cloudInitScript).toString("base64");
153
+ await ssh(
154
+ `echo '${b64}' | base64 -d | sudo tee /var/lib/cloud/scripts/per-boot/foundation-startup.sh > /dev/null && sudo chmod +x /var/lib/cloud/scripts/per-boot/foundation-startup.sh`,
155
+ );
156
+ console.log(OK(" ✓ Cloud-init per-boot script installed"));
157
+ }
158
+
159
+ await runScript("Setting MOTD", [
160
+ "chmod -x /etc/update-motd.d/* 2>/dev/null || true",
161
+ `cat > /etc/motd <<'MOTD'
162
+
163
+ ___ _ _ _
164
+ / __\\__ _ _ _ __ __| | __ _| |_(_) ___ _ __
165
+ / _\\/ _ \\| | | | '_ \\ / _\` |/ _\` | __| |/ _ \\| '_ \\
166
+ / / | (_) | |_| | | | | (_| | (_| | |_| | (_) | | | |
167
+ \\/ \\___/ \\__,_|_| |_|\\__,_|\\__,_|\\__|_|\\___/|_| |_|
168
+
169
+ Data Mesh Platform
170
+
171
+ Quick start:
172
+ fops status Show running services
173
+ fops up Start the platform
174
+ fops down Stop the platform
175
+ fops logs <service> Tail service logs
176
+ fops doctor Diagnose issues
177
+
178
+ Project dir: /opt/foundation-compose
179
+
180
+ MOTD`,
181
+ ].join("\n"));
182
+
183
+ await ssh("sudo apt-get clean && sudo rm -rf /var/lib/apt/lists/*", 30000);
184
+
185
+ console.log(OK("\n ✓ Provisioning complete"));
186
+ }