@rulebricks/cli 2.0.5 → 2.0.6

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.
@@ -4,8 +4,9 @@ import { Box, Text, useApp, useInput } from "ink";
4
4
  import { platform } from "os";
5
5
  import { BorderBox, Spinner, StatusLine, ThemeProvider, useTheme, Logo, } from "../components/common/index.js";
6
6
  import { DNSWaitScreen } from "../components/DNSWaitScreen.js";
7
- import { loadDeploymentConfig, loadDeploymentState, saveDeploymentState, updateDeploymentStatus, } from "../lib/config.js";
8
- import { setupTerraformWorkspace, terraformInit, terraformPlan, terraformApply, terraformDestroy, updateKubeconfig, hasTerraformState, isTerraformInstalled, } from "../lib/terraform.js";
7
+ import { loadDeploymentConfig, loadDeploymentState, saveDeploymentState, updateDeploymentStatus, saveTerraformVars, } from "../lib/config.js";
8
+ import { setupTerraformWorkspace, terraformInit, terraformPlan, terraformApply, terraformDestroy, updateKubeconfig, hasTerraformState, isTerraformInstalled, generateTerraformVars, } from "../lib/terraform.js";
9
+ import { checkGcpApplicationDefaultCredentials, checkAzureResourceProviders, checkAzureVmQuota, AZURE_TIER_CORES, } from "../lib/cloudCli.js";
9
10
  import { installOrUpgradeChart, upgradeChart, isHelmInstalled, } from "../lib/helm.js";
10
11
  import { isKubectlInstalled, checkClusterAccessible, } from "../lib/kubernetes.js";
11
12
  import { generateHelmValues, updateHelmValuesForTLS, } from "../lib/helmValues.js";
@@ -158,6 +159,10 @@ function DeployCommandInner({ name, skipInfra, skipDns, version, }) {
158
159
  setStep("infra-setup");
159
160
  await setupTerraformWorkspace(name, cfg.infrastructure.provider);
160
161
  }
162
+ // Generate and save terraform variables (always do this before plan,
163
+ // even if state exists, in case config changed)
164
+ const terraformVars = generateTerraformVars(cfg);
165
+ await saveTerraformVars(name, terraformVars);
161
166
  setStep("infra-init");
162
167
  await terraformInit(name);
163
168
  setStep("infra-plan");
