@meshxdata/fops 0.1.49 → 0.1.51

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 (30) hide show
  1. package/CHANGELOG.md +368 -0
  2. package/package.json +1 -1
  3. package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-core.js +347 -6
  4. package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-data-bootstrap.js +421 -0
  5. package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-flux.js +5 -179
  6. package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-naming.js +14 -4
  7. package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-postgres.js +171 -4
  8. package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-storage.js +303 -8
  9. package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks.js +2 -0
  10. package/src/plugins/bundled/fops-plugin-azure/lib/azure-auth.js +1 -1
  11. package/src/plugins/bundled/fops-plugin-azure/lib/azure-fleet-swarm.js +936 -0
  12. package/src/plugins/bundled/fops-plugin-azure/lib/azure-fleet.js +10 -918
  13. package/src/plugins/bundled/fops-plugin-azure/lib/azure-helpers.js +5 -0
  14. package/src/plugins/bundled/fops-plugin-azure/lib/azure-keyvault-keys.js +413 -0
  15. package/src/plugins/bundled/fops-plugin-azure/lib/azure-keyvault.js +14 -399
  16. package/src/plugins/bundled/fops-plugin-azure/lib/azure-ops-config.js +754 -0
  17. package/src/plugins/bundled/fops-plugin-azure/lib/azure-ops-knock.js +527 -0
  18. package/src/plugins/bundled/fops-plugin-azure/lib/azure-ops-ssh.js +427 -0
  19. package/src/plugins/bundled/fops-plugin-azure/lib/azure-ops.js +99 -1686
  20. package/src/plugins/bundled/fops-plugin-azure/lib/azure-provision-health.js +279 -0
  21. package/src/plugins/bundled/fops-plugin-azure/lib/azure-provision-init.js +186 -0
  22. package/src/plugins/bundled/fops-plugin-azure/lib/azure-provision.js +66 -444
  23. package/src/plugins/bundled/fops-plugin-azure/lib/azure-results.js +11 -0
  24. package/src/plugins/bundled/fops-plugin-azure/lib/azure-vm-lifecycle.js +5 -540
  25. package/src/plugins/bundled/fops-plugin-azure/lib/azure-vm-terraform.js +544 -0
  26. package/src/plugins/bundled/fops-plugin-azure/lib/commands/infra-cmds.js +75 -3
  27. package/src/plugins/bundled/fops-plugin-azure/lib/commands/test-cmds.js +227 -11
  28. package/src/plugins/bundled/fops-plugin-azure/lib/commands/vm-cmds.js +2 -1
  29. package/src/plugins/bundled/fops-plugin-azure/lib/pytest-parse.js +21 -0
  30. package/src/plugins/bundled/fops-plugin-foundation/index.js +371 -44
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import chalk from "chalk";
5
5
  import { resolveRemoteAuth, suppressTlsWarning } from "../azure-auth.js";
6
- import { parsePytestSummary, parsePytestDurations } from "../pytest-parse.js";
6
+ import { parsePytestSummary, parsePytestDurations, parsePytestFailedTests } from "../pytest-parse.js";
7
7
 
8
8
  export function registerTestCommands(azure) {
9
9
  const test = azure
@@ -89,6 +89,39 @@ export function registerTestCommands(azure) {
89
89
  } catch { /* optional — tests still run without it */ }
90
90
  }
91
91
 
