@meshxdata/fops 0.1.36 → 0.1.37
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 +22 -0
- package/fops.mjs +37 -14
- package/package.json +1 -1
- package/src/plugins/bundled/fops-plugin-azure/index.js +44 -2896
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-auth.js +454 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-helpers.js +51 -13
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-ops.js +182 -27
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-provision.js +62 -8
- package/src/plugins/bundled/fops-plugin-azure/lib/azure.js +2 -2
- package/src/plugins/bundled/fops-plugin-azure/lib/commands/fleet-cmds.js +254 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/commands/infra-cmds.js +890 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/commands/test-cmds.js +314 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/commands/vm-cmds.js +892 -0
- package/src/plugins/bundled/fops-plugin-file/demo/orders_renamed.aligned.csv +1 -1
- package/src/plugins/bundled/fops-plugin-file/index.js +81 -12
- package/src/plugins/bundled/fops-plugin-file/lib/match.js +133 -15
- package/src/plugins/bundled/fops-plugin-file/lib/report.js +3 -0
- package/src/plugins/bundled/fops-plugin-foundation/lib/client.js +9 -5
- package/src/plugins/bundled/fops-plugin-foundation-graphql/lib/graphql/resolvers/data-product.js +32 -0
- package/src/plugins/bundled/fops-plugin-foundation-graphql/lib/graphql/schema.js +20 -1
|
@@ -0,0 +1,890 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infrastructure commands: storage, openai, keyvault, AKS, embed index.
|
|
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 registerInfraCommands(azure) {
|
|
9
|
+
// ── Storage (blob management) ──────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
const storage = azure
|
|
12
|
+
.command("storage")
|
|
13
|
+
.description("Manage Azure Blob Storage accounts, containers, and blobs");
|
|
14
|
+
|
|
15
|
+
storage
|
|
16
|
+
.command("accounts")
|
|
17
|
+
.description("List storage accounts")
|
|
18
|
+
.option("--profile <subscription>", "Azure subscription name or ID")
|
|
19
|
+
.action(async (opts) => {
|
|
20
|
+
const { storageList } = await import("../azure-storage.js");
|
|
21
|
+
await storageList({ profile: opts.profile });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
storage
|
|
25
|
+
.command("create <name>")
|
|
26
|
+
.description("Create a new storage account")
|
|
27
|
+
.option("--resource-group <name>", "Resource group (lists available if omitted)")
|
|
28
|
+
.requiredOption("--location <region>", "Azure region (e.g. eastus, westeurope, uaenorth)")
|
|
29
|
+
.option("--sku <sku>", "Replication SKU (default: Standard_LRS)", "Standard_LRS")
|
|
30
|
+
.option("--kind <kind>", "Account kind (default: StorageV2)", "StorageV2")
|
|
31
|
+
.option("--infra-encryption", "Enable infrastructure (double) encryption")
|
|
32
|
+
.option("--tags <tags>", "Space-separated tags: key1=val1 key2=val2")
|
|
33
|
+
.option("--profile <subscription>", "Azure subscription name or ID")
|
|
34
|
+
.action(async (name, opts) => {
|
|
35
|
+
const { storageCreate } = await import("../azure-storage.js");
|
|
36
|
+
await storageCreate({
|
|
37
|
+
name, resourceGroup: opts.resourceGroup, location: opts.location,
|
|
38
|
+
sku: opts.sku, kind: opts.kind, infraEncryption: opts.infraEncryption,
|
|
39
|
+
tags: opts.tags, profile: opts.profile,
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const containers = storage
|
|
44
|
+
.command("container")
|
|
45
|
+
.description("Manage blob containers");
|
|
46
|
+
|
|
47
|
+
containers
|
|
48
|
+
.command("list")
|
|
49
|
+
.description("List containers in a storage account")
|
|
50
|
+
.option("--account <name>", "Storage account (auto-detected if only one)")
|
|
51
|
+
.option("--profile <subscription>", "Azure subscription name or ID")
|
|
52
|
+
.action(async (opts) => {
|
|
53
|
+
const { containerList } = await import("../azure-storage.js");
|
|
54
|
+
await containerList({ account: opts.account, profile: opts.profile });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
containers
|
|
58
|
+
.command("create <name>")
|
|
59
|
+
.description("Create a new container")
|
|
60
|
+
.option("--account <name>", "Storage account (auto-detected if only one)")
|
|
61
|
+
.option("--profile <subscription>", "Azure subscription name or ID")
|
|
62
|
+
.action(async (name, opts) => {
|
|
63
|
+
const { containerCreate } = await import("../azure-storage.js");
|
|
64
|
+
await containerCreate({ account: opts.account, name, profile: opts.profile });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
containers
|
|
68
|
+
.command("delete <name>")
|
|
69
|
+
.description("Delete a container (asks for confirmation)")
|
|
70
|
+
.option("--account <name>", "Storage account (auto-detected if only one)")
|
|
71
|
+
.option("--profile <subscription>", "Azure subscription name or ID")
|
|
72
|
+
.action(async (name, opts) => {
|
|
73
|
+
const { containerDelete } = await import("../azure-storage.js");
|
|
74
|
+
await containerDelete({ account: opts.account, name, profile: opts.profile });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const blob = storage
|
|
78
|
+
.command("blob")
|
|
79
|
+
.description("Manage blobs in a container");
|
|
80
|
+
|
|
81
|
+
blob
|
|
82
|
+
.command("list")
|
|
83
|
+
.description("List blobs in a container")
|
|
84
|
+
.option("--account <name>", "Storage account (auto-detected if only one)")
|
|
85
|
+
.requiredOption("--container <name>", "Container name")
|
|
86
|
+
.option("--prefix <prefix>", "Filter by blob name prefix")
|
|
87
|
+
.option("--profile <subscription>", "Azure subscription name or ID")
|
|
88
|
+
.action(async (opts) => {
|
|
89
|
+
const { blobList } = await import("../azure-storage.js");
|
|
90
|
+
await blobList({ account: opts.account, container: opts.container, prefix: opts.prefix, profile: opts.profile });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
blob
|
|
94
|
+
.command("upload <file>")
|
|
95
|
+
.description("Upload a file as a blob")
|
|
96
|
+
.option("--account <name>", "Storage account (auto-detected if only one)")
|
|
97
|
+
.requiredOption("--container <name>", "Container name")
|
|
98
|
+
.option("--name <blob-name>", "Blob name (default: filename)")
|
|
99
|
+
.option("--overwrite", "Overwrite existing blob")
|
|
100
|
+
.option("--profile <subscription>", "Azure subscription name or ID")
|
|
101
|
+
.action(async (file, opts) => {
|
|
102
|
+
const { blobUpload } = await import("../azure-storage.js");
|
|
103
|
+
await blobUpload({ account: opts.account, container: opts.container, file, name: opts.name, overwrite: opts.overwrite, profile: opts.profile });
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
blob
|
|
107
|
+
.command("download <name>")
|
|
108
|
+
.description("Download a blob to a local file")
|
|
109
|
+
.option("--account <name>", "Storage account (auto-detected if only one)")
|
|
110
|
+
.requiredOption("--container <name>", "Container name")
|
|
111
|
+
.option("--dest <path>", "Local destination path (default: blob filename)")
|
|
112
|
+
.option("--profile <subscription>", "Azure subscription name or ID")
|
|
113
|
+
.action(async (name, opts) => {
|
|
114
|
+
const { blobDownload } = await import("../azure-storage.js");
|
|
115
|
+
await blobDownload({ account: opts.account, container: opts.container, name, dest: opts.dest, profile: opts.profile });
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
blob
|
|
119
|
+
.command("delete <name>")
|
|
120
|
+
.description("Delete a blob (asks for confirmation)")
|
|
121
|
+
.option("--account <name>", "Storage account (auto-detected if only one)")
|
|
122
|
+
.requiredOption("--container <name>", "Container name")
|
|
123
|
+
.option("--profile <subscription>", "Azure subscription name or ID")
|
|
124
|
+
.action(async (name, opts) => {
|
|
125
|
+
const { blobDelete } = await import("../azure-storage.js");
|
|
126
|
+
await blobDelete({ account: opts.account, container: opts.container, name, profile: opts.profile });
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
blob
|
|
130
|
+
.command("url <name>")
|
|
131
|
+
.description("Generate a temporary SAS URL for a blob")
|
|
132
|
+
.option("--account <name>", "Storage account (auto-detected if only one)")
|
|
133
|
+
.requiredOption("--container <name>", "Container name")
|
|
134
|
+
.option("--expiry <hours>", "Link expiry in hours (default: 24)", "24")
|
|
135
|
+
.option("--profile <subscription>", "Azure subscription name or ID")
|
|
136
|
+
.action(async (name, opts) => {
|
|
137
|
+
const { blobUrl } = await import("../azure-storage.js");
|
|
138
|
+
await blobUrl({ account: opts.account, container: opts.container, name, expiry: opts.expiry, profile: opts.profile });
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const encryption = storage
|
|
142
|
+
.command("encryption")
|
|
143
|
+
.description("Show or configure storage account encryption settings");
|
|
144
|
+
|
|
145
|
+
encryption
|
|
146
|
+
.command("show", { isDefault: true })
|
|
147
|
+
.description("Show encryption posture for a storage account")
|
|
148
|
+
.option("--account <name>", "Storage account (auto-detected if only one)")
|
|
149
|
+
.option("--profile <subscription>", "Azure subscription name or ID")
|
|
150
|
+
.action(async (opts) => {
|
|
151
|
+
const { storageEncryption } = await import("../azure-storage.js");
|
|
152
|
+
await storageEncryption({ account: opts.account, profile: opts.profile });
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
encryption
|
|
156
|
+
.command("configure")
|
|
157
|
+
.description("Configure encryption and security settings")
|
|
158
|
+
.option("--account <name>", "Storage account (auto-detected if only one)")
|
|
159
|
+
.option("--profile <subscription>", "Azure subscription name or ID")
|
|
160
|
+
.option("--https-only", "Enforce HTTPS-only traffic")
|
|
161
|
+
.option("--min-tls <version>", "Minimum TLS version (TLS1_0, TLS1_1, TLS1_2)")
|
|
162
|
+
.option("--disable-shared-key", "Disable shared key auth (force AAD-only)")
|
|
163
|
+
.option("--enable-shared-key", "Re-enable shared key auth")
|
|
164
|
+
.option("--cmk", "Use customer-managed key (requires --key-vault, --key-name)")
|
|
165
|
+
.option("--key-vault <uri>", "Key Vault URI for CMK")
|
|
166
|
+
.option("--key-name <name>", "Key name in Key Vault")
|
|
167
|
+
.option("--key-version <version>", "Key version (default: latest)")
|
|
168
|
+
.option("--microsoft-keys", "Revert to Microsoft-managed keys")
|
|
169
|
+
.option("--infra-encryption", "Check/recommend infrastructure (double) encryption")
|
|
170
|
+
.option("--soft-delete <days>", "Enable blob soft delete (0 to disable)")
|
|
171
|
+
.option("--versioning", "Enable blob versioning")
|
|
172
|
+
.option("--no-versioning", "Disable blob versioning")
|
|
173
|
+
.option("--disable-blob-public", "Block anonymous read access on all containers")
|
|
174
|
+
.option("--deny-public-access", "Firewall: deny all networks except allowed IPs/VNets")
|
|
175
|
+
.option("--allow-public-access", "Firewall: allow all networks (remove restriction)")
|
|
176
|
+
.action(async (opts) => {
|
|
177
|
+
const { storageEncryptionConfigure } = await import("../azure-storage.js");
|
|
178
|
+
await storageEncryptionConfigure({
|
|
179
|
+
account: opts.account, profile: opts.profile,
|
|
180
|
+
httpsOnly: opts.httpsOnly, minTls: opts.minTls,
|
|
181
|
+
disableSharedKey: opts.disableSharedKey, enableSharedKey: opts.enableSharedKey,
|
|
182
|
+
cmk: opts.cmk, keyVault: opts.keyVault, keyName: opts.keyName, keyVersion: opts.keyVersion,
|
|
183
|
+
microsoftKeys: opts.microsoftKeys, infraEncryption: opts.infraEncryption,
|
|
184
|
+
softDelete: opts.softDelete, versioning: opts.versioning,
|
|
185
|
+
disableBlobPublic: opts.disableBlobPublic,
|
|
186
|
+
denyPublicAccess: opts.denyPublicAccess, allowPublicAccess: opts.allowPublicAccess,
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// ── Azure OpenAI ───────────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
const openai = azure
|
|
193
|
+
.command("openai")
|
|
194
|
+
.description("Create/list Azure OpenAI resources (e.g. in uaenorth so VMs can use them)");
|
|
195
|
+
|
|
196
|
+
openai
|
|
197
|
+
.command("list", { isDefault: true })
|
|
198
|
+
.description("List Azure OpenAI resources in the subscription")
|
|
199
|
+
.option("--profile <subscription>", "Azure subscription name or ID")
|
|
200
|
+
.action(async (opts) => {
|
|
201
|
+
const { openaiList } = await import("../azure-openai.js");
|
|
202
|
+
await openaiList({ profile: opts.profile });
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
openai
|
|
206
|
+
.command("create <name>")
|
|
207
|
+
.description("Create an Azure OpenAI resource in a region (default: uaenorth)")
|
|
208
|
+
.requiredOption("--resource-group <name>", "Resource group (create with: az group create --name <name> --location uaenorth)")
|
|
209
|
+
.option("--location <region>", "Azure region (default: uaenorth)", "uaenorth")
|
|
210
|
+
.option("--sku <sku>", "SKU (default: S0)", "S0")
|
|
211
|
+
.option("--profile <subscription>", "Azure subscription name or ID")
|
|
212
|
+
.action(async (name, opts) => {
|
|
213
|
+
const { openaiCreate } = await import("../azure-openai.js");
|
|
214
|
+
await openaiCreate({
|
|
215
|
+
name,
|
|
216
|
+
resourceGroup: opts.resourceGroup,
|
|
217
|
+
location: opts.location,
|
|
218
|
+
sku: opts.sku,
|
|
219
|
+
profile: opts.profile,
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
openai
|
|
224
|
+
.command("allowlist-me")
|
|
225
|
+
.description("Add an IP to Azure OpenAI resource allowlists (uses az cli)")
|
|
226
|
+
.option("--profile <subscription>", "Azure subscription name or ID")
|
|
227
|
+
.option("--all", "Allowlist on every OpenAI/AIServices resource across all subscriptions")
|
|
228
|
+
.option("--name <resource>", "Target a specific Azure OpenAI resource by name")
|
|
229
|
+
.option("--resource-group <rg>", "Resource group (auto-detected if omitted)")
|
|
230
|
+
.option("--ip <address>", "IP address to allowlist (auto-detected if omitted)")
|
|
231
|
+
.action(async (opts) => {
|
|
232
|
+
const { openaiAllowlistMe } = await import("../azure-openai.js");
|
|
233
|
+
await openaiAllowlistMe({
|
|
234
|
+
profile: opts.profile,
|
|
235
|
+
all: opts.all,
|
|
236
|
+
name: opts.name,
|
|
237
|
+
resourceGroup: opts.resourceGroup,
|
|
238
|
+
ip: opts.ip,
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
openai
|
|
243
|
+
.command("debug-vm [vmName] [run]")
|
|
244
|
+
.description("Run a single agent request on the VM with DEBUG=1 to capture Azure OpenAI connection errors")
|
|
245
|
+
.option("--vm-name <name>", "Target VM (default: active VM)")
|
|
246
|
+
.option("--agent <name>", "Agent name (default: foundation)", "foundation")
|
|
247
|
+
.option("--profile <subscription>", "Azure subscription name or ID")
|
|
248
|
+
.action(async (vmName, _run, opts) => {
|
|
249
|
+
const name = vmName === "run" ? undefined : (vmName || opts.vmName);
|
|
250
|
+
const { azureOpenAiDebugVm } = await import("../azure.js");
|
|
251
|
+
await azureOpenAiDebugVm({
|
|
252
|
+
vmName: name,
|
|
253
|
+
agent: opts.agent,
|
|
254
|
+
profile: opts.profile,
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// ── Key Vault ──────────────────────────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
const keyvault = azure
|
|
261
|
+
.command("keyvault")
|
|
262
|
+
.description("Manage Azure Key Vaults and secrets");
|
|
263
|
+
|
|
264
|
+
keyvault
|
|
265
|
+
.command("list", { isDefault: true })
|
|
266
|
+
.description("List Key Vaults in the subscription")
|
|
267
|
+
.option("--profile <subscription>", "Azure subscription name or ID")
|
|
268
|
+
.action(async (opts) => {
|
|
269
|
+
const { keyvaultList } = await import("../azure-keyvault.js");
|
|
270
|
+
await keyvaultList({ profile: opts.profile });
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
keyvault
|
|
274
|
+
.command("show")
|
|
275
|
+
.description("Show Key Vault details and object counts")
|
|
276
|
+
.option("--vault <name>", "Key Vault name (auto-detected if only one)")
|
|
277
|
+
.option("--profile <subscription>", "Azure subscription name or ID")
|
|
278
|
+
.action(async (opts) => {
|
|
279
|
+
const { keyvaultShow } = await import("../azure-keyvault.js");
|
|
280
|
+
await keyvaultShow({ vault: opts.vault, profile: opts.profile });
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
keyvault
|
|
284
|
+
.command("create <name>")
|
|
285
|
+
.description("Create a new Key Vault")
|
|
286
|
+
.option("--resource-group <name>", "Resource group (lists available if omitted)")
|
|
287
|
+
.requiredOption("--location <region>", "Azure region (e.g. eastus, westeurope, uaenorth)")
|
|
288
|
+
.option("--sku <sku>", "SKU: standard or premium (default: standard)", "standard")
|
|
289
|
+
.option("--enable-purge-protection", "Enable purge protection (irreversible)")
|
|
290
|
+
.option("--retention-days <days>", "Soft-delete retention days (default: 90)")
|
|
291
|
+
.option("--profile <subscription>", "Azure subscription name or ID")
|
|
292
|
+
.action(async (name, opts) => {
|
|
293
|
+
const { keyvaultCreate } = await import("../azure-keyvault.js");
|
|
294
|
+
await keyvaultCreate({
|
|
295
|
+
name, resourceGroup: opts.resourceGroup, location: opts.location,
|
|
296
|
+
sku: opts.sku, enablePurgeProtection: opts.enablePurgeProtection,
|
|
297
|
+
retentionDays: opts.retentionDays, profile: opts.profile,
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
keyvault
|
|
302
|
+
.command("delete")
|
|
303
|
+
.description("Delete a Key Vault (soft-delete)")
|
|
304
|
+
.option("--vault <name>", "Key Vault name (auto-detected if only one)")
|
|
305
|
+
.option("--purge", "Permanently purge after deletion (requires purge protection off)")
|
|
306
|
+
.option("--profile <subscription>", "Azure subscription name or ID")
|
|
307
|
+
.action(async (opts) => {
|
|
308
|
+
const { keyvaultDelete } = await import("../azure-keyvault.js");
|
|
309
|
+
await keyvaultDelete({ vault: opts.vault, purge: opts.purge, profile: opts.profile });
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
keyvault
|
|
313
|
+
.command("sync")
|
|
314
|
+
.description("Pull secrets from Key Vault into .env files (reads .env.keyvault templates)")
|
|
315
|
+
.option("--vault <name>", "Override default vault for all references")
|
|
316
|
+
.option("--profile <subscription>", "Azure subscription name or ID")
|
|
317
|
+
.action(async (opts) => {
|
|
318
|
+
const { keyvaultSync } = await import("../azure-keyvault.js");
|
|
319
|
+
await keyvaultSync({ vault: opts.vault, profile: opts.profile });
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
keyvault
|
|
323
|
+
.command("setup")
|
|
324
|
+
.description("Interactive setup: pick vault, set auto-sync, scaffold .env.keyvault templates")
|
|
325
|
+
.option("--profile <subscription>", "Azure subscription name or ID")
|
|
326
|
+
.action(async (opts) => {
|
|
327
|
+
const { keyvaultSetup } = await import("../azure-keyvault.js");
|
|
328
|
+
await keyvaultSetup({ profile: opts.profile });
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
keyvault
|
|
332
|
+
.command("status")
|
|
333
|
+
.description("Show Key Vault sync status: auth, config, discovered templates")
|
|
334
|
+
.option("--profile <subscription>", "Azure subscription name or ID")
|
|
335
|
+
.action(async (opts) => {
|
|
336
|
+
const { keyvaultStatus } = await import("../azure-keyvault.js");
|
|
337
|
+
await keyvaultStatus({ profile: opts.profile });
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
const kvSecret = keyvault
|
|
341
|
+
.command("secret")
|
|
342
|
+
.description("Manage secrets in a Key Vault");
|
|
343
|
+
|
|
344
|
+
kvSecret
|
|
345
|
+
.command("list", { isDefault: true })
|
|
346
|
+
.description("List secrets in a Key Vault")
|
|
347
|
+
.option("--vault <name>", "Key Vault name (auto-detected if only one)")
|
|
348
|
+
.option("--include-managed", "Include managed (certificate-linked) secrets")
|
|
349
|
+
.option("--profile <subscription>", "Azure subscription name or ID")
|
|
350
|
+
.action(async (opts) => {
|
|
351
|
+
const { secretList } = await import("../azure-keyvault.js");
|
|
352
|
+
await secretList({ vault: opts.vault, includeManaged: opts.includeManaged, profile: opts.profile });
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
kvSecret
|
|
356
|
+
.command("show <name>")
|
|
357
|
+
.description("Show secret metadata (add --show-value to reveal)")
|
|
358
|
+
.option("--vault <name>", "Key Vault name (auto-detected if only one)")
|
|
359
|
+
.option("--version <version>", "Specific secret version (default: latest)")
|
|
360
|
+
.option("--show-value", "Display the secret value")
|
|
361
|
+
.option("--profile <subscription>", "Azure subscription name or ID")
|
|
362
|
+
.action(async (name, opts) => {
|
|
363
|
+
const { secretShow } = await import("../azure-keyvault.js");
|
|
364
|
+
await secretShow({ vault: opts.vault, name, version: opts.version, showValue: opts.showValue, profile: opts.profile });
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
kvSecret
|
|
368
|
+
.command("set <name>")
|
|
369
|
+
.description("Set a secret value (creates or updates)")
|
|
370
|
+
.option("--vault <name>", "Key Vault name (auto-detected if only one)")
|
|
371
|
+
.option("--value <value>", "Secret value (use --file for binary/large)")
|
|
372
|
+
.option("--file <path>", "Read secret value from a file")
|
|
373
|
+
.option("--content-type <type>", "Content type (e.g. text/plain, application/json)")
|
|
374
|
+
.option("--expires <datetime>", "Expiration date (ISO 8601)")
|
|
375
|
+
.option("--disabled", "Create the secret in disabled state")
|
|
376
|
+
.option("--profile <subscription>", "Azure subscription name or ID")
|
|
377
|
+
.action(async (name, opts) => {
|
|
378
|
+
const { secretSet } = await import("../azure-keyvault.js");
|
|
379
|
+
await secretSet({
|
|
380
|
+
vault: opts.vault, name, value: opts.value, file: opts.file,
|
|
381
|
+
contentType: opts.contentType, expires: opts.expires,
|
|
382
|
+
disabled: opts.disabled, profile: opts.profile,
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
kvSecret
|
|
387
|
+
.command("delete <name>")
|
|
388
|
+
.description("Delete a secret (soft-delete, recoverable)")
|
|
389
|
+
.option("--vault <name>", "Key Vault name (auto-detected if only one)")
|
|
390
|
+
.option("--profile <subscription>", "Azure subscription name or ID")
|
|
391
|
+
.action(async (name, opts) => {
|
|
392
|
+
const { secretDelete } = await import("../azure-keyvault.js");
|
|
393
|
+
await secretDelete({ vault: opts.vault, name, profile: opts.profile });
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
const kvKey = keyvault
|
|
397
|
+
.command("key")
|
|
398
|
+
.description("Manage cryptographic keys in a Key Vault");
|
|
399
|
+
|
|
400
|
+
kvKey
|
|
401
|
+
.command("list", { isDefault: true })
|
|
402
|
+
.description("List keys in a Key Vault")
|
|
403
|
+
.option("--vault <name>", "Key Vault name (auto-detected if only one)")
|
|
404
|
+
.option("--profile <subscription>", "Azure subscription name or ID")
|
|
405
|
+
.action(async (opts) => {
|
|
406
|
+
const { keyList } = await import("../azure-keyvault.js");
|
|
407
|
+
await keyList({ vault: opts.vault, profile: opts.profile });
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
kvKey
|
|
411
|
+
.command("show <name>")
|
|
412
|
+
.description("Show key details and rotation policy")
|
|
413
|
+
.option("--vault <name>", "Key Vault name (auto-detected if only one)")
|
|
414
|
+
.option("--profile <subscription>", "Azure subscription name or ID")
|
|
415
|
+
.action(async (name, opts) => {
|
|
416
|
+
const { keyShow } = await import("../azure-keyvault.js");
|
|
417
|
+
await keyShow({ vault: opts.vault, name, profile: opts.profile });
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
kvKey
|
|
421
|
+
.command("create <name>")
|
|
422
|
+
.description("Create a key with optional auto-rotation policy")
|
|
423
|
+
.option("--vault <name>", "Key Vault name (auto-detected if only one)")
|
|
424
|
+
.option("--type <kty>", "Key type: RSA, RSA-HSM, EC, EC-HSM, oct, oct-HSM (default: RSA)", "RSA")
|
|
425
|
+
.option("--size <bits>", "RSA key size: 2048, 3072, 4096 (default: 2048)", "2048")
|
|
426
|
+
.option("--curve <curve>", "EC curve: P-256, P-384, P-521 (default: P-256)")
|
|
427
|
+
.option("--ops <operations>", "Space-separated key operations (encrypt decrypt sign verify wrapKey unwrapKey)")
|
|
428
|
+
.option("--rotate-every <duration>", "Auto-rotation interval as ISO 8601 duration (e.g. P90D, P6M, P1Y)")
|
|
429
|
+
.option("--expires-in <duration>", "Key expiry as ISO 8601 duration (e.g. P1Y, P2Y)")
|
|
430
|
+
.option("--notify-before <duration>", "Notify before expiry (default: P30D)")
|
|
431
|
+
.option("--disabled", "Create the key in disabled state")
|
|
432
|
+
.option("--profile <subscription>", "Azure subscription name or ID")
|
|
433
|
+
.action(async (name, opts) => {
|
|
434
|
+
const { keyCreate } = await import("../azure-keyvault.js");
|
|
435
|
+
await keyCreate({
|
|
436
|
+
vault: opts.vault, name, type: opts.type, size: opts.size,
|
|
437
|
+
curve: opts.curve, ops: opts.ops, rotateEvery: opts.rotateEvery,
|
|
438
|
+
expiresIn: opts.expiresIn, notifyBefore: opts.notifyBefore,
|
|
439
|
+
disabled: opts.disabled, profile: opts.profile,
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
kvKey
|
|
444
|
+
.command("rotate <name>")
|
|
445
|
+
.description("Rotate a key on-demand (creates a new version)")
|
|
446
|
+
.option("--vault <name>", "Key Vault name (auto-detected if only one)")
|
|
447
|
+
.option("--profile <subscription>", "Azure subscription name or ID")
|
|
448
|
+
.action(async (name, opts) => {
|
|
449
|
+
const { keyRotate } = await import("../azure-keyvault.js");
|
|
450
|
+
await keyRotate({ vault: opts.vault, name, profile: opts.profile });
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
const kvRotationPolicy = kvKey
|
|
454
|
+
.command("rotation-policy")
|
|
455
|
+
.description("Manage key auto-rotation policy");
|
|
456
|
+
|
|
457
|
+
kvRotationPolicy
|
|
458
|
+
.command("show <name>")
|
|
459
|
+
.description("Show the rotation policy for a key")
|
|
460
|
+
.option("--vault <name>", "Key Vault name (auto-detected if only one)")
|
|
461
|
+
.option("--profile <subscription>", "Azure subscription name or ID")
|
|
462
|
+
.action(async (name, opts) => {
|
|
463
|
+
const { keyRotationPolicyShow } = await import("../azure-keyvault.js");
|
|
464
|
+
await keyRotationPolicyShow({ vault: opts.vault, name, profile: opts.profile });
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
kvRotationPolicy
|
|
468
|
+
.command("set <name>")
|
|
469
|
+
.description("Set auto-rotation policy for a key")
|
|
470
|
+
.option("--vault <name>", "Key Vault name (auto-detected if only one)")
|
|
471
|
+
.requiredOption("--rotate-every <duration>", "Rotation interval (ISO 8601: P30D, P90D, P6M, P1Y)")
|
|
472
|
+
.option("--expires-in <duration>", "Key expiry duration (ISO 8601: P1Y, P2Y)")
|
|
473
|
+
.option("--notify-before <duration>", "Notify before expiry (default: P30D)")
|
|
474
|
+
.option("--profile <subscription>", "Azure subscription name or ID")
|
|
475
|
+
.action(async (name, opts) => {
|
|
476
|
+
const { keyRotationPolicySet } = await import("../azure-keyvault.js");
|
|
477
|
+
await keyRotationPolicySet({
|
|
478
|
+
vault: opts.vault, name, rotateEvery: opts.rotateEvery,
|
|
479
|
+
expiresIn: opts.expiresIn, notifyBefore: opts.notifyBefore,
|
|
480
|
+
profile: opts.profile,
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
// ── AKS (Azure Kubernetes Service) ────────────────────────────────────
|
|
485
|
+
|
|
486
|
+
const aks = azure
|
|
487
|
+
.command("aks")
|
|
488
|
+
.description("Manage Foundation on AKS clusters (bootstrapped via Flux)");
|
|
489
|
+
|
|
490
|
+
aks
|
|
491
|
+
.command("up [name]")
|
|
492
|
+
.description("Create an AKS cluster, create GHCR pull secret, and bootstrap Flux (meshxdata/flux)")
|
|
493
|
+
.option("--profile <subscription>", "Azure subscription name or ID")
|
|
494
|
+
.option("--location <region>", "Azure region (default: uaenorth)")
|
|
495
|
+
.option("--node-count <count>", "Initial node count (default: 3)")
|
|
496
|
+
.option("--min-count <count>", "Autoscaler min nodes (default: 1)")
|
|
497
|
+
.option("--max-count <count>", "Autoscaler max nodes (default: 5)")
|
|
498
|
+
.option("--node-vm-size <size>", "Node VM size (default: Standard_D8s_v3)")
|
|
499
|
+
.option("--kubernetes-version <version>", "K8s version (default: 1.30)")
|
|
500
|
+
.option("--tier <tier>", "AKS tier: free | standard | premium (default: standard)")
|
|
501
|
+
.option("--network-plugin <plugin>", "Network plugin: azure | kubenet (default: azure)")
|
|
502
|
+
.option("--max-pods <count>", "Max pods per node (default: 110)")
|
|
503
|
+
.option("--resource-group <name>", "Resource group (default: foundation-aks-rg)")
|
|
504
|
+
.option("--flux-repo <repo>", "GitHub repo for Flux (default: flux)")
|
|
505
|
+
.option("--flux-owner <owner>", "GitHub owner/org for Flux (default: meshxdata)")
|
|
506
|
+
.option("--flux-path <path>", "Path in repo for cluster manifests (default: clusters/<name>)")
|
|
507
|
+
.option("--flux-branch <branch>", "Git branch for Flux (default: main)")
|
|
508
|
+
.option("--github-token <token>", "GitHub PAT for Flux + GHCR pull (default: $GITHUB_TOKEN)")
|
|
509
|
+
.option("--no-flux", "Skip Flux bootstrap")
|
|
510
|
+
.option("--no-postgres", "Skip Postgres Flexible Server provisioning")
|
|
511
|
+
.option("--dai", "Include DAI (Dashboards AI) workloads")
|
|
512
|
+
.action(async (name, opts) => {
|
|
513
|
+
const { aksUp } = await import("../azure-aks.js");
|
|
514
|
+
await aksUp({
|
|
515
|
+
clusterName: name, profile: opts.profile, location: opts.location,
|
|
516
|
+
nodeCount: opts.nodeCount ? Number(opts.nodeCount) : undefined,
|
|
517
|
+
minCount: opts.minCount ? Number(opts.minCount) : undefined,
|
|
518
|
+
maxCount: opts.maxCount ? Number(opts.maxCount) : undefined,
|
|
519
|
+
nodeVmSize: opts.nodeVmSize, kubernetesVersion: opts.kubernetesVersion,
|
|
520
|
+
tier: opts.tier, networkPlugin: opts.networkPlugin, maxPods: opts.maxPods ? Number(opts.maxPods) : undefined,
|
|
521
|
+
resourceGroup: opts.resourceGroup,
|
|
522
|
+
fluxRepo: opts.fluxRepo, fluxOwner: opts.fluxOwner,
|
|
523
|
+
fluxPath: opts.fluxPath, fluxBranch: opts.fluxBranch,
|
|
524
|
+
githubToken: opts.githubToken,
|
|
525
|
+
noFlux: opts.flux === false,
|
|
526
|
+
noPostgres: opts.postgres === false,
|
|
527
|
+
dai: opts.dai === true,
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
aks
|
|
532
|
+
.command("down [name]")
|
|
533
|
+
.description("Destroy the AKS cluster and all workloads")
|
|
534
|
+
.option("--profile <subscription>", "Azure subscription name or ID")
|
|
535
|
+
.option("--yes", "Skip confirmation prompt")
|
|
536
|
+
.action(async (name, opts) => {
|
|
537
|
+
const { aksDown } = await import("../azure-aks.js");
|
|
538
|
+
await aksDown({ clusterName: name, profile: opts.profile, yes: opts.yes });
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
aks
|
|
542
|
+
.command("list")
|
|
543
|
+
.description("List all tracked AKS clusters")
|
|
544
|
+
.option("--profile <subscription>", "Azure subscription name or ID")
|
|
545
|
+
.action(async (opts) => {
|
|
546
|
+
const { aksList } = await import("../azure-aks.js");
|
|
547
|
+
await aksList({ profile: opts.profile });
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
aks
|
|
551
|
+
.command("status [name]")
|
|
552
|
+
.description("Show cluster state, node pools, and Flux health")
|
|
553
|
+
.option("--profile <subscription>", "Azure subscription name or ID")
|
|
554
|
+
.action(async (name, opts) => {
|
|
555
|
+
const { aksStatus } = await import("../azure-aks.js");
|
|
556
|
+
await aksStatus({ clusterName: name, profile: opts.profile });
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
const aksConfig = aks
|
|
560
|
+
.command("config")
|
|
561
|
+
.description("Manage service versions on the AKS cluster");
|
|
562
|
+
|
|
563
|
+
aksConfig
|
|
564
|
+
.command("versions [name]", { isDefault: true })
|
|
565
|
+
.description("Show Foundation service image tags running on the cluster")
|
|
566
|
+
.action(async (name) => {
|
|
567
|
+
const { aksConfigVersions } = await import("../azure-aks.js");
|
|
568
|
+
await aksConfigVersions({ clusterName: name });
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
aks
|
|
572
|
+
.command("terraform [name]")
|
|
573
|
+
.description("Generate Terraform HCL for the AKS cluster and its resources")
|
|
574
|
+
.option("--profile <subscription>", "Azure subscription name or ID")
|
|
575
|
+
.option("--output <file>", "Write HCL to a file instead of stdout")
|
|
576
|
+
.action(async (name, opts) => {
|
|
577
|
+
const { aksTerraform } = await import("../azure-aks.js");
|
|
578
|
+
await aksTerraform({ clusterName: name, profile: opts.profile, output: opts.output });
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
aks
|
|
582
|
+
.command("test [name]")
|
|
583
|
+
.description("Run QA automation tests locally against an AKS cluster")
|
|
584
|
+
.action(async (name) => {
|
|
585
|
+
const { readClusterState, writeClusterState, clusterDomain } = await import("../azure-aks.js");
|
|
586
|
+
const { resolveCliSrc } = await import("../azure-helpers.js");
|
|
587
|
+
const { rootDir } = await import(resolveCliSrc("project.js"));
|
|
588
|
+
const fsp = await import("node:fs/promises");
|
|
589
|
+
const path = await import("node:path");
|
|
590
|
+
|
|
591
|
+
const cluster = readClusterState(name);
|
|
592
|
+
if (!cluster?.clusterName) {
|
|
593
|
+
console.error(chalk.red(`\n No AKS cluster tracked: "${name || "(none active)"}"`));
|
|
594
|
+
console.error(chalk.dim(" Create one: fops azure aks up <name>\n"));
|
|
595
|
+
process.exit(1);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const domain = cluster.domain || clusterDomain(cluster.clusterName);
|
|
599
|
+
const baseUrl = `https://${domain}`;
|
|
600
|
+
const apiUrl = `${baseUrl}/api`;
|
|
601
|
+
|
|
602
|
+
const root = rootDir();
|
|
603
|
+
if (!root) {
|
|
604
|
+
console.error(chalk.red("\n Foundation project root not found. Run from the compose repo or set FOUNDATION_ROOT.\n"));
|
|
605
|
+
process.exit(1);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const qaDir = path.join(root, "foundation-qa-automation");
|
|
609
|
+
try { await fsp.access(qaDir); } catch {
|
|
610
|
+
console.error(chalk.red("\n foundation-qa-automation/ not found in project root."));
|
|
611
|
+
console.error(chalk.dim(" Run: git submodule update --init\n"));
|
|
612
|
+
process.exit(1);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const { execa: execaFn } = await import("execa");
|
|
616
|
+
|
|
617
|
+
// For AKS: use a tracked VM for SSH fallback
|
|
618
|
+
const { listVms: lv, sshCmd: sc, knockForVm: kfv } = await import("../azure.js");
|
|
619
|
+
const { vms: allVms } = lv();
|
|
620
|
+
const vmEntry = Object.entries(allVms).find(([, v]) => v.publicIp);
|
|
621
|
+
|
|
622
|
+
console.log(chalk.dim(` Authenticating against ${apiUrl}…`));
|
|
623
|
+
const auth = await resolveRemoteAuth({
|
|
624
|
+
apiUrl,
|
|
625
|
+
ip: vmEntry?.[1]?.publicIp,
|
|
626
|
+
vmState: vmEntry?.[1],
|
|
627
|
+
execaFn, sshCmd: sc, knockForVm: kfv, suppressTlsWarning,
|
|
628
|
+
});
|
|
629
|
+
let { bearerToken, qaUser, qaPass, useTokenMode } = auth;
|
|
630
|
+
|
|
631
|
+
if (!bearerToken && !qaUser) {
|
|
632
|
+
console.error(chalk.red("\n No credentials found (local or remote)."));
|
|
633
|
+
console.error(chalk.dim(" Set BEARER_TOKEN or QA_USERNAME/QA_PASSWORD in env or ~/.fops.json\n"));
|
|
634
|
+
process.exit(1);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Write QA .env
|
|
638
|
+
const envPath = path.join(qaDir, ".env");
|
|
639
|
+
const examplePath = path.join(qaDir, ".env.example");
|
|
640
|
+
let envContent;
|
|
641
|
+
try { envContent = await fsp.readFile(examplePath, "utf8"); } catch {
|
|
642
|
+
envContent = await fsp.readFile(envPath, "utf8").catch(() => "");
|
|
643
|
+
}
|
|
644
|
+
const setVar = (content, key, value) => {
|
|
645
|
+
const re = new RegExp(`^${key}=.*`, "m");
|
|
646
|
+
return re.test(content) ? content.replace(re, `${key}=${value}`) : content + `\n${key}=${value}`;
|
|
647
|
+
};
|
|
648
|
+
envContent = setVar(envContent, "API_URL", apiUrl);
|
|
649
|
+
envContent = setVar(envContent, "DEV_API_URL", apiUrl);
|
|
650
|
+
envContent = setVar(envContent, "LIVE_API_URL", apiUrl);
|
|
651
|
+
envContent = setVar(envContent, "QA_USERNAME", qaUser);
|
|
652
|
+
envContent = setVar(envContent, "QA_PASSWORD", qaPass);
|
|
653
|
+
envContent = setVar(envContent, "QA_X_ACCOUNT", "root");
|
|
654
|
+
envContent = setVar(envContent, "ADMIN_USERNAME", qaUser);
|
|
655
|
+
envContent = setVar(envContent, "ADMIN_PASSWORD", qaPass);
|
|
656
|
+
envContent = setVar(envContent, "ADMIN_X_ACCOUNT", "root");
|
|
657
|
+
envContent = setVar(envContent, "CDO_USERNAME", qaUser);
|
|
658
|
+
envContent = setVar(envContent, "CDO_PASSWORD", qaPass);
|
|
659
|
+
envContent = setVar(envContent, "CDO_X_ACCOUNT", "root");
|
|
660
|
+
envContent = setVar(envContent, "OWNER_EMAIL", qaUser);
|
|
661
|
+
envContent = setVar(envContent, "OWNER_NAME", "Foundation Operator");
|
|
662
|
+
if (bearerToken) {
|
|
663
|
+
envContent = setVar(envContent, "BEARER_TOKEN", bearerToken);
|
|
664
|
+
envContent = setVar(envContent, "TOKEN_AUTH0", bearerToken);
|
|
665
|
+
}
|
|
666
|
+
await fsp.writeFile(envPath, envContent);
|
|
667
|
+
console.log(chalk.green(` ✓ Configured QA .env → ${apiUrl}`));
|
|
668
|
+
|
|
669
|
+
// Ensure venv + deps
|
|
670
|
+
try { await fsp.access(path.join(qaDir, "venv")); } catch {
|
|
671
|
+
console.log(chalk.cyan(" Setting up QA automation environment…"));
|
|
672
|
+
await execaFn("python3", ["-m", "venv", "venv"], { cwd: qaDir, stdio: "inherit" });
|
|
673
|
+
await execaFn("bash", ["-c", "source venv/bin/activate && pip install -r requirements.txt && playwright install"], { cwd: qaDir, stdio: "inherit" });
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const authMode = useTokenMode ? "bearer token (--use-token)" : `user/pass (${qaUser})`;
|
|
677
|
+
console.log(chalk.cyan(`\n Running QA tests against AKS ${cluster.clusterName} (${baseUrl}) [${authMode}]…\n`));
|
|
678
|
+
const pytestArgsList = [
|
|
679
|
+
"tests/",
|
|
680
|
+
"--env", "staging",
|
|
681
|
+
"--role", "FOUNDATION_ADMIN",
|
|
682
|
+
"--numprocesses=0",
|
|
683
|
+
"-v",
|
|
684
|
+
"--durations=0",
|
|
685
|
+
"--html=./playwright-report/report.html",
|
|
686
|
+
"--self-contained-html",
|
|
687
|
+
"--ignore=tests/e2e/landscape",
|
|
688
|
+
"--ignore=tests/e2e/procedures",
|
|
689
|
+
"--ignore=tests/e2e/upload_file_s3",
|
|
690
|
+
"--ignore=tests/roles",
|
|
691
|
+
"--ignore=tests/unit/data_product/test_data_product_consumers_v2.py",
|
|
692
|
+
"--ignore=tests/unit/data_product/test_data_product_query.py",
|
|
693
|
+
"--ignore=tests/unit/data_product/test_data_product_quality_v2.py",
|
|
694
|
+
"--ignore=tests/unit/data_product/test_data_product_schema_v2.py",
|
|
695
|
+
];
|
|
696
|
+
if (useTokenMode) pytestArgsList.push("--use-token");
|
|
697
|
+
const pytestArgs = pytestArgsList.join(" ");
|
|
698
|
+
|
|
699
|
+
const testEnv = {
|
|
700
|
+
...process.env,
|
|
701
|
+
API_URL: apiUrl,
|
|
702
|
+
DEV_API_URL: apiUrl,
|
|
703
|
+
LIVE_API_URL: apiUrl,
|
|
704
|
+
ROLE_NAME: "FOUNDATION_ADMIN",
|
|
705
|
+
QA_USERNAME: qaUser,
|
|
706
|
+
QA_PASSWORD: qaPass,
|
|
707
|
+
ADMIN_USERNAME: qaUser,
|
|
708
|
+
ADMIN_PASSWORD: qaPass,
|
|
709
|
+
QA_X_ACCOUNT: "root",
|
|
710
|
+
ADMIN_X_ACCOUNT: "root",
|
|
711
|
+
CDO_USERNAME: qaUser,
|
|
712
|
+
CDO_PASSWORD: qaPass,
|
|
713
|
+
CDO_X_ACCOUNT: "root",
|
|
714
|
+
OWNER_EMAIL: qaUser,
|
|
715
|
+
OWNER_NAME: "Foundation Operator",
|
|
716
|
+
};
|
|
717
|
+
if (bearerToken) {
|
|
718
|
+
testEnv.BEARER_TOKEN = bearerToken;
|
|
719
|
+
testEnv.TOKEN_AUTH0 = bearerToken;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const aksStartMs = Date.now();
|
|
723
|
+
const aksProc = execaFn(
|
|
724
|
+
"bash",
|
|
725
|
+
["-c", `source venv/bin/activate && pytest ${pytestArgs}`],
|
|
726
|
+
{ cwd: qaDir, timeout: 600_000, reject: false, env: testEnv },
|
|
727
|
+
);
|
|
728
|
+
let aksCaptured = "";
|
|
729
|
+
aksProc.stdout?.on("data", (d) => { const s = d.toString(); aksCaptured += s; process.stdout.write(s); });
|
|
730
|
+
aksProc.stderr?.on("data", (d) => { const s = d.toString(); aksCaptured += s; process.stderr.write(s); });
|
|
731
|
+
const { exitCode } = await aksProc;
|
|
732
|
+
const aksWallSec = Math.round((Date.now() - aksStartMs) / 1000);
|
|
733
|
+
|
|
734
|
+
const aksCounts = parsePytestSummary(aksCaptured);
|
|
735
|
+
const aksTiming = parsePytestDurations(aksCaptured);
|
|
736
|
+
const qaResult = {
|
|
737
|
+
passed: exitCode === 0,
|
|
738
|
+
exitCode,
|
|
739
|
+
at: new Date().toISOString(),
|
|
740
|
+
...(aksCounts.passed != null && { numPassed: aksCounts.passed }),
|
|
741
|
+
...(aksCounts.failed != null && { numFailed: aksCounts.failed }),
|
|
742
|
+
...(aksCounts.error != null && { numErrors: aksCounts.error }),
|
|
743
|
+
...(aksCounts.errors != null && { numErrors: aksCounts.errors }),
|
|
744
|
+
...(aksCounts.skipped != null && { numSkipped: aksCounts.skipped }),
|
|
745
|
+
durationSec: aksCounts.durationSec || aksWallSec,
|
|
746
|
+
...(aksTiming && { timing: aksTiming }),
|
|
747
|
+
};
|
|
748
|
+
writeClusterState(cluster.clusterName, { qa: qaResult });
|
|
749
|
+
|
|
750
|
+
if (exitCode === 0) {
|
|
751
|
+
console.log(chalk.green("\n ✓ QA tests passed\n"));
|
|
752
|
+
} else {
|
|
753
|
+
console.error(chalk.red(`\n QA tests failed (exit ${exitCode}).`));
|
|
754
|
+
console.error(chalk.dim(` Report: ${path.join(qaDir, "playwright-report", "report.html")}\n`));
|
|
755
|
+
process.exitCode = 1;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
try {
|
|
759
|
+
const { resultsPush } = await import("../azure-results.js");
|
|
760
|
+
await resultsPush(cluster.clusterName, qaResult, { quiet: false });
|
|
761
|
+
} catch (pushErr) {
|
|
762
|
+
console.log(chalk.dim(` (Result push skipped: ${pushErr.message})`));
|
|
763
|
+
}
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
aks
|
|
767
|
+
.command("kubeconfig [name]")
|
|
768
|
+
.description("Merge cluster kubeconfig into ~/.kube/config")
|
|
769
|
+
.option("--profile <subscription>", "Azure subscription name or ID")
|
|
770
|
+
.option("--admin", "Get admin credentials (cluster-admin role)")
|
|
771
|
+
.action(async (name, opts) => {
|
|
772
|
+
const { aksKubeconfig } = await import("../azure-aks.js");
|
|
773
|
+
await aksKubeconfig({ clusterName: name, profile: opts.profile, admin: opts.admin });
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
aks
|
|
777
|
+
.command("bootstrap [clusterName]")
|
|
778
|
+
.description("Create demo data mesh on the cluster (same as fops bootstrap, targeting AKS backend)")
|
|
779
|
+
.option("--api-url <url>", "Foundation backend API URL (e.g. https://foundation.example.com/api)")
|
|
780
|
+
.option("--yes", "Use credentials from env or ~/.fops.json, skip prompt")
|
|
781
|
+
.option("--profile <subscription>", "Azure subscription name or ID")
|
|
782
|
+
.action(async (clusterName, opts) => {
|
|
783
|
+
const { aksDataBootstrap } = await import("../azure-aks.js");
|
|
784
|
+
await aksDataBootstrap({
|
|
785
|
+
clusterName,
|
|
786
|
+
profile: opts.profile,
|
|
787
|
+
apiUrl: opts.apiUrl,
|
|
788
|
+
yes: opts.yes,
|
|
789
|
+
});
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
// ── Node pool management ───────────────────────────────────────────────
|
|
793
|
+
|
|
794
|
+
const nodePool = aks
|
|
795
|
+
.command("node-pool")
|
|
796
|
+
.description("Manage AKS node pools");
|
|
797
|
+
|
|
798
|
+
nodePool
|
|
799
|
+
.command("add [clusterName]")
|
|
800
|
+
.description("Add a node pool to the cluster")
|
|
801
|
+
.requiredOption("--pool-name <name>", "Node pool name")
|
|
802
|
+
.option("--profile <subscription>", "Azure subscription name or ID")
|
|
803
|
+
.option("--node-count <count>", "Node count (default: 3)")
|
|
804
|
+
.option("--node-vm-size <size>", "VM size (default: Standard_D8s_v3)")
|
|
805
|
+
.option("--mode <mode>", "Pool mode: System | User")
|
|
806
|
+
.option("--labels <labels>", "Node labels (key=value pairs)")
|
|
807
|
+
.option("--taints <taints>", "Node taints (key=value:effect)")
|
|
808
|
+
.option("--max-pods <count>", "Max pods per node")
|
|
809
|
+
.action(async (clusterName, opts) => {
|
|
810
|
+
const { aksNodePoolAdd } = await import("../azure-aks.js");
|
|
811
|
+
await aksNodePoolAdd({
|
|
812
|
+
clusterName, profile: opts.profile, poolName: opts.poolName,
|
|
813
|
+
nodeCount: opts.nodeCount ? Number(opts.nodeCount) : undefined,
|
|
814
|
+
nodeVmSize: opts.nodeVmSize, mode: opts.mode,
|
|
815
|
+
labels: opts.labels, taints: opts.taints,
|
|
816
|
+
maxPods: opts.maxPods ? Number(opts.maxPods) : undefined,
|
|
817
|
+
});
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
nodePool
|
|
821
|
+
.command("remove [clusterName]")
|
|
822
|
+
.description("Remove a node pool from the cluster")
|
|
823
|
+
.requiredOption("--pool-name <name>", "Node pool name to remove")
|
|
824
|
+
.option("--profile <subscription>", "Azure subscription name or ID")
|
|
825
|
+
.action(async (clusterName, opts) => {
|
|
826
|
+
const { aksNodePoolRemove } = await import("../azure-aks.js");
|
|
827
|
+
await aksNodePoolRemove({ clusterName, profile: opts.profile, poolName: opts.poolName });
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
// ── Flux subcommands ───────────────────────────────────────────────────
|
|
831
|
+
|
|
832
|
+
const flux = aks
|
|
833
|
+
.command("flux")
|
|
834
|
+
.description("Manage Flux GitOps on the AKS cluster");
|
|
835
|
+
|
|
836
|
+
flux
|
|
837
|
+
.command("init <clusterName>")
|
|
838
|
+
.description("Scaffold cluster manifests from the Foundation Flux template")
|
|
839
|
+
.requiredOption("--flux-repo <path>", "Path to local flux repo clone")
|
|
840
|
+
.option("--overlay <name>", "Overlay name for app kustomizations (default: <name>-azure)")
|
|
841
|
+
.option("--namespace <ns>", "Kubernetes namespace for the tenant (default: velora)")
|
|
842
|
+
.option("--postgres-host <host>", "Postgres hostname")
|
|
843
|
+
.option("--acr-username <user>", "ACR pull username")
|
|
844
|
+
.option("--acr-password <pass>", "ACR pull password")
|
|
845
|
+
.action(async (clusterName, opts) => {
|
|
846
|
+
const { aksFluxInit } = await import("../azure-aks.js");
|
|
847
|
+
await aksFluxInit({
|
|
848
|
+
clusterName, fluxRepo: opts.fluxRepo,
|
|
849
|
+
overlay: opts.overlay, namespace: opts.namespace,
|
|
850
|
+
postgresHost: opts.postgresHost,
|
|
851
|
+
acrUsername: opts.acrUsername, acrPassword: opts.acrPassword,
|
|
852
|
+
});
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
flux
|
|
856
|
+
.command("bootstrap [clusterName]")
|
|
857
|
+
.description("Bootstrap Flux on the cluster with a GitHub repo")
|
|
858
|
+
.requiredOption("--flux-repo <repo>", "GitHub repo name")
|
|
859
|
+
.requiredOption("--flux-owner <owner>", "GitHub owner/org")
|
|
860
|
+
.option("--flux-path <path>", "Path in repo for cluster manifests (default: clusters/<name>)")
|
|
861
|
+
.option("--flux-branch <branch>", "Git branch (default: main)")
|
|
862
|
+
.option("--github-token <token>", "GitHub PAT (default: $GITHUB_TOKEN)")
|
|
863
|
+
.option("--profile <subscription>", "Azure subscription name or ID")
|
|
864
|
+
.action(async (clusterName, opts) => {
|
|
865
|
+
const { aksFluxBootstrap } = await import("../azure-aks.js");
|
|
866
|
+
await aksFluxBootstrap({
|
|
867
|
+
clusterName, profile: opts.profile,
|
|
868
|
+
repo: opts.fluxRepo, owner: opts.fluxOwner,
|
|
869
|
+
path: opts.fluxPath, branch: opts.fluxBranch,
|
|
870
|
+
githubToken: opts.githubToken,
|
|
871
|
+
});
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
flux
|
|
875
|
+
.command("status [clusterName]")
|
|
876
|
+
.description("Show Flux sources, kustomizations, and helm releases")
|
|
877
|
+
.action(async (clusterName) => {
|
|
878
|
+
const { aksFluxStatus } = await import("../azure-aks.js");
|
|
879
|
+
await aksFluxStatus({ clusterName });
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
flux
|
|
883
|
+
.command("reconcile [clusterName]")
|
|
884
|
+
.description("Trigger a Flux reconciliation (pull + apply)")
|
|
885
|
+
.option("--source <name>", "Git source name (default: flux-system)")
|
|
886
|
+
.action(async (clusterName, opts) => {
|
|
887
|
+
const { aksFluxReconcile } = await import("../azure-aks.js");
|
|
888
|
+
await aksFluxReconcile({ clusterName, source: opts.source });
|
|
889
|
+
});
|
|
890
|
+
}
|