@rulebricks/cli 2.1.7 → 2.3.2
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/README.md +51 -16
- package/cluster-setup/aws/README.md +96 -47
- package/cluster-setup/aws/check-aws-access.sh +216 -52
- package/cluster-setup/aws/parameters.json +13 -0
- package/cluster-setup/aws/rulebricks-cluster.cfn.yaml +355 -0
- package/cluster-setup/azure/README.md +103 -55
- package/cluster-setup/azure/check-aks-prereqs.sh +236 -56
- package/cluster-setup/azure/parameters.json +30 -0
- package/cluster-setup/azure/rulebricks-cluster.bicep +546 -0
- package/cluster-setup/gcp/README.md +51 -34
- package/cluster-setup/gcp/check-gke-prereqs.sh +222 -60
- package/dist/commands/backup.d.ts +5 -0
- package/dist/commands/backup.js +104 -0
- package/dist/commands/deploy.d.ts +3 -1
- package/dist/commands/deploy.js +226 -326
- package/dist/commands/destroy.d.ts +1 -1
- package/dist/commands/destroy.js +73 -123
- package/dist/commands/init.d.ts +5 -1
- package/dist/commands/init.js +78 -54
- package/dist/commands/list.d.ts +1 -0
- package/dist/commands/list.js +74 -0
- package/dist/commands/open.d.ts +1 -1
- package/dist/commands/open.js +4 -12
- package/dist/commands/redeploy.d.ts +6 -0
- package/dist/commands/redeploy.js +310 -0
- package/dist/commands/restore.d.ts +5 -0
- package/dist/commands/restore.js +338 -0
- package/dist/commands/status.js +62 -49
- package/dist/commands/upgrade.js +74 -51
- package/dist/components/DNSWaitScreen.d.ts +5 -1
- package/dist/components/DNSWaitScreen.js +47 -41
- package/dist/components/Wizard/WizardContext.d.ts +157 -36
- package/dist/components/Wizard/WizardContext.js +872 -160
- package/dist/components/Wizard/steps/CloudProviderStep.js +192 -107
- package/dist/components/Wizard/steps/DomainStep.js +5 -24
- package/dist/components/Wizard/steps/ExternalServicesStep.d.ts +6 -0
- package/dist/components/Wizard/steps/ExternalServicesStep.js +645 -0
- package/dist/components/Wizard/steps/FeatureConfigStep.d.ts +2 -1
- package/dist/components/Wizard/steps/FeatureConfigStep.js +739 -425
- package/dist/components/Wizard/steps/FeaturesStep.js +31 -35
- package/dist/components/Wizard/steps/ObservabilityStep.d.ts +6 -0
- package/dist/components/Wizard/steps/ObservabilityStep.js +137 -0
- package/dist/components/Wizard/steps/ReviewStep.d.ts +2 -1
- package/dist/components/Wizard/steps/ReviewStep.js +56 -12
- package/dist/components/Wizard/steps/StorageStep.d.ts +9 -0
- package/dist/components/Wizard/steps/StorageStep.js +592 -0
- package/dist/components/Wizard/steps/SupabaseCredentialsStep.js +20 -21
- package/dist/components/Wizard/steps/VersionStep.js +45 -23
- package/dist/components/Wizard/steps/index.d.ts +3 -3
- package/dist/components/Wizard/steps/index.js +3 -3
- package/dist/components/common/CommandApproval.d.ts +12 -0
- package/dist/components/common/CommandApproval.js +91 -0
- package/dist/components/common/DeploymentPicker.d.ts +14 -0
- package/dist/components/common/DeploymentPicker.js +16 -0
- package/dist/components/common/index.d.ts +2 -0
- package/dist/components/common/index.js +2 -0
- package/dist/index.js +94 -62
- package/dist/lib/cloudCli.d.ts +134 -63
- package/dist/lib/cloudCli.js +512 -220
- package/dist/lib/clusterSetupDefaults.d.ts +30 -0
- package/dist/lib/clusterSetupDefaults.js +64 -0
- package/dist/lib/commandApproval.d.ts +26 -0
- package/dist/lib/commandApproval.js +114 -0
- package/dist/lib/config.d.ts +12 -10
- package/dist/lib/config.js +91 -33
- package/dist/lib/configFixtures.d.ts +5 -0
- package/dist/lib/configFixtures.js +513 -0
- package/dist/lib/deploymentHealth.d.ts +32 -0
- package/dist/lib/deploymentHealth.js +157 -0
- package/dist/lib/dns.d.ts +1 -1
- package/dist/lib/dns.js +19 -1
- package/dist/lib/dns.test.d.ts +1 -0
- package/dist/lib/dns.test.js +27 -0
- package/dist/lib/dockerHub.d.ts +12 -1
- package/dist/lib/dockerHub.js +18 -8
- package/dist/lib/helm.d.ts +4 -0
- package/dist/lib/helm.js +16 -0
- package/dist/lib/helmValues.d.ts +25 -0
- package/dist/lib/helmValues.js +1841 -289
- package/dist/lib/helmValues.test.d.ts +1 -0
- package/dist/lib/helmValues.test.js +1012 -0
- package/dist/lib/htpasswd.d.ts +1 -0
- package/dist/lib/htpasswd.js +15 -0
- package/dist/lib/kubernetes.d.ts +124 -17
- package/dist/lib/kubernetes.js +576 -145
- package/dist/lib/secrets.d.ts +23 -0
- package/dist/lib/secrets.js +158 -0
- package/dist/lib/validateValues.d.ts +31 -0
- package/dist/lib/validateValues.js +253 -0
- package/dist/lib/versions.d.ts +82 -11
- package/dist/lib/versions.js +131 -31
- package/dist/lib/versions.test.d.ts +1 -0
- package/dist/lib/versions.test.js +81 -0
- package/dist/lib/wizardSteps.d.ts +14 -0
- package/dist/lib/wizardSteps.js +23 -0
- package/dist/lib/workloadIdentity.d.ts +26 -0
- package/dist/lib/workloadIdentity.js +323 -0
- package/dist/lib/workloadIdentity.test.d.ts +1 -0
- package/dist/lib/workloadIdentity.test.js +57 -0
- package/dist/types/index.d.ts +1860 -164
- package/dist/types/index.js +518 -295
- package/package.json +9 -4
- package/schema/values.schema.json +1934 -0
- package/cluster-setup/aws/cluster.yaml +0 -33
- package/cluster-setup/azure/main.bicep +0 -282
- package/cluster-setup/azure/main.parameters.json +0 -21
- package/dist/components/Wizard/steps/CredentialsStep.d.ts +0 -6
- package/dist/components/Wizard/steps/CredentialsStep.js +0 -22
- package/dist/components/Wizard/steps/DeploymentModeStep.d.ts +0 -5
- package/dist/components/Wizard/steps/DeploymentModeStep.js +0 -26
- package/dist/components/Wizard/steps/TierStep.d.ts +0 -6
- package/dist/components/Wizard/steps/TierStep.js +0 -29
- package/dist/lib/terraform.d.ts +0 -66
- package/dist/lib/terraform.js +0 -754
- package/terraform/aws/main.tf +0 -355
- package/terraform/azure/main.tf +0 -371
- package/terraform/gcp/main.tf +0 -407
package/dist/lib/cloudCli.js
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
* Cloud CLI detection and dynamic resource listing
|
|
3
3
|
*
|
|
4
4
|
* Detects installed cloud CLIs (AWS, GCP, Azure), checks authentication status,
|
|
5
|
-
* and provides functions to list regions and
|
|
5
|
+
* and provides functions to list regions, clusters, and storage dynamically.
|
|
6
6
|
*/
|
|
7
7
|
import { exec } from "child_process";
|
|
8
8
|
import { promisify } from "util";
|
|
9
9
|
import { CLOUD_REGIONS } from "../types/index.js";
|
|
10
|
+
import { approveCloudCommandOrThrow } from "./commandApproval.js";
|
|
10
11
|
const execAsync = promisify(exec);
|
|
11
12
|
// Timeout for CLI commands (in ms)
|
|
12
13
|
const CLI_TIMEOUT = 15000;
|
|
@@ -23,11 +24,17 @@ function sortRegionsByPriority(regions, provider) {
|
|
|
23
24
|
const otherRegions = regions.filter((r) => !prioritySet.has(r)).sort();
|
|
24
25
|
return [...priorityRegions, ...otherRegions];
|
|
25
26
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
async function execCommand(command, timeout = CLI_TIMEOUT) {
|
|
27
|
+
async function execCommand(command, options = {}) {
|
|
28
|
+
const opts = typeof options === "number" ? { timeout: options } : options;
|
|
29
|
+
const timeout = opts.timeout ?? CLI_TIMEOUT;
|
|
30
30
|
try {
|
|
31
|
+
await approveCloudCommandOrThrow({
|
|
32
|
+
intent: opts.intent ?? inferCommandIntent(command),
|
|
33
|
+
command,
|
|
34
|
+
description: opts.description,
|
|
35
|
+
provider: opts.provider ?? inferProvider(command),
|
|
36
|
+
mutating: opts.mutating,
|
|
37
|
+
});
|
|
31
38
|
const result = await execAsync(command, { timeout });
|
|
32
39
|
return result;
|
|
33
40
|
}
|
|
@@ -43,6 +50,53 @@ async function execCommand(command, timeout = CLI_TIMEOUT) {
|
|
|
43
50
|
throw error;
|
|
44
51
|
}
|
|
45
52
|
}
|
|
53
|
+
function inferProvider(command) {
|
|
54
|
+
if (command.startsWith("aws "))
|
|
55
|
+
return "aws";
|
|
56
|
+
if (command.startsWith("gcloud "))
|
|
57
|
+
return "gcp";
|
|
58
|
+
if (command.startsWith("az "))
|
|
59
|
+
return "azure";
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
function inferCommandIntent(command) {
|
|
63
|
+
if (command.includes("--version") ||
|
|
64
|
+
command.includes("get-caller-identity") ||
|
|
65
|
+
command.includes("gcloud config list") ||
|
|
66
|
+
command.includes("az account show")) {
|
|
67
|
+
return "Detect cloud CLIs";
|
|
68
|
+
}
|
|
69
|
+
if (command.includes("describe-regions") ||
|
|
70
|
+
command.includes("compute regions list") ||
|
|
71
|
+
command.includes("list-locations")) {
|
|
72
|
+
return "List available regions";
|
|
73
|
+
}
|
|
74
|
+
if (command.includes("eks list-clusters") ||
|
|
75
|
+
command.includes("eks describe-cluster") ||
|
|
76
|
+
command.includes("container clusters list") ||
|
|
77
|
+
command.includes("az aks list")) {
|
|
78
|
+
return "Discover clusters";
|
|
79
|
+
}
|
|
80
|
+
if (command.includes("update-kubeconfig") ||
|
|
81
|
+
command.includes("get-credentials")) {
|
|
82
|
+
return "Refresh kubeconfig";
|
|
83
|
+
}
|
|
84
|
+
if (command.includes("s3api") ||
|
|
85
|
+
command.includes("storage buckets") ||
|
|
86
|
+
command.includes("storage account") ||
|
|
87
|
+
command.includes("storage container")) {
|
|
88
|
+
return "Discover storage resources";
|
|
89
|
+
}
|
|
90
|
+
if (command.includes("iam list-roles") ||
|
|
91
|
+
command.includes("service-accounts list") ||
|
|
92
|
+
command.includes("identity list")) {
|
|
93
|
+
return "List workload identities";
|
|
94
|
+
}
|
|
95
|
+
if (command.includes("amp ") || command.includes("monitor data-collection")) {
|
|
96
|
+
return "Discover monitoring destinations";
|
|
97
|
+
}
|
|
98
|
+
return "Run cloud CLI command";
|
|
99
|
+
}
|
|
46
100
|
// ============================================================================
|
|
47
101
|
// AWS CLI
|
|
48
102
|
// ============================================================================
|
|
@@ -128,11 +182,11 @@ export async function listS3Buckets() {
|
|
|
128
182
|
}
|
|
129
183
|
}
|
|
130
184
|
/**
|
|
131
|
-
* Static fallback for AWS regions
|
|
185
|
+
* Static fallback for common AWS regions.
|
|
132
186
|
*/
|
|
133
187
|
function getStaticAwsRegions() {
|
|
134
188
|
return [
|
|
135
|
-
// US regions
|
|
189
|
+
// US regions
|
|
136
190
|
"us-east-1",
|
|
137
191
|
"us-east-2",
|
|
138
192
|
"us-west-1",
|
|
@@ -140,7 +194,7 @@ function getStaticAwsRegions() {
|
|
|
140
194
|
// Canada
|
|
141
195
|
"ca-central-1",
|
|
142
196
|
"ca-west-1",
|
|
143
|
-
// Europe
|
|
197
|
+
// Europe
|
|
144
198
|
"eu-west-1",
|
|
145
199
|
"eu-west-2",
|
|
146
200
|
"eu-west-3",
|
|
@@ -149,7 +203,7 @@ function getStaticAwsRegions() {
|
|
|
149
203
|
"eu-north-1",
|
|
150
204
|
"eu-south-1",
|
|
151
205
|
"eu-south-2",
|
|
152
|
-
// Asia Pacific
|
|
206
|
+
// Asia Pacific
|
|
153
207
|
"ap-northeast-1",
|
|
154
208
|
"ap-northeast-2",
|
|
155
209
|
"ap-northeast-3",
|
|
@@ -176,7 +230,10 @@ function getStaticAwsRegions() {
|
|
|
176
230
|
*/
|
|
177
231
|
export async function listEksClusters(region) {
|
|
178
232
|
try {
|
|
179
|
-
const result = await execCommand(`aws eks list-clusters --region ${region} --output json
|
|
233
|
+
const result = await execCommand(`aws eks list-clusters --region ${region} --output json`, {
|
|
234
|
+
intent: `Discover clusters in ${region}`,
|
|
235
|
+
provider: "aws",
|
|
236
|
+
});
|
|
180
237
|
if (result.stderr && !result.stdout) {
|
|
181
238
|
return [];
|
|
182
239
|
}
|
|
@@ -187,16 +244,135 @@ export async function listEksClusters(region) {
|
|
|
187
244
|
return [];
|
|
188
245
|
}
|
|
189
246
|
}
|
|
247
|
+
/**
|
|
248
|
+
* Best-effort preflight for an external AWS Aurora Postgres cluster: Supabase
|
|
249
|
+
* Realtime needs logical replication (wal_level=logical), which on Aurora is the
|
|
250
|
+
* STATIC cluster parameter rds.logical_replication - it lives in a custom DB
|
|
251
|
+
* cluster parameter group and needs a reboot, so bootstrap.sql can't set it and
|
|
252
|
+
* Realtime crashloops without it. Parses the cluster id + region from the writer
|
|
253
|
+
* endpoint and reads the attached parameter group. Fails OPEN ("unknown") on any
|
|
254
|
+
* ambiguity (non-Aurora host, denied describe, unexpected value) so it never
|
|
255
|
+
* blocks a deploy spuriously.
|
|
256
|
+
*/
|
|
257
|
+
export async function checkAuroraLogicalReplication(host, fallbackRegion) {
|
|
258
|
+
// <cluster>.cluster[-ro]-<hash>.<region>.rds.amazonaws.com
|
|
259
|
+
const match = /^([^.]+)\.cluster(?:-ro)?-[^.]+\.([^.]+)\.rds\.amazonaws\.com$/i.exec(host.trim());
|
|
260
|
+
if (!match)
|
|
261
|
+
return { status: "unknown" };
|
|
262
|
+
const clusterId = match[1];
|
|
263
|
+
const region = match[2] || fallbackRegion;
|
|
264
|
+
if (!region)
|
|
265
|
+
return { status: "unknown" };
|
|
266
|
+
try {
|
|
267
|
+
const pgRes = await execCommand(`aws rds describe-db-clusters --db-cluster-identifier ${clusterId} ` +
|
|
268
|
+
`--region ${region} --query "DBClusters[0].DBClusterParameterGroup" --output text`, {
|
|
269
|
+
intent: `Check Aurora logical replication (${clusterId})`,
|
|
270
|
+
provider: "aws",
|
|
271
|
+
});
|
|
272
|
+
const parameterGroup = pgRes.stdout.trim();
|
|
273
|
+
if (!parameterGroup || parameterGroup === "None") {
|
|
274
|
+
return { status: "unknown" };
|
|
275
|
+
}
|
|
276
|
+
const valRes = await execCommand(`aws rds describe-db-cluster-parameters ` +
|
|
277
|
+
`--db-cluster-parameter-group-name ${parameterGroup} --region ${region} ` +
|
|
278
|
+
`--query "Parameters[?ParameterName=='rds.logical_replication'].ParameterValue | [0]" ` +
|
|
279
|
+
`--output text`, { intent: "Read rds.logical_replication", provider: "aws" });
|
|
280
|
+
const value = valRes.stdout.trim();
|
|
281
|
+
if (value === "1")
|
|
282
|
+
return { status: "enabled", parameterGroup };
|
|
283
|
+
if (value === "0" || value === "" || value === "None") {
|
|
284
|
+
return { status: "disabled", parameterGroup };
|
|
285
|
+
}
|
|
286
|
+
return { status: "unknown", parameterGroup };
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
return { status: "unknown" };
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
async function describeEksCluster(name, region) {
|
|
293
|
+
try {
|
|
294
|
+
const result = await execCommand(`aws eks describe-cluster --name ${name} --region ${region} --query "cluster.{name:name,status:status,version:version}" --output json`, {
|
|
295
|
+
intent: `Discover clusters in ${region}`,
|
|
296
|
+
provider: "aws",
|
|
297
|
+
});
|
|
298
|
+
if (result.stderr && !result.stdout) {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
const cluster = JSON.parse(result.stdout);
|
|
302
|
+
if (cluster.status !== "ACTIVE") {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
provider: "aws",
|
|
307
|
+
name: cluster.name,
|
|
308
|
+
region,
|
|
309
|
+
status: cluster.status,
|
|
310
|
+
version: cluster.version,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* List EKS clusters across all accessible AWS regions.
|
|
319
|
+
*/
|
|
320
|
+
export async function listAllEksClusters() {
|
|
321
|
+
const regions = await listAwsRegions();
|
|
322
|
+
const clustersByRegion = await Promise.all(regions.map(async (region) => {
|
|
323
|
+
const names = await listEksClusters(region);
|
|
324
|
+
return Promise.all(names.map((name) => describeEksCluster(name, region)));
|
|
325
|
+
}));
|
|
326
|
+
return clustersByRegion
|
|
327
|
+
.flat()
|
|
328
|
+
.filter((cluster) => cluster !== null)
|
|
329
|
+
.sort((a, b) => a.region.localeCompare(b.region) || a.name.localeCompare(b.name));
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Discover active EKS clusters in one region.
|
|
333
|
+
*/
|
|
334
|
+
export async function discoverEksClustersInRegion(region) {
|
|
335
|
+
const names = await listEksClusters(region);
|
|
336
|
+
const clusters = await Promise.all(names.map((name) => describeEksCluster(name, region)));
|
|
337
|
+
return clusters
|
|
338
|
+
.filter((cluster) => cluster !== null)
|
|
339
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* List IAM roles for selection (e.g. IRSA roles for S3 / AMP). Returns an empty
|
|
343
|
+
* list on any failure so callers can fall back to manual entry.
|
|
344
|
+
*/
|
|
345
|
+
export async function listIamRoles() {
|
|
346
|
+
try {
|
|
347
|
+
const result = await execCommand('aws iam list-roles --query "Roles[].{name:RoleName,arn:Arn}" --output json');
|
|
348
|
+
if (result.stderr && !result.stdout) {
|
|
349
|
+
return [];
|
|
350
|
+
}
|
|
351
|
+
const roles = JSON.parse(result.stdout);
|
|
352
|
+
return roles.sort((a, b) => a.name.localeCompare(b.name));
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
return [];
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Get the active AWS account ID (useful for constructing/validating ARNs).
|
|
360
|
+
*/
|
|
361
|
+
export async function getAwsAccountId() {
|
|
362
|
+
try {
|
|
363
|
+
const result = await execCommand("aws sts get-caller-identity --query Account --output text");
|
|
364
|
+
const accountId = result.stdout.trim();
|
|
365
|
+
return accountId || null;
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
190
371
|
// ============================================================================
|
|
191
372
|
// GCP CLI (gcloud)
|
|
192
373
|
// ============================================================================
|
|
193
374
|
/**
|
|
194
|
-
* Check if gcloud CLI is installed and
|
|
195
|
-
*
|
|
196
|
-
* For GCP to be considered "authenticated", the user must have:
|
|
197
|
-
* 1. Logged in with `gcloud auth login`
|
|
198
|
-
* 2. Set a default project with `gcloud config set project PROJECT_ID`
|
|
199
|
-
* 3. Configured Application Default Credentials with `gcloud auth application-default login`
|
|
375
|
+
* Check if gcloud CLI is installed and authenticated enough to list clusters.
|
|
200
376
|
*/
|
|
201
377
|
export async function checkGcloudCli() {
|
|
202
378
|
const status = {
|
|
@@ -221,25 +397,15 @@ export async function checkGcloudCli() {
|
|
|
221
397
|
const config = JSON.parse(configResult.stdout);
|
|
222
398
|
const account = config.core?.account;
|
|
223
399
|
const project = config.core?.project;
|
|
224
|
-
// Step 1: Check if logged in
|
|
225
400
|
if (!account) {
|
|
226
401
|
status.error = 'Not authenticated - run "gcloud auth login"';
|
|
227
402
|
return status;
|
|
228
403
|
}
|
|
229
|
-
// Step 2: Check if project is set
|
|
230
404
|
if (!project) {
|
|
231
405
|
status.error =
|
|
232
406
|
'No default project set - run "gcloud config set project PROJECT_ID"';
|
|
233
407
|
return status;
|
|
234
408
|
}
|
|
235
|
-
// Step 3: Check Application Default Credentials
|
|
236
|
-
const adcResult = await checkGcpApplicationDefaultCredentials();
|
|
237
|
-
if (!adcResult.configured) {
|
|
238
|
-
status.error =
|
|
239
|
-
'Application Default Credentials not configured - run "gcloud auth application-default login"';
|
|
240
|
-
return status;
|
|
241
|
-
}
|
|
242
|
-
// All checks passed
|
|
243
409
|
status.authenticated = true;
|
|
244
410
|
status.identity = `Project: ${project}`;
|
|
245
411
|
}
|
|
@@ -265,39 +431,6 @@ export async function getGcpProjectId() {
|
|
|
265
431
|
return null;
|
|
266
432
|
}
|
|
267
433
|
}
|
|
268
|
-
/**
|
|
269
|
-
* Check if GCP Application Default Credentials (ADC) are configured
|
|
270
|
-
* ADC is required for Terraform to authenticate with Google Cloud
|
|
271
|
-
*/
|
|
272
|
-
export async function checkGcpApplicationDefaultCredentials() {
|
|
273
|
-
try {
|
|
274
|
-
// Try to get an access token using ADC
|
|
275
|
-
const result = await execCommand("gcloud auth application-default print-access-token");
|
|
276
|
-
if (result.stdout && result.stdout.trim().length > 0) {
|
|
277
|
-
return { configured: true };
|
|
278
|
-
}
|
|
279
|
-
return {
|
|
280
|
-
configured: false,
|
|
281
|
-
error: "Application Default Credentials not configured",
|
|
282
|
-
};
|
|
283
|
-
}
|
|
284
|
-
catch (error) {
|
|
285
|
-
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
286
|
-
// Check if it's specifically an authentication error
|
|
287
|
-
if (errorMessage.includes("not found") ||
|
|
288
|
-
errorMessage.includes("not configured") ||
|
|
289
|
-
errorMessage.includes("Could not automatically determine credentials")) {
|
|
290
|
-
return {
|
|
291
|
-
configured: false,
|
|
292
|
-
error: "Application Default Credentials not configured",
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
return {
|
|
296
|
-
configured: false,
|
|
297
|
-
error: errorMessage,
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
434
|
/**
|
|
302
435
|
* List available GCP regions
|
|
303
436
|
*/
|
|
@@ -335,33 +468,53 @@ export async function listGcsBuckets() {
|
|
|
335
468
|
}
|
|
336
469
|
}
|
|
337
470
|
/**
|
|
338
|
-
*
|
|
339
|
-
*
|
|
471
|
+
* List GCP service accounts for selection (e.g. for GKE Workload Identity).
|
|
472
|
+
* Returns an empty list on any failure so callers can fall back to manual entry.
|
|
473
|
+
*/
|
|
474
|
+
export async function listGcpServiceAccounts() {
|
|
475
|
+
try {
|
|
476
|
+
const result = await execCommand('gcloud iam service-accounts list --format="json(email,displayName)"');
|
|
477
|
+
if (result.stderr && !result.stdout) {
|
|
478
|
+
return [];
|
|
479
|
+
}
|
|
480
|
+
const accounts = JSON.parse(result.stdout);
|
|
481
|
+
return accounts
|
|
482
|
+
.map((account) => ({
|
|
483
|
+
email: account.email,
|
|
484
|
+
displayName: account.displayName,
|
|
485
|
+
}))
|
|
486
|
+
.sort((a, b) => a.email.localeCompare(b.email));
|
|
487
|
+
}
|
|
488
|
+
catch {
|
|
489
|
+
return [];
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Static fallback for common GCP regions.
|
|
340
494
|
*/
|
|
341
495
|
function getStaticGcpRegions() {
|
|
342
496
|
return [
|
|
343
|
-
// Tier 1: Full C4A (Google Axion ARM64) availability - 3+ zones confirmed
|
|
344
497
|
// US regions
|
|
345
|
-
"us-central1",
|
|
346
|
-
"us-east1",
|
|
347
|
-
"us-east4",
|
|
348
|
-
"us-west1",
|
|
349
|
-
"us-west4",
|
|
498
|
+
"us-central1",
|
|
499
|
+
"us-east1",
|
|
500
|
+
"us-east4",
|
|
501
|
+
"us-west1",
|
|
502
|
+
"us-west4",
|
|
350
503
|
// North America
|
|
351
|
-
"northamerica-south1",
|
|
504
|
+
"northamerica-south1",
|
|
352
505
|
// Europe
|
|
353
|
-
"europe-west1",
|
|
354
|
-
"europe-west2",
|
|
355
|
-
"europe-west3",
|
|
356
|
-
"europe-west4",
|
|
357
|
-
"europe-north1",
|
|
506
|
+
"europe-west1",
|
|
507
|
+
"europe-west2",
|
|
508
|
+
"europe-west3",
|
|
509
|
+
"europe-west4",
|
|
510
|
+
"europe-north1",
|
|
358
511
|
// Asia Pacific
|
|
359
|
-
"asia-east1",
|
|
360
|
-
"asia-northeast1",
|
|
361
|
-
"asia-south1",
|
|
362
|
-
"asia-southeast1",
|
|
512
|
+
"asia-east1",
|
|
513
|
+
"asia-northeast1",
|
|
514
|
+
"asia-south1",
|
|
515
|
+
"asia-southeast1",
|
|
363
516
|
// Australia
|
|
364
|
-
"australia-southeast2",
|
|
517
|
+
"australia-southeast2",
|
|
365
518
|
];
|
|
366
519
|
}
|
|
367
520
|
/**
|
|
@@ -371,7 +524,10 @@ function getStaticGcpRegions() {
|
|
|
371
524
|
export async function listGkeClusters(region) {
|
|
372
525
|
try {
|
|
373
526
|
// List clusters in the specified region (includes both regional and zonal clusters in that region)
|
|
374
|
-
const result = await execCommand(`gcloud container clusters list --region ${region} --format="json(name)" 2>/dev/null || gcloud container clusters list --filter="location~^${region}" --format="json(name)"
|
|
527
|
+
const result = await execCommand(`gcloud container clusters list --region ${region} --format="json(name)" 2>/dev/null || gcloud container clusters list --filter="location~^${region}" --format="json(name)"`, {
|
|
528
|
+
intent: `Discover clusters in ${region}`,
|
|
529
|
+
provider: "gcp",
|
|
530
|
+
});
|
|
375
531
|
if (result.stderr && !result.stdout) {
|
|
376
532
|
return [];
|
|
377
533
|
}
|
|
@@ -382,17 +538,70 @@ export async function listGkeClusters(region) {
|
|
|
382
538
|
return [];
|
|
383
539
|
}
|
|
384
540
|
}
|
|
541
|
+
/**
|
|
542
|
+
* List GKE clusters across the active GCP project.
|
|
543
|
+
*/
|
|
544
|
+
export async function listAllGkeClusters() {
|
|
545
|
+
const projectId = await getGcpProjectId();
|
|
546
|
+
try {
|
|
547
|
+
const result = await execCommand('gcloud container clusters list --format="json(name,location,status,currentMasterVersion,currentNodeCount)"');
|
|
548
|
+
if (result.stderr && !result.stdout) {
|
|
549
|
+
return [];
|
|
550
|
+
}
|
|
551
|
+
const clusters = JSON.parse(result.stdout);
|
|
552
|
+
return clusters
|
|
553
|
+
.filter((cluster) => cluster.status === "RUNNING")
|
|
554
|
+
.map((cluster) => ({
|
|
555
|
+
provider: "gcp",
|
|
556
|
+
name: cluster.name,
|
|
557
|
+
region: cluster.location,
|
|
558
|
+
projectId: projectId || undefined,
|
|
559
|
+
status: cluster.status,
|
|
560
|
+
version: cluster.currentMasterVersion,
|
|
561
|
+
nodeCount: cluster.currentNodeCount,
|
|
562
|
+
}))
|
|
563
|
+
.sort((a, b) => a.region.localeCompare(b.region) || a.name.localeCompare(b.name));
|
|
564
|
+
}
|
|
565
|
+
catch {
|
|
566
|
+
return [];
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Discover running GKE clusters in a selected region/location.
|
|
571
|
+
*/
|
|
572
|
+
export async function discoverGkeClustersInRegion(region) {
|
|
573
|
+
const projectId = await getGcpProjectId();
|
|
574
|
+
try {
|
|
575
|
+
const result = await execCommand(`gcloud container clusters list --region ${region} --format="json(name,location,status,currentMasterVersion,currentNodeCount)" 2>/dev/null || gcloud container clusters list --filter="location~^${region}" --format="json(name,location,status,currentMasterVersion,currentNodeCount)"`, {
|
|
576
|
+
intent: `Discover clusters in ${region}`,
|
|
577
|
+
provider: "gcp",
|
|
578
|
+
});
|
|
579
|
+
if (result.stderr && !result.stdout) {
|
|
580
|
+
return [];
|
|
581
|
+
}
|
|
582
|
+
const clusters = JSON.parse(result.stdout);
|
|
583
|
+
return clusters
|
|
584
|
+
.filter((cluster) => cluster.status === "RUNNING")
|
|
585
|
+
.map((cluster) => ({
|
|
586
|
+
provider: "gcp",
|
|
587
|
+
name: cluster.name,
|
|
588
|
+
region: cluster.location,
|
|
589
|
+
projectId: projectId || undefined,
|
|
590
|
+
status: cluster.status,
|
|
591
|
+
version: cluster.currentMasterVersion,
|
|
592
|
+
nodeCount: cluster.currentNodeCount,
|
|
593
|
+
}))
|
|
594
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
595
|
+
}
|
|
596
|
+
catch {
|
|
597
|
+
return [];
|
|
598
|
+
}
|
|
599
|
+
}
|
|
385
600
|
// ============================================================================
|
|
386
601
|
// Azure CLI
|
|
387
602
|
// ============================================================================
|
|
388
603
|
/**
|
|
389
|
-
* Check if Azure CLI is installed and
|
|
390
|
-
*
|
|
391
|
-
* For Azure to be considered "authenticated", the user must have:
|
|
392
|
-
* 1. Logged in with `az login`
|
|
393
|
-
* 2. An active subscription in "Enabled" state
|
|
394
|
-
* 3. Required resource providers registered (Microsoft.ContainerService, etc.)
|
|
395
|
-
* 4. Sufficient vCPU quota for at least the small tier (8 cores)
|
|
604
|
+
* Check if Azure CLI is installed and authenticated enough to list clusters.
|
|
396
605
|
*/
|
|
397
606
|
export async function checkAzureCli() {
|
|
398
607
|
const status = {
|
|
@@ -411,7 +620,6 @@ export async function checkAzureCli() {
|
|
|
411
620
|
// Extract version (e.g., "azure-cli 2.51.0")
|
|
412
621
|
const versionMatch = versionResult.stdout.match(/azure-cli\s+([\d.]+)/);
|
|
413
622
|
status.version = versionMatch ? versionMatch[1] : undefined;
|
|
414
|
-
// Step 1: Check authentication and subscription
|
|
415
623
|
const accountResult = await execCommand("az account show --output json");
|
|
416
624
|
if (accountResult.stderr && accountResult.stderr.includes("Please run")) {
|
|
417
625
|
status.error = 'Not authenticated - run "az login"';
|
|
@@ -421,7 +629,6 @@ export async function checkAzureCli() {
|
|
|
421
629
|
try {
|
|
422
630
|
const account = JSON.parse(accountResult.stdout);
|
|
423
631
|
subscriptionName = account.name;
|
|
424
|
-
// Step 2: Check subscription state is Enabled
|
|
425
632
|
if (account.state !== "Enabled") {
|
|
426
633
|
status.error = `Subscription "${account.name}" is not enabled (state: ${account.state})`;
|
|
427
634
|
return status;
|
|
@@ -434,23 +641,6 @@ export async function checkAzureCli() {
|
|
|
434
641
|
status.error = "Failed to parse account info";
|
|
435
642
|
return status;
|
|
436
643
|
}
|
|
437
|
-
// Step 3: Check required resource providers are registered
|
|
438
|
-
const providerCheck = await checkAzureResourceProviders();
|
|
439
|
-
if (!providerCheck.allRegistered) {
|
|
440
|
-
status.error =
|
|
441
|
-
`Resource providers not registered: ${providerCheck.missing.join(", ")}. ` +
|
|
442
|
-
`Run: ${providerCheck.missing.map((p) => `az provider register --namespace ${p}`).join(" && ")}`;
|
|
443
|
-
return status;
|
|
444
|
-
}
|
|
445
|
-
// Step 4: Check minimum vCPU quota (small tier = 8 cores)
|
|
446
|
-
const quotaCheck = await checkAzureVmQuota(AZURE_DEFAULT_QUOTA_CHECK_REGION, AZURE_TIER_CORES.small);
|
|
447
|
-
if (!quotaCheck.sufficient) {
|
|
448
|
-
status.error =
|
|
449
|
-
`Insufficient vCPU quota (${quotaCheck.available}/${quotaCheck.limit} available in ${AZURE_DEFAULT_QUOTA_CHECK_REGION}). ` +
|
|
450
|
-
"Request increase at Azure portal > Subscriptions > Usage + quotas";
|
|
451
|
-
return status;
|
|
452
|
-
}
|
|
453
|
-
// All checks passed
|
|
454
644
|
status.authenticated = true;
|
|
455
645
|
}
|
|
456
646
|
catch (error) {
|
|
@@ -470,6 +660,99 @@ export async function getAzureSubscriptionId() {
|
|
|
470
660
|
return null;
|
|
471
661
|
}
|
|
472
662
|
}
|
|
663
|
+
/**
|
|
664
|
+
* Get the active Azure tenant ID. Used to auto-fill workload-identity tenant
|
|
665
|
+
* fields so users don't have to look it up manually.
|
|
666
|
+
*/
|
|
667
|
+
export async function getAzureTenantId() {
|
|
668
|
+
try {
|
|
669
|
+
const result = await execCommand("az account show --query tenantId --output tsv");
|
|
670
|
+
return result.stdout.trim() || null;
|
|
671
|
+
}
|
|
672
|
+
catch {
|
|
673
|
+
return null;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* List Azure user-assigned managed identities for selection (workload identity
|
|
678
|
+
* client IDs). Returns an empty list on any failure so callers can fall back to
|
|
679
|
+
* manual entry.
|
|
680
|
+
*/
|
|
681
|
+
export async function listAzureManagedIdentities() {
|
|
682
|
+
try {
|
|
683
|
+
const result = await execCommand('az identity list --query "[].{name:name,clientId:clientId,resourceGroup:resourceGroup}" --output json');
|
|
684
|
+
if (result.stderr && !result.stdout) {
|
|
685
|
+
return [];
|
|
686
|
+
}
|
|
687
|
+
const identities = JSON.parse(result.stdout);
|
|
688
|
+
return identities.sort((a, b) => a.name.localeCompare(b.name));
|
|
689
|
+
}
|
|
690
|
+
catch {
|
|
691
|
+
return [];
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Discovers Azure Monitor Prometheus remote_write targets: every Data Collection
|
|
696
|
+
* Rule that ingests the Microsoft-PrometheusMetrics stream, paired with its Data
|
|
697
|
+
* Collection Endpoint's metrics-ingestion endpoint, assembled into the exact
|
|
698
|
+
* remote_write URL. Works for any DCR the caller can see (not just ones we made).
|
|
699
|
+
*/
|
|
700
|
+
export async function listAzurePrometheusTargets() {
|
|
701
|
+
try {
|
|
702
|
+
const dceResult = await execCommand('az monitor data-collection endpoint list --query "[].{id:id,endpoint:metricsIngestion.endpoint}" --output json');
|
|
703
|
+
const dces = JSON.parse(dceResult.stdout || "[]");
|
|
704
|
+
const endpointById = new Map();
|
|
705
|
+
for (const dce of dces) {
|
|
706
|
+
if (dce.id && dce.endpoint) {
|
|
707
|
+
endpointById.set(dce.id.toLowerCase(), dce.endpoint);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
const dcrResult = await execCommand('az monitor data-collection rule list --query "[].{name:name,immutableId:immutableId,dce:dataCollectionEndpointId,streams:dataFlows[].streams[]}" --output json');
|
|
711
|
+
const dcrs = JSON.parse(dcrResult.stdout || "[]");
|
|
712
|
+
const targets = [];
|
|
713
|
+
for (const dcr of dcrs) {
|
|
714
|
+
if (!dcr.immutableId || !dcr.dce)
|
|
715
|
+
continue;
|
|
716
|
+
if (!(dcr.streams || []).includes("Microsoft-PrometheusMetrics"))
|
|
717
|
+
continue;
|
|
718
|
+
const endpoint = endpointById.get(dcr.dce.toLowerCase());
|
|
719
|
+
if (!endpoint)
|
|
720
|
+
continue;
|
|
721
|
+
const url = `${endpoint.replace(/\/+$/, "")}/dataCollectionRules/${dcr.immutableId}/streams/Microsoft-PrometheusMetrics/api/v1/write?api-version=2023-04-24`;
|
|
722
|
+
targets.push({ name: dcr.name, url });
|
|
723
|
+
}
|
|
724
|
+
return targets.sort((a, b) => a.name.localeCompare(b.name));
|
|
725
|
+
}
|
|
726
|
+
catch {
|
|
727
|
+
return [];
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Discovers AWS Managed Prometheus (AMP) workspaces in a region and assembles the
|
|
732
|
+
* remote_write URL (<prometheusEndpoint>api/v1/remote_write) for each.
|
|
733
|
+
*/
|
|
734
|
+
export async function listAwsPrometheusWorkspaces(region) {
|
|
735
|
+
try {
|
|
736
|
+
const listResult = await execCommand(`aws amp list-workspaces --region ${region} --query "workspaces[].{id:workspaceId,alias:alias}" --output json`);
|
|
737
|
+
const workspaces = JSON.parse(listResult.stdout || "[]");
|
|
738
|
+
const targets = [];
|
|
739
|
+
for (const ws of workspaces) {
|
|
740
|
+
const descResult = await execCommand(`aws amp describe-workspace --workspace-id ${ws.id} --region ${region} --query "workspace.prometheusEndpoint" --output text`);
|
|
741
|
+
const endpoint = descResult.stdout.trim();
|
|
742
|
+
if (!endpoint || endpoint === "None")
|
|
743
|
+
continue;
|
|
744
|
+
const url = `${endpoint.replace(/\/+$/, "")}/api/v1/remote_write`;
|
|
745
|
+
targets.push({
|
|
746
|
+
name: ws.alias ? `${ws.alias} (${ws.id})` : ws.id,
|
|
747
|
+
url,
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
return targets.sort((a, b) => a.name.localeCompare(b.name));
|
|
751
|
+
}
|
|
752
|
+
catch {
|
|
753
|
+
return [];
|
|
754
|
+
}
|
|
755
|
+
}
|
|
473
756
|
/**
|
|
474
757
|
* List available Azure regions (locations)
|
|
475
758
|
*/
|
|
@@ -519,11 +802,11 @@ export async function listAzureBlobContainers(storageAccount) {
|
|
|
519
802
|
}
|
|
520
803
|
}
|
|
521
804
|
/**
|
|
522
|
-
* Static fallback for Azure regions
|
|
805
|
+
* Static fallback for common Azure regions.
|
|
523
806
|
*/
|
|
524
807
|
function getStaticAzureRegions() {
|
|
525
808
|
return [
|
|
526
|
-
// US regions
|
|
809
|
+
// US regions
|
|
527
810
|
"eastus",
|
|
528
811
|
"eastus2",
|
|
529
812
|
"westus",
|
|
@@ -538,7 +821,7 @@ function getStaticAzureRegions() {
|
|
|
538
821
|
"canadaeast",
|
|
539
822
|
// South America
|
|
540
823
|
"brazilsouth",
|
|
541
|
-
// Europe
|
|
824
|
+
// Europe
|
|
542
825
|
"northeurope",
|
|
543
826
|
"westeurope",
|
|
544
827
|
"uksouth",
|
|
@@ -594,101 +877,60 @@ export async function listAksClusters(resourceGroup) {
|
|
|
594
877
|
}
|
|
595
878
|
}
|
|
596
879
|
/**
|
|
597
|
-
*
|
|
598
|
-
*/
|
|
599
|
-
const AZURE_REQUIRED_PROVIDERS = [
|
|
600
|
-
"Microsoft.ContainerService", // For AKS
|
|
601
|
-
"Microsoft.Network", // For VNets, NSGs
|
|
602
|
-
"Microsoft.ManagedIdentity", // For managed identities
|
|
603
|
-
"Microsoft.Compute", // For VMs
|
|
604
|
-
];
|
|
605
|
-
/**
|
|
606
|
-
* Azure tier to vCPU core requirements mapping
|
|
607
|
-
*/
|
|
608
|
-
export const AZURE_TIER_CORES = {
|
|
609
|
-
small: 8, // 4 nodes × 2 vCPU
|
|
610
|
-
medium: 16, // 4 nodes × 4 vCPU
|
|
611
|
-
large: 40, // 5 nodes × 8 vCPU
|
|
612
|
-
};
|
|
613
|
-
/**
|
|
614
|
-
* Default region used for baseline quota checks when region is not yet selected
|
|
615
|
-
*/
|
|
616
|
-
const AZURE_DEFAULT_QUOTA_CHECK_REGION = "eastus";
|
|
617
|
-
/**
|
|
618
|
-
* Check if required Azure resource providers are registered
|
|
880
|
+
* List AKS clusters across the active Azure subscription.
|
|
619
881
|
*/
|
|
620
|
-
export async function
|
|
621
|
-
const missing = [];
|
|
882
|
+
export async function listAllAksClusters() {
|
|
622
883
|
try {
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
if (state !== "Registered") {
|
|
627
|
-
missing.push(provider);
|
|
628
|
-
}
|
|
884
|
+
const result = await execCommand('az aks list --query "[].{name:name,resourceGroup:resourceGroup,location:location,kubernetesVersion:kubernetesVersion,powerState:powerState,agentPoolProfiles:agentPoolProfiles}" --output json');
|
|
885
|
+
if (result.stderr && !result.stdout) {
|
|
886
|
+
return [];
|
|
629
887
|
}
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
888
|
+
const clusters = JSON.parse(result.stdout);
|
|
889
|
+
return clusters
|
|
890
|
+
.filter((cluster) => cluster.powerState?.code === "Running")
|
|
891
|
+
.map((cluster) => ({
|
|
892
|
+
provider: "azure",
|
|
893
|
+
name: cluster.name,
|
|
894
|
+
region: cluster.location,
|
|
895
|
+
resourceGroup: cluster.resourceGroup,
|
|
896
|
+
status: cluster.powerState?.code,
|
|
897
|
+
version: cluster.kubernetesVersion,
|
|
898
|
+
nodeCount: cluster.agentPoolProfiles?.reduce((sum, pool) => sum + (pool.count || 0), 0),
|
|
899
|
+
}))
|
|
900
|
+
.sort((a, b) => a.region.localeCompare(b.region) || a.name.localeCompare(b.name));
|
|
634
901
|
}
|
|
635
902
|
catch {
|
|
636
|
-
|
|
637
|
-
return {
|
|
638
|
-
allRegistered: false,
|
|
639
|
-
missing: AZURE_REQUIRED_PROVIDERS,
|
|
640
|
-
};
|
|
903
|
+
return [];
|
|
641
904
|
}
|
|
642
905
|
}
|
|
643
906
|
/**
|
|
644
|
-
*
|
|
645
|
-
*
|
|
646
|
-
* @param region - Azure region to check quota for
|
|
647
|
-
* @param requiredCores - Number of vCPUs required
|
|
648
|
-
* @returns Quota check result with availability info
|
|
907
|
+
* Discover running AKS clusters in a selected Azure location.
|
|
649
908
|
*/
|
|
650
|
-
export async function
|
|
909
|
+
export async function discoverAksClustersInRegion(region) {
|
|
651
910
|
try {
|
|
652
|
-
const result = await execCommand(
|
|
911
|
+
const result = await execCommand('az aks list --query "[].{name:name,resourceGroup:resourceGroup,location:location,kubernetesVersion:kubernetesVersion,powerState:powerState,agentPoolProfiles:agentPoolProfiles}" --output json', {
|
|
912
|
+
intent: `Discover clusters in ${region}`,
|
|
913
|
+
provider: "azure",
|
|
914
|
+
});
|
|
653
915
|
if (result.stderr && !result.stdout) {
|
|
654
|
-
return
|
|
655
|
-
sufficient: false,
|
|
656
|
-
available: 0,
|
|
657
|
-
limit: 0,
|
|
658
|
-
used: 0,
|
|
659
|
-
error: "Failed to check VM quota",
|
|
660
|
-
};
|
|
661
|
-
}
|
|
662
|
-
const usageList = JSON.parse(result.stdout);
|
|
663
|
-
// Find total regional vCPU quota
|
|
664
|
-
const regionalQuota = usageList.find((u) => u.name.value === "cores" || u.name.localizedValue === "Total Regional vCPUs");
|
|
665
|
-
if (!regionalQuota) {
|
|
666
|
-
return {
|
|
667
|
-
sufficient: false,
|
|
668
|
-
available: 0,
|
|
669
|
-
limit: 0,
|
|
670
|
-
used: 0,
|
|
671
|
-
error: "Could not find regional vCPU quota",
|
|
672
|
-
};
|
|
916
|
+
return [];
|
|
673
917
|
}
|
|
674
|
-
const
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
918
|
+
const clusters = JSON.parse(result.stdout);
|
|
919
|
+
return clusters
|
|
920
|
+
.filter((cluster) => cluster.location === region && cluster.powerState?.code === "Running")
|
|
921
|
+
.map((cluster) => ({
|
|
922
|
+
provider: "azure",
|
|
923
|
+
name: cluster.name,
|
|
924
|
+
region: cluster.location,
|
|
925
|
+
resourceGroup: cluster.resourceGroup,
|
|
926
|
+
status: cluster.powerState?.code,
|
|
927
|
+
version: cluster.kubernetesVersion,
|
|
928
|
+
nodeCount: cluster.agentPoolProfiles?.reduce((sum, pool) => sum + (pool.count || 0), 0),
|
|
929
|
+
}))
|
|
930
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
683
931
|
}
|
|
684
|
-
catch
|
|
685
|
-
return
|
|
686
|
-
sufficient: false,
|
|
687
|
-
available: 0,
|
|
688
|
-
limit: 0,
|
|
689
|
-
used: 0,
|
|
690
|
-
error: error instanceof Error ? error.message : "Failed to check VM quota",
|
|
691
|
-
};
|
|
932
|
+
catch {
|
|
933
|
+
return [];
|
|
692
934
|
}
|
|
693
935
|
}
|
|
694
936
|
// ============================================================================
|
|
@@ -752,6 +994,86 @@ export async function listClusters(provider, region, options) {
|
|
|
752
994
|
return [];
|
|
753
995
|
}
|
|
754
996
|
}
|
|
997
|
+
/**
|
|
998
|
+
* List managed Kubernetes clusters discoverable through a provider CLI.
|
|
999
|
+
*/
|
|
1000
|
+
export async function listManagedClusters(provider) {
|
|
1001
|
+
switch (provider) {
|
|
1002
|
+
case "aws":
|
|
1003
|
+
return listAllEksClusters();
|
|
1004
|
+
case "gcp":
|
|
1005
|
+
return listAllGkeClusters();
|
|
1006
|
+
case "azure":
|
|
1007
|
+
return listAllAksClusters();
|
|
1008
|
+
default:
|
|
1009
|
+
return [];
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
/**
|
|
1013
|
+
* List managed Kubernetes clusters discoverable through a provider CLI in a
|
|
1014
|
+
* selected region/location. This is used by init to avoid account-wide fan-out.
|
|
1015
|
+
*/
|
|
1016
|
+
export async function discoverClustersInRegion(provider, region) {
|
|
1017
|
+
switch (provider) {
|
|
1018
|
+
case "aws":
|
|
1019
|
+
return discoverEksClustersInRegion(region);
|
|
1020
|
+
case "gcp":
|
|
1021
|
+
return discoverGkeClustersInRegion(region);
|
|
1022
|
+
case "azure":
|
|
1023
|
+
return discoverAksClustersInRegion(region);
|
|
1024
|
+
default:
|
|
1025
|
+
return [];
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Refresh kubeconfig credentials for a selected managed Kubernetes cluster.
|
|
1030
|
+
*/
|
|
1031
|
+
export async function updateKubeconfig(provider, clusterName, region, options = {}) {
|
|
1032
|
+
switch (provider) {
|
|
1033
|
+
case "aws":
|
|
1034
|
+
{
|
|
1035
|
+
const result = await execCommand(`aws eks update-kubeconfig --name ${clusterName} --region ${region}`, {
|
|
1036
|
+
timeout: 30000,
|
|
1037
|
+
intent: `Refresh kubeconfig for ${clusterName}`,
|
|
1038
|
+
provider: "aws",
|
|
1039
|
+
mutating: true,
|
|
1040
|
+
});
|
|
1041
|
+
if (result.stderr && !result.stdout)
|
|
1042
|
+
throw new Error(result.stderr);
|
|
1043
|
+
}
|
|
1044
|
+
return;
|
|
1045
|
+
case "gcp":
|
|
1046
|
+
if (!options.gcpProjectId) {
|
|
1047
|
+
throw new Error("GCP project ID is required to refresh kubeconfig");
|
|
1048
|
+
}
|
|
1049
|
+
{
|
|
1050
|
+
const result = await execCommand(`gcloud container clusters get-credentials ${clusterName} --location ${region} --project ${options.gcpProjectId}`, {
|
|
1051
|
+
timeout: 30000,
|
|
1052
|
+
intent: `Refresh kubeconfig for ${clusterName}`,
|
|
1053
|
+
provider: "gcp",
|
|
1054
|
+
mutating: true,
|
|
1055
|
+
});
|
|
1056
|
+
if (result.stderr && !result.stdout)
|
|
1057
|
+
throw new Error(result.stderr);
|
|
1058
|
+
}
|
|
1059
|
+
return;
|
|
1060
|
+
case "azure":
|
|
1061
|
+
if (!options.azureResourceGroup) {
|
|
1062
|
+
throw new Error("Azure resource group is required to refresh kubeconfig");
|
|
1063
|
+
}
|
|
1064
|
+
{
|
|
1065
|
+
const result = await execCommand(`az aks get-credentials --name ${clusterName} --resource-group ${options.azureResourceGroup} --overwrite-existing`, {
|
|
1066
|
+
timeout: 30000,
|
|
1067
|
+
intent: `Refresh kubeconfig for ${clusterName}`,
|
|
1068
|
+
provider: "azure",
|
|
1069
|
+
mutating: true,
|
|
1070
|
+
});
|
|
1071
|
+
if (result.stderr && !result.stdout)
|
|
1072
|
+
throw new Error(result.stderr);
|
|
1073
|
+
}
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
755
1077
|
/**
|
|
756
1078
|
* Get installation URLs for cloud CLIs
|
|
757
1079
|
*/
|
|
@@ -780,42 +1102,12 @@ export const CLI_LOGIN_COMMANDS = {
|
|
|
780
1102
|
gcp: [
|
|
781
1103
|
"gcloud auth login",
|
|
782
1104
|
"gcloud config set project PROJECT_ID",
|
|
783
|
-
"gcloud auth application-default login",
|
|
784
1105
|
],
|
|
785
1106
|
azure: [
|
|
786
1107
|
"az login",
|
|
787
1108
|
"az account set --subscription YOUR_SUBSCRIPTION_ID",
|
|
788
|
-
"az provider register --namespace Microsoft.ContainerService",
|
|
789
1109
|
],
|
|
790
1110
|
};
|
|
791
|
-
/**
|
|
792
|
-
* Check if Terraform is installed
|
|
793
|
-
*/
|
|
794
|
-
export async function checkTerraform() {
|
|
795
|
-
try {
|
|
796
|
-
const result = await execCommand("terraform --version");
|
|
797
|
-
if (result.stderr && !result.stdout) {
|
|
798
|
-
return { installed: false, error: "Terraform not found" };
|
|
799
|
-
}
|
|
800
|
-
// Extract version (e.g., "Terraform v1.5.0")
|
|
801
|
-
const versionMatch = result.stdout.match(/Terraform v([\d.]+)/);
|
|
802
|
-
return {
|
|
803
|
-
installed: true,
|
|
804
|
-
version: versionMatch ? versionMatch[1] : undefined,
|
|
805
|
-
};
|
|
806
|
-
}
|
|
807
|
-
catch {
|
|
808
|
-
return { installed: false, error: "Terraform not found" };
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
/**
|
|
812
|
-
* Terraform installation info
|
|
813
|
-
*/
|
|
814
|
-
export const TERRAFORM_INSTALL_INFO = {
|
|
815
|
-
name: "Terraform",
|
|
816
|
-
url: "https://developer.hashicorp.com/terraform/downloads",
|
|
817
|
-
installCmd: "brew install terraform",
|
|
818
|
-
};
|
|
819
1111
|
// ============================================================================
|
|
820
1112
|
// Region-filtered bucket listing
|
|
821
1113
|
// ============================================================================
|