@meshxdata/fops 0.1.45 → 0.1.47
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 +202 -17
- package/package.json +1 -1
- package/src/commands/lifecycle.js +81 -5
- package/src/commands/setup.js +45 -4
- package/src/plugins/bundled/fops-plugin-azure/index.js +29 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-core.js +1185 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-flux.js +1180 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-ingress.js +393 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-naming.js +104 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-network.js +296 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-postgres.js +768 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-reconcilers.js +538 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-secrets.js +849 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-stacks.js +643 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-state.js +145 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-storage.js +496 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-terraform.js +1032 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks.js +155 -4245
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-keyvault.js +186 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-results.js +5 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/commands/infra-cmds.js +758 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/commands/registry-cmds.js +250 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/commands/test-cmds.js +2 -1
- package/src/plugins/bundled/fops-plugin-foundation/lib/apply.js +3 -2
- package/src/plugins/bundled/fops-plugin-foundation/lib/helpers.js +21 -0
- package/src/plugins/bundled/fops-plugin-foundation/lib/tools-read.js +3 -5
- package/src/ui/tui/App.js +13 -13
- package/src/web/dist/assets/index-NXC8Hvnp.css +1 -0
- package/src/web/dist/assets/index-QH1N4ejK.js +112 -0
- package/src/web/dist/index.html +2 -2
- package/src/web/server.js +4 -4
- package/src/web/dist/assets/index-BphVaAUd.css +0 -1
- package/src/web/dist/assets/index-CSckLzuG.js +0 -129
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* azure-aks-network.js - Network access, CIDR, and VNet operations
|
|
3
|
+
*
|
|
4
|
+
* Depends on: azure-aks-naming.js
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { OK, WARN, ERR, DIM, hint, banner, kvLine, lazyExeca, ensureAzCli, ensureAzAuth, fetchMyIp, subArgs } from "./azure.js";
|
|
8
|
+
import { parseCidr, cidrOverlaps } from "./azure-aks-naming.js";
|
|
9
|
+
import { requireCluster } from "./azure-aks-state.js";
|
|
10
|
+
|
|
11
|
+
// ── API Server IP reconciliation ──────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
export async function reconcileApiServerIp(ctx) {
|
|
14
|
+
const { execa, clusterName, rg, sub, cluster } = ctx;
|
|
15
|
+
const myIp = await fetchMyIp();
|
|
16
|
+
if (!myIp) return;
|
|
17
|
+
|
|
18
|
+
const ranges = cluster.apiServerAccessProfile?.authorizedIpRanges || [];
|
|
19
|
+
if (ranges.length === 0) {
|
|
20
|
+
hint(`Scoping API server to ${myIp}…`);
|
|
21
|
+
await execa("az", [
|
|
22
|
+
"aks", "update", "-g", rg, "-n", clusterName,
|
|
23
|
+
"--api-server-authorized-ip-ranges", `${myIp}/32`,
|
|
24
|
+
"--output", "none", ...subArgs(sub),
|
|
25
|
+
], { timeout: 120000 });
|
|
26
|
+
console.log(OK(` ✓ API server scoped to ${myIp}/32`));
|
|
27
|
+
} else if (!ranges.some(r => r.startsWith(myIp))) {
|
|
28
|
+
const updated = [...ranges, `${myIp}/32`].join(",");
|
|
29
|
+
hint(`Adding ${myIp} to authorized IP ranges…`);
|
|
30
|
+
await execa("az", [
|
|
31
|
+
"aks", "update", "-g", rg, "-n", clusterName,
|
|
32
|
+
"--api-server-authorized-ip-ranges", updated,
|
|
33
|
+
"--output", "none", ...subArgs(sub),
|
|
34
|
+
], { timeout: 120000 });
|
|
35
|
+
console.log(OK(` ✓ API server ranges updated (${ranges.length + 1} IPs)`));
|
|
36
|
+
} else {
|
|
37
|
+
console.log(OK(` ✓ API server already includes ${myIp}`));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── Standalone whitelist-me command ──────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
export async function aksWhitelistMe(opts = {}) {
|
|
44
|
+
const execa = await lazyExeca();
|
|
45
|
+
const sub = opts.profile;
|
|
46
|
+
await ensureAzCli(execa);
|
|
47
|
+
await ensureAzAuth(execa, { subscription: sub });
|
|
48
|
+
|
|
49
|
+
const cluster = requireCluster(opts.clusterName);
|
|
50
|
+
const { clusterName, resourceGroup: rg } = cluster;
|
|
51
|
+
|
|
52
|
+
const myIp = await fetchMyIp();
|
|
53
|
+
if (!myIp) {
|
|
54
|
+
console.error(ERR("\n Could not detect your public IP address.\n"));
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
hint(`Checking API server authorized IP ranges for ${clusterName}…`);
|
|
59
|
+
|
|
60
|
+
const { stdout: clusterJson, exitCode } = await execa("az", [
|
|
61
|
+
"aks", "show", "-g", rg, "-n", clusterName, "--output", "json", ...subArgs(sub),
|
|
62
|
+
], { reject: false, timeout: 30000 });
|
|
63
|
+
|
|
64
|
+
if (exitCode !== 0) {
|
|
65
|
+
console.error(ERR(`\n Failed to query cluster ${clusterName}\n`));
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const clusterData = JSON.parse(clusterJson);
|
|
70
|
+
const ranges = clusterData.apiServerAccessProfile?.authorizedIpRanges || [];
|
|
71
|
+
const myCidr = `${myIp}/32`;
|
|
72
|
+
|
|
73
|
+
if (ranges.length === 0) {
|
|
74
|
+
hint(`Scoping API server to ${myIp}…`);
|
|
75
|
+
const { exitCode: upCode } = await execa("az", [
|
|
76
|
+
"aks", "update", "-g", rg, "-n", clusterName,
|
|
77
|
+
"--api-server-authorized-ip-ranges", myCidr,
|
|
78
|
+
"--output", "none", ...subArgs(sub),
|
|
79
|
+
], { reject: false, timeout: 120000 });
|
|
80
|
+
if (upCode !== 0) {
|
|
81
|
+
console.error(ERR(`\n Failed to update API server authorized IP ranges\n`));
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
console.log(OK(`\n ✓ API server scoped to ${myCidr}\n`));
|
|
85
|
+
} else if (ranges.some(r => r.startsWith(myIp))) {
|
|
86
|
+
console.log(OK(`\n ✓ ${myCidr} is already whitelisted for API server on ${clusterName}\n`));
|
|
87
|
+
} else {
|
|
88
|
+
const updated = [...ranges, myCidr].join(",");
|
|
89
|
+
hint(`Adding ${myIp} to authorized IP ranges (${ranges.length} existing)…`);
|
|
90
|
+
const { exitCode: upCode } = await execa("az", [
|
|
91
|
+
"aks", "update", "-g", rg, "-n", clusterName,
|
|
92
|
+
"--api-server-authorized-ip-ranges", updated,
|
|
93
|
+
"--output", "none", ...subArgs(sub),
|
|
94
|
+
], { reject: false, timeout: 120000 });
|
|
95
|
+
if (upCode !== 0) {
|
|
96
|
+
console.error(ERR(`\n Failed to update API server authorized IP ranges\n`));
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
console.log(OK(`\n ✓ API server whitelisted for ${myCidr} on ${clusterName}\n`));
|
|
100
|
+
console.log(` Authorized IPs: ${[...ranges, myCidr].join(", ")}\n`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── Network access configuration (Key Vault, Storage) ─────────────────────────
|
|
105
|
+
|
|
106
|
+
export async function reconcileNetworkAccess(ctx) {
|
|
107
|
+
const { execa, clusterName, rg, sub, cluster, opts } = ctx;
|
|
108
|
+
const nodeRg = cluster.nodeResourceGroup;
|
|
109
|
+
|
|
110
|
+
if (!nodeRg) {
|
|
111
|
+
console.log(WARN(" ⚠ Cannot configure network access — nodeResourceGroup not found"));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Find the AKS VNet and subnet
|
|
116
|
+
const { stdout: vnetJson } = await execa("az", [
|
|
117
|
+
"network", "vnet", "list", "-g", nodeRg, "--output", "json", ...subArgs(sub),
|
|
118
|
+
], { timeout: 30000, reject: false });
|
|
119
|
+
|
|
120
|
+
const vnets = JSON.parse(vnetJson || "[]");
|
|
121
|
+
if (vnets.length === 0) {
|
|
122
|
+
console.log(WARN(" ⚠ No VNet found in node resource group"));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const vnet = vnets[0];
|
|
127
|
+
const aksSubnet = vnet.subnets?.find(s => s.name === "aks-subnet") || vnet.subnets?.[0];
|
|
128
|
+
if (!aksSubnet) {
|
|
129
|
+
console.log(WARN(" ⚠ No subnet found in AKS VNet"));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const vnetName = vnet.name;
|
|
134
|
+
const subnetName = aksSubnet.name;
|
|
135
|
+
const subnetId = aksSubnet.id;
|
|
136
|
+
|
|
137
|
+
banner("Network Access Configuration");
|
|
138
|
+
kvLine("VNet", DIM(`${vnetName} (${nodeRg})`));
|
|
139
|
+
kvLine("Subnet", DIM(subnetName));
|
|
140
|
+
|
|
141
|
+
// 1. Add service endpoints to subnet
|
|
142
|
+
const existingEndpoints = (aksSubnet.serviceEndpoints || []).map(e => e.service);
|
|
143
|
+
const neededEndpoints = ["Microsoft.KeyVault", "Microsoft.Storage"];
|
|
144
|
+
const missingEndpoints = neededEndpoints.filter(e => !existingEndpoints.includes(e));
|
|
145
|
+
|
|
146
|
+
if (missingEndpoints.length > 0) {
|
|
147
|
+
hint(`Adding service endpoints: ${missingEndpoints.join(", ")}...`);
|
|
148
|
+
const allEndpoints = [...new Set([...existingEndpoints, ...neededEndpoints])];
|
|
149
|
+
|
|
150
|
+
const { exitCode: seCode, stderr: seErr } = await execa("az", [
|
|
151
|
+
"network", "vnet", "subnet", "update",
|
|
152
|
+
"-g", nodeRg,
|
|
153
|
+
"--vnet-name", vnetName,
|
|
154
|
+
"-n", subnetName,
|
|
155
|
+
"--service-endpoints", ...allEndpoints,
|
|
156
|
+
"--output", "none",
|
|
157
|
+
...subArgs(sub),
|
|
158
|
+
], { timeout: 120000, reject: false });
|
|
159
|
+
|
|
160
|
+
if (seCode !== 0) {
|
|
161
|
+
console.log(WARN(` ⚠ Service endpoints: ${(seErr || "").split("\n")[0]}`));
|
|
162
|
+
} else {
|
|
163
|
+
console.log(OK(` ✓ Service endpoints added: ${missingEndpoints.join(", ")}`));
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
console.log(OK(" ✓ Service endpoints already configured"));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 2. Configure Key Vault network access
|
|
170
|
+
const kvNameVal = opts.keyvault || `fops-${clusterName}-kv`;
|
|
171
|
+
const { exitCode: kvExists } = await execa("az", [
|
|
172
|
+
"keyvault", "show", "--name", kvNameVal, "--output", "none", ...subArgs(sub),
|
|
173
|
+
], { reject: false, timeout: 15000 });
|
|
174
|
+
|
|
175
|
+
if (kvExists === 0) {
|
|
176
|
+
hint(`Configuring Key Vault "${kvNameVal}" network access...`);
|
|
177
|
+
|
|
178
|
+
// Add VNet rule
|
|
179
|
+
const { exitCode: kvRuleCode } = await execa("az", [
|
|
180
|
+
"keyvault", "network-rule", "add",
|
|
181
|
+
"--name", kvNameVal,
|
|
182
|
+
"--subnet", subnetId,
|
|
183
|
+
"--output", "none",
|
|
184
|
+
...subArgs(sub),
|
|
185
|
+
], { timeout: 60000, reject: false });
|
|
186
|
+
|
|
187
|
+
if (kvRuleCode === 0) {
|
|
188
|
+
console.log(OK(` ✓ Key Vault VNet rule added`));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Set default action to Deny (private)
|
|
192
|
+
const { exitCode: kvDenyCode } = await execa("az", [
|
|
193
|
+
"keyvault", "update",
|
|
194
|
+
"--name", kvNameVal,
|
|
195
|
+
"--default-action", "Deny",
|
|
196
|
+
"--bypass", "AzureServices",
|
|
197
|
+
"--output", "none",
|
|
198
|
+
...subArgs(sub),
|
|
199
|
+
], { timeout: 60000, reject: false });
|
|
200
|
+
|
|
201
|
+
if (kvDenyCode === 0) {
|
|
202
|
+
console.log(OK(` ✓ Key Vault set to private`));
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
console.log(DIM(` · Key Vault "${kvNameVal}" not found — skipping`));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// 3. Configure Storage Account network access
|
|
209
|
+
const { stdout: storageJson } = await execa("az", [
|
|
210
|
+
"storage", "account", "list", "-g", rg, "--output", "json", ...subArgs(sub),
|
|
211
|
+
], { timeout: 30000, reject: false });
|
|
212
|
+
|
|
213
|
+
const storageAccounts = JSON.parse(storageJson || "[]");
|
|
214
|
+
for (const sa of storageAccounts) {
|
|
215
|
+
const saName = sa.name;
|
|
216
|
+
hint(`Configuring Storage Account "${saName}" network access...`);
|
|
217
|
+
|
|
218
|
+
// Add VNet rule
|
|
219
|
+
const { exitCode: saRuleCode } = await execa("az", [
|
|
220
|
+
"storage", "account", "network-rule", "add",
|
|
221
|
+
"--account-name", saName,
|
|
222
|
+
"-g", rg,
|
|
223
|
+
"--subnet", subnetId,
|
|
224
|
+
"--output", "none",
|
|
225
|
+
...subArgs(sub),
|
|
226
|
+
], { timeout: 60000, reject: false });
|
|
227
|
+
|
|
228
|
+
if (saRuleCode === 0) {
|
|
229
|
+
console.log(OK(` ✓ Storage "${saName}" VNet rule added`));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Set default action to Deny (private)
|
|
233
|
+
const { exitCode: saDenyCode } = await execa("az", [
|
|
234
|
+
"storage", "account", "update",
|
|
235
|
+
"--name", saName,
|
|
236
|
+
"-g", rg,
|
|
237
|
+
"--default-action", "Deny",
|
|
238
|
+
"--bypass", "AzureServices",
|
|
239
|
+
"--output", "none",
|
|
240
|
+
...subArgs(sub),
|
|
241
|
+
], { timeout: 60000, reject: false });
|
|
242
|
+
|
|
243
|
+
if (saDenyCode === 0) {
|
|
244
|
+
console.log(OK(` ✓ Storage "${saName}" set to private`));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ── Find available subnet CIDR ────────────────────────────────────────────────
|
|
250
|
+
|
|
251
|
+
export async function findAvailableSubnetCidr(execa, nodeRg, vnetName, vnetPrefix, sub) {
|
|
252
|
+
let existingCidrs = [];
|
|
253
|
+
try {
|
|
254
|
+
const { stdout } = await execa("az", [
|
|
255
|
+
"network", "vnet", "subnet", "list",
|
|
256
|
+
"-g", nodeRg, "--vnet-name", vnetName, "--output", "json",
|
|
257
|
+
...subArgs(sub),
|
|
258
|
+
], { timeout: 15000 });
|
|
259
|
+
const subnets = JSON.parse(stdout || "[]");
|
|
260
|
+
existingCidrs = subnets.flatMap(s => s.addressPrefix ? [s.addressPrefix] : (s.addressPrefixes || []));
|
|
261
|
+
} catch (err) {
|
|
262
|
+
console.log(WARN(` ⚠ Could not list existing subnets: ${err.message}`));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const { start: vnetStart, end: vnetEnd } = parseCidr(vnetPrefix);
|
|
266
|
+
|
|
267
|
+
// Scan /24 blocks from the top of the VNet range downward to avoid
|
|
268
|
+
// collisions with AKS's default low-range subnets (often a wide /16).
|
|
269
|
+
const blockSize = 256;
|
|
270
|
+
const topBlock = (vnetEnd - blockSize + 1) >>> 0;
|
|
271
|
+
let iterations = 0;
|
|
272
|
+
for (let addr = topBlock; addr >= vnetStart && iterations < 4096; addr = (addr - blockSize) >>> 0, iterations++) {
|
|
273
|
+
const o1 = (addr >>> 24) & 0xFF;
|
|
274
|
+
const o2 = (addr >>> 16) & 0xFF;
|
|
275
|
+
const o3 = (addr >>> 8) & 0xFF;
|
|
276
|
+
const candidate = `${o1}.${o2}.${o3}.0/24`;
|
|
277
|
+
if (!cidrOverlaps(candidate, existingCidrs)) return candidate;
|
|
278
|
+
if (addr === 0) break;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
throw new Error(
|
|
282
|
+
`No available /24 subnet in VNet ${vnetName} (${vnetPrefix}). ` +
|
|
283
|
+
`Existing subnets: ${existingCidrs.join(", ")}. Free a subnet or expand the VNet address space.`
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ── Find AKS VNet ─────────────────────────────────────────────────────────────
|
|
288
|
+
|
|
289
|
+
export async function findAksVnet(execa, { nodeRg, sub }) {
|
|
290
|
+
const { stdout } = await execa("az", [
|
|
291
|
+
"network", "vnet", "list",
|
|
292
|
+
"--resource-group", nodeRg,
|
|
293
|
+
"--query", "[0].name", "-o", "tsv", ...subArgs(sub),
|
|
294
|
+
], { reject: false, timeout: 15000 });
|
|
295
|
+
return (stdout || "").trim();
|
|
296
|
+
}
|