@@ -287,6 +292,49 @@ function DeployCommandInner({ name, skipInfra, skipDns, version, }) {
287
292
  if (cfg.infrastructure.mode === "provision" && !terraform) {
288
293
  throw new Error("Terraform is not installed. Required for infrastructure provisioning.");
289
294
  }
295
+ // Check GCP Application Default Credentials if provisioning GCP infrastructure
296
+ if (cfg.infrastructure.mode === "provision" &&
297
+ cfg.infrastructure.provider === "gcp") {
298
+ const adcCheck = await checkGcpApplicationDefaultCredentials();
299
+ if (!adcCheck.configured) {
300
+ throw new Error("GCP Application Default Credentials (ADC) not configured.\n\n" +
301
+ "Terraform requires ADC to authenticate with Google Cloud.\n\n" +
302
+ "To fix this:\n" +
303
+ " • Run: gcloud auth login\n" +
304
+ " • Run: gcloud auth application-default login\n" +
305
+ " • Verify: gcloud auth application-default print-access-token\n\n" +
306
+ "For more information: https://cloud.google.com/docs/authentication/application-default-credentials");
307
+ }
308
+ }
309
+ // Check Azure prerequisites if provisioning Azure infrastructure
310
+ if (cfg.infrastructure.mode === "provision" &&
311
+ cfg.infrastructure.provider === "azure") {
312
+ // 1. Verify resource providers are registered
313
+ const providerCheck = await checkAzureResourceProviders();
314
+ if (!providerCheck.allRegistered) {
315
+ throw new Error(`Azure resource providers not registered: ${providerCheck.missing.join(", ")}\n\n` +
316
+ "To register:\n" +
317
+ providerCheck.missing
318
+ .map((p) => ` • az provider register --namespace ${p}`)
319
+ .join("\n") +
320
+ "\n\nNote: Registration may take a few minutes to complete.");
321
+ }
322
+ // 2. Check VM quota for the selected tier
323
+ const tier = cfg.tier || "small";
324
+ const region = cfg.infrastructure.region;
325
+ if (!region) {
326
+ throw new Error("Azure region is required for infrastructure provisioning");
327
+ }
328
+ const requiredCores = AZURE_TIER_CORES[tier] || AZURE_TIER_CORES.small;
329
+ const quotaCheck = await checkAzureVmQuota(region, requiredCores);
330
+ if (!quotaCheck.sufficient) {
331
+ throw new Error(`Insufficient Azure vCPU quota in ${region}.\n` +
332
+ `Required: ${requiredCores} cores (${tier} tier), Available: ${quotaCheck.available}/${quotaCheck.limit}\n\n` +
333
+ "Request a quota increase in the Azure portal:\n" +
334
+ " • Go to: Subscriptions > Usage + quotas\n" +
335
+ " • Request increase for 'Total Regional vCPUs' in your region");
336
+ }
337
+ }
290
338
  // Check cluster access if using existing infrastructure
291
339
  if (cfg.infrastructure.mode === "existing") {
292
340
  let clusterError = await checkClusterAccessible();
@@ -243,7 +243,7 @@ function DestroyCommandInner({ name, cluster, config, force, }) {
243
243
  // Only cleaning local files (with --config)
244
244
  _jsxs(_Fragment, { children: [_jsx(Text, { color: colors.warning, bold: true, children: "\u2139 Local Cleanup" }), _jsxs(Box, { marginY: 1, flexDirection: "column", children: [_jsx(Text, { children: "No cluster resources found to clean up." }), _jsx(Text, { children: "This will delete local configuration files." })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.warning, children: "Press Enter to confirm, Esc to cancel" }) })] })) : (
245
245
  // Full destruction
246
- _jsxs(_Fragment, { children: [_jsx(Text, { color: colors.accent, bold: true, children: "\u26A0 WARNING" }), _jsxs(Box, { marginY: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.muted, children: "This will permanently delete:" }), (scope?.hasHelmRelease || scope?.hasNamespace) && (_jsxs(_Fragment, { children: [_jsx(Text, { color: colors.muted, children: " \u2022 Rulebricks application" }), _jsxs(Text, { color: colors.muted, children: [" ", "\u2022 All databases and stored data"] }), _jsx(Text, { color: colors.muted, children: " \u2022 All persistent volumes" }), _jsx(Text, { color: colors.muted, children: " \u2022 Monitoring stack" }), _jsx(Text, { color: colors.muted, children: " \u2022 Kubernetes namespace" })] })), needsInfraConfirm && (_jsxs(_Fragment, { children: [_jsx(Text, { color: colors.accent, children: " \u2022 Kubernetes cluster" }), _jsx(Text, { color: colors.accent, children: " \u2022 All cloud infrastructure" })] })), willDeleteConfig && (_jsx(Text, { color: colors.muted, children: " \u2022 Local configuration files" })), !cluster && scope?.hasInfrastructure && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, dimColor: true, children: "Cloud infrastructure will be preserved. Use --cluster to remove it." }) })), cluster && !scope?.hasInfrastructure && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, dimColor: true, children: "No CLI managed infrastructure found for this deployment." }) })), !willDeleteConfig && (_jsx(Box, { marginTop: !needsInfraConfirm && !cluster ? 0 : 1, children: _jsx(Text, { color: colors.muted, dimColor: true, children: "Local config files will be preserved. Use --config to remove them." }) })), !scope?.clusterAccessible && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.warning, dimColor: true, children: "\u26A0 Cluster is not accessible. Cluster resources may need manual cleanup." }) }))] }), needsInfraConfirm ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["Type", " ", _jsx(Text, { color: colors.accent, bold: true, children: "destroy-all" }), " ", "to confirm:"] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: colors.accent, children: "\u276F " }), _jsx(Text, { children: confirmText }), _jsx(Text, { color: colors.muted, children: "\u2588" })] })] })) : (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.warning, children: "Press Enter to confirm, Esc to cancel" }) }))] })) }) }));
246
+ _jsxs(_Fragment, { children: [_jsx(Text, { color: colors.accent, bold: true, children: "\u26A0 WARNING" }), _jsxs(Box, { marginY: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.muted, children: "This will permanently delete:" }), (scope?.hasHelmRelease || scope?.hasNamespace) && (_jsxs(_Fragment, { children: [_jsx(Text, { color: colors.muted, children: " \u2022 Rulebricks application" }), _jsxs(Text, { color: colors.muted, children: [" ", "\u2022 All databases and stored data"] }), _jsx(Text, { color: colors.muted, children: " \u2022 All persistent volumes" }), _jsx(Text, { color: colors.muted, children: " \u2022 Monitoring stack" }), _jsx(Text, { color: colors.muted, children: " \u2022 Kubernetes namespace" })] })), needsInfraConfirm && (_jsxs(_Fragment, { children: [_jsx(Text, { color: colors.accent, children: " \u2022 Kubernetes cluster" }), _jsx(Text, { color: colors.accent, children: " \u2022 All cloud infrastructure" })] })), willDeleteConfig && (_jsx(Text, { color: colors.muted, children: " \u2022 Local configuration files" })), !cluster && scope?.hasInfrastructure && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, dimColor: true, children: "Cloud infrastructure will be preserved. Use --cluster to remove it." }) })), cluster && !scope?.hasInfrastructure && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, dimColor: true, children: "No CLI managed infrastructure found for this deployment." }) })), !willDeleteConfig && (_jsx(Box, { marginTop: !needsInfraConfirm && !cluster ? 0 : 1, children: _jsx(Text, { color: colors.muted, dimColor: true, children: "Local config files will be preserved. Use --config to remove them." }) })), !scope?.clusterAccessible && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.warning, dimColor: true, children: "\u26A0 Cluster is not accessible. Some cluster resources may need manual cleanup." }) }))] }), needsInfraConfirm ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["Type", " ", _jsx(Text, { color: colors.accent, bold: true, children: "destroy-all" }), " ", "to confirm:"] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: colors.accent, children: "\u276F " }), _jsx(Text, { children: confirmText }), _jsx(Text, { color: colors.muted, children: "\u2588" })] })] })) : (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.warning, children: "Press Enter to confirm, Esc to cancel" }) }))] })) }) }));
247
247
  }
