@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
@@ -171,6 +171,11 @@ export function reconcileOk(label, detail) {
171
171
  console.log(chalk.green(line));
172
172
  }
173
173
 
174
+ /** Section divider for grouping reconcile steps */
175
+ export function reconcileSection(title) {
176
+ console.log(chalk.dim(`\n ─ ${title} ─`));
177
+ }
178
+
174
179
  export function banner(title) {
175
180
  const line = "─".repeat(Math.max(0, 46 - title.length));
176
181
  console.log(ACCENT(`\n ── ${title} ${line}`));
@@ -0,0 +1,413 @@
1
+ /**
2
+ * Azure Key Vault — Key operations (with auto-rotation)
3
+ * Extracted from azure-keyvault.js for maintainability
4
+ */
5
+ import chalk from "chalk";
6
+ import {
7
+ DIM, OK, WARN, ACCENT,
8
+ banner, kvLine, hint,
9
+ lazyExeca, subArgs, ensureAzCli, ensureAzAuth,
10
+ resolveCliSrc,
11
+ } from "./azure-helpers.js";
12
+
13
+ // ── Shared keyvault helpers (also used by azure-keyvault.js) ─────────────────
14
+
15
+ export function shortDate(val) {
16
+ if (!val) return "—";
17
+ const d = typeof val === "number" ? new Date(val * 1000) : new Date(val);
18
+ if (isNaN(d.getTime())) return String(val);
19
+ return d.toLocaleString();
20
+ }
21
+
22
+ export async function resolveVault(execa, explicit, sub) {
23
+ if (explicit) return explicit;
24
+
25
+ hint("No --vault given, listing vaults…");
26
+ const { stdout } = await execa("az", [
27
+ "keyvault", "list", "--query", "[].{name:name, location:location}",
28
+ "--output", "json", ...subArgs(sub),
29
+ ], { timeout: 30000 });
30
+ const vaults = JSON.parse(stdout);
31
+
32
+ if (vaults.length === 0) {
33
+ console.error(chalk.red("\n No Key Vaults found in this subscription.\n"));
34
+ hint("Create one: fops azure keyvault create <name> --resource-group <rg> --location <region>\n");
35
+ process.exit(1);
36
+ }
37
+ if (vaults.length === 1) {
38
+ hint(`Using vault: ${vaults[0].name}`);
39
+ return vaults[0].name;
40
+ }
41
+
42
+ const { selectOption } = await import(resolveCliSrc("ui/confirm.js"));
43
+ const selected = await selectOption(
44
+ "Select Key Vault",
45
+ vaults.map((v) => ({ label: `${v.name} ${DIM(v.location)}`, value: v.name })),
46
+ );
47
+ if (!selected) { console.log(DIM("\n Cancelled.\n")); process.exit(0); }
48
+ return selected;
49
+ }
50
+
51
+ // ── Key operation helpers ────────────────────────────────────────────────────
52
+
53
+ function humanDuration(iso) {
54
+ if (!iso) return "—";
55
+ const m = iso.match(/^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?$/i);
56
+ if (!m) return iso;
57
+ const parts = [];
58
+ if (m[1]) parts.push(`${m[1]} year${m[1] === "1" ? "" : "s"}`);
59
+ if (m[2]) parts.push(`${m[2]} month${m[2] === "1" ? "" : "s"}`);
60
+ if (m[3]) parts.push(`${m[3]} day${m[3] === "1" ? "" : "s"}`);
61
+ return parts.join(", ") || iso;
62
+ }
63
+
64
+ function computeExpiryDate(isoDuration) {
65
+ const m = isoDuration.match(/^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?$/i);
66
+ if (!m) return new Date(Date.now() + 365 * 86400000).toISOString().replace(/\.\d{3}Z$/, "Z");
67
+ const d = new Date();
68
+ if (m[1]) d.setFullYear(d.getFullYear() + Number(m[1]));
69
+ if (m[2]) d.setMonth(d.getMonth() + Number(m[2]));
70
+ if (m[3]) d.setDate(d.getDate() + Number(m[3]));
71
+ return d.toISOString().replace(/\.\d{3}Z$/, "Z");
72
+ }
73
+
74
+ async function setRotationPolicyInner(execa, vault, name, sub, opts) {
75
+ const lifetimeActions = [
76
+ {
77
+ trigger: { timeAfterCreate: opts.rotateEvery },
78
+ action: { type: "Rotate" },
79
+ },
80
+ ];
81
+
82
+ if (opts.notifyBefore || opts.expiresIn) {
83
+ lifetimeActions.push({
84
+ trigger: { timeBeforeExpiry: opts.notifyBefore || "P30D" },
85
+ action: { type: "Notify" },
86
+ });
87
+ }
88
+
89
+ const policy = { lifetimeActions };
90
+ if (opts.expiresIn) {
91
+ policy.attributes = { expiryTime: opts.expiresIn };
92
+ }
93
+
94
+ const policyJson = JSON.stringify(policy);
95
+
96
+ const { exitCode, stderr } = await execa("az", [
97
+ "keyvault", "key", "rotation-policy", "update", "--vault-name", vault,
98
+ "--name", name, "--value", policyJson, "--output", "none", ...subArgs(sub),
99
+ ], { timeout: 30000, reject: false });
100
+
101
+ if (exitCode !== 0) {
102
+ console.log(WARN(` ⚠ Rotation policy failed: ${(stderr || "").split("\n")[0]}`));
103
+ hint("Ensure the vault uses premium SKU for HSM keys, and key type supports rotation.");
104
+ return;
105
+ }
106
+
107
+ console.log(OK(` ✓ Rotation policy set — auto-rotate every ${humanDuration(opts.rotateEvery)}`));
108
+ if (opts.expiresIn) console.log(OK(` ✓ Key expiry: ${humanDuration(opts.expiresIn)}`));
109
+ }
110
+
111
+ // ═══════════════════════════════════════════════════════════════════════════
112
+ // Key operations
113
+ // ═══════════════════════════════════════════════════════════════════════════
114
+
115
+ export async function keyList(opts = {}) {
116
+ const execa = await lazyExeca();
117
+ const sub = opts.profile;
118
+ await ensureAzCli(execa);
119
+ await ensureAzAuth(execa, { subscription: sub });
120
+
121
+ const vault = await resolveVault(execa, opts.vault, sub);
122
+
123
+ banner(`Keys in "${vault}"`);
124
+
125
+ const { stdout, exitCode, stderr } = await execa("az", [
126
+ "keyvault", "key", "list", "--vault-name", vault,
127
+ "--output", "json", ...subArgs(sub),
128
+ ], { timeout: 30000, reject: false });
129
+
130
+ if (exitCode !== 0) {
131
+ const msg = (stderr || "").includes("Forbidden")
132
+ ? `Access denied — you need "Key Vault Crypto Officer" or "Key Vault Reader" role on "${vault}"`
133
+ : (stderr || "").split("\n")[0];
134
+ console.error(chalk.red(`\n ✗ ${msg}\n`));
135
+ hint(`Grant access: az role assignment create --assignee <your-object-id> --role "Key Vault Crypto Officer" --scope $(az keyvault show --name ${vault} --query id -o tsv)`);
136
+ console.log("");
137
+ process.exit(1);
138
+ }
139
+
140
+ const keys = JSON.parse(stdout);
141
+ if (!keys.length) {
142
+ hint("No keys found.\n");
143
+ return;
144
+ }
145
+
146
+ const nameWidth = Math.min(50, Math.max(20, ...keys.map((k) => {
147
+ const name = k.kid?.split("/").pop() || k.name || "—";
148
+ return name.length;
149
+ })));
150
+
151
+ for (const k of keys) {
152
+ const name = k.kid?.split("/").pop() || k.name || "—";
153
+ const enabled = k.attributes?.enabled !== false;
154
+ const updated = shortDate(k.attributes?.updated || k.attributes?.created);
155
+ const managed = k.managed ? DIM(" [managed]") : "";
156
+ const status = enabled ? "" : WARN(" [disabled]");
157
+
158
+ console.log(` ${chalk.bold.white(name.padEnd(nameWidth))} ${DIM(updated)}${status}${managed}`);
159
+ }
160
+ console.log(DIM(`\n ${keys.length} key(s)`));
161
+ hint(`Details: fops azure keyvault key show <name> --vault ${vault}\n`);
162
+ }
163
+
164
+ export async function keyShow(opts = {}) {
165
+ const execa = await lazyExeca();
166
+ const sub = opts.profile;
167
+ await ensureAzCli(execa);
168
+ await ensureAzAuth(execa, { subscription: sub });
169
+
170
+ const vault = await resolveVault(execa, opts.vault, sub);
171
+ const { name } = opts;
172
+ if (!name) {
173
+ console.error(chalk.red("\n Key name is required.\n"));
174
+ process.exit(1);
175
+ }
176
+
177
+ const [keyResult, policyResult] = await Promise.allSettled([
178
+ execa("az", [
179
+ "keyvault", "key", "show", "--vault-name", vault,
180
+ "--name", name, "--output", "json", ...subArgs(sub),
181
+ ], { timeout: 30000, reject: false }),
182
+ execa("az", [
183
+ "keyvault", "key", "rotation-policy", "show", "--vault-name", vault,
184
+ "--name", name, "--output", "json", ...subArgs(sub),
185
+ ], { timeout: 15000, reject: false }),
186
+ ]);
187
+
188
+ if (keyResult.status === "rejected" || keyResult.value.exitCode !== 0) {
189
+ const stderr = keyResult.value?.stderr || keyResult.reason?.message || "";
190
+ const msg = stderr.includes("KeyNotFound")
191
+ ? `Key "${name}" not found in vault "${vault}"`
192
+ : stderr.split("\n")[0];
193
+ console.error(chalk.red(`\n ✗ ${msg}\n`));
194
+ process.exit(1);
195
+ }
196
+
197
+ const k = JSON.parse(keyResult.value.stdout);
198
+ const keyProps = k.key || {};
199
+
200
+ banner(`Key — "${name}"`);
201
+ kvLine("Vault", DIM(vault));
202
+ kvLine("Name", chalk.bold.white(name));
203
+ kvLine("Type", DIM(keyProps.kty || "—"));
204
+ if (keyProps.kty?.startsWith("RSA")) kvLine("Size", DIM(String(keyProps.n ? Math.ceil(keyProps.n.length * 6 / 8) * 8 : "—")));
205
+ if (keyProps.kty?.startsWith("EC")) kvLine("Curve", DIM(keyProps.crv || "—"));
206
+ kvLine("Operations", DIM((keyProps.keyOps || []).join(", ") || "—"));
207
+ kvLine("Enabled", k.attributes?.enabled !== false ? OK("true") : WARN("false"));
208
+ kvLine("Created", DIM(shortDate(k.attributes?.created)));
209
+ kvLine("Updated", DIM(shortDate(k.attributes?.updated)));
210
+ if (k.attributes?.expires) kvLine("Expires", DIM(shortDate(k.attributes.expires)));
211
+ kvLine("Version", DIM((k.key?.kid || k.kid || "").split("/").pop() || "—"));
212
+
213
+ // Rotation policy
214
+ console.log("");
215
+ if (policyResult.status === "fulfilled" && policyResult.value.exitCode === 0) {
216
+ const policy = JSON.parse(policyResult.value.stdout);
217
+ const actions = policy.lifetimeActions || [];
218
+ const expiry = policy.expiresIn || policy.attributes?.expiryTime;
219
+
220
+ console.log(ACCENT(" Rotation Policy"));
221
+ kvLine(" Expiry", DIM(expiry ? humanDuration(expiry) : "none"));
222
+
223
+ if (actions.length === 0) {
224
+ kvLine(" Actions", DIM("none"));
225
+ }
226
+ for (const a of actions) {
227
+ const actionType = a.action?.type || a.action || "?";
228
+ const trigger = (a.timeAfterCreate || a.trigger?.timeAfterCreate)
229
+ ? `${humanDuration(a.timeAfterCreate || a.trigger?.timeAfterCreate)} after create`
230
+ : (a.timeBeforeExpiry || a.trigger?.timeBeforeExpiry)
231
+ ? `${humanDuration(a.timeBeforeExpiry || a.trigger?.timeBeforeExpiry)} before expiry`
232
+ : "—";
233
+ kvLine(` ${actionType}`, DIM(trigger));
234
+ }
235
+ } else {
236
+ console.log(DIM(" Rotation Policy none"));
237
+ hint(`Enable: fops azure keyvault key rotation-policy set ${name} --vault ${vault} --rotate-every P90D`);
238
+ }
239
+ console.log("");
240
+ }
241
+
242
+ export async function keyCreate(opts = {}) {
243
+ const execa = await lazyExeca();
244
+ const sub = opts.profile;
245
+ await ensureAzCli(execa);
246
+ await ensureAzAuth(execa, { subscription: sub });
247
+
248
+ const vault = await resolveVault(execa, opts.vault, sub);
249
+ const { name } = opts;
250
+ if (!name) {
251
+ console.error(chalk.red("\n Key name is required.\n"));
252
+ process.exit(1);
253
+ }
254
+
255
+ const kty = opts.type || "RSA";
256
+ const size = opts.size || (kty.startsWith("RSA") ? "2048" : undefined);
257
+ const curve = opts.curve || (kty.startsWith("EC") ? "P-256" : undefined);
258
+ const ops = opts.ops || "encrypt decrypt sign verify wrapKey unwrapKey";
259
+
260
+ banner("Creating Key");
261
+ kvLine("Name", chalk.bold.white(name));
262
+ kvLine("Vault", DIM(vault));
263
+ kvLine("Type", DIM(kty));
264
+ if (size) kvLine("Size", DIM(size));
265
+ if (curve) kvLine("Curve", DIM(curve));
266
+ kvLine("Operations", DIM(ops));
267
+ if (opts.rotateEvery) kvLine("Auto-rotate", DIM(humanDuration(opts.rotateEvery)));
268
+ if (opts.expiresIn) kvLine("Expiry", DIM(humanDuration(opts.expiresIn)));
269
+ console.log("");
270
+
271
+ // 1. Create the key
272
+ const args = [
273
+ "keyvault", "key", "create", "--vault-name", vault,
274
+ "--name", name, "--kty", kty, "--output", "json", ...subArgs(sub),
275
+ ];
276
+ if (size && kty.startsWith("RSA")) args.push("--size", size);
277
+ if (curve && kty.startsWith("EC")) args.push("--curve", curve);
278
+ if (opts.ops) args.push("--ops", ...ops.split(/[\s,]+/));
279
+ if (opts.disabled) args.push("--disabled", "true");
280
+ if (opts.expiresIn) args.push("--expires", computeExpiryDate(opts.expiresIn));
281
+
282
+ const { stdout, exitCode, stderr } = await execa("az", args, { timeout: 30000, reject: false });
283
+ if (exitCode !== 0) {
284
+ console.error(chalk.red(`\n ✗ ${(stderr || "").split("\n")[0]}\n`));
285
+ process.exit(1);
286
+ }
287
+
288
+ const k = JSON.parse(stdout);
289
+ console.log(OK(` ✓ Key "${name}" created`));
290
+ kvLine("Version", DIM(k.key?.kid?.split("/").pop() || "—"));
291
+
292
+ // 2. Set rotation policy if requested
293
+ if (opts.rotateEvery) {
294
+ await setRotationPolicyInner(execa, vault, name, sub, {
295
+ rotateEvery: opts.rotateEvery,
296
+ expiresIn: opts.expiresIn,
297
+ notifyBefore: opts.notifyBefore,
298
+ });
299
+ }
300
+ console.log("");
301
+ hint(`Show: fops azure keyvault key show ${name} --vault ${vault}`);
302
+ hint(`Rotate: fops azure keyvault key rotate ${name} --vault ${vault}\n`);
303
+ }
304
+
305
+ export async function keyRotate(opts = {}) {
306
+ const execa = await lazyExeca();
307
+ const sub = opts.profile;
308
+ await ensureAzCli(execa);
309
+ await ensureAzAuth(execa, { subscription: sub });
310
+
311
+ const vault = await resolveVault(execa, opts.vault, sub);
312
+ const { name } = opts;
313
+ if (!name) {
314
+ console.error(chalk.red("\n Key name is required.\n"));
315
+ process.exit(1);
316
+ }
317
+
318
+ hint(`Rotating key "${name}"…`);
319
+ const { stdout, exitCode, stderr } = await execa("az", [
320
+ "keyvault", "key", "rotate", "--vault-name", vault,
321
+ "--name", name, "--output", "json", ...subArgs(sub),
322
+ ], { timeout: 30000, reject: false });
323
+
324
+ if (exitCode !== 0) {
325
+ console.error(chalk.red(`\n ✗ ${(stderr || "").split("\n")[0]}\n`));
326
+ process.exit(1);
327
+ }
328
+
329
+ const k = JSON.parse(stdout);
330
+ console.log(OK(` ✓ Key "${name}" rotated — new version: ${k.key?.kid?.split("/").pop() || "?"}\n`));
331
+ }
332
+
333
+ export async function keyRotationPolicyShow(opts = {}) {
334
+ const execa = await lazyExeca();
335
+ const sub = opts.profile;
336
+ await ensureAzCli(execa);
337
+ await ensureAzAuth(execa, { subscription: sub });
338
+
339
+ const vault = await resolveVault(execa, opts.vault, sub);
340
+ const { name } = opts;
341
+ if (!name) {
342
+ console.error(chalk.red("\n Key name is required.\n"));
343
+ process.exit(1);
344
+ }
345
+
346
+ const { stdout, exitCode, stderr } = await execa("az", [
347
+ "keyvault", "key", "rotation-policy", "show", "--vault-name", vault,
348
+ "--name", name, "--output", "json", ...subArgs(sub),
349
+ ], { timeout: 15000, reject: false });
350
+
351
+ if (exitCode !== 0) {
352
+ console.error(chalk.red(`\n ✗ ${(stderr || "").split("\n")[0]}\n`));
353
+ process.exit(1);
354
+ }
355
+
356
+ const policy = JSON.parse(stdout);
357
+ const actions = policy.lifetimeActions || [];
358
+ const expiry = policy.expiresIn || policy.attributes?.expiryTime;
359
+
360
+ banner(`Rotation Policy — "${name}"`);
361
+ kvLine("Vault", DIM(vault));
362
+ kvLine("Key", chalk.bold.white(name));
363
+ kvLine("Expiry", DIM(expiry ? humanDuration(expiry) : "none"));
364
+ console.log("");
365
+
366
+ if (actions.length === 0) {
367
+ hint("No rotation actions configured.");
368
+ } else {
369
+ for (const a of actions) {
370
+ const actionType = a.action?.type || a.action || "?";
371
+ const trigger = (a.timeAfterCreate || a.trigger?.timeAfterCreate)
372
+ ? `${humanDuration(a.timeAfterCreate || a.trigger?.timeAfterCreate)} after create`
373
+ : (a.timeBeforeExpiry || a.trigger?.timeBeforeExpiry)
374
+ ? `${humanDuration(a.timeBeforeExpiry || a.trigger?.timeBeforeExpiry)} before expiry`
375
+ : "—";
376
+ kvLine(` ${actionType}`, DIM(trigger));
377
+ }
378
+ }
379
+ console.log("");
380
+ hint(`Update: fops azure keyvault key rotation-policy set ${name} --vault ${vault} --rotate-every P90D`);
381
+ hint(`Rotate now: fops azure keyvault key rotate ${name} --vault ${vault}\n`);
382
+ }
383
+
384
+ export async function keyRotationPolicySet(opts = {}) {
385
+ const execa = await lazyExeca();
386
+ const sub = opts.profile;
387
+ await ensureAzCli(execa);
388
+ await ensureAzAuth(execa, { subscription: sub });
389
+
390
+ const vault = await resolveVault(execa, opts.vault, sub);
391
+ const { name } = opts;
392
+ if (!name) {
393
+ console.error(chalk.red("\n Key name is required.\n"));
394
+ process.exit(1);
395
+ }
396
+
397
+ if (!opts.rotateEvery) {
398
+ console.error(chalk.red("\n --rotate-every <duration> is required (e.g. P90D, P30D, P1Y).\n"));
399
+ hint("ISO 8601 durations: P30D = 30 days, P90D = 90 days, P6M = 6 months, P1Y = 1 year\n");
400
+ process.exit(1);
401
+ }
402
+
403
+ banner(`Setting Rotation Policy — "${name}"`);
404
+ kvLine("Vault", DIM(vault));
405
+ kvLine("Key", chalk.bold.white(name));
406
+ kvLine("Rotate every", DIM(humanDuration(opts.rotateEvery)));
407
+ if (opts.expiresIn) kvLine("Key expiry", DIM(humanDuration(opts.expiresIn)));
408
+ if (opts.notifyBefore) kvLine("Notify before", DIM(humanDuration(opts.notifyBefore)));
409
+ console.log("");
410
+
411
+ await setRotationPolicyInner(execa, vault, name, sub, opts);
412
+ console.log("");
413
+ }