@meshxdata/fops 0.1.36 → 0.1.38

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 (59) hide show
  1. package/CHANGELOG.md +207 -0
  2. package/fops.mjs +37 -14
  3. package/package.json +1 -1
  4. package/src/agent/llm.js +2 -0
  5. package/src/auth/azure.js +92 -0
  6. package/src/auth/cloudflare.js +125 -0
  7. package/src/auth/index.js +2 -0
  8. package/src/commands/index.js +8 -4
  9. package/src/commands/lifecycle.js +31 -10
  10. package/src/plugins/bundled/fops-plugin-azure/index.js +44 -2896
  11. package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks.js +130 -2
  12. package/src/plugins/bundled/fops-plugin-azure/lib/azure-auth.js +497 -0
  13. package/src/plugins/bundled/fops-plugin-azure/lib/azure-helpers.js +51 -13
  14. package/src/plugins/bundled/fops-plugin-azure/lib/azure-ops.js +206 -52
  15. package/src/plugins/bundled/fops-plugin-azure/lib/azure-provision.js +128 -34
  16. package/src/plugins/bundled/fops-plugin-azure/lib/azure-shared-cache.js +1 -1
  17. package/src/plugins/bundled/fops-plugin-azure/lib/azure-sync.js +4 -4
  18. package/src/plugins/bundled/fops-plugin-azure/lib/azure.js +2 -2
  19. package/src/plugins/bundled/fops-plugin-azure/lib/commands/fleet-cmds.js +254 -0
  20. package/src/plugins/bundled/fops-plugin-azure/lib/commands/infra-cmds.js +894 -0
  21. package/src/plugins/bundled/fops-plugin-azure/lib/commands/test-cmds.js +314 -0
  22. package/src/plugins/bundled/fops-plugin-azure/lib/commands/vm-cmds.js +893 -0
  23. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/apps/dai-backend.yaml +13 -0
  24. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/apps/dai-frontend.yaml +13 -0
  25. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/apps/foundation-backend.yaml +13 -0
  26. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/apps/foundation-frontend.yaml +13 -0
  27. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/apps/foundation-hive.yaml +13 -0
  28. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/apps/foundation-kafka.yaml +13 -0
  29. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/apps/foundation-meltano.yaml +13 -0
  30. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/apps/foundation-mlflow.yaml +13 -0
  31. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/apps/foundation-opa.yaml +13 -0
  32. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/apps/foundation-processor.yaml +13 -0
  33. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/apps/foundation-scheduler.yaml +13 -0
  34. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/apps/foundation-storage-engine.yaml +13 -0
  35. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/apps/foundation-trino.yaml +13 -0
  36. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/apps/foundation-watcher.yaml +13 -0
  37. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/config/repository.yaml +66 -0
  38. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/kustomization.yaml +30 -0
  39. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/operator/acr-webhook-controller.yaml +63 -0
  40. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/operator/externalsecrets.yaml +15 -0
  41. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/operator/istio.yaml +42 -0
  42. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/operator/kafka.yaml +15 -0
  43. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/operator/kube-reflector.yaml +33 -0
  44. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/operator/kubecost.yaml +12 -0
  45. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/operator/nats-server.yaml +15 -0
  46. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/operator/prometheus-agent.yaml +34 -0
  47. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/operator/reloader.yaml +12 -0
  48. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/operator/spark.yaml +112 -0
  49. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/operator/tailscale.yaml +67 -0
  50. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/operator/vertical-pod-autoscaler.yaml +15 -0
  51. package/src/plugins/bundled/fops-plugin-file/demo/orders_renamed.aligned.csv +1 -1
  52. package/src/plugins/bundled/fops-plugin-file/index.js +81 -12
  53. package/src/plugins/bundled/fops-plugin-file/lib/match.js +133 -15
  54. package/src/plugins/bundled/fops-plugin-file/lib/report.js +3 -0
  55. package/src/plugins/bundled/fops-plugin-foundation/index.js +26 -6
  56. package/src/plugins/bundled/fops-plugin-foundation/lib/client.js +9 -5
  57. package/src/plugins/bundled/fops-plugin-foundation-graphql/lib/graphql/resolvers/data-product.js +32 -0
  58. package/src/plugins/bundled/fops-plugin-foundation-graphql/lib/graphql/schema.js +20 -1
  59. package/src/plugins/loader.js +23 -6
