@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.
@@ -2,11 +2,39 @@ import { execa } from 'execa';
2
2
  import { promises as fs } from 'fs';
3
3
  import path from 'path';
4
4
  import { fileURLToPath } from 'url';
5
+ import { isSupportedDnsProvider } from '../types/index.js';
5
6
  import { getTerraformDir } from './config.js';
6
7
  const __filename = fileURLToPath(import.meta.url);
7
8
  const __dirname = path.dirname(__filename);
8
9
  // Path to embedded terraform templates
9
10
  const TERRAFORM_TEMPLATES_DIR = path.resolve(__dirname, '../../terraform');
11
+ /**
12
+ * Detects if an error is a GCP authentication error
13
+ */
14
+ function isGcpAuthError(output) {
15
+ const lowerOutput = output.toLowerCase();
16
+ return (lowerOutput.includes('oauth2') ||
17
+ lowerOutput.includes('invalid_grant') ||
18
+ lowerOutput.includes('reauth') ||
19
+ lowerOutput.includes('invalid_rapt') ||
20
+ lowerOutput.includes('authentication') && lowerOutput.includes('google') ||
21
+ lowerOutput.includes('unable to find default credentials') ||
22
+ lowerOutput.includes('application default credentials'));
23
+ }
24
+ /**
25
+ * Enhances GCP authentication errors with helpful guidance
26
+ */
27
+ function enhanceGcpAuthError(output) {
28
+ return ('GCP Authentication Error\n\n' +
29
+ 'Terraform requires Application Default Credentials (ADC) to authenticate with Google Cloud.\n\n' +
30
+ 'To fix this:\n' +
31
+ ' • Run: gcloud auth login\n' +
32
+ ' • Run: gcloud auth application-default login\n' +
33
+ ' • Verify: gcloud auth application-default print-access-token\n\n' +
34
+ 'For more information: https://cloud.google.com/docs/authentication/application-default-credentials\n\n' +
35
+ 'Original error:\n' +
36
+ (output.length > 500 ? '...' + output.slice(-500) : output));
37
+ }
10
38
  /**
11
39
  * Extracts meaningful error message from execa error
12
40
  */
