@meshxdata/fops 0.1.45 → 0.1.46
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 +16 -18
- 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,768 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* azure-aks-postgres.js - PostgreSQL Flexible Server and replicas
|
|
3
|
+
*
|
|
4
|
+
* Depends on: azure-aks-naming.js, azure-aks-state.js, azure-aks-network.js
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import chalk from "chalk";
|
|
8
|
+
import { DEFAULTS, OK, WARN, ERR, DIM, hint, banner, kvLine, subArgs } from "./azure.js";
|
|
9
|
+
import { pgServerName, generatePassword, PG_DEFAULTS, PG_REPLICA_REGIONS, EH_DEFAULTS, ehNamespaceName } from "./azure-aks-naming.js";
|
|
10
|
+
import { readClusterState, writeClusterState, requireCluster } from "./azure-aks-state.js";
|
|
11
|
+
import { findAvailableSubnetCidr, findAksVnet } from "./azure-aks-network.js";
|
|
12
|
+
|
|
13
|
+
// ── Service databases ─────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
export const PG_SERVICE_DBS = ["foundation", "processor", "scheduler", "watcher", "mlflow"];
|
|
16
|
+
|
|
17
|
+
// ── Postgres reconciliation ───────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
export async function reconcilePostgres(ctx) {
|
|
20
|
+
const { execa, clusterName, rg, sub, cluster } = ctx;
|
|
21
|
+
const location = cluster.location || DEFAULTS.location;
|
|
22
|
+
const serverName = pgServerName(clusterName);
|
|
23
|
+
|
|
24
|
+
// 1. Check if server already exists
|
|
25
|
+
const { exitCode: pgExists, stdout: pgJson } = await execa("az", [
|
|
26
|
+
"postgres", "flexible-server", "show",
|
|
27
|
+
"--name", serverName, "--resource-group", rg,
|
|
28
|
+
"--output", "json", ...subArgs(sub),
|
|
29
|
+
], { reject: false, timeout: 30000 });
|
|
30
|
+
|
|
31
|
+
let fqdn;
|
|
32
|
+
let adminPassword;
|
|
33
|
+
|
|
34
|
+
if (pgExists === 0 && pgJson?.trim()) {
|
|
35
|
+
const pg = JSON.parse(pgJson);
|
|
36
|
+
fqdn = pg.fullyQualifiedDomainName;
|
|
37
|
+
console.log(OK(` ✓ Postgres Flexible Server "${serverName}" exists (${fqdn})`));
|
|
38
|
+
|
|
39
|
+
// Reconcile storage auto-grow
|
|
40
|
+
const autoGrow = pg.storage?.autoGrow;
|
|
41
|
+
if (autoGrow !== "Enabled") {
|
|
42
|
+
hint("Enabling storage auto-grow…");
|
|
43
|
+
await execa("az", [
|
|
44
|
+
"postgres", "flexible-server", "update",
|
|
45
|
+
"--name", serverName, "--resource-group", rg,
|
|
46
|
+
"--storage-auto-grow", "Enabled",
|
|
47
|
+
"--output", "none", ...subArgs(sub),
|
|
48
|
+
], { reject: false, timeout: 120000 });
|
|
49
|
+
console.log(OK(" ✓ Storage auto-grow enabled"));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
adminPassword = readClusterState(clusterName)?.postgres?.adminPassword;
|
|
53
|
+
if (!adminPassword) {
|
|
54
|
+
console.log(WARN(" ⚠ Postgres admin password not in local state — secret sync skipped"));
|
|
55
|
+
hint(` Password was set at creation time. If lost, reset with:`);
|
|
56
|
+
hint(` az postgres flexible-server update -g ${rg} -n ${serverName} --admin-password <new>`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
// 2. Resolve the AKS VNet + create a delegated subnet for Postgres
|
|
61
|
+
const nodeRg = cluster.nodeResourceGroup;
|
|
62
|
+
if (!nodeRg) {
|
|
63
|
+
console.log(WARN(" ⚠ Could not determine node resource group — skipping Postgres"));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
hint(`Resolving AKS VNet in ${nodeRg}…`);
|
|
68
|
+
const { stdout: vnetListJson, exitCode: vnetCode } = await execa("az", [
|
|
69
|
+
"network", "vnet", "list", "-g", nodeRg, "--output", "json",
|
|
70
|
+
...subArgs(sub),
|
|
71
|
+
], { reject: false, timeout: 15000 });
|
|
72
|
+
|
|
73
|
+
if (vnetCode !== 0 || !vnetListJson?.trim()) {
|
|
74
|
+
console.log(WARN(" ⚠ No VNet found in node resource group — skipping Postgres"));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const vnets = JSON.parse(vnetListJson);
|
|
79
|
+
const vnet = vnets[0];
|
|
80
|
+
if (!vnet) {
|
|
81
|
+
console.log(WARN(" ⚠ No VNet found — skipping Postgres"));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const vnetName = vnet.name;
|
|
86
|
+
const pgSubnetName = "postgres-subnet";
|
|
87
|
+
|
|
88
|
+
// Check if the postgres subnet already exists
|
|
89
|
+
const { exitCode: subnetExists } = await execa("az", [
|
|
90
|
+
"network", "vnet", "subnet", "show",
|
|
91
|
+
"-g", nodeRg, "--vnet-name", vnetName, "-n", pgSubnetName,
|
|
92
|
+
"--output", "none", ...subArgs(sub),
|
|
93
|
+
], { reject: false, timeout: 15000 });
|
|
94
|
+
|
|
95
|
+
if (subnetExists !== 0) {
|
|
96
|
+
const vnetPrefix = vnet.addressSpace?.addressPrefixes?.[0] || "10.224.0.0/12";
|
|
97
|
+
const pgCidr = await findAvailableSubnetCidr(execa, nodeRg, vnetName, vnetPrefix, sub);
|
|
98
|
+
|
|
99
|
+
hint(`Creating subnet "${pgSubnetName}" (${pgCidr}) in ${vnetName}…`);
|
|
100
|
+
const { exitCode: createCode, stderr: createErr } = await execa("az", [
|
|
101
|
+
"network", "vnet", "subnet", "create",
|
|
102
|
+
"-g", nodeRg, "--vnet-name", vnetName, "-n", pgSubnetName,
|
|
103
|
+
"--address-prefixes", pgCidr,
|
|
104
|
+
"--delegations", "Microsoft.DBforPostgreSQL/flexibleServers",
|
|
105
|
+
"--output", "none", ...subArgs(sub),
|
|
106
|
+
], { reject: false, timeout: 60000 });
|
|
107
|
+
if (createCode !== 0) {
|
|
108
|
+
const detail = (createErr || "").split("\n").filter(l => l.trim()).slice(-2).join(" ");
|
|
109
|
+
throw new Error(`Subnet creation failed (${pgCidr}): ${detail || "exit code " + createCode}`);
|
|
110
|
+
}
|
|
111
|
+
console.log(OK(` ✓ Subnet "${pgSubnetName}" created`));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Get subnet ID
|
|
115
|
+
const { stdout: subnetJson } = await execa("az", [
|
|
116
|
+
"network", "vnet", "subnet", "show",
|
|
117
|
+
"-g", nodeRg, "--vnet-name", vnetName, "-n", pgSubnetName,
|
|
118
|
+
"--output", "json", ...subArgs(sub),
|
|
119
|
+
], { timeout: 15000 });
|
|
120
|
+
const subnetId = JSON.parse(subnetJson).id;
|
|
121
|
+
|
|
122
|
+
// Create a private DNS zone for Postgres in the VNet
|
|
123
|
+
const dnsZone = `${serverName}.private.postgres.database.azure.com`;
|
|
124
|
+
const { exitCode: dnsExists } = await execa("az", [
|
|
125
|
+
"network", "private-dns", "zone", "show",
|
|
126
|
+
"-g", rg, "-n", dnsZone, "--output", "none",
|
|
127
|
+
...subArgs(sub),
|
|
128
|
+
], { reject: false, timeout: 15000 });
|
|
129
|
+
|
|
130
|
+
if (dnsExists !== 0) {
|
|
131
|
+
hint(`Creating private DNS zone ${dnsZone}…`);
|
|
132
|
+
await execa("az", [
|
|
133
|
+
"network", "private-dns", "zone", "create",
|
|
134
|
+
"-g", rg, "-n", dnsZone, "--output", "none",
|
|
135
|
+
...subArgs(sub),
|
|
136
|
+
], { timeout: 60000 });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 3. Create the Flexible Server
|
|
140
|
+
adminPassword = generatePassword();
|
|
141
|
+
console.log(chalk.yellow(` ↻ Creating Postgres Flexible Server "${serverName}"…`));
|
|
142
|
+
hint("This takes 3–5 minutes…");
|
|
143
|
+
|
|
144
|
+
await execa("az", [
|
|
145
|
+
"postgres", "flexible-server", "create",
|
|
146
|
+
"--name", serverName,
|
|
147
|
+
"--resource-group", rg,
|
|
148
|
+
"--location", location,
|
|
149
|
+
"--admin-user", PG_DEFAULTS.adminUser,
|
|
150
|
+
"--admin-password", adminPassword,
|
|
151
|
+
"--sku-name", PG_DEFAULTS.sku,
|
|
152
|
+
"--tier", PG_DEFAULTS.tier,
|
|
153
|
+
"--version", PG_DEFAULTS.version,
|
|
154
|
+
"--storage-size", String(PG_DEFAULTS.storageSizeGb),
|
|
155
|
+
"--storage-auto-grow", "Enabled",
|
|
156
|
+
"--subnet", subnetId,
|
|
157
|
+
"--private-dns-zone", dnsZone,
|
|
158
|
+
"--yes",
|
|
159
|
+
"--output", "json", ...subArgs(sub),
|
|
160
|
+
], { timeout: 600000 });
|
|
161
|
+
|
|
162
|
+
// Read the created server to get the FQDN
|
|
163
|
+
const { stdout: createdJson } = await execa("az", [
|
|
164
|
+
"postgres", "flexible-server", "show",
|
|
165
|
+
"--name", serverName, "--resource-group", rg,
|
|
166
|
+
"--output", "json", ...subArgs(sub),
|
|
167
|
+
], { timeout: 15000 });
|
|
168
|
+
fqdn = JSON.parse(createdJson).fullyQualifiedDomainName;
|
|
169
|
+
|
|
170
|
+
console.log(OK(` ✓ Postgres Flexible Server created (${fqdn})`));
|
|
171
|
+
|
|
172
|
+
// Save password + FQDN to local state
|
|
173
|
+
writeClusterState(clusterName, {
|
|
174
|
+
postgres: { serverName, fqdn, adminUser: PG_DEFAULTS.adminUser, adminPassword },
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 4. Allowlist pg_trgm extension (required by backend search migration)
|
|
179
|
+
const { stdout: extVal } = await execa("az", [
|
|
180
|
+
"postgres", "flexible-server", "parameter", "show",
|
|
181
|
+
"--resource-group", rg, "--server-name", serverName,
|
|
182
|
+
"--name", "azure.extensions", "--query", "value", "-o", "tsv",
|
|
183
|
+
...subArgs(sub),
|
|
184
|
+
], { reject: false, timeout: 15000 });
|
|
185
|
+
const currentExts = (extVal || "").trim().split(",").map(e => e.trim()).filter(Boolean);
|
|
186
|
+
if (!currentExts.includes("PG_TRGM") && !currentExts.includes("pg_trgm")) {
|
|
187
|
+
const newExts = [...currentExts, "pg_trgm"].join(",");
|
|
188
|
+
hint("Allowlisting pg_trgm extension…");
|
|
189
|
+
await execa("az", [
|
|
190
|
+
"postgres", "flexible-server", "parameter", "set",
|
|
191
|
+
"--resource-group", rg, "--server-name", serverName,
|
|
192
|
+
"--name", "azure.extensions", "--value", newExts,
|
|
193
|
+
"--output", "none", ...subArgs(sub),
|
|
194
|
+
], { reject: false, timeout: 60000 });
|
|
195
|
+
console.log(OK(" ✓ pg_trgm extension allowlisted"));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 5. Sync the K8s "postgres" secret into the foundation namespace
|
|
199
|
+
if (fqdn && adminPassword) {
|
|
200
|
+
await syncPostgresSecret(execa, { clusterName, fqdn, adminUser: PG_DEFAULTS.adminUser, adminPassword });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ── Sync Postgres secret to K8s ───────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
export async function syncPostgresSecret(execa, { clusterName, fqdn, adminUser, adminPassword }) {
|
|
207
|
+
const namespaces = ["foundation"];
|
|
208
|
+
for (const ns of namespaces) {
|
|
209
|
+
// Ensure namespace exists
|
|
210
|
+
await execa("kubectl", [
|
|
211
|
+
"--context", clusterName,
|
|
212
|
+
"create", "namespace", ns, "--dry-run=client", "-o", "yaml",
|
|
213
|
+
], { reject: false, timeout: 10000 }).then(({ stdout }) =>
|
|
214
|
+
execa("kubectl", ["--context", clusterName, "apply", "-f", "-"], { input: stdout, timeout: 10000, reject: false })
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
// Create or update the postgres secret
|
|
218
|
+
const { exitCode } = await execa("kubectl", [
|
|
219
|
+
"--context", clusterName, "-n", ns,
|
|
220
|
+
"create", "secret", "generic", "postgres",
|
|
221
|
+
"--from-literal=host=" + fqdn,
|
|
222
|
+
"--from-literal=superUserPassword=" + adminPassword,
|
|
223
|
+
"--from-literal=user=" + adminUser,
|
|
224
|
+
"--from-literal=password=" + adminPassword,
|
|
225
|
+
"--dry-run=client", "-o", "yaml",
|
|
226
|
+
], { timeout: 10000 }).then(({ stdout }) =>
|
|
227
|
+
execa("kubectl", [
|
|
228
|
+
"--context", clusterName, "-n", ns, "apply", "-f", "-",
|
|
229
|
+
], { input: stdout, timeout: 10000, reject: false })
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
if (exitCode === 0) {
|
|
233
|
+
console.log(OK(` ✓ Secret "postgres" synced to ${ns} namespace`));
|
|
234
|
+
} else {
|
|
235
|
+
console.log(WARN(` ⚠ Could not sync postgres secret to ${ns}`));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ── Postgres databases reconciliation ─────────────────────────────────────────
|
|
241
|
+
|
|
242
|
+
export async function reconcilePgDatabases(ctx) {
|
|
243
|
+
const { execa, clusterName } = ctx;
|
|
244
|
+
const kubectl = (args, opts = {}) =>
|
|
245
|
+
execa("kubectl", ["--context", clusterName, ...args], { timeout: 60000, reject: false, ...opts });
|
|
246
|
+
|
|
247
|
+
const pgServer = pgServerName(clusterName);
|
|
248
|
+
const pgHost = `${pgServer}.postgres.database.azure.com`;
|
|
249
|
+
|
|
250
|
+
// Read password from the postgres secret
|
|
251
|
+
const { stdout: pwB64 } = await kubectl([
|
|
252
|
+
"get", "secret", "postgres", "-n", "foundation",
|
|
253
|
+
"-o", "jsonpath={.data.password}",
|
|
254
|
+
]);
|
|
255
|
+
if (!pwB64) {
|
|
256
|
+
console.log(WARN(" ⚠ No postgres secret found — skipping DB setup"));
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const pgPass = Buffer.from(pwB64, "base64").toString();
|
|
260
|
+
|
|
261
|
+
// Run a psql job to create all databases and roles
|
|
262
|
+
const sqlLines = PG_SERVICE_DBS.map(db => [
|
|
263
|
+
`SELECT 'CREATE DATABASE ${db}' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '${db}');`,
|
|
264
|
+
`DO \\$\\$ BEGIN IF NOT EXISTS (SELECT FROM pg_database WHERE datname = '${db}') THEN EXECUTE 'CREATE DATABASE ${db}'; END IF; IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${db}') THEN CREATE ROLE ${db} LOGIN PASSWORD '${pgPass}'; END IF; END \\$\\$;`,
|
|
265
|
+
`GRANT ALL ON DATABASE ${db} TO ${db};`,
|
|
266
|
+
]).flat();
|
|
267
|
+
|
|
268
|
+
const script = sqlLines.map(s => `psql -c "${s}"`).join(" && ") + " && echo DONE";
|
|
269
|
+
|
|
270
|
+
const jobManifest = JSON.stringify({
|
|
271
|
+
apiVersion: "batch/v1", kind: "Job",
|
|
272
|
+
metadata: { name: "fops-pg-setup", namespace: "foundation" },
|
|
273
|
+
spec: {
|
|
274
|
+
backoffLimit: 2, ttlSecondsAfterFinished: 60,
|
|
275
|
+
template: {
|
|
276
|
+
spec: {
|
|
277
|
+
restartPolicy: "Never",
|
|
278
|
+
containers: [{
|
|
279
|
+
name: "psql",
|
|
280
|
+
image: "postgres:16-alpine",
|
|
281
|
+
env: [
|
|
282
|
+
{ name: "PGHOST", value: pgHost },
|
|
283
|
+
{ name: "PGUSER", value: "foundation" },
|
|
284
|
+
{ name: "PGDATABASE", value: "postgres" },
|
|
285
|
+
{ name: "PGPASSWORD", value: pgPass },
|
|
286
|
+
{ name: "PGSSLMODE", value: "require" },
|
|
287
|
+
],
|
|
288
|
+
command: ["sh", "-c", script],
|
|
289
|
+
}],
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// Delete old job if it exists
|
|
296
|
+
await kubectl(["delete", "job", "fops-pg-setup", "-n", "foundation", "--ignore-not-found"]);
|
|
297
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
298
|
+
|
|
299
|
+
const { exitCode, stderr } = await kubectl(["apply", "-f", "-"], { input: jobManifest });
|
|
300
|
+
if (exitCode !== 0) {
|
|
301
|
+
console.log(WARN(` ⚠ pg-setup job failed: ${(stderr || "").split("\n")[0]}`));
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Wait for job to complete (max 60s)
|
|
306
|
+
const { exitCode: waitCode } = await execa("kubectl", [
|
|
307
|
+
"--context", clusterName,
|
|
308
|
+
"wait", "--for=condition=complete", "job/fops-pg-setup",
|
|
309
|
+
"-n", "foundation", "--timeout=60s",
|
|
310
|
+
], { timeout: 70000, reject: false });
|
|
311
|
+
|
|
312
|
+
if (waitCode === 0) {
|
|
313
|
+
console.log(OK(` ✓ Postgres databases ready (${PG_SERVICE_DBS.join(", ")})`));
|
|
314
|
+
} else {
|
|
315
|
+
const { stdout: logs } = await kubectl([
|
|
316
|
+
"logs", "job/fops-pg-setup", "-n", "foundation", "--tail=5",
|
|
317
|
+
]);
|
|
318
|
+
if (logs?.includes("DONE")) {
|
|
319
|
+
console.log(OK(` ✓ Postgres databases ready (${PG_SERVICE_DBS.join(", ")})`));
|
|
320
|
+
} else {
|
|
321
|
+
console.log(WARN(" ⚠ pg-setup job didn't complete — check: kubectl logs job/fops-pg-setup -n foundation"));
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
await kubectl(["delete", "job", "fops-pg-setup", "-n", "foundation", "--ignore-not-found"]);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ── Postgres read replicas ────────────────────────────────────────────────────
|
|
329
|
+
|
|
330
|
+
export async function aksPostgresReplicaCreate(opts = {}) {
|
|
331
|
+
const { lazyExeca, ensureAzCli, ensureAzAuth } = await import("./azure.js");
|
|
332
|
+
const execa = await lazyExeca();
|
|
333
|
+
const sub = opts.profile;
|
|
334
|
+
await ensureAzCli(execa);
|
|
335
|
+
await ensureAzAuth(execa, { subscription: sub });
|
|
336
|
+
|
|
337
|
+
const { clusterName, resourceGroup: rg } = requireCluster(opts.clusterName);
|
|
338
|
+
const sourceServer = pgServerName(clusterName);
|
|
339
|
+
const targetRegion = opts.region || PG_REPLICA_REGIONS[opts.sourceRegion] || "westeurope";
|
|
340
|
+
const replicaName = opts.replicaName || `${sourceServer}-replica-${targetRegion.replace(/[^a-z]/g, "")}`;
|
|
341
|
+
|
|
342
|
+
banner("Postgres Read Replica");
|
|
343
|
+
kvLine("Source", sourceServer);
|
|
344
|
+
kvLine("Replica", replicaName);
|
|
345
|
+
kvLine("Region", targetRegion);
|
|
346
|
+
|
|
347
|
+
// Get source server resource ID
|
|
348
|
+
const { stdout: sourceJson, exitCode: sourceExists } = await execa("az", [
|
|
349
|
+
"postgres", "flexible-server", "show",
|
|
350
|
+
"--name", sourceServer, "--resource-group", rg,
|
|
351
|
+
"--output", "json", ...subArgs(sub),
|
|
352
|
+
], { reject: false, timeout: 30000 });
|
|
353
|
+
|
|
354
|
+
if (sourceExists !== 0) {
|
|
355
|
+
console.error(ERR(`\n ✗ Source server "${sourceServer}" not found`));
|
|
356
|
+
process.exit(1);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const source = JSON.parse(sourceJson);
|
|
360
|
+
const sourceId = source.id;
|
|
361
|
+
|
|
362
|
+
// Check if replica already exists
|
|
363
|
+
const { exitCode: replicaExists } = await execa("az", [
|
|
364
|
+
"postgres", "flexible-server", "show",
|
|
365
|
+
"--name", replicaName, "--resource-group", rg,
|
|
366
|
+
"--output", "none", ...subArgs(sub),
|
|
367
|
+
], { reject: false, timeout: 30000 });
|
|
368
|
+
|
|
369
|
+
if (replicaExists === 0) {
|
|
370
|
+
console.log(OK(`\n ✓ Replica "${replicaName}" already exists`));
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
hint("Creating read replica (this takes 10-15 minutes)…\n");
|
|
375
|
+
|
|
376
|
+
const { exitCode, stderr } = await execa("az", [
|
|
377
|
+
"postgres", "flexible-server", "replica", "create",
|
|
378
|
+
"--replica-name", replicaName,
|
|
379
|
+
"--resource-group", rg,
|
|
380
|
+
"--source-server", sourceId,
|
|
381
|
+
"--location", targetRegion,
|
|
382
|
+
"--output", "json", ...subArgs(sub),
|
|
383
|
+
], { timeout: 900000, reject: false });
|
|
384
|
+
|
|
385
|
+
if (exitCode !== 0) {
|
|
386
|
+
const errMsg = (stderr || "").split("\n").find(l => l.includes("ERROR")) || stderr;
|
|
387
|
+
console.error(ERR(`\n ✗ Replica creation failed: ${errMsg}`));
|
|
388
|
+
process.exit(1);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Get replica FQDN
|
|
392
|
+
const { stdout: replicaJson } = await execa("az", [
|
|
393
|
+
"postgres", "flexible-server", "show",
|
|
394
|
+
"--name", replicaName, "--resource-group", rg,
|
|
395
|
+
"--output", "json", ...subArgs(sub),
|
|
396
|
+
], { timeout: 30000 });
|
|
397
|
+
const replica = JSON.parse(replicaJson);
|
|
398
|
+
|
|
399
|
+
console.log(OK(`\n ✓ Read replica created`));
|
|
400
|
+
kvLine("FQDN", replica.fullyQualifiedDomainName);
|
|
401
|
+
kvLine("Region", replica.location);
|
|
402
|
+
kvLine("State", replica.state);
|
|
403
|
+
|
|
404
|
+
// Save replica info to state
|
|
405
|
+
const cl = readClusterState(clusterName);
|
|
406
|
+
const replicas = cl.postgres?.replicas || [];
|
|
407
|
+
replicas.push({
|
|
408
|
+
name: replicaName,
|
|
409
|
+
fqdn: replica.fullyQualifiedDomainName,
|
|
410
|
+
region: targetRegion,
|
|
411
|
+
createdAt: new Date().toISOString(),
|
|
412
|
+
});
|
|
413
|
+
writeClusterState(clusterName, {
|
|
414
|
+
postgres: { ...cl.postgres, replicas },
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
hint(`\n Connect: psql "host=${replica.fullyQualifiedDomainName} user=foundation sslmode=require"`);
|
|
418
|
+
hint(" Note: Replica is read-only. Use for read scaling or DR.\n");
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export async function aksPostgresReplicaList(opts = {}) {
|
|
422
|
+
const { lazyExeca, ensureAzCli, ensureAzAuth, LABEL } = await import("./azure.js");
|
|
423
|
+
const execa = await lazyExeca();
|
|
424
|
+
const sub = opts.profile;
|
|
425
|
+
await ensureAzCli(execa);
|
|
426
|
+
await ensureAzAuth(execa, { subscription: sub });
|
|
427
|
+
|
|
428
|
+
const { clusterName, resourceGroup: rg } = requireCluster(opts.clusterName);
|
|
429
|
+
const sourceServer = pgServerName(clusterName);
|
|
430
|
+
|
|
431
|
+
banner(`Postgres Replicas: ${sourceServer}`);
|
|
432
|
+
|
|
433
|
+
const { stdout: replicasJson } = await execa("az", [
|
|
434
|
+
"postgres", "flexible-server", "replica", "list",
|
|
435
|
+
"--name", sourceServer, "--resource-group", rg,
|
|
436
|
+
"--output", "json", ...subArgs(sub),
|
|
437
|
+
], { timeout: 60000 });
|
|
438
|
+
|
|
439
|
+
const replicas = JSON.parse(replicasJson || "[]");
|
|
440
|
+
|
|
441
|
+
if (replicas.length === 0) {
|
|
442
|
+
console.log(DIM("\n No replicas found.\n"));
|
|
443
|
+
hint(`Create one: fops azure aks postgres replica create --region westeurope`);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
console.log("");
|
|
448
|
+
for (const r of replicas) {
|
|
449
|
+
const state = r.state === "Ready" ? OK(r.state) : WARN(r.state);
|
|
450
|
+
console.log(` ${LABEL(r.name)}`);
|
|
451
|
+
kvLine(" FQDN", DIM(r.fullyQualifiedDomainName), { pad: 10 });
|
|
452
|
+
kvLine(" Region", DIM(r.location), { pad: 10 });
|
|
453
|
+
kvLine(" State", state, { pad: 10 });
|
|
454
|
+
kvLine(" Role", DIM(r.replicationRole || "Replica"), { pad: 10 });
|
|
455
|
+
console.log("");
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export async function aksPostgresReplicaPromote(opts = {}) {
|
|
460
|
+
const { lazyExeca, ensureAzCli, ensureAzAuth } = await import("./azure.js");
|
|
461
|
+
const execa = await lazyExeca();
|
|
462
|
+
const sub = opts.profile;
|
|
463
|
+
await ensureAzCli(execa);
|
|
464
|
+
await ensureAzAuth(execa, { subscription: sub });
|
|
465
|
+
|
|
466
|
+
const { resourceGroup: rg } = requireCluster(opts.clusterName);
|
|
467
|
+
const replicaName = opts.replicaName;
|
|
468
|
+
|
|
469
|
+
if (!replicaName) {
|
|
470
|
+
console.error(ERR("\n --replica-name is required"));
|
|
471
|
+
process.exit(1);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
banner("Promote Replica");
|
|
475
|
+
kvLine("Replica", replicaName);
|
|
476
|
+
|
|
477
|
+
console.log(WARN("\n ⚠ WARNING: Promoting a replica breaks replication permanently."));
|
|
478
|
+
console.log(WARN(" The replica becomes a standalone server."));
|
|
479
|
+
console.log(WARN(" This is typically used for disaster recovery failover.\n"));
|
|
480
|
+
|
|
481
|
+
if (!opts.yes) {
|
|
482
|
+
const readline = await import("node:readline");
|
|
483
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
484
|
+
const answer = await new Promise(resolve => {
|
|
485
|
+
rl.question(` Promote "${replicaName}" to standalone? [y/N] `, resolve);
|
|
486
|
+
});
|
|
487
|
+
rl.close();
|
|
488
|
+
if (answer.toLowerCase() !== "y") {
|
|
489
|
+
console.log(DIM(" Cancelled.\n"));
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
hint("Promoting replica (this takes a few minutes)…\n");
|
|
495
|
+
|
|
496
|
+
const { exitCode, stderr } = await execa("az", [
|
|
497
|
+
"postgres", "flexible-server", "replica", "stop-replication",
|
|
498
|
+
"--name", replicaName, "--resource-group", rg,
|
|
499
|
+
"--yes", "--output", "none", ...subArgs(sub),
|
|
500
|
+
], { timeout: 600000, reject: false });
|
|
501
|
+
|
|
502
|
+
if (exitCode !== 0) {
|
|
503
|
+
console.error(ERR(`\n ✗ Promotion failed: ${stderr}`));
|
|
504
|
+
process.exit(1);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
console.log(OK(`\n ✓ Replica "${replicaName}" promoted to standalone server`));
|
|
508
|
+
hint(" Update your application connection strings to point to the new primary.\n");
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
export async function aksPostgresReplicaDelete(opts = {}) {
|
|
512
|
+
const { lazyExeca, ensureAzCli, ensureAzAuth } = await import("./azure.js");
|
|
513
|
+
const execa = await lazyExeca();
|
|
514
|
+
const sub = opts.profile;
|
|
515
|
+
await ensureAzCli(execa);
|
|
516
|
+
await ensureAzAuth(execa, { subscription: sub });
|
|
517
|
+
|
|
518
|
+
const { clusterName, resourceGroup: rg } = requireCluster(opts.clusterName);
|
|
519
|
+
const replicaName = opts.replicaName;
|
|
520
|
+
|
|
521
|
+
if (!replicaName) {
|
|
522
|
+
console.error(ERR("\n --replica-name is required"));
|
|
523
|
+
process.exit(1);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
banner("Delete Replica");
|
|
527
|
+
kvLine("Replica", replicaName);
|
|
528
|
+
|
|
529
|
+
if (!opts.yes) {
|
|
530
|
+
const readline = await import("node:readline");
|
|
531
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
532
|
+
const answer = await new Promise(resolve => {
|
|
533
|
+
rl.question(` Delete replica "${replicaName}"? [y/N] `, resolve);
|
|
534
|
+
});
|
|
535
|
+
rl.close();
|
|
536
|
+
if (answer.toLowerCase() !== "y") {
|
|
537
|
+
console.log(DIM(" Cancelled.\n"));
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
hint("Deleting replica…\n");
|
|
543
|
+
|
|
544
|
+
const { exitCode, stderr } = await execa("az", [
|
|
545
|
+
"postgres", "flexible-server", "delete",
|
|
546
|
+
"--name", replicaName, "--resource-group", rg,
|
|
547
|
+
"--yes", "--output", "none", ...subArgs(sub),
|
|
548
|
+
], { timeout: 300000, reject: false });
|
|
549
|
+
|
|
550
|
+
if (exitCode !== 0) {
|
|
551
|
+
console.error(ERR(`\n ✗ Delete failed: ${stderr}`));
|
|
552
|
+
process.exit(1);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Remove from state
|
|
556
|
+
const cl = readClusterState(clusterName);
|
|
557
|
+
if (cl.postgres?.replicas) {
|
|
558
|
+
cl.postgres.replicas = cl.postgres.replicas.filter(r => r.name !== replicaName);
|
|
559
|
+
writeClusterState(clusterName, { postgres: cl.postgres });
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
console.log(OK(`\n ✓ Replica "${replicaName}" deleted\n`));
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// ── Event Hubs (managed Kafka) ─────────────────────────────────────────────────
|
|
566
|
+
|
|
567
|
+
export async function reconcileEventHubs(ctx) {
|
|
568
|
+
if (!ctx.opts?.managedKafka) return;
|
|
569
|
+
|
|
570
|
+
const { execa, clusterName, rg, sub, cluster } = ctx;
|
|
571
|
+
const location = cluster.location || DEFAULTS.location;
|
|
572
|
+
const nsName = ehNamespaceName(clusterName);
|
|
573
|
+
|
|
574
|
+
// 1. Check if namespace already exists
|
|
575
|
+
const { exitCode: ehExists, stdout: ehJson } = await execa("az", [
|
|
576
|
+
"eventhubs", "namespace", "show",
|
|
577
|
+
"--name", nsName, "--resource-group", rg,
|
|
578
|
+
"--output", "json", ...subArgs(sub),
|
|
579
|
+
], { reject: false, timeout: 30000 });
|
|
580
|
+
|
|
581
|
+
let endpoint = "";
|
|
582
|
+
if (ehExists === 0 && ehJson) {
|
|
583
|
+
try {
|
|
584
|
+
const ns = JSON.parse(ehJson);
|
|
585
|
+
endpoint = ns.serviceBusEndpoint || "";
|
|
586
|
+
console.log(OK(` ✓ Event Hubs namespace "${nsName}" exists`));
|
|
587
|
+
} catch {}
|
|
588
|
+
} else {
|
|
589
|
+
// Create namespace
|
|
590
|
+
console.log(chalk.yellow(` ↻ Creating Event Hubs namespace "${nsName}"…`));
|
|
591
|
+
try {
|
|
592
|
+
const { stdout: createdJson } = await execa("az", [
|
|
593
|
+
"eventhubs", "namespace", "create",
|
|
594
|
+
"--name", nsName,
|
|
595
|
+
"--resource-group", rg,
|
|
596
|
+
"--location", location,
|
|
597
|
+
"--sku", EH_DEFAULTS.sku,
|
|
598
|
+
"--capacity", String(EH_DEFAULTS.capacity),
|
|
599
|
+
"--enable-kafka", "true",
|
|
600
|
+
"--output", "json", ...subArgs(sub),
|
|
601
|
+
], { timeout: 300000 });
|
|
602
|
+
const ns = JSON.parse(createdJson);
|
|
603
|
+
endpoint = ns.serviceBusEndpoint || "";
|
|
604
|
+
console.log(OK(` ✓ Event Hubs namespace created`));
|
|
605
|
+
} catch (err) {
|
|
606
|
+
const msg = (err.stderr || err.message || "").split("\n")[0];
|
|
607
|
+
console.log(WARN(` ⚠ Event Hubs creation failed: ${msg}`));
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// 2. Disable public network access
|
|
613
|
+
hint("Disabling public access…");
|
|
614
|
+
await execa("az", [
|
|
615
|
+
"eventhubs", "namespace", "update",
|
|
616
|
+
"--name", nsName, "--resource-group", rg,
|
|
617
|
+
"--public-network-access", "Disabled",
|
|
618
|
+
"--output", "none", ...subArgs(sub),
|
|
619
|
+
], { reject: false, timeout: 120000 });
|
|
620
|
+
|
|
621
|
+
// 3. Create private endpoint if not exists
|
|
622
|
+
const nodeRg = cluster.nodeResourceGroup;
|
|
623
|
+
const vnetName = await findAksVnet(execa, { nodeRg, sub });
|
|
624
|
+
if (!vnetName) {
|
|
625
|
+
console.log(WARN(" ⚠ Could not find AKS VNet — skipping private endpoint"));
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const peName = `${nsName}-pe`;
|
|
630
|
+
const { exitCode: peExists } = await execa("az", [
|
|
631
|
+
"network", "private-endpoint", "show",
|
|
632
|
+
"--name", peName, "--resource-group", rg,
|
|
633
|
+
"--output", "none", ...subArgs(sub),
|
|
634
|
+
], { reject: false, timeout: 30000 });
|
|
635
|
+
|
|
636
|
+
if (peExists !== 0) {
|
|
637
|
+
hint("Creating private endpoint…");
|
|
638
|
+
|
|
639
|
+
// Get Event Hubs resource ID
|
|
640
|
+
const { stdout: ehIdJson } = await execa("az", [
|
|
641
|
+
"eventhubs", "namespace", "show",
|
|
642
|
+
"--name", nsName, "--resource-group", rg,
|
|
643
|
+
"--query", "id", "-o", "tsv", ...subArgs(sub),
|
|
644
|
+
], { timeout: 15000 });
|
|
645
|
+
const ehId = ehIdJson.trim();
|
|
646
|
+
|
|
647
|
+
// Create subnet for private endpoints if not exists
|
|
648
|
+
const peSubnetName = "private-endpoints";
|
|
649
|
+
const { exitCode: subnetExists } = await execa("az", [
|
|
650
|
+
"network", "vnet", "subnet", "show",
|
|
651
|
+
"--resource-group", nodeRg, "--vnet-name", vnetName, "--name", peSubnetName,
|
|
652
|
+
"--output", "none", ...subArgs(sub),
|
|
653
|
+
], { reject: false, timeout: 15000 });
|
|
654
|
+
|
|
655
|
+
if (subnetExists !== 0) {
|
|
656
|
+
await execa("az", [
|
|
657
|
+
"network", "vnet", "subnet", "create",
|
|
658
|
+
"--resource-group", nodeRg, "--vnet-name", vnetName, "--name", peSubnetName,
|
|
659
|
+
"--address-prefixes", "10.225.0.0/24",
|
|
660
|
+
"--disable-private-endpoint-network-policies", "true",
|
|
661
|
+
"--output", "none", ...subArgs(sub),
|
|
662
|
+
], { timeout: 60000 });
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Create private endpoint
|
|
666
|
+
await execa("az", [
|
|
667
|
+
"network", "private-endpoint", "create",
|
|
668
|
+
"--name", peName,
|
|
669
|
+
"--resource-group", rg,
|
|
670
|
+
"--vnet-name", vnetName,
|
|
671
|
+
"--subnet", peSubnetName,
|
|
672
|
+
"--private-connection-resource-id", ehId,
|
|
673
|
+
"--group-id", "namespace",
|
|
674
|
+
"--connection-name", `${nsName}-connection`,
|
|
675
|
+
"--output", "none", ...subArgs(sub),
|
|
676
|
+
], { timeout: 120000 });
|
|
677
|
+
console.log(OK(" ✓ Private endpoint created"));
|
|
678
|
+
|
|
679
|
+
// 4. Create private DNS zone and link
|
|
680
|
+
const dnsZoneName = "privatelink.servicebus.windows.net";
|
|
681
|
+
const { exitCode: zoneExists } = await execa("az", [
|
|
682
|
+
"network", "private-dns", "zone", "show",
|
|
683
|
+
"--resource-group", rg, "--name", dnsZoneName,
|
|
684
|
+
"--output", "none", ...subArgs(sub),
|
|
685
|
+
], { reject: false, timeout: 15000 });
|
|
686
|
+
|
|
687
|
+
if (zoneExists !== 0) {
|
|
688
|
+
await execa("az", [
|
|
689
|
+
"network", "private-dns", "zone", "create",
|
|
690
|
+
"--resource-group", rg, "--name", dnsZoneName,
|
|
691
|
+
"--output", "none", ...subArgs(sub),
|
|
692
|
+
], { timeout: 60000 });
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Link DNS zone to VNet
|
|
696
|
+
const linkName = `${clusterName}-eh-link`;
|
|
697
|
+
const { exitCode: linkExists } = await execa("az", [
|
|
698
|
+
"network", "private-dns", "link", "vnet", "show",
|
|
699
|
+
"--resource-group", rg, "--zone-name", dnsZoneName, "--name", linkName,
|
|
700
|
+
"--output", "none", ...subArgs(sub),
|
|
701
|
+
], { reject: false, timeout: 15000 });
|
|
702
|
+
|
|
703
|
+
if (linkExists !== 0) {
|
|
704
|
+
const { stdout: vnetIdJson } = await execa("az", [
|
|
705
|
+
"network", "vnet", "show",
|
|
706
|
+
"--resource-group", nodeRg, "--name", vnetName,
|
|
707
|
+
"--query", "id", "-o", "tsv", ...subArgs(sub),
|
|
708
|
+
], { timeout: 15000 });
|
|
709
|
+
await execa("az", [
|
|
710
|
+
"network", "private-dns", "link", "vnet", "create",
|
|
711
|
+
"--resource-group", rg, "--zone-name", dnsZoneName, "--name", linkName,
|
|
712
|
+
"--virtual-network", vnetIdJson.trim(),
|
|
713
|
+
"--registration-enabled", "false",
|
|
714
|
+
"--output", "none", ...subArgs(sub),
|
|
715
|
+
], { timeout: 60000 });
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// Create DNS record for the private endpoint
|
|
719
|
+
const { stdout: peIpJson } = await execa("az", [
|
|
720
|
+
"network", "private-endpoint", "show",
|
|
721
|
+
"--name", peName, "--resource-group", rg,
|
|
722
|
+
"--query", "customDnsConfigs[0].ipAddresses[0]", "-o", "tsv", ...subArgs(sub),
|
|
723
|
+
], { reject: false, timeout: 15000 });
|
|
724
|
+
const peIp = (peIpJson || "").trim();
|
|
725
|
+
|
|
726
|
+
if (peIp) {
|
|
727
|
+
await execa("az", [
|
|
728
|
+
"network", "private-dns", "record-set", "a", "add-record",
|
|
729
|
+
"--resource-group", rg, "--zone-name", dnsZoneName,
|
|
730
|
+
"--record-set-name", nsName, "--ipv4-address", peIp,
|
|
731
|
+
"--output", "none", ...subArgs(sub),
|
|
732
|
+
], { reject: false, timeout: 30000 });
|
|
733
|
+
console.log(OK(" ✓ Private DNS configured"));
|
|
734
|
+
}
|
|
735
|
+
} else {
|
|
736
|
+
console.log(OK(" ✓ Private endpoint already exists"));
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// 5. Get connection string and create K8s secret
|
|
740
|
+
const { stdout: connStrJson } = await execa("az", [
|
|
741
|
+
"eventhubs", "namespace", "authorization-rule", "keys", "list",
|
|
742
|
+
"--resource-group", rg, "--namespace-name", nsName, "--name", "RootManageSharedAccessKey",
|
|
743
|
+
"--query", "primaryConnectionString", "-o", "tsv", ...subArgs(sub),
|
|
744
|
+
], { reject: false, timeout: 15000 });
|
|
745
|
+
const connStr = (connStrJson || "").trim();
|
|
746
|
+
|
|
747
|
+
if (connStr) {
|
|
748
|
+
const kafkaBootstrap = `${nsName}.servicebus.windows.net:9093`;
|
|
749
|
+
const kubectl = (args, opts = {}) =>
|
|
750
|
+
execa("kubectl", ["--context", clusterName, ...args], { timeout: 30000, reject: false, ...opts });
|
|
751
|
+
|
|
752
|
+
await kubectl([
|
|
753
|
+
"-n", "foundation", "create", "secret", "generic", "kafka-eventhubs",
|
|
754
|
+
"--from-literal=bootstrap-servers=" + kafkaBootstrap,
|
|
755
|
+
"--from-literal=connection-string=" + connStr,
|
|
756
|
+
"--from-literal=sasl-username=$ConnectionString",
|
|
757
|
+
"--from-literal=sasl-password=" + connStr,
|
|
758
|
+
"--dry-run=client", "-o", "yaml",
|
|
759
|
+
]).then(({ stdout }) =>
|
|
760
|
+
kubectl(["-n", "foundation", "apply", "-f", "-"], { input: stdout })
|
|
761
|
+
);
|
|
762
|
+
console.log(OK(` ✓ Kafka connection secret created (${kafkaBootstrap})`));
|
|
763
|
+
|
|
764
|
+
writeClusterState(clusterName, {
|
|
765
|
+
eventHubs: { namespace: nsName, bootstrap: kafkaBootstrap },
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
}
|