@@ -0,0 +1,314 @@
1
+ /**
2
+ * QA test and CVE scan commands.
3
+ */
4
+ import chalk from "chalk";
5
+ import { resolveRemoteAuth, suppressTlsWarning } from "../azure-auth.js";
6
+ import { parsePytestSummary, parsePytestDurations } from "../pytest-parse.js";
7
+
8
+ export function registerTestCommands(azure) {
9
+ const test = azure
10
+ .command("test")
11
+ .description("Run and manage QA test results");
12
+
13
+ test
14
+ .command("run [name]", { isDefault: true })
15
+ .description("Run QA automation tests locally against a remote VM")
16
+ .option("--vm-name <name>", "Target VM (default: active)")
17
+ .action(async (name, opts) => {
18
+ const { requireVmState, knockForVm } = await import("../azure.js");
19
+ const { resolveCliSrc } = await import("../azure-helpers.js");
20
+ const { rootDir } = await import(resolveCliSrc("project.js"));
21
+ const fsp = await import("node:fs/promises");
22
+ const path = await import("node:path");
23
+
24
+ const state = requireVmState(opts.vmName || name);
25
+ const ip = state.publicIp;
26
+ if (!ip) {
27
+ console.error(chalk.red("\n No IP address. Is the VM running? Try: fops azure start\n"));
28
+ process.exit(1);
29
+ }
30
+
31
+ const root = rootDir();
32
+ if (!root) {
33
+ console.error(chalk.red("\n Foundation project root not found. Run from the compose repo or set FOUNDATION_ROOT.\n"));
34
+ process.exit(1);
35
+ }
36
+
37
+ const qaDir = path.join(root, "foundation-qa-automation");
38
+ try {
39
+ await fsp.access(qaDir);
40
+ } catch {
41
+ console.error(chalk.red("\n foundation-qa-automation/ not found in project root."));
42
+ console.error(chalk.dim(" Run: git submodule update --init\n"));
43
+ process.exit(1);
44
+ }
45
+
46
+ const vmUrl = state.publicUrl || `https://${ip}`;
47
+ const apiUrl = `${vmUrl}/api`;
48
+ const { execa: execaFn } = await import("execa");
49
+ const { sshCmd, MUX_OPTS } = await import("../azure.js");
50
+
51
+ console.log(chalk.dim(` Authenticating against ${vmUrl}…`));
52
+ const auth = await resolveRemoteAuth({
53
+ apiUrl, ip, vmState: state,
54
+ execaFn, sshCmd, knockForVm, suppressTlsWarning,
55
+ });
56
+ let { bearerToken, qaUser, qaPass, useTokenMode } = auth;
57
+
58
+ if (!bearerToken && !qaUser) {
59
+ console.error(chalk.red("\n No credentials found (local or remote)."));
60
+ console.error(chalk.dim(" Set BEARER_TOKEN or QA_USERNAME/QA_PASSWORD, or ensure the VM has Auth0 configured in .env\n"));
61
+ process.exit(1);
62
+ }
63
+
64
+ // Write .env pointed at the remote VM
65
+ const envPath = path.join(qaDir, ".env");
66
+ const examplePath = path.join(qaDir, ".env.example");
67
+ let envContent;
68
+ try {
69
+ envContent = await fsp.readFile(examplePath, "utf8");
70
+ } catch {
71
+ envContent = await fsp.readFile(envPath, "utf8").catch(() => "");
72
+ }
73
+
74
+ const setVar = (content, key, value) => {
75
+ const re = new RegExp(`^${key}=.*`, "m");
76
+ return re.test(content)
77
+ ? content.replace(re, `${key}=${value}`)
78
+ : content + `\n${key}=${value}`;
79
+ };
80
+
81
+ envContent = setVar(envContent, "API_URL", apiUrl);
82
+ envContent = setVar(envContent, "DEV_API_URL", apiUrl);
83
+ envContent = setVar(envContent, "LIVE_API_URL", apiUrl);
84
+ envContent = setVar(envContent, "QA_USERNAME", qaUser);
85
+ envContent = setVar(envContent, "QA_PASSWORD", qaPass);
86
+ envContent = setVar(envContent, "QA_X_ACCOUNT", "root");
87
+ envContent = setVar(envContent, "ADMIN_USERNAME", qaUser);
88
+ envContent = setVar(envContent, "ADMIN_PASSWORD", qaPass);
89
+ envContent = setVar(envContent, "ADMIN_X_ACCOUNT", "root");
90
+ envContent = setVar(envContent, "OWNER_EMAIL", qaUser);
91
+ envContent = setVar(envContent, "OWNER_NAME", "Foundation Operator");
92
+ if (bearerToken) {
93
+ envContent = setVar(envContent, "BEARER_TOKEN", bearerToken);
94
+ envContent = setVar(envContent, "TOKEN_AUTH0", bearerToken);
95
+ }
96
+
97
+ await fsp.writeFile(envPath, envContent);
98
+ console.log(chalk.green(` ✓ Configured QA .env → ${apiUrl}`));
99
+
100
+ // Ensure venv + deps
101
+ try {
102
+ await fsp.access(path.join(qaDir, "venv"));
103
+ } catch {
104
+ console.log(chalk.cyan(" Setting up QA automation environment…"));
105
+ await execaFn("python3", ["-m", "venv", "venv"], { cwd: qaDir, stdio: "inherit" });
106
+ await execaFn("bash", ["-c", "source venv/bin/activate && pip install -r requirements.txt && playwright install"], { cwd: qaDir, stdio: "inherit" });
107
+ }
108
+
109
+ // Knock to ensure VM is reachable
110
+ await knockForVm(state);
111
+
112
+ const authMode = useTokenMode ? "bearer token (--use-token)" : `user/pass (${qaUser})`;
113
+ console.log(chalk.cyan(`\n Running QA tests against ${state.vmName} (${vmUrl}) [${authMode}]…\n`));
114
+ const pytestArgsList = [
115
+ "tests/",
116
+ "--env", "staging",
117
+ "--role", "FOUNDATION_ADMIN",
118
+ "--numprocesses=0",
119
+ "-v",
120
+ "--durations=0",
121
+ "--html=./playwright-report/report.html",
122
+ "--self-contained-html",
123
+ "--ignore=tests/e2e/landscape",
124
+ "--ignore=tests/e2e/procedures",
125
+ "--ignore=tests/e2e/upload_file_s3",
126
+ "--ignore=tests/roles",
127
+ "--ignore=tests/unit/data_product/test_data_product_consumers_v2.py",
128
+ "--ignore=tests/unit/data_product/test_data_product_query.py",
129
+ "--ignore=tests/unit/data_product/test_data_product_quality_v2.py",
130
+ "--ignore=tests/unit/data_product/test_data_product_schema_v2.py",
131
+ ];
132
+ if (useTokenMode) pytestArgsList.push("--use-token");
133
+ const pytestArgs = pytestArgsList.join(" ");
134
+
135
+ const testEnv = {
136
+ ...process.env,
137
+ API_URL: apiUrl,
138
+ DEV_API_URL: apiUrl,
139
+ LIVE_API_URL: apiUrl,
140
+ ROLE_NAME: "FOUNDATION_ADMIN",
141
+ QA_USERNAME: qaUser,
142
+ QA_PASSWORD: qaPass,
143
+ CDO_USERNAME: qaUser,
144
+ CDO_PASSWORD: qaPass,
145
+ ADMIN_USERNAME: qaUser,
146
+ ADMIN_PASSWORD: qaPass,
147
+ QA_X_ACCOUNT: "root",
148
+ ADMIN_X_ACCOUNT: "root",
149
+ CDO_X_ACCOUNT: "root",
150
+ OWNER_EMAIL: qaUser,
151
+ OWNER_NAME: "Foundation Operator",
152
+ };
153
+ if (bearerToken) {
154
+ testEnv.BEARER_TOKEN = bearerToken;
155
+ testEnv.TOKEN_AUTH0 = bearerToken;
156
+ }
157
+
158
+ const startMs = Date.now();
159
+ const proc = execaFn(
160
+ "bash",
161
+ ["-c", `source venv/bin/activate && pytest ${pytestArgs}`],
162
+ { cwd: qaDir, timeout: 600_000, reject: false, env: testEnv },
163
+ );
164
+ let captured = "";
165
+ proc.stdout?.on("data", (d) => { const s = d.toString(); captured += s; process.stdout.write(s); });
166
+ proc.stderr?.on("data", (d) => { const s = d.toString(); captured += s; process.stderr.write(s); });
167
+ const { exitCode } = await proc;
168
+ const wallSec = Math.round((Date.now() - startMs) / 1000);
169
+
170
+ const counts = parsePytestSummary(captured);
171
+ const timing = parsePytestDurations(captured);
172
+ const { writeVmState } = await import("../azure-state.js");
173
+ const qaResult = {
174
+ passed: exitCode === 0,
175
+ exitCode,
176
+ at: new Date().toISOString(),
177
+ ...(counts.passed != null && { numPassed: counts.passed }),
178
+ ...(counts.failed != null && { numFailed: counts.failed }),
179
+ ...(counts.error != null && { numErrors: counts.error }),
180
+ ...(counts.errors != null && { numErrors: counts.errors }),
181
+ ...(counts.skipped != null && { numSkipped: counts.skipped }),
182
+ durationSec: counts.durationSec || wallSec,
183
+ ...(timing && { timing }),
184
+ };
185
+ writeVmState(state.vmName, { qa: qaResult });
186
+
187
+ if (exitCode === 0) {
188
+ console.log(chalk.green("\n ✓ QA tests passed\n"));
189
+ } else {
190
+ console.error(chalk.red(`\n QA tests failed (exit ${exitCode}).`));
191
+ console.error(chalk.dim(` Report: ${path.join(qaDir, "playwright-report", "report.html")}\n`));
192
+ process.exitCode = 1;
193
+ }
194
+
195
+ try {
196
+ const { resultsPush } = await import("../azure-results.js");
197
+ const enriched = { ...qaResult };
198
+ try {
199
+ const gitExeca = (await import("execa")).execa;
200
+ const { stdout: branch } = await gitExeca("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: qaDir, reject: false, timeout: 5000 });
201
+ const { stdout: sha } = await gitExeca("git", ["rev-parse", "--short", "HEAD"], { cwd: qaDir, reject: false, timeout: 5000 });
202
+ if (branch?.trim()) enriched.branch = branch.trim();
203
+ if (sha?.trim()) enriched.sha = sha.trim();
204
+ } catch {}
205
+ await resultsPush(state.vmName, enriched, { quiet: false });
206
+ } catch (pushErr) {
207
+ console.log(chalk.dim(` (Result push skipped: ${pushErr.message})`));
208
+ }
209
+ });
210
+
211
+ test
212
+ .command("setup")
213
+ .description("Configure the Azure Blob Storage account for test results")
214
+ .option("--account <name>", "Storage account name")
215
+ .action(async (opts) => {
216
+ const { resultsSetup } = await import("../azure-results.js");
217
+ await resultsSetup({ account: opts.account });
218
+ });
219
+
220
+ test
221
+ .command("list [target]")
222
+ .description("List stored test results across VMs (optionally filter by name)")
223
+ .option("--last <n>", "Number of results to show", "20")
224
+ .action(async (target, opts) => {
225
+ const { resultsList } = await import("../azure-results.js");
226
+ await resultsList({ target, last: parseInt(opts.last) });
227
+ });
228
+
229
+ test
230
+ .command("show <target>")
231
+ .description("Show details of the latest test result for a VM/cluster")
232
+ .option("--run <id>", "Show a specific run by timestamp")
233
+ .action(async (target, opts) => {
234
+ const { resultsShow } = await import("../azure-results.js");
235
+ await resultsShow({ target, run: opts.run });
236
+ });
237
+
238
+ test
239
+ .command("compare [target]")
240
+ .description("Compare test runs across time — detect regressions and improvements")
241
+ .option("--last <n>", "Number of runs to compare", "10")
242
+ .action(async (target, opts) => {
243
+ const { resultsCompare } = await import("../azure-results.js");
244
+ await resultsCompare({ target, last: parseInt(opts.last) });
245
+ });
246
+
247
+ test
248
+ .command("push [target]")
249
+ .description("Push local QA results to blob storage (default: all VMs with results)")
250
+ .action(async (target) => {
251
+ const { readVmState, listVms } = await import("../azure-state.js");
252
+ const { resultsPush } = await import("../azure-results.js");
253
+
254
+ const targets = [];
255
+ if (target) {
256
+ targets.push(target);
257
+ } else {
258
+ const { activeVm, vms } = listVms();
259
+ const names = Object.keys(vms);
260
+ if (names.length === 0) {
261
+ console.log(chalk.dim("\n No VMs tracked.\n"));
262
+ return;
263
+ }
264
+ for (const name of names) {
265
+ const st = readVmState(name);
266
+ if (st?.qa) targets.push(name);
267
+ }
268
+ if (targets.length === 0) {
269
+ console.log(chalk.dim("\n No local QA results found. Run tests first: fops azure test <vm>\n"));
270
+ return;
271
+ }
272
+ }
273
+
274
+ let pushed = 0;
275
+ for (const t of targets) {
276
+ const state = readVmState(t);
277
+ if (!state?.qa) {
278
+ console.log(chalk.dim(` ${t}: no local QA result — skipped`));
279
+ continue;
280
+ }
281
+ await resultsPush(t, state.qa);
282
+ pushed++;
283
+ }
284
+ if (pushed === 0) {
285
+ console.log(chalk.dim("\n No results to push.\n"));
286
+ }
287
+ });
288
+
289
+ // ── Scan ───────────────────────────────────────────────────────────────
290
+
291
+ azure
292
+ .command("scan [name]")
293
+ .description("Scan stack container images for CVEs using Trivy (same as fops scan)")
294
+ .option("--severity <levels>", "Comma-separated severity filter (default: CRITICAL,HIGH)", "CRITICAL,HIGH")
295
+ .option("--json", "Output raw JSON instead of table")
296
+ .option("--fix", "Show only vulnerabilities with a fix available")
297
+ .option("--service <name>", "Scan a specific compose service's image")
298
+ .action(async (name, opts) => {
299
+ const { resolveCliSrc } = await import("../azure-helpers.js");
300
+ const { rootDir } = await import(resolveCliSrc("project.js"));
301
+ const root = rootDir();
302
+ if (!root) {
303
+ console.error(chalk.red("\n Foundation project root not found. Run from the compose repo or set FOUNDATION_ROOT.\n"));
304
+ process.exit(1);
305
+ }
306
+ const { runScan } = await import(resolveCliSrc("commands/lifecycle.js"));
307
+ await runScan(root, {
308
+ severity: opts.severity,
309
+ json: opts.json,
310
+ fix: opts.fix,
311
+ service: opts.service,
312
+ });
313
+ });
314
+ }