@@ -15,6 +43,10 @@ function getErrorMessage(error, fallback) {
15
43
  // Try stderr first, then stdout (terraform sometimes writes errors to stdout)
16
44
  const output = execaError.stderr || execaError.stdout || '';
17
45
  if (output) {
46
+ // Check if this is a GCP authentication error
47
+ if (isGcpAuthError(output)) {
48
+ return enhanceGcpAuthError(output);
49
+ }
18
50
  // Get last 500 chars of output for the error message
19
51
  const truncated = output.length > 500 ? '...' + output.slice(-500) : output;
20
52
  return truncated;
@@ -137,11 +169,27 @@ export async function terraformApply(deploymentName) {
137
169
  }
138
170
  }
139
171
  /**
140
- * Destroys Terraform infrastructure
172
+ * Destroys Terraform infrastructure.
173
+ * Runs init first to ensure .terraform folder exists (handles partial deployments).
141
174
  */
142
175
  export async function terraformDestroy(deploymentName) {
143
176
  const workDir = getTerraformDir(deploymentName);
144
177
  try {
178
+ // Run init first to ensure terraform is ready (handles partial deployments
179
+ // where .terraform folder might be missing or corrupted)
180
+ try {
181
+ await execa('terraform', ['init', '-upgrade'], {
182
+ cwd: workDir
183
+ });
184
+ }
185
+ catch (initError) {
186
+ // If init fails, still try destroy - it might work if state exists
187
+ const execaInitError = initError;
188
+ if (execaInitError.stdout || execaInitError.stderr) {
189
+ await saveLogFile(workDir, 'destroy-init', execaInitError.stdout || '', execaInitError.stderr || '');
190
+ }
191
+ // Don't throw - continue to try destroy anyway
192
+ }
145
193
  await execa('terraform', ['destroy', '-auto-approve'], {
146
194
  cwd: workDir
147
195
  });
@@ -175,19 +223,98 @@ export async function getTerraformOutputs(deploymentName) {
175
223
  }
176
224
  }
177
225
  /**
178
- * Checks if Terraform state exists for a deployment
226
+ * Checks if Terraform files/state exist for a deployment.
227
+ * Returns true if the terraform directory contains any terraform files,
228
+ * not just the state file. This allows destroy to work on partial infrastructure.
179
229
  */
180
230
  export async function hasTerraformState(deploymentName) {
181
231
  const workDir = getTerraformDir(deploymentName);
182
- const statePath = path.join(workDir, 'terraform.tfstate');
183
232
  try {
184
- await fs.access(statePath);
185
- return true;
233
+ // Check if terraform directory exists
234
+ await fs.access(workDir);
235
+ // Check for any of: state file, .terraform folder, or .tf files
236
+ const entries = await fs.readdir(workDir);
237
+ const hasTerraformFiles = entries.some((e) => e === 'terraform.tfstate' ||
238
+ e === '.terraform' ||
239
+ e.endsWith('.tf'));
240
+ return hasTerraformFiles;
186
241
  }
187
242
  catch {
188
243
  return false;
189
244
  }
190
245
  }
246
+ /**
247
+ * Generates Terraform variables from deployment configuration
248
+ */
249
+ export function generateTerraformVars(config) {
250
+ const provider = config.infrastructure.provider;
251
+ if (!provider) {
252
+ throw new Error('Cloud provider is required for infrastructure provisioning');
253
+ }
254
+ const region = config.infrastructure.region || (provider === 'gcp' ? 'us-central1' : provider === 'aws' ? 'us-east-1' : 'eastus');
255
+ const clusterName = config.infrastructure.clusterName || `${config.name}-cluster`;
256
+ const tier = config.tier || 'small';
257
+ const kubernetesVersion = '1.34';
258
+ // Determine if external DNS should be enabled
259
+ const enableExternalDns = config.dns.autoManage && isSupportedDnsProvider(config.dns.provider);
260
+ // Determine logging configuration
261
+ const loggingSink = config.features.logging.sink;
262
+ const loggingBucket = config.features.logging.bucket || '';
263
+ switch (provider) {
264
+ case 'gcp': {
265
+ if (!config.infrastructure.gcpProjectId) {
266
+ throw new Error('GCP project ID is required for GCP infrastructure provisioning');
267
+ }
268
+ const vars = {
269
+ project_id: config.infrastructure.gcpProjectId,
270
+ region,
271
+ cluster_name: clusterName,
272
+ tier,
273
+ kubernetes_version: kubernetesVersion,
274
+ enable_external_dns: enableExternalDns,
275
+ enable_gcs_logging: loggingSink === 'gcs',
276
+ logging_gcs_bucket: loggingSink === 'gcs' ? loggingBucket : '',
277
+ };
278
+ return vars;
279
+ }
280
+ case 'aws': {
281
+ // Extract domain suffix for external DNS domain filter
282
+ const domainSuffix = enableExternalDns && config.domain ? config.domain.split('.').slice(1).join('.') : '';
283
+ const vars = {
284
+ region,
285
+ cluster_name: clusterName,
286
+ tier,
287
+ kubernetes_version: kubernetesVersion,
288
+ enable_external_dns: enableExternalDns,
289
+ external_dns_domain: enableExternalDns ? domainSuffix : '',
290
+ enable_s3_logging: loggingSink === 's3',
291
+ logging_s3_bucket: loggingSink === 's3' ? loggingBucket : '',
292
+ };
293
+ return vars;
294
+ }
295
+ case 'azure': {
296
+ const resourceGroupName = config.infrastructure.azureResourceGroup || `${config.name}-rg`;
297
+ // For Azure DNS, we need the DNS zone resource group
298
+ // This is typically the same as the resource group, but can be different
299
+ const dnsZoneResourceGroup = enableExternalDns ? resourceGroupName : '';
300
+ const vars = {
301
+ resource_group_name: resourceGroupName,
302
+ location: region,
303
+ cluster_name: clusterName,
304
+ tier,
305
+ kubernetes_version: kubernetesVersion,
306
+ enable_external_dns: enableExternalDns,
307
+ dns_zone_resource_group: dnsZoneResourceGroup,
308
+ enable_blob_logging: loggingSink === 'azure-blob',
309
+ logging_storage_account: loggingSink === 'azure-blob' ? loggingBucket : '',
310
+ logging_container_name: loggingSink === 'azure-blob' ? 'logs' : '',
311
+ };
312
+ return vars;
313
+ }
314
+ default:
315
+ throw new Error(`Unsupported cloud provider: ${provider}`);
316
+ }
317
+ }
191
318
  /**
192
319
  * Updates kubeconfig for the provisioned cluster
193
320
  */
@@ -21,51 +21,120 @@ export const LOGGING_SINK_CATEGORIES = {
21
21
  // Region mappings
22
22
  export const CLOUD_REGIONS = {
23
23
  aws: [
24
+ // US regions (c8g Graviton4 available)
24
25
  "us-east-1",
25
26
  "us-east-2",
26
27
  "us-west-1",
27
28
  "us-west-2",
29
+ // Canada
30
+ "ca-central-1",
31
+ "ca-west-1",
32
+ // Europe (c8g available)
28
33
  "eu-west-1",
29
34
  "eu-west-2",
30
35
  "eu-west-3",
31
36
  "eu-central-1",
32
- "ap-south-1",
33
- "ap-southeast-1",
34
- "ap-southeast-2",
37
+ "eu-central-2",
38
+ "eu-north-1",
39
+ "eu-south-1",
40
+ "eu-south-2",
41
+ // Asia Pacific (c8g available)
35
42
  "ap-northeast-1",
36
43
  "ap-northeast-2",
37
- "ca-central-1",
44
+ "ap-northeast-3",
45
+ "ap-southeast-1",
46
+ "ap-southeast-2",
47
+ "ap-southeast-3",
48
+ "ap-southeast-4",
49
+ "ap-southeast-5",
50
+ "ap-southeast-7",
51
+ "ap-south-1",
52
+ "ap-south-2",
53
+ "ap-east-1",
54
+ // South America
38
55
  "sa-east-1",
56
+ // Middle East & Africa
57
+ "me-south-1",
58
+ "me-central-1",
59
+ "af-south-1",
60
+ "il-central-1",
39
61
  ],
40
62
  gcp: [
41
- "us-central1",
42
- "us-east1",
43
- "us-west1",
44
- "us-west2",
45
- "europe-west1",
46
- "europe-west2",
47
- "europe-west3",
48
- "asia-east1",
49
- "asia-northeast1",
50
- "asia-southeast1",
51
- "australia-southeast1",
52
- "southamerica-east1",
63
+ // Tier 1: Full C4A (Google Axion ARM64) availability - 3+ zones confirmed
64
+ // US regions
65
+ "us-central1", // C4A in zones a, b, c, f (best availability)
66
+ "us-east1", // C4A in zones b, c, d
67
+ "us-east4", // C4A in zones a, b, c
68
+ "us-west1", // C4A in zones a, b, c
69
+ "us-west4", // C4A in zones a, b, c
70
+ // North America
71
+ "northamerica-south1", // C4A in zones a, b, c (Mexico)
72
+ // Europe
73
+ "europe-west1", // C4A in zones b, c, d
74
+ "europe-west2", // C4A in zones a, b, c
75
+ "europe-west3", // C4A in zones a, b, c
76
+ "europe-west4", // C4A in zones a, b, c
77
+ "europe-north1", // C4A in zones a, b
78
+ // Asia Pacific
79
+ "asia-east1", // C4A in zones a, b, c
80
+ "asia-northeast1", // C4A in zones b, c
81
+ "asia-south1", // C4A in zones a, b, c
82
+ "asia-southeast1", // C4A in zones a, b, c
83
+ // Australia
84
+ "australia-southeast2", // C4A in zones a, b, c
53
85
  ],
54
86
  azure: [
87
+ // US regions (Dpsv5 ARM64 available)
55
88
  "eastus",
56
89
  "eastus2",
57
90
  "westus",
58
91
  "westus2",
92
+ "westus3",
59
93
  "centralus",
94
+ "northcentralus",
95
+ "southcentralus",
96
+ "westcentralus",
97
+ // Canada
98
+ "canadacentral",
99
+ "canadaeast",
100
+ // South America
101
+ "brazilsouth",
102
+ // Europe (Dpsv5 available)
60
103
  "northeurope",
61
104
  "westeurope",
62
105
  "uksouth",
106
+ "ukwest",
107
+ "francecentral",
108
+ "francesouth",
109
+ "germanywestcentral",
110
+ "germanynorth",
111
+ "switzerlandnorth",
112
+ "switzerlandwest",
113
+ "norwayeast",
114
+ "norwaywest",
115
+ "swedencentral",
116
+ "polandcentral",
117
+ // Asia Pacific
63
118
  "eastasia",
64
119
  "southeastasia",
65
120
  "japaneast",
121
+ "japanwest",
122
+ "koreacentral",
123
+ "koreasouth",
124
+ // Australia
66
125
  "australiaeast",
67
- "canadacentral",
68
- "brazilsouth",
126
+ "australiasoutheast",
127
+ "australiacentral",
128
+ // India
129
+ "centralindia",
130
+ "southindia",
131
+ "westindia",
132
+ // Middle East & Africa
133
+ "uaenorth",
134
+ "uaecentral",
135
+ "southafricanorth",
136
+ "qatarcentral",
137
+ "israelcentral",
69
138
  ],
70
139
  };
71
140
  // Performance tier configurations
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulebricks/cli",
3
- "version": "2.0.5",
3
+ "version": "2.0.6",
4
4
  "description": "CLI for deploying and managing private Rulebricks instances",
5
5
  "type": "module",
6
6
  "bin": {
@@ -38,7 +38,7 @@ variable "tier" {
38
38
  variable "kubernetes_version" {
39
39
  description = "Kubernetes version"
40
40
  type = string
41
- default = "1.29"
41
+ default = "1.34"
42
42
  }
43
43
 
44
44
  variable "enable_external_dns" {
@@ -74,21 +74,21 @@ locals {
74
74
  instance_type = "c8g.large" # 2 vCPU, 4GB (Graviton4 ARM64)
75
75
  min_nodes = 4
76
76
  max_nodes = 4
77
- disk_size = 50
77
+ disk_size = 20
78
78
  }
79
79
  medium = {
80
80
  node_count = 4
81
81
  instance_type = "c8g.xlarge" # 4 vCPU, 8GB (Graviton4 ARM64)
82
82
  min_nodes = 4
83
83
  max_nodes = 8
84
- disk_size = 100
84
+ disk_size = 30
85
85
  }
86
86
  large = {
87
87
  node_count = 5
88
88
  instance_type = "c8g.2xlarge" # 8 vCPU, 16GB (Graviton4 ARM64)
89
89
  min_nodes = 5
90
90
  max_nodes = 16
91
- disk_size = 200
91
+ disk_size = 50
92
92
  }
93
93
  }
94
94
 
@@ -44,7 +44,7 @@ variable "tier" {
44
44
  variable "kubernetes_version" {
45
45
  description = "Kubernetes version"
46
46
  type = string
47
- default = "1.29"
47
+ default = "1.34"
48
48
  }
49
49
 
50
50
  variable "enable_external_dns" {
@@ -86,21 +86,21 @@ locals {
86
86
  vm_size = "Standard_D2ps_v5" # 2 vCPU, 8GB (Ampere ARM64)
87
87
  min_nodes = 4
88
88
  max_nodes = 4
89
- disk_size = 50
89
+ disk_size = 20
90
90
  }
91
91
  medium = {
92
92
  node_count = 4
93
93
  vm_size = "Standard_D4ps_v5" # 4 vCPU, 16GB (Ampere ARM64)
94
94
  min_nodes = 4
95
95
  max_nodes = 8
96
- disk_size = 100
96
+ disk_size = 30
97
97
  }
98
98
  large = {
99
99
  node_count = 5
100
100
  vm_size = "Standard_D8ps_v5" # 8 vCPU, 32GB (Ampere ARM64)
101
101
  min_nodes = 5
102
102
  max_nodes = 16
103
- disk_size = 200
103
+ disk_size = 50
104
104
  }
105
105
  }
106
106
 
@@ -53,7 +53,7 @@ variable "tier" {
53
53
  variable "kubernetes_version" {
54
54
  description = "Kubernetes version"
55
55
  type = string
56
- default = "1.29"
56
+ default = "1.34"
57
57
  }
58
58
 
59
59
  variable "enable_external_dns" {
@@ -75,7 +75,8 @@ variable "logging_gcs_bucket" {
75
75
  }
76
76
 
77
77
  # Tier configurations
78
- # Using Axion C4A (ARM64) instances for compatibility with arm64 container images
78
+ # Using C4A (Google Axion ARM64) instances for best ARM64 performance
79
+ # C4A requires Hyperdisk (does not support Persistent Disk)
79
80
  locals {
80
81
  tier_configs = {
81
82
  small = {
@@ -83,21 +84,21 @@ locals {
83
84
  machine_type = "c4a-standard-2" # 2 vCPU, 8GB (Google Axion ARM64)
84
85
  min_nodes = 4
85
86
  max_nodes = 4
86
- disk_size = 50
87
+ disk_size = 20
87
88
  }
88
89
  medium = {
89
90
  node_count = 4
90
91
  machine_type = "c4a-standard-4" # 4 vCPU, 16GB (Google Axion ARM64)
91
92
  min_nodes = 4
92
93
  max_nodes = 8
93
- disk_size = 100
94
+ disk_size = 30
94
95
  }
95
96
  large = {
96
97
  node_count = 5
97
98
  machine_type = "c4a-standard-8" # 8 vCPU, 32GB (Google Axion ARM64)
98
99
  min_nodes = 5
99
100
  max_nodes = 16
100
- disk_size = 200
101
+ disk_size = 50
101
102
  }
102
103
  }
103
104
 
@@ -211,6 +212,9 @@ resource "google_container_cluster" "cluster" {
211
212
  remove_default_node_pool = true
212
213
  initial_node_count = 1
213
214
 
215
+ # Allow terraform destroy to delete the cluster
216
+ deletion_protection = false
217
+
214
218
  # Cluster configuration
215
219
  min_master_version = var.kubernetes_version
216
220
 
@@ -286,7 +290,7 @@ resource "google_container_node_pool" "primary" {
286
290
  preemptible = false
287
291
  machine_type = local.config.machine_type
288
292
  disk_size_gb = local.config.disk_size
289
- disk_type = "pd-ssd"
293
+ disk_type = "hyperdisk-balanced"
290
294
 
291
295
  oauth_scopes = [
292
296
  "https://www.googleapis.com/auth/cloud-platform"