248
248
  export function DestroyCommand(props) {
249
249
  return (_jsxs(ThemeProvider, { theme: "destroy", children: [_jsx(Logo, {}), _jsx(DestroyCommandInner, { ...props })] }));
@@ -89,12 +89,17 @@ export function CloudProviderStep({ onComplete, onBack, }) {
89
89
  const provider = item.value;
90
90
  dispatch({ type: "SET_PROVIDER", provider });
91
91
  if (provider === "gcp") {
92
- // Try to auto-fill GCP project ID
92
+ // GCP requires project to be pre-configured, so use the detected project
93
93
  const detectedProject = await getGcpProjectId();
94
94
  if (detectedProject) {
95
- setGcpProject(detectedProject);
95
+ dispatch({ type: "SET_GCP_PROJECT", projectId: detectedProject });
96
+ // Skip project input and go directly to regions
97
+ loadRegions(provider);
98
+ }
99
+ else {
100
+ // Fallback to project input if somehow not detected (shouldn't happen with new auth check)
101
+ setSubStep("gcp-project");
96
102
  }
97
- setSubStep("gcp-project");
98
103
  }
99
104
  else if (provider === "azure") {
100
105
  setSubStep("azure-rg");
@@ -166,8 +171,9 @@ export function CloudProviderStep({ onComplete, onBack, }) {
166
171
  dispatch({ type: "SET_CLUSTER_NAME", clusterName });
167
172
  onComplete();
168
173
  };
169
- const handleGcpProjectSubmit = () => {
174
+ const handleGcpProjectSubmit = async () => {
170
175
  dispatch({ type: "SET_GCP_PROJECT", projectId: gcpProject });
176
+ // ADC is now checked upfront in checkGcloudCli(), so proceed directly to regions
171
177
  loadRegions("gcp");
172
178
  };
173
179
  const handleAzureRgSubmit = () => {
@@ -181,11 +187,18 @@ export function CloudProviderStep({ onComplete, onBack, }) {
181
187
  return _jsx(Text, { color: "gray", children: " (not installed)" });
182
188
  }
183
189
  if (!status.authenticated) {
190
+ // Check for specific error types to show more helpful messages
191
+ if (status.error?.toLowerCase().includes("quota")) {
192
+ return _jsx(Text, { color: "yellow", children: " (insufficient quota)" });
193
+ }
194
+ if (status.error?.toLowerCase().includes("resource provider")) {
195
+ return _jsx(Text, { color: "yellow", children: " (providers not registered)" });
196
+ }
184
197
  return _jsx(Text, { color: "yellow", children: " (log in required)" });
185
198
  }
186
199
  return _jsx(Text, { color: "green", children: " \u2713" });
187
200
  };
188
- return (_jsxs(BorderBox, { title: "Cloud Provider", children: [subStep === "checking" && (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Spinner, { label: "Checking cloud CLI tools..." }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: "Detecting AWS, GCP, and Azure CLIs..." }) })] })), subStep === "no-cli" && (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { color: "red", bold: true, children: "No cloud CLI tools detected" }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(Text, { children: "To provision infrastructure, you need to install and authenticate with at least one cloud CLI:" }) }), _jsx(Box, { marginTop: 1, flexDirection: "column", marginLeft: 2, children: Object.entries(CLI_INSTALL_URLS).map(([provider, info]) => (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { bold: true, children: [info.name, ":"] }), _jsxs(Text, { color: "gray", children: [" ", info.installCmd] }), _jsxs(Text, { color: "gray", children: [" ", info.url] })] }, provider))) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: "After installing, authenticate:" }) }), _jsx(Box, { marginLeft: 2, flexDirection: "column", children: Object.entries(CLI_LOGIN_COMMANDS).map(([provider, cmd]) => (_jsxs(Text, { color: "gray", children: [" ", provider, ": ", cmd] }, provider))) })] })), subStep === "provider" && cliStatus && (_jsxs(_Fragment, { children: [_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { children: "Select your cloud provider:" }), !cliStatus.anyAvailable && cliStatus.anyInstalled && (_jsx(Text, { color: "yellow", dimColor: true, children: "\u26A0 Some CLIs are installed but not authenticated" })), needsTerraform &&
201
+ return (_jsxs(BorderBox, { title: "Cloud Provider", children: [subStep === "checking" && (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Spinner, { label: "Checking cloud CLI tools..." }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: "Detecting AWS, GCP, and Azure CLIs..." }) })] })), subStep === "no-cli" && (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { color: "red", bold: true, children: "No cloud CLI tools detected" }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(Text, { children: "To provision infrastructure, you need to install and authenticate with at least one cloud CLI:" }) }), _jsx(Box, { marginTop: 1, flexDirection: "column", marginLeft: 2, children: Object.entries(CLI_INSTALL_URLS).map(([provider, info]) => (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { bold: true, children: [info.name, ":"] }), _jsxs(Text, { color: "gray", children: [" ", info.installCmd] }), _jsxs(Text, { color: "gray", children: [" ", info.url] })] }, provider))) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: "After installing, authenticate:" }) }), _jsx(Box, { marginLeft: 2, flexDirection: "column", children: Object.entries(CLI_LOGIN_COMMANDS).map(([provider, cmd]) => (_jsx(Box, { flexDirection: "column", children: Array.isArray(cmd) ? (_jsxs(_Fragment, { children: [_jsxs(Text, { color: "gray", children: [" ", provider, ":"] }), cmd.map((c, i) => (_jsxs(Text, { color: "gray", children: [" ", c] }, i)))] })) : (_jsxs(Text, { color: "gray", children: [" ", provider, ": ", cmd] })) }, provider))) })] })), subStep === "provider" && cliStatus && (_jsxs(_Fragment, { children: [_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { children: "Select your cloud provider:" }), !cliStatus.anyAvailable && cliStatus.anyInstalled && (_jsx(Text, { color: "yellow", dimColor: true, children: "\u26A0 Some CLIs are installed but not authenticated" })), needsTerraform &&
189
202
  terraformStatus &&
190
203
  !terraformStatus.installed && (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: "yellow", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: "yellow", bold: true, children: "\u26A0 Terraform not installed" }), _jsx(Text, { color: "gray", children: "You'll need Terraform to provision infrastructure." }), _jsxs(Text, { color: "gray", children: ["Install: ", TERRAFORM_INSTALL_INFO.installCmd] }), _jsx(Text, { color: "gray", dimColor: true, children: TERRAFORM_INSTALL_INFO.url })] })), needsTerraform && terraformStatus?.installed && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "green", children: "\u2713" }), _jsxs(Text, { color: "gray", children: [" ", "Terraform", " ", terraformStatus.version
191
204
  ? `v${terraformStatus.version}`
@@ -43,13 +43,26 @@ export declare function listS3Buckets(): Promise<string[]>;
43
43
  */
44
44
  export declare function listEksClusters(region: string): Promise<string[]>;
45
45
  /**
46
- * Check if gcloud CLI is installed and authenticated
46
+ * Check if gcloud CLI is installed and fully authenticated
47
+ *
48
+ * For GCP to be considered "authenticated", the user must have:
49
+ * 1. Logged in with `gcloud auth login`
50
+ * 2. Set a default project with `gcloud config set project PROJECT_ID`
51
+ * 3. Configured Application Default Credentials with `gcloud auth application-default login`
47
52
  */
48
53
  export declare function checkGcloudCli(): Promise<CloudCliStatus>;
49
54
  /**
50
55
  * Get the active GCP project ID
51
56
  */
52
57
  export declare function getGcpProjectId(): Promise<string | null>;
58
+ /**
59
+ * Check if GCP Application Default Credentials (ADC) are configured
60
+ * ADC is required for Terraform to authenticate with Google Cloud
61
+ */
62
+ export declare function checkGcpApplicationDefaultCredentials(): Promise<{
63
+ configured: boolean;
64
+ error?: string;
65
+ }>;
53
66
  /**
54
67
  * List available GCP regions
55
68
  */
@@ -64,7 +77,13 @@ export declare function listGcsBuckets(): Promise<string[]>;
64
77
  */
65
78
  export declare function listGkeClusters(region: string): Promise<string[]>;
66
79
  /**
67
- * Check if Azure CLI is installed and authenticated
80
+ * Check if Azure CLI is installed and fully authenticated
81
+ *
82
+ * For Azure to be considered "authenticated", the user must have:
83
+ * 1. Logged in with `az login`
84
+ * 2. An active subscription in "Enabled" state
85
+ * 3. Required resource providers registered (Microsoft.ContainerService, etc.)
86
+ * 4. Sufficient vCPU quota for at least the small tier (8 cores)
68
87
  */
69
88
  export declare function checkAzureCli(): Promise<CloudCliStatus>;
70
89
  /**
@@ -87,6 +106,31 @@ export declare function listAzureBlobContainers(storageAccount: string): Promise
87
106
  * List AKS clusters, optionally filtered by resource group
88
107
  */
89
108
  export declare function listAksClusters(resourceGroup?: string): Promise<string[]>;
109
+ /**
110
+ * Azure tier to vCPU core requirements mapping
111
+ */
112
+ export declare const AZURE_TIER_CORES: Record<string, number>;
113
+ /**
114
+ * Check if required Azure resource providers are registered
115
+ */
116
+ export declare function checkAzureResourceProviders(): Promise<{
117
+ allRegistered: boolean;
118
+ missing: string[];
119
+ }>;
120
+ /**
121
+ * Check Azure VM quota for a specific region
122
+ *
123
+ * @param region - Azure region to check quota for
124
+ * @param requiredCores - Number of vCPUs required
125
+ * @returns Quota check result with availability info
126
+ */
127
+ export declare function checkAzureVmQuota(region: string, requiredCores: number): Promise<{
128
+ sufficient: boolean;
129
+ available: number;
130
+ limit: number;
131
+ used: number;
132
+ error?: string;
133
+ }>;
90
134
  /**
91
135
  * Check all cloud CLIs in parallel
92
136
  */
@@ -116,7 +160,7 @@ export declare const CLI_INSTALL_URLS: Record<CloudProvider, {
116
160
  /**
117
161
  * Get login commands for cloud CLIs
118
162
  */
119
- export declare const CLI_LOGIN_COMMANDS: Record<CloudProvider, string>;
163
+ export declare const CLI_LOGIN_COMMANDS: Record<CloudProvider, string | string[]>;
120
164
  /**
121
165
  * Terraform installation status
122
166
  */