@meshxdata/fops 0.1.51 → 0.1.53
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/CHANGELOG.md +207 -21
- package/package.json +2 -6
- package/src/agent/agent.js +6 -0
- package/src/commands/setup.js +34 -0
- package/src/doctor.js +11 -8
- package/src/fleet-registry.js +38 -2
- package/src/plugins/__test-fixtures__/fake-plugin.js +2 -0
- package/src/plugins/__test-fixtures__/no-register-plugin.js +2 -0
- package/src/plugins/__test-fixtures__/with-register/index.js +2 -0
- package/src/plugins/__test-fixtures__/without-register/index.js +2 -0
- package/src/plugins/api.js +4 -0
- package/src/plugins/builtins/docker-compose.js +59 -0
- package/src/plugins/bundled/fops-plugin-azure/index.js +4 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-core.js +53 -53
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-secrets.js +151 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-storage.js +2 -2
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-cost.js +52 -22
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-fleet.js +12 -4
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-helpers.js +6 -2
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-ops.js +113 -7
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-provision-init.js +13 -4
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-provision.js +91 -14
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-service.js +507 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-sync.js +146 -7
- package/src/plugins/bundled/fops-plugin-azure/lib/azure.js +1 -1
- package/src/plugins/bundled/fops-plugin-azure/lib/commands/test-cmds.js +28 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/commands/vm-cmds.js +61 -0
- package/src/plugins/bundled/fops-plugin-cloud/api.js +712 -0
- package/src/plugins/bundled/fops-plugin-cloud/fops.plugin.json +6 -0
- package/src/plugins/bundled/fops-plugin-cloud/index.js +208 -0
- package/src/plugins/bundled/fops-plugin-cloud/lib/azure-provider.js +81 -0
- package/src/plugins/bundled/fops-plugin-cloud/lib/provider.js +50 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/dist/assets/favicon-C49brna2.svg +15 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/dist/assets/index-CVqQ_kKW.js +65 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/dist/assets/index-DZetahP3.css +1 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/dist/index.html +28 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/index.html +27 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/package-lock.json +2634 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/package.json +29 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/postcss.config.cjs +5 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/App.jsx +32 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/api/client.js +114 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/api/queries.js +111 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/components/LogPanel.jsx +162 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/components/ThemeToggle.jsx +46 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/css/additional-styles/utility-patterns.css +147 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/css/style.css +138 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/favicon.svg +15 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/lib/utils.ts +19 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/main.jsx +25 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/pages/Audit.jsx +164 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/pages/Costs.jsx +305 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/pages/CreateResource.jsx +285 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/pages/Fleet.jsx +307 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/pages/Resources.jsx +229 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/partials/Header.jsx +132 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/partials/Sidebar.jsx +174 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/partials/SidebarLinkGroup.jsx +21 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/utils/AuthContext.jsx +170 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/utils/Info.jsx +49 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/utils/ThemeContext.jsx +37 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/utils/Transition.jsx +116 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/src/utils/Utils.js +63 -0
- package/src/plugins/bundled/fops-plugin-cloud/ui/vite.config.js +23 -0
- package/src/plugins/bundled/fops-plugin-foundation/test-helpers.js +65 -0
- package/src/plugins/loader.js +34 -1
- package/src/plugins/registry.js +15 -0
- package/src/plugins/schemas.js +17 -0
- package/src/project.js +1 -1
- package/src/serve.js +196 -2
- package/src/shell.js +21 -1
- package/src/web/admin.html.js +236 -0
- package/src/web/api.js +73 -0
- package/src/web/dist/assets/index-BphVaAUd.css +1 -0
- package/src/web/dist/assets/index-CSckLzuG.js +129 -0
- package/src/web/dist/index.html +2 -2
- package/src/web/frontend/index.html +16 -0
- package/src/web/frontend/src/App.jsx +445 -0
- package/src/web/frontend/src/components/ChatView.jsx +910 -0
- package/src/web/frontend/src/components/InputBox.jsx +523 -0
- package/src/web/frontend/src/components/Sidebar.jsx +410 -0
- package/src/web/frontend/src/components/StatusBar.jsx +37 -0
- package/src/web/frontend/src/components/TabBar.jsx +87 -0
- package/src/web/frontend/src/hooks/useWebSocket.js +412 -0
- package/src/web/frontend/src/index.css +78 -0
- package/src/web/frontend/src/main.jsx +6 -0
- package/src/web/frontend/vite.config.js +21 -0
- package/src/web/server.js +64 -1
- package/src/web/dist/assets/index-NXC8Hvnp.css +0 -1
- package/src/web/dist/assets/index-QH1N4ejK.js +0 -112
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
2
5
|
import {
|
|
3
6
|
DEFAULTS, DIM, OK, WARN, ERR, LABEL,
|
|
4
7
|
banner, hint, kvLine,
|
|
@@ -24,7 +27,7 @@ const SVC_MAP = {
|
|
|
24
27
|
"foundation-storage-engine": "se",
|
|
25
28
|
};
|
|
26
29
|
|
|
27
|
-
// Parse "foundation-backend=
|
|
30
|
+
// Parse "foundation-backend=ghcr.io/…:tag|||sha256:abc…" → { be: { tag: "latest", sha: "a1b2c3d" } }
|
|
28
31
|
export function parseServiceVersions(raw) {
|
|
29
32
|
if (!raw?.trim()) return {};
|
|
30
33
|
const versions = {};
|
|
@@ -39,8 +42,8 @@ export function parseServiceVersions(raw) {
|
|
|
39
42
|
const [imagePart = "", idPart = ""] = rest.split("|||");
|
|
40
43
|
const colon = imagePart.lastIndexOf(":");
|
|
41
44
|
const tag = colon >= 0 ? imagePart.slice(colon + 1) : imagePart;
|
|
42
|
-
const
|
|
43
|
-
versions[short] =
|
|
45
|
+
const sha = idPart.trim().slice(0, 7);
|
|
46
|
+
versions[short] = { tag: tag || null, sha: sha || null };
|
|
44
47
|
}
|
|
45
48
|
return versions;
|
|
46
49
|
}
|
|
@@ -196,9 +199,13 @@ async function syncVms(execa) {
|
|
|
196
199
|
"echo '___SHA___'",
|
|
197
200
|
"git rev-parse --short HEAD 2>/dev/null || echo unknown",
|
|
198
201
|
"echo '___VER___'",
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
"
|
|
202
|
+
// Use docker compose ps to resolve actual container names (handles project-name prefixes)
|
|
203
|
+
"for svc in foundation-backend foundation-frontend foundation-processor foundation-watcher foundation-scheduler foundation-storage-engine; do" +
|
|
204
|
+
" cid=$(docker compose ps -q $svc 2>/dev/null | head -1);" +
|
|
205
|
+
" if [ -n \"$cid\" ]; then docker inspect --format \"$svc={{.Config.Image}}|||{{slice .Image 7 19}}\" $cid 2>/dev/null; fi;" +
|
|
206
|
+
" done",
|
|
207
|
+
"echo '___FLAGS___'",
|
|
208
|
+
"grep -h 'MX_FF_' /opt/foundation-compose/docker-compose.yaml 2>/dev/null | sed 's/.*- //' | sort -u || true",
|
|
202
209
|
].join(" && "),
|
|
203
210
|
20000,
|
|
204
211
|
);
|
|
@@ -211,7 +218,8 @@ async function syncVms(execa) {
|
|
|
211
218
|
|
|
212
219
|
const [containerPart = "", rest = ""] = stdout.split("___BR___");
|
|
213
220
|
const [branchRaw = "", afterBranch = ""] = rest.split("___SHA___");
|
|
214
|
-
const [shaRaw = "",
|
|
221
|
+
const [shaRaw = "", afterSha = ""] = afterBranch.split("___VER___");
|
|
222
|
+
const [verRaw = "", flagsRaw = ""] = afterSha.split("___FLAGS___");
|
|
215
223
|
entry.branch = branchRaw.trim() || "unknown";
|
|
216
224
|
entry.sha = shaRaw.trim() || "unknown";
|
|
217
225
|
|
|
@@ -226,6 +234,16 @@ async function syncVms(execa) {
|
|
|
226
234
|
|
|
227
235
|
// Parse service image tags: "foundation-backend=ghcr.io/…:tag" → { be: "tag", … }
|
|
228
236
|
entry.services = parseServiceVersions(verRaw);
|
|
237
|
+
|
|
238
|
+
// Parse feature flags: "MX_FF_NAME=true" lines
|
|
239
|
+
if (flagsRaw.trim()) {
|
|
240
|
+
const flags = {};
|
|
241
|
+
for (const line of flagsRaw.trim().split("\n")) {
|
|
242
|
+
const match = line.match(/^(MX_FF_\w+)=(.+)/);
|
|
243
|
+
if (match) flags[match[1]] = match[2].trim().replace(/['"]/g, "") === "true";
|
|
244
|
+
}
|
|
245
|
+
if (Object.keys(flags).length > 0) entry.flags = flags;
|
|
246
|
+
}
|
|
229
247
|
} catch {
|
|
230
248
|
entry.status = "error";
|
|
231
249
|
}
|
|
@@ -236,6 +254,73 @@ async function syncVms(execa) {
|
|
|
236
254
|
return results;
|
|
237
255
|
}
|
|
238
256
|
|
|
257
|
+
// ── Flux overlay version resolver ────────────────────────────────────────────
|
|
258
|
+
|
|
259
|
+
const AKS_SVC_DIRS = {
|
|
260
|
+
backend: "be",
|
|
261
|
+
frontend: "fe",
|
|
262
|
+
processor: "pr",
|
|
263
|
+
watcher: "wa",
|
|
264
|
+
scheduler: "sc",
|
|
265
|
+
"storage-engine": "se",
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Resolve the flux repo directory from projectRoot or known locations.
|
|
270
|
+
*/
|
|
271
|
+
function findFluxRepo() {
|
|
272
|
+
// Check projectRoot sibling (../flux relative to foundation-compose)
|
|
273
|
+
try {
|
|
274
|
+
const config = JSON.parse(fs.readFileSync(path.join(os.homedir(), ".fops.json"), "utf8"));
|
|
275
|
+
if (config.projectRoot) {
|
|
276
|
+
const sibling = path.join(path.dirname(config.projectRoot), "flux");
|
|
277
|
+
if (fs.existsSync(path.join(sibling, "apps"))) return sibling;
|
|
278
|
+
}
|
|
279
|
+
} catch {}
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Extract image tags from flux overlay kustomization files for a cluster.
|
|
285
|
+
* Looks for overlays matching <clusterName>-azure under each service.
|
|
286
|
+
*/
|
|
287
|
+
function getFluxServiceVersions(clusterName) {
|
|
288
|
+
const fluxDir = findFluxRepo();
|
|
289
|
+
if (!fluxDir) return null;
|
|
290
|
+
|
|
291
|
+
const appsDir = path.join(fluxDir, "apps", "foundation");
|
|
292
|
+
if (!fs.existsSync(appsDir)) return null;
|
|
293
|
+
|
|
294
|
+
const versions = {};
|
|
295
|
+
const overlayName = `${clusterName}-azure`;
|
|
296
|
+
|
|
297
|
+
for (const [svcDir, shortKey] of Object.entries(AKS_SVC_DIRS)) {
|
|
298
|
+
// Search all overlay groups (meshx, customers, etc.)
|
|
299
|
+
const overlaysRoot = path.join(appsDir, svcDir, "overlays");
|
|
300
|
+
if (!fs.existsSync(overlaysRoot)) continue;
|
|
301
|
+
|
|
302
|
+
let found = false;
|
|
303
|
+
try {
|
|
304
|
+
for (const group of fs.readdirSync(overlaysRoot)) {
|
|
305
|
+
const kustomPath = path.join(overlaysRoot, group, overlayName, "kustomization.yaml");
|
|
306
|
+
if (!fs.existsSync(kustomPath)) continue;
|
|
307
|
+
|
|
308
|
+
const content = fs.readFileSync(kustomPath, "utf8");
|
|
309
|
+
// Extract image tag from "tag: v0.3.28" in the kustomization patch
|
|
310
|
+
const tagMatch = content.match(/\btag:\s*(\S+)/);
|
|
311
|
+
if (tagMatch) {
|
|
312
|
+
versions[shortKey] = { tag: tagMatch[1], sha: null };
|
|
313
|
+
found = true;
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
} catch {}
|
|
318
|
+
if (!found) versions[shortKey] = null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return Object.values(versions).some(v => v) ? versions : null;
|
|
322
|
+
}
|
|
323
|
+
|
|
239
324
|
// ── Sync: probe AKS clusters ─────────────────────────────────────────────────
|
|
240
325
|
|
|
241
326
|
async function syncClusters(execa) {
|
|
@@ -277,6 +362,60 @@ async function syncClusters(execa) {
|
|
|
277
362
|
entry.status = "probe-failed";
|
|
278
363
|
}
|
|
279
364
|
|
|
365
|
+
// HA pairing: infer from naming convention
|
|
366
|
+
const isStandby = name.endsWith("-standby");
|
|
367
|
+
if (isStandby) {
|
|
368
|
+
const primaryName = name.replace(/-standby$/, "");
|
|
369
|
+
if (names.includes(primaryName)) {
|
|
370
|
+
entry.isStandby = true;
|
|
371
|
+
entry.primaryCluster = primaryName;
|
|
372
|
+
}
|
|
373
|
+
} else {
|
|
374
|
+
const standbyName = `${name}-standby`;
|
|
375
|
+
if (names.includes(standbyName)) {
|
|
376
|
+
entry.ha = { standbyCluster: standbyName, standbyRegion: clusters[standbyName]?.location || null };
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Discover Key Vault and Storage Account in the resource group
|
|
381
|
+
try {
|
|
382
|
+
const rg = cl.resourceGroup;
|
|
383
|
+
const subArgs = cl.subscriptionId ? ["--subscription", cl.subscriptionId] : [];
|
|
384
|
+
|
|
385
|
+
const [kvResult, saResult, pgResult] = await Promise.all([
|
|
386
|
+
execa("az", ["keyvault", "list", "-g", rg, "--query", "[].{name:name}", "-o", "json", ...subArgs], { timeout: 15000, reject: false }),
|
|
387
|
+
execa("az", ["storage", "account", "list", "-g", rg, "--query", "[].{name:name,location:location}", "-o", "json", ...subArgs], { timeout: 15000, reject: false }),
|
|
388
|
+
execa("az", ["postgres", "flexible-server", "list", "-g", rg, "--query", "[].{name:name,fqdn:fullyQualifiedDomainName}", "-o", "json", ...subArgs], { timeout: 15000, reject: false }),
|
|
389
|
+
]);
|
|
390
|
+
|
|
391
|
+
if (kvResult.exitCode === 0 && kvResult.stdout?.trim()) {
|
|
392
|
+
const vaults = JSON.parse(kvResult.stdout);
|
|
393
|
+
if (vaults.length > 0) entry.vault = { keyVaultName: vaults[0].name };
|
|
394
|
+
}
|
|
395
|
+
if (saResult.exitCode === 0 && saResult.stdout?.trim()) {
|
|
396
|
+
const accounts = JSON.parse(saResult.stdout);
|
|
397
|
+
if (accounts.length > 0) {
|
|
398
|
+
entry.storageAccount = accounts[0].name;
|
|
399
|
+
if (accounts.length > 1) {
|
|
400
|
+
entry.storageHA = { sourceAccount: accounts[0].name, destAccount: accounts[1].name, destRegion: accounts[1].location };
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (pgResult.exitCode === 0 && pgResult.stdout?.trim()) {
|
|
405
|
+
const servers = JSON.parse(pgResult.stdout);
|
|
406
|
+
if (servers.length > 0) entry.postgres = { serverName: servers[0].name, fqdn: servers[0].fqdn };
|
|
407
|
+
}
|
|
408
|
+
} catch { /* resource discovery is best-effort */ }
|
|
409
|
+
|
|
410
|
+
// Extract service image tags from flux overlays.
|
|
411
|
+
// Standbys share the primary's overlay.
|
|
412
|
+
let fluxVersions = getFluxServiceVersions(name);
|
|
413
|
+
if (!fluxVersions && isStandby) {
|
|
414
|
+
const primaryName = name.replace(/-standby$/, "");
|
|
415
|
+
fluxVersions = getFluxServiceVersions(primaryName);
|
|
416
|
+
}
|
|
417
|
+
if (fluxVersions) entry.services = fluxVersions;
|
|
418
|
+
|
|
280
419
|
results[name] = entry;
|
|
281
420
|
}));
|
|
282
421
|
|
|
@@ -46,7 +46,7 @@ export {
|
|
|
46
46
|
|
|
47
47
|
// ── VM operations ────────────────────────────────────────────────────────────
|
|
48
48
|
export {
|
|
49
|
-
azureStatus, azureTrinoStatus, azureSsh, azureSshWhitelistMe, azurePortForward, azureSshAdminAdd, azureVmCheck, azureAgent, azureOpenAiDebugVm,
|
|
49
|
+
azureStatus, azureTrinoStatus, azurePing, azureSsh, azureSshWhitelistMe, azurePortForward, azureSshAdminAdd, azureVmCheck, azureAgent, azureOpenAiDebugVm,
|
|
50
50
|
azureDeploy, azurePull, azureDeployVersion, azureRunUp, azureConfig, azureConfigVersions, azureUpdate,
|
|
51
51
|
azureLogs, azureRestart, azureGrantAdmin, azureContext,
|
|
52
52
|
azureList, azureApply,
|
|
@@ -14,6 +14,8 @@ export function registerTestCommands(azure) {
|
|
|
14
14
|
.command("run [name]", { isDefault: true })
|
|
15
15
|
.description("Run QA automation tests locally against a remote VM")
|
|
16
16
|
.option("--vm-name <name>", "Target VM (default: active)")
|
|
17
|
+
.option("--landscape <file>", "Apply landscape file (FCL/HCL/YAML) before running tests")
|
|
18
|
+
.option("--landscape-template <name>", "Use built-in landscape template (demo, pipeline_demo)")
|
|
17
19
|
.action(async (name, opts) => {
|
|
18
20
|
const { resolveCliSrc, lazyExeca, ensureAzCli, ensureAzAuth, resolvePublicIp } = await import("../azure-helpers.js");
|
|
19
21
|
const { requireVmState, knockForVm, sshCmd, MUX_OPTS } = await import("../azure.js");
|
|
@@ -46,6 +48,32 @@ export function registerTestCommands(azure) {
|
|
|
46
48
|
process.exit(1);
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
// Apply landscape if specified
|
|
52
|
+
let landscapeFile = opts.landscape;
|
|
53
|
+
if (!landscapeFile && opts.landscapeTemplate) {
|
|
54
|
+
const templateDir = path.join(root, "operator-cli/src/plugins/bundled/fops-plugin-foundation/templates/landscapes");
|
|
55
|
+
const templateName = opts.landscapeTemplate.endsWith(".fcl") ? opts.landscapeTemplate : `${opts.landscapeTemplate}.fcl`;
|
|
56
|
+
landscapeFile = path.join(templateDir, templateName);
|
|
57
|
+
try {
|
|
58
|
+
await fsp.access(landscapeFile);
|
|
59
|
+
} catch {
|
|
60
|
+
console.error(chalk.red(`\n Landscape template not found: ${templateName}`));
|
|
61
|
+
console.error(chalk.dim(` Available: demo.fcl, pipeline_demo.fcl\n`));
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (landscapeFile) {
|
|
66
|
+
console.log(chalk.cyan(`\n Applying landscape: ${path.basename(landscapeFile)}…\n`));
|
|
67
|
+
const { azureApply } = await import("../azure.js");
|
|
68
|
+
try {
|
|
69
|
+
await azureApply(landscapeFile, { vmName: state.vmName });
|
|
70
|
+
console.log(chalk.green(" ✓ Landscape applied\n"));
|
|
71
|
+
} catch (err) {
|
|
72
|
+
console.error(chalk.red(`\n Failed to apply landscape: ${err.message}\n`));
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
49
77
|
const vmUrl = state.publicUrl || `https://${ip}`;
|
|
50
78
|
const apiUrl = `${vmUrl}/api`;
|
|
51
79
|
|
|
@@ -248,6 +248,55 @@ export function registerVmCommands(azure, api, registry) {
|
|
|
248
248
|
console.log();
|
|
249
249
|
});
|
|
250
250
|
|
|
251
|
+
// ── reconcile ───────────────────────────────────────────────────────────
|
|
252
|
+
azure
|
|
253
|
+
.command("reconcile [name]")
|
|
254
|
+
.description("Reconcile an existing VM — fix drift without full provisioning (DNS, env, repo, services)")
|
|
255
|
+
.option("--profile <subscription>", "Azure subscription name or ID")
|
|
256
|
+
.option("--url <url>", "Public URL override (default: from tracked state)")
|
|
257
|
+
.option("--cf-token <token>", "Cloudflare API token for DNS (default: $CLOUDFLARE_API_TOKEN)")
|
|
258
|
+
.option("--k3s", "Include k3s Kubernetes services")
|
|
259
|
+
.option("--traefik", "Include traefik reverse proxy")
|
|
260
|
+
.option("--dai", "Also start DAI services (implies --k3s)")
|
|
261
|
+
.option("--no-knock", "Skip port-knock setup")
|
|
262
|
+
.action(async (name, opts) => {
|
|
263
|
+
if (opts.dai) opts.k3s = true;
|
|
264
|
+
const {
|
|
265
|
+
lazyExeca, ensureAzCli, ensureAzAuth, resolveGithubToken, verifyGithubToken,
|
|
266
|
+
reconcileVm, DEFAULTS,
|
|
267
|
+
} = await import("../azure.js");
|
|
268
|
+
const { resolveCfToken } = await import("../cloudflare.js");
|
|
269
|
+
const { readVmState, writeVmState } = await import("../azure-state.js");
|
|
270
|
+
const execa = await lazyExeca();
|
|
271
|
+
await ensureAzCli(execa);
|
|
272
|
+
const account = await ensureAzAuth(execa, { subscription: opts.profile });
|
|
273
|
+
const { token: githubToken, login: githubLogin } = await verifyGithubToken(resolveGithubToken(opts));
|
|
274
|
+
const sub = opts.profile;
|
|
275
|
+
const subId = account.id || DEFAULTS.subscriptionId;
|
|
276
|
+
const tracked = readVmState(name);
|
|
277
|
+
if (!tracked) {
|
|
278
|
+
console.error(chalk.red(`\n VM "${name}" not tracked. Use 'fops azure up ${name}' first.\n`));
|
|
279
|
+
process.exit(1);
|
|
280
|
+
}
|
|
281
|
+
const rg = tracked.resourceGroup;
|
|
282
|
+
const desiredUrl = opts.url || tracked.publicUrl;
|
|
283
|
+
const cfToken = resolveCfToken(opts.cfToken);
|
|
284
|
+
const { publicIp, publicUrl, rg: actualRg } = await reconcileVm(execa, {
|
|
285
|
+
vmName: name, rg, sub, subId, location: tracked.location || DEFAULTS.location,
|
|
286
|
+
port: DEFAULTS.port, adminUser: DEFAULTS.adminUser,
|
|
287
|
+
githubToken, githubLogin, desiredUrl,
|
|
288
|
+
knockSequence: tracked.knockSequence,
|
|
289
|
+
k3s: opts.k3s, traefik: opts.traefik, dai: opts.dai,
|
|
290
|
+
knock: opts.knock, cfToken, waitMode: "http-any",
|
|
291
|
+
});
|
|
292
|
+
const resolvedRg = actualRg || rg;
|
|
293
|
+
writeVmState(name, { resourceGroup: resolvedRg, location: tracked.location, publicIp, publicUrl, subscriptionId: subId });
|
|
294
|
+
console.log(chalk.green(`\n ✓ Reconciled ${chalk.bold(name)}`));
|
|
295
|
+
if (publicUrl) console.log(chalk.dim(` ${publicUrl}`));
|
|
296
|
+
if (publicIp) console.log(chalk.dim(` IP: ${publicIp}`));
|
|
297
|
+
console.log();
|
|
298
|
+
});
|
|
299
|
+
|
|
251
300
|
// ── down ─────────────────────────────────────────────────────────────────
|
|
252
301
|
azure
|
|
253
302
|
.command("down [name]")
|
|
@@ -329,6 +378,18 @@ export function registerVmCommands(azure, api, registry) {
|
|
|
329
378
|
await azureTrinoStatus({ vmName: opts.vmName || name, profile: opts.profile });
|
|
330
379
|
});
|
|
331
380
|
|
|
381
|
+
// ── ping ────────────────────────────────────────────────────────────────
|
|
382
|
+
azure
|
|
383
|
+
.command("ping [name]")
|
|
384
|
+
.description("Check Foundation backend health via /api/ping/json")
|
|
385
|
+
.option("--profile <subscription>", "Azure subscription name or ID")
|
|
386
|
+
.option("--vm-name <name>", "Target VM (default: active VM)")
|
|
387
|
+
.option("--token <token>", "Ping auth token (or set FOPS_PING_TOKEN)")
|
|
388
|
+
.action(async (name, opts) => {
|
|
389
|
+
const { azurePing } = await import("../azure.js");
|
|
390
|
+
await azurePing({ vmName: opts.vmName || name, profile: opts.profile, token: opts.token });
|
|
391
|
+
});
|
|
392
|
+
|
|
332
393
|
// ── terraform ────────────────────────────────────────────────────────────
|
|
333
394
|
azure
|
|
334
395
|
.command("terraform [name]")
|