92
+ // Fetch QA_TEST_USER_PASSWORD for role-based test accounts
93
+ // Priority: local .env file → process.env → ~/.fops.json → remote VM .env
94
+ let qaTestUserPassword = "";
95
+ try {
96
+ const localEnv = await fsp.readFile(localEnvPath, "utf8");
97
+ const pwMatch = localEnv.match(/^QA_TEST_USER_PASSWORD=(.+)$/m);
98
+ if (pwMatch) qaTestUserPassword = pwMatch[1].trim().replace(/^["']|["']$/g, "");
99
+ } catch { /* no local .env */ }
100
+ if (!qaTestUserPassword) {
101
+ qaTestUserPassword = process.env.QA_TEST_USER_PASSWORD || "";
102
+ }
103
+ if (!qaTestUserPassword) {
104
+ try {
105
+ const os = await import("node:os");
106
+ const fopsJson = JSON.parse(await fsp.readFile(path.join(os.homedir(), ".fops.json"), "utf8"));
107
+ qaTestUserPassword = fopsJson?.plugins?.entries?.["fops-plugin-foundation"]?.config?.qaTestUserPassword || "";
108
+ } catch { /* no ~/.fops.json */ }
109
+ }
110
+ if (!qaTestUserPassword && ip) {
111
+ try {
112
+ const sshUser = state?.adminUser || "azureuser";
113
+ const { stdout: pwOut } = await sshCmd(execa, ip, sshUser,
114
+ "grep -E '^QA_TEST_USER_PASSWORD=' /opt/foundation-compose/.env",
115
+ 10_000,
116
+ );
117
+ const pwMatch = (pwOut || "").match(/^QA_TEST_USER_PASSWORD=(.+)$/m);
118
+ if (pwMatch) {
119
+ qaTestUserPassword = pwMatch[1].trim().replace(/^["']|["']$/g, "");
120
+ console.log(chalk.green(" ✓ Got QA_TEST_USER_PASSWORD from VM"));
121
+ }
122
+ } catch { /* optional */ }
123
+ }
124
+
92
125
  if (!bearerToken && !qaUser) {
93
126
  console.error(chalk.red("\n No credentials found (local or remote)."));
94
127
  console.error(chalk.dim(" Set BEARER_TOKEN or QA_USERNAME/QA_PASSWORD, or ensure the VM has Auth0 configured in .env\n"));
@@ -124,7 +157,7 @@ export function registerTestCommands(azure) {
124
157
  envContent = setVar(envContent, "ADMIN_PASSWORD", qaPass);
125
158
  envContent = setVar(envContent, "ADMIN_X_ACCOUNT", "root");
126
159
  envContent = setVar(envContent, "OWNER_EMAIL", qaUser);
127
- envContent = setVar(envContent, "OWNER_NAME", "Foundation Operator");
160
+ envContent = setVar(envContent, "OWNER_NAME", '"Foundation Operator"');
128
161
  if (bearerToken) {
129
162
  envContent = setVar(envContent, "BEARER_TOKEN", bearerToken);
130
163
  envContent = setVar(envContent, "TOKEN_AUTH0", bearerToken);
@@ -133,6 +166,15 @@ export function registerTestCommands(azure) {
133
166
  envContent = setVar(envContent, "CF_ACCESS_CLIENT_ID", cfAccessClientId);
134
167
  envContent = setVar(envContent, "CF_ACCESS_CLIENT_SECRET", cfAccessClientSecret);
135
168
  }
169
+ if (qaTestUserPassword) {
170
+ envContent = setVar(envContent, "QA_TEST_USER_PASSWORD", `'${qaTestUserPassword}'`);
171
+ }
172
+
173
+ // Set URN values for policy creation (required by setup_user_role.py)
174
+ envContent = setVar(envContent, "ORG_ROOT_RESOURCE_PREFIX", "urn:meshx::data:root");
175
+ envContent = setVar(envContent, "ORG_A_RESOURCE_PREFIX", "urn:meshx::data:khoa");
176
+ envContent = setVar(envContent, "ORG_ROOT_URN", "urn:meshx::iam:root:organization:root");
177
+ envContent = setVar(envContent, "ORG_A_URN", "urn:meshx::iam:root:organization:khoa");
136
178
 
137
179
  await fsp.writeFile(envPath, envContent);
138
180
  console.log(chalk.green(` ✓ Configured QA .env → ${apiUrl}`));
@@ -151,16 +193,13 @@ export function registerTestCommands(azure) {
151
193
 
152
194
  const authMode = useTokenMode ? "bearer token (--use-token)" : `user/pass (${qaUser})`;
153
195
  console.log(chalk.cyan(`\n Running QA tests against ${state.vmName} (${vmUrl}) [${authMode}]…\n`));
154
- let setupCmd = `./scripts/set_up_role.sh --env ${targetName}`;
155
- let runCmd = `./scripts/run_all_tests.sh --env ${targetName} --role-priority p0`;
156
196
 
197
+ // Run pytest directly - role tests with P0 priority
198
+ let pytestArgs = `pytest tests/roles --env ${targetName} -v -x --tb=short`;
157
199
  if (useTokenMode) {
158
- setupCmd += " --use-token";
159
- runCmd += " --use-token";
200
+ pytestArgs += " --use-token";
160
201
  }
161
202
 
162
- const pytestArgs = `${setupCmd} && ${runCmd}`;
163
-
164
203
  const testEnv = {
165
204
  ...process.env,
166
205
  API_URL: apiUrl,
@@ -192,7 +231,7 @@ export function registerTestCommands(azure) {
192
231
  const proc = execa(
193
232
  "bash",
194
233
  ["-c", `source venv/bin/activate && ${pytestArgs}`],
195
- { cwd: qaDir, timeout: 600_000, reject: false, env: testEnv },
234
+ { cwd: qaDir, timeout: 1_800_000, reject: false, env: testEnv }, // 30 min timeout
196
235
  );
197
236
  let captured = "";
198
237
  proc.stdout?.on("data", (d) => { const s = d.toString(); captured += s; process.stdout.write(s); });
@@ -203,6 +242,7 @@ export function registerTestCommands(azure) {
203
242
 
204
243
  const counts = parsePytestSummary(captured);
205
244
  const timing = parsePytestDurations(captured);
245
+ const failedTests = parsePytestFailedTests(captured);
206
246
  const { writeVmState } = await import("../azure-state.js");
207
247
  const qaResult = {
208
248
  passed: actualExit === 0,
@@ -215,14 +255,35 @@ export function registerTestCommands(azure) {
215
255
  ...(counts.skipped != null && { numSkipped: counts.skipped }),
216
256
  durationSec: counts.durationSec || wallSec,
217
257
  ...(timing && { timing }),
258
+ ...(failedTests.length > 0 && { failedTests: failedTests.slice(0, 50) }),
218
259
  };
219
260
  writeVmState(state.vmName, { qa: qaResult });
220
261
 
221
262
  if (actualExit === 0) {
222
263
  console.log(chalk.green("\n ✓ QA tests passed\n"));
223
264
  } else {
224
- console.error(chalk.red(`\n QA tests failed (exit ${actualExit}).`));
225
- console.error(chalk.dim(` Report: ${path.join(qaDir, "playwright-report")}\n`));
265
+ console.error(chalk.red(`\n QA tests failed (exit ${actualExit})`));
266
+
267
+ // Parse and display failed tests summary
268
+ const failedTests = parsePytestFailedTests(captured);
269
+ if (failedTests.length > 0) {
270
+ console.error(chalk.red(`\n Failed tests (${failedTests.length}):`));
271
+ for (const { test, reason } of failedTests.slice(0, 20)) {
272
+ const shortTest = test.replace(/^tests\//, "");
273
+ const reasonStr = reason ? chalk.dim(` - ${reason.slice(0, 60)}`) : "";
274
+ console.error(chalk.red(` • ${shortTest}${reasonStr}`));
275
+ }
276
+ if (failedTests.length > 20) {
277
+ console.error(chalk.dim(` ... and ${failedTests.length - 20} more`));
278
+ }
279
+ }
280
+
281
+ // Show summary counts
282
+ if (counts.passed != null || counts.failed != null) {
283
+ console.error(chalk.dim(`\n Summary: ${counts.passed || 0} passed, ${counts.failed || 0} failed, ${counts.skipped || 0} skipped`));
284
+ }
285
+
286
+ console.error(chalk.dim(`\n Report: ${path.join(qaDir, "playwright-report")}\n`));
226
287
  process.exitCode = 1;
227
288
  }
228
289
 
@@ -251,6 +312,161 @@ export function registerTestCommands(azure) {
251
312
  await resultsSetup({ account: opts.account });
252
313
  });
253
314
 
315
+ test
316
+ .command("setup-users [name]")
317
+ .description("Create/update QA test users on target environment")
318
+ .option("--vm-name <name>", "Target VM (default: active)")
319
+ .option("--env <name>", "Target environment name (staging, dev, etc.)")
320
+ .action(async (name, opts) => {
321
+ const { resolveCliSrc, lazyExeca, ensureAzCli, ensureAzAuth, resolvePublicIp } = await import("../azure-helpers.js");
322
+ const { requireVmState, knockForVm, sshCmd } = await import("../azure.js");
323
+ const { rootDir } = await import(resolveCliSrc("project.js"));
324
+ const { resolveRemoteAuth, suppressTlsWarning } = await import("../azure-auth.js");
325
+ const fsp = await import("node:fs/promises");
326
+ const path = await import("node:path");
327
+
328
+ const root = rootDir();
329
+ if (!root) {
330
+ console.error(chalk.red("\n Foundation project root not found.\n"));
331
+ process.exit(1);
332
+ }
333
+
334
+ const qaDir = path.join(root, "foundation-qa-automation");
335
+ try {
336
+ await fsp.access(qaDir);
337
+ } catch {
338
+ console.error(chalk.red("\n foundation-qa-automation/ not found."));
339
+ process.exit(1);
340
+ }
341
+
342
+ const execa = await lazyExeca();
343
+ let envName = opts.env;
344
+ let apiUrl;
345
+ let bearerToken;
346
+
347
+ if (!envName) {
348
+ // Resolve from VM
349
+ await ensureAzCli(execa);
350
+ await ensureAzAuth(execa);
351
+ const state = requireVmState(opts.vmName || name);
352
+ const ip = await resolvePublicIp(execa, state.resourceGroup, state.vmName, state.publicIp);
353
+ if (!ip) {
354
+ console.error(chalk.red("\n No IP address. Is the VM running?\n"));
355
+ process.exit(1);
356
+ }
357
+ const vmUrl = state.publicUrl || `https://${ip}`;
358
+ apiUrl = `${vmUrl}/api`;
359
+ envName = opts.vmName || name || state.vmName;
360
+
361
+ const auth = await resolveRemoteAuth({
362
+ apiUrl, ip, vmState: state,
363
+ execaFn: execa, sshCmd, knockForVm, suppressTlsWarning,
364
+ });
365
+ bearerToken = auth.bearerToken;
366
+ } else {
367
+ // Use named environment - authenticate via Auth0 ROPC
368
+ const envUrls = {
369
+ staging: "https://staging.meshx.app/api",
370
+ dev: "https://dev.meshx.app/api",
371
+ demo: "https://demo.meshx.app/api",
372
+ integration: "https://integration.meshx.app/api",
373
+ };
374
+ apiUrl = envUrls[envName] || `https://${envName}.meshx.app/api`;
375
+
376
+ // Try to get bearer token for named environment
377
+ const { resolveFoundationCreds, resolveAuth0Config, isJwt, isJwtExpired } = await import("../azure-auth.js");
378
+ const creds = resolveFoundationCreds();
379
+
380
+ // Check for existing valid bearer token
381
+ if (creds?.bearerToken && isJwt(creds.bearerToken) && !isJwtExpired(creds.bearerToken)) {
382
+ bearerToken = creds.bearerToken;
383
+ console.log(chalk.green(" ✓ Using existing bearer token"));
384
+ } else if (creds?.user && creds?.password) {
385
+ // Authenticate via Auth0 ROPC
386
+ const auth0Cfg = resolveAuth0Config();
387
+ if (auth0Cfg) {
388
+ console.log(chalk.dim(` Authenticating via Auth0 (${auth0Cfg.domain})…`));
389
+ const body = {
390
+ grant_type: "password",
391
+ client_id: auth0Cfg.clientId,
392
+ username: creds.user,
393
+ password: creds.password,
394
+ scope: "openid",
395
+ };
396
+ if (auth0Cfg.clientSecret) body.client_secret = auth0Cfg.clientSecret;
397
+ if (auth0Cfg.audience) body.audience = auth0Cfg.audience;
398
+
399
+ try {
400
+ const resp = await fetch(`https://${auth0Cfg.domain}/oauth/token`, {
401
+ method: "POST",
402
+ headers: { "Content-Type": "application/json" },
403
+ body: JSON.stringify(body),
404
+ signal: AbortSignal.timeout(15_000),
405
+ });
406
+ if (resp.ok) {
407
+ const data = await resp.json();
408
+ if (data.access_token) {
409
+ bearerToken = data.access_token;
410
+ console.log(chalk.green(` ✓ Authenticated as ${creds.user}`));
411
+ }
412
+ } else {
413
+ const errText = await resp.text().catch(() => "");
414
+ console.error(chalk.yellow(` ⚠ Auth0 rejected credentials: HTTP ${resp.status}`));
415
+ if (errText) console.error(chalk.dim(` ${errText.slice(0, 200)}`));
416
+ }
417
+ } catch (e) {
418
+ console.error(chalk.yellow(` ⚠ Auth0 auth failed: ${e.message}`));
419
+ }
420
+ } else {
421
+ console.error(chalk.yellow(" ⚠ No Auth0 config found (check .env or ~/.fops.json)"));
422
+ }
423
+ }
424
+
425
+ if (!bearerToken) {
426
+ console.error(chalk.red("\n No bearer token available for named environment."));
427
+ console.error(chalk.dim(" Set BEARER_TOKEN env var or ensure QA_USERNAME/QA_PASSWORD + Auth0 config are set.\n"));
428
+ process.exit(1);
429
+ }
430
+ }
431
+
432
+ console.log(chalk.cyan(`\n Setting up QA test users on ${envName} (${apiUrl})…\n`));
433
+
434
+ // Ensure venv
435
+ try {
436
+ await fsp.access(path.join(qaDir, "venv"));
437
+ } catch {
438
+ console.log(chalk.cyan(" Setting up QA automation environment…"));
439
+ await execa("python3", ["-m", "venv", "venv"], { cwd: qaDir, stdio: "inherit" });
440
+ await execa("bash", ["-c", "source venv/bin/activate && pip install -r requirements.txt"], { cwd: qaDir, stdio: "inherit" });
441
+ }
442
+
443
+ const setupEnv = {
444
+ ...process.env,
445
+ TARGET_ENV: envName,
446
+ USE_TOKEN: bearerToken ? "1" : "0",
447
+ API_URL: apiUrl,
448
+ };
449
+ if (bearerToken) {
450
+ setupEnv.TOKEN_AUTH0 = bearerToken;
451
+ setupEnv.BEARER_TOKEN = bearerToken;
452
+ }
453
+
454
+ // Use the shell script which handles all setup
455
+ const useTokenFlag = bearerToken ? "--use-token" : "";
456
+ const { exitCode } = await execa(
457
+ "bash",
458
+ ["scripts/set_up_user_role.sh", "--env", envName, useTokenFlag].filter(Boolean),
459
+ { cwd: qaDir, stdio: "inherit", env: setupEnv, reject: false, timeout: 300_000 },
460
+ );
461
+
462
+ if (exitCode === 0) {
463
+ console.log(chalk.green("\n ✓ QA test users created/updated\n"));
464
+ } else {
465
+ console.error(chalk.red(`\n ✗ User setup failed (exit ${exitCode})\n`));
466
+ process.exitCode = 1;
467
+ }
468
+ });
469
+
254
470
  test
255
471
  .command("list [target]")
256
472
  .description("List stored test results across VMs (optionally filter by name)")
@@ -166,9 +166,10 @@ export function registerVmCommands(azure, api, registry) {
166
166
  .option("--cost", "Show estimated cost per resource (queries Azure Cost Management)")
167
167
  .option("--days <days>", "Days to look back for cost (default: 30)", "30")
168
168
  .option("--versions", "Show service image version matrix")
169
+ .option("--json", "Output as JSON (for programmatic use)")
169
170
  .action(async (opts) => {
170
171
  const { azureList } = await import("../azure.js");
171
- await azureList({ live: opts.live, verbose: opts.verbose, cost: opts.cost, days: parseInt(opts.days), versions: opts.versions });
172
+ await azureList({ live: opts.live, verbose: opts.verbose, cost: opts.cost, days: parseInt(opts.days), versions: opts.versions, json: opts.json });
172
173
  });
173
174
 
174
175
  // ── ip ─────────────────────────────────────────────────────────────────
@@ -25,6 +25,27 @@ export function parsePytestSummary(output) {
25
25
  return counts;
26
26
  }
27
27
 
28
+ export function parsePytestFailedTests(output) {
29
+ const failed = [];
30
+ const lines = output.split("\n");
31
+
32
+ for (const line of lines) {
33
+ // Match "FAILED tests/path/test_file.py::test_name - reason"
34
+ const failMatch = line.match(/^FAILED\s+(\S+)(?:\s+-\s+(.*))?$/);
35
+ if (failMatch) {
36
+ failed.push({ test: failMatch[1], reason: failMatch[2] || "" });
37
+ continue;
38
+ }
39
+ // Match verbose format "tests/path::test_name FAILED"
40
+ const vMatch = line.match(/^(\S+::\S+)\s+FAILED/);
41
+ if (vMatch) {
42
+ failed.push({ test: vMatch[1], reason: "" });
43
+ }
44
+ }
45
+
46
+ return failed;
47
+ }
48
+
28
49
  export function parsePytestDurations(output) {
29
50
  const durations = [];
30
51
  const lines = output.split("\n");