@rulebricks/cli 2.1.4 → 2.1.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.
@@ -8,7 +8,7 @@ import { loadDeploymentConfig, loadDeploymentState, saveDeploymentState, updateD
8
8
  import { setupTerraformWorkspace, terraformInit, terraformPlan, terraformApply, terraformDestroy, cleanupOrphanedResources, updateKubeconfig, hasTerraformState, isTerraformInstalled, generateTerraformVars, } from "../lib/terraform.js";
9
9
  import { checkGcpApplicationDefaultCredentials, checkAzureResourceProviders, checkAzureVmQuota, AZURE_TIER_CORES, } from "../lib/cloudCli.js";
10
10
  import { installOrUpgradeChart, upgradeChart, isHelmInstalled, } from "../lib/helm.js";
11
- import { isKubectlInstalled, checkClusterAccessible, } from "../lib/kubernetes.js";
11
+ import { isKubectlInstalled, checkClusterAccessible, waitForCertificatesReady, } from "../lib/kubernetes.js";
12
12
  import { generateHelmValues, updateHelmValuesForTLS, } from "../lib/helmValues.js";
13
13
  import { isSupportedDnsProvider, getNamespace, getReleaseName, } from "../types/index.js";
14
14
  function DeployCommandInner({ name, skipInfra, skipDns, version, }) {
@@ -20,11 +20,13 @@ function DeployCommandInner({ name, skipInfra, skipDns, version, }) {
20
20
  const [useExternalDns, setUseExternalDns] = useState(false);
21
21
  const infraStartedRef = useRef(false); // Track if we started infra provisioning (ref for sync access)
22
22
  const [cleanupError, setCleanupError] = useState(null);
23
+ const [tlsWarning, setTlsWarning] = useState(null);
23
24
  const [status, setStatus] = useState({
24
25
  preflight: "pending",
25
26
  infrastructure: "pending",
26
27
  kubeconfig: "pending",
27
28
  helmInstall: "pending",
29
+ certCheck: "pending",
28
30
  dnsConfig: "pending",
29
31
  helmUpgradeTls: "pending",
30
32
  });
@@ -83,7 +85,17 @@ function DeployCommandInner({ name, skipInfra, skipDns, version, }) {
83
85
  const releaseName = getReleaseName(config.name);
84
86
  // Upgrade the chart with TLS enabled
85
87
  await upgradeChart(name, { releaseName, namespace, version, wait: true });
86
- setStatus((s) => ({ ...s, helmUpgradeTls: "success" }));
88
+ setStatus((s) => ({ ...s, helmUpgradeTls: "success", certCheck: "running" }));
89
+ setStep("cert-check");
90
+ try {
91
+ await waitForCertificatesReady(namespace);
92
+ setStatus((s) => ({ ...s, certCheck: "success" }));
93
+ }
94
+ catch (certErr) {
95
+ setStatus((s) => ({ ...s, certCheck: "error" }));
96
+ setTlsWarning("TLS certificates are still being issued. " +
97
+ "HTTPS may not be available yet.");
98
+ }
87
99
  // Update state
88
100
  await updateDeploymentStatus(name, "running", {
89
101
  application: {
@@ -113,6 +125,7 @@ function DeployCommandInner({ name, skipInfra, skipDns, version, }) {
113
125
  ...s,
114
126
  dnsConfig: "skipped",
115
127
  helmUpgradeTls: "skipped",
128
+ certCheck: "skipped",
116
129
  }));
117
130
  const namespace = getNamespace(config.name);
118
131
  // Mark as running without TLS upgrade
@@ -223,7 +236,18 @@ function DeployCommandInner({ name, skipInfra, skipDns, version, }) {
223
236
  helmInstall: "success",
224
237
  dnsConfig: "skipped", // External DNS handles this
225
238
  helmUpgradeTls: "skipped", // TLS enabled from start
239
+ certCheck: "running",
226
240
  }));
241
+ setStep("cert-check");
242
+ try {
243
+ await waitForCertificatesReady(namespace);
244
+ setStatus((s) => ({ ...s, certCheck: "success" }));
245
+ }
246
+ catch (certErr) {
247
+ setStatus((s) => ({ ...s, certCheck: "error" }));
248
+ setTlsWarning("TLS certificates are still being issued. " +
249
+ "HTTPS may not be available yet.");
250
+ }
227
251
  // Update state to running
228
252
  await updateDeploymentStatus(name, "running", {
229
253
  application: {
@@ -254,6 +278,7 @@ function DeployCommandInner({ name, skipInfra, skipDns, version, }) {
254
278
  ...s,
255
279
  dnsConfig: "skipped",
256
280
  helmUpgradeTls: "skipped",
281
+ certCheck: "skipped",
257
282
  }));
258
283
  await updateDeploymentStatus(name, "waiting-dns", {
259
284
  application: {
@@ -421,7 +446,7 @@ function DeployCommandInner({ name, skipInfra, skipDns, version, }) {
421
446
  // Complete screen
422
447
  if (step === "complete") {
423
448
  const tlsSkipped = status.helmUpgradeTls === "skipped" && !useExternalDns;
424
- return (_jsx(BorderBox, { title: "Deployment Complete", children: _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { color: colors.success, bold: true, children: "\u2713 Rulebricks deployed successfully!" }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { children: ["URL:", " ", _jsxs(Text, { color: colors.accent, children: ["https://", config?.domain, "/auth/signup"] })] }), useExternalDns && (_jsx(Text, { color: colors.muted, children: "DNS records will be created automatically by external-dns" })), tlsSkipped && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.warning, children: ["\u26A0 TLS not configured. Run `rulebricks deploy ", name, "` again after DNS setup."] }) }))] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Next steps:" }), _jsxs(Text, { color: colors.muted, children: [" ", "\u2022 Visit the URL to complete initial setup"] }), _jsxs(Text, { color: colors.muted, children: [" ", "\u2022 Run `rulebricks status ", name, "` to check deployment health"] }), tlsSkipped && (_jsxs(Text, { color: colors.muted, children: [" ", "\u2022 Configure DNS and re-run deploy for TLS"] }))] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.muted, dimColor: true, children: "Tip: If the URL isn't accessible yet, your local DNS may need time to propagate." }), _jsxs(Text, { color: colors.muted, dimColor: true, children: ["Flush DNS cache: ", getDnsFlushCommand()] })] })] }) }));
449
+ return (_jsx(BorderBox, { title: "Deployment Complete", children: _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { color: colors.success, bold: true, children: "\u2713 Rulebricks deployed successfully!" }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { children: ["URL:", " ", _jsxs(Text, { color: colors.accent, children: ["https://", config?.domain, "/auth/signup"] })] }), useExternalDns && (_jsx(Text, { color: colors.muted, children: "DNS records will be created automatically by external-dns" })), tlsSkipped && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.warning, children: ["\u26A0 TLS not configured. Run `rulebricks deploy ", name, "` again after DNS setup."] }) })), tlsWarning && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.warning, children: ["\u26A0 ", tlsWarning] }) }))] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Next steps:" }), _jsxs(Text, { color: colors.muted, children: [" ", "\u2022 Visit the URL to complete initial setup"] }), _jsxs(Text, { color: colors.muted, children: [" ", "\u2022 Run `rulebricks status ", name, "` to check deployment health"] }), tlsSkipped && (_jsxs(Text, { color: colors.muted, children: [" ", "\u2022 Configure DNS and re-run deploy for TLS"] })), tlsWarning && (_jsxs(Text, { color: colors.muted, children: [" ", "\u2022 Run `rulebricks status ", name, "` to check TLS certificate status"] }))] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.muted, dimColor: true, children: "Tip: If the URL isn't accessible yet, your local DNS may need time to propagate." }), _jsxs(Text, { color: colors.muted, dimColor: true, children: ["Flush DNS cache: ", getDnsFlushCommand()] })] })] }) }));
425
450
  }
426
451
  // Progress screen
427
452
  const helmInstallLabel = useExternalDns
@@ -435,7 +460,7 @@ function DeployCommandInner({ name, skipInfra, skipDns, version, }) {
435
460
  ? "Planning changes"
436
461
  : step === "infra-apply"
437
462
  ? "Applying infrastructure"
438
- : undefined }), _jsx(StatusLine, { status: status.kubeconfig, label: "Kubernetes configuration" }), _jsx(StatusLine, { status: status.helmInstall, label: helmInstallLabel }), !useExternalDns && (_jsxs(_Fragment, { children: [_jsx(StatusLine, { status: status.dnsConfig, label: "DNS configuration" }), _jsx(StatusLine, { status: status.helmUpgradeTls, label: "TLS configuration" })] })), step !== "dns-wait" && (_jsx(Box, { marginTop: 1, children: _jsx(Spinner, { label: getStepLabel(step, useExternalDns) }) }))] }) }));
463
+ : undefined }), _jsx(StatusLine, { status: status.kubeconfig, label: "Kubernetes configuration" }), _jsx(StatusLine, { status: status.helmInstall, label: helmInstallLabel }), !useExternalDns && (_jsxs(_Fragment, { children: [_jsx(StatusLine, { status: status.dnsConfig, label: "DNS configuration" }), _jsx(StatusLine, { status: status.helmUpgradeTls, label: "TLS configuration" })] })), _jsx(StatusLine, { status: status.certCheck, label: "TLS certificate verification" }), step !== "dns-wait" && (_jsx(Box, { marginTop: 1, children: _jsx(Spinner, { label: getStepLabel(step, useExternalDns) }) }))] }) }));
439
464
  }
440
465
  function getDnsFlushCommand() {
441
466
  switch (platform()) {
@@ -471,6 +496,8 @@ function getStepLabel(step, useExternalDns) {
471
496
  return "Waiting for DNS configuration...";
472
497
  case "helm-upgrade-tls":
473
498
  return "Enabling TLS certificates...";
499
+ case "cert-check":
500
+ return "Verifying TLS certificates...";
474
501
  default:
475
502
  return "Processing...";
476
503
  }
@@ -58,7 +58,31 @@ export interface CertificateStatus {
58
58
  name: string;
59
59
  dnsNames: string[];
60
60
  ready: boolean;
61
+ failed: boolean;
62
+ message?: string;
61
63
  }
64
+ /**
65
+ * Deletes a failed cert-manager Certificate and recreates it from its spec,
66
+ * bypassing cert-manager's exponential backoff on failed issuance attempts.
67
+ * The delete cascades to the failed CertificateRequest and ACME Order via
68
+ * owner references, so the recreated Certificate starts with a clean slate.
69
+ */
70
+ export declare function recreateFailedCertificate(namespace: string, certName: string): Promise<boolean>;
71
+ /**
72
+ * Polls cert-manager Certificates until all are Ready, with automatic retry
73
+ * for transient ACME failures (e.g. order finalization race conditions).
74
+ *
75
+ * On failure detection: deletes and recreates the Certificate resource to
76
+ * bypass cert-manager's 1-hour exponential backoff, then continues polling.
77
+ *
78
+ * Throws on timeout with details about which certs are not ready.
79
+ * Returns silently if no Certificate resources exist in the namespace.
80
+ */
81
+ export declare function waitForCertificatesReady(namespace: string, options?: {
82
+ timeoutMs?: number;
83
+ pollIntervalMs?: number;
84
+ maxRetries?: number;
85
+ }): Promise<void>;
62
86
  /**
63
87
  * Streams logs from a pod
64
88
  */
@@ -258,16 +258,109 @@ export async function getCertificateStatus(namespace = DEFAULT_NAMESPACE) {
258
258
  "json",
259
259
  ]);
260
260
  const data = JSON.parse(stdout);
261
- return data.items.map((cert) => ({
262
- name: cert.metadata.name,
263
- dnsNames: cert.spec.dnsNames ?? [],
264
- ready: cert.status.conditions?.some((c) => c.type === "Ready" && c.status === "True") ?? false,
265
- }));
261
+ return data.items.map((cert) => {
262
+ const readyCond = cert.status.conditions?.find((c) => c.type === "Ready");
263
+ const issuingCond = cert.status.conditions?.find((c) => c.type === "Issuing");
264
+ const ready = readyCond?.status === "True";
265
+ const failed = !ready &&
266
+ issuingCond?.status === "False" &&
267
+ issuingCond?.reason === "Failed";
268
+ return {
269
+ name: cert.metadata.name,
270
+ dnsNames: cert.spec.dnsNames ?? [],
271
+ ready,
272
+ failed: failed ?? false,
273
+ message: failed ? issuingCond?.message : readyCond?.message,
274
+ };
275
+ });
266
276
  }
267
277
  catch {
268
278
  return [];
269
279
  }
270
280
  }
281
+ /**
282
+ * Deletes a failed cert-manager Certificate and recreates it from its spec,
283
+ * bypassing cert-manager's exponential backoff on failed issuance attempts.
284
+ * The delete cascades to the failed CertificateRequest and ACME Order via
285
+ * owner references, so the recreated Certificate starts with a clean slate.
286
+ */
287
+ export async function recreateFailedCertificate(namespace, certName) {
288
+ try {
289
+ const { stdout } = await execa("kubectl", [
290
+ "get",
291
+ "certificate",
292
+ certName,
293
+ "-n",
294
+ namespace,
295
+ "-o",
296
+ "json",
297
+ ]);
298
+ const cert = JSON.parse(stdout);
299
+ const recreated = {
300
+ apiVersion: "cert-manager.io/v1",
301
+ kind: "Certificate",
302
+ metadata: {
303
+ name: cert.metadata.name,
304
+ namespace: cert.metadata.namespace,
305
+ ...(cert.metadata.labels ? { labels: cert.metadata.labels } : {}),
306
+ ...(cert.metadata.annotations
307
+ ? { annotations: cert.metadata.annotations }
308
+ : {}),
309
+ },
310
+ spec: cert.spec,
311
+ };
312
+ await execa("kubectl", ["delete", "certificate", certName, "-n", namespace]);
313
+ await execa("kubectl", ["apply", "-f", "-"], {
314
+ input: JSON.stringify(recreated),
315
+ });
316
+ return true;
317
+ }
318
+ catch {
319
+ return false;
320
+ }
321
+ }
322
+ /**
323
+ * Polls cert-manager Certificates until all are Ready, with automatic retry
324
+ * for transient ACME failures (e.g. order finalization race conditions).
325
+ *
326
+ * On failure detection: deletes and recreates the Certificate resource to
327
+ * bypass cert-manager's 1-hour exponential backoff, then continues polling.
328
+ *
329
+ * Throws on timeout with details about which certs are not ready.
330
+ * Returns silently if no Certificate resources exist in the namespace.
331
+ */
332
+ export async function waitForCertificatesReady(namespace, options) {
333
+ const { timeoutMs = 120_000, pollIntervalMs = 5_000, maxRetries = 1, } = options ?? {};
334
+ let retriesUsed = 0;
335
+ const deadline = Date.now() + timeoutMs;
336
+ while (Date.now() < deadline) {
337
+ const certs = await getCertificateStatus(namespace);
338
+ if (certs.length === 0)
339
+ return;
340
+ if (certs.every((c) => c.ready))
341
+ return;
342
+ const failed = certs.filter((c) => c.failed);
343
+ if (failed.length > 0 && retriesUsed < maxRetries) {
344
+ for (const cert of failed) {
345
+ await recreateFailedCertificate(namespace, cert.name);
346
+ }
347
+ retriesUsed++;
348
+ }
349
+ await sleep(pollIntervalMs);
350
+ }
351
+ // Final check after timeout
352
+ const certs = await getCertificateStatus(namespace);
353
+ if (certs.length > 0 && certs.every((c) => c.ready))
354
+ return;
355
+ const notReady = certs.filter((c) => !c.ready);
356
+ if (notReady.length > 0) {
357
+ const details = notReady
358
+ .map((c) => ` ${c.name}: ${c.message || "not ready"}`)
359
+ .join("\n");
360
+ throw new Error(`TLS certificates not ready after ${timeoutMs / 1000}s:\n${details}\n\n` +
361
+ `Run 'rulebricks status' to check certificate status.`);
362
+ }
363
+ }
271
364
  /**
272
365
  * Streams logs from a pod
273
366
  */
@@ -265,7 +265,32 @@ async function deleteAwsCloudWatchLogGroup(clusterName, region) {
265
265
  }
266
266
  catch { /* may not exist */ }
267
267
  }
268
- async function deleteAwsOidcProvider(clusterName) {
268
+ /**
269
+ * Captures the OIDC issuer URL from an EKS cluster before it's deleted.
270
+ * The URL uses a random cluster ID (not the cluster name), so we must
271
+ * grab it while the cluster still exists to identify the OIDC provider later.
272
+ */
273
+ async function getEksOidcIssuer(clusterName, region) {
274
+ try {
275
+ const { stdout } = await execa('aws', [
276
+ 'eks', 'describe-cluster',
277
+ '--name', clusterName,
278
+ '--region', region,
279
+ '--query', 'cluster.identity.oidc.issuer',
280
+ '--output', 'text',
281
+ ]);
282
+ const url = stdout.trim();
283
+ return url && url !== 'None' ? url : undefined;
284
+ }
285
+ catch {
286
+ return undefined;
287
+ }
288
+ }
289
+ async function deleteAwsOidcProvider(oidcIssuerUrl) {
290
+ if (!oidcIssuerUrl)
291
+ return;
292
+ // Strip the https:// prefix to match how IAM stores the URL
293
+ const issuerHost = oidcIssuerUrl.replace('https://', '');
269
294
  let providerArns;
270
295
  try {
271
296
  const { stdout } = await execa('aws', [
@@ -286,7 +311,7 @@ async function deleteAwsOidcProvider(clusterName) {
286
311
  '--output', 'json',
287
312
  ]);
288
313
  const parsed = JSON.parse(stdout);
289
- if (parsed.Url && parsed.Url.includes(clusterName)) {
314
+ if (parsed.Url && issuerHost.includes(parsed.Url)) {
290
315
  await execa('aws', [
291
316
  'iam', 'delete-open-id-connect-provider',
292
317
  '--open-id-connect-provider-arn', arn,
@@ -296,6 +321,29 @@ async function deleteAwsOidcProvider(clusterName) {
296
321
  catch { /* skip */ }
297
322
  }
298
323
  }
324
+ async function releaseAwsElasticIps(clusterName, region) {
325
+ try {
326
+ const { stdout } = await execa('aws', [
327
+ 'ec2', 'describe-addresses',
328
+ '--filters', `Name=tag:Name,Values=*${clusterName}*`,
329
+ '--region', region,
330
+ '--query', 'Addresses[?AssociationId==null].AllocationId',
331
+ '--output', 'json',
332
+ ]);
333
+ const allocationIds = JSON.parse(stdout);
334
+ for (const id of allocationIds) {
335
+ try {
336
+ await execa('aws', [
337
+ 'ec2', 'release-address',
338
+ '--allocation-id', id,
339
+ '--region', region,
340
+ ]);
341
+ }
342
+ catch { /* may already be released */ }
343
+ }
344
+ }
345
+ catch { /* skip */ }
346
+ }
299
347
  async function deleteAwsIamRole(roleName) {
300
348
  // Detach all managed policies
301
349
  try {
@@ -343,6 +391,107 @@ async function deleteAwsIamRole(roleName) {
343
391
  }
344
392
  catch { /* may not exist */ }
345
393
  }
394
+ async function deleteAwsKmsAlias(clusterName, region) {
395
+ const aliasName = `alias/eks/${clusterName}`;
396
+ let keyId;
397
+ // Find the KMS key behind the alias so we can schedule it for deletion
398
+ try {
399
+ const { stdout } = await execa('aws', [
400
+ 'kms', 'list-aliases',
401
+ '--query', `Aliases[?AliasName=='${aliasName}'].TargetKeyId | [0]`,
402
+ '--output', 'text',
403
+ '--region', region,
404
+ ]);
405
+ const id = stdout.trim();
406
+ if (id && id !== 'None') {
407
+ keyId = id;
408
+ }
409
+ }
410
+ catch { /* skip */ }
411
+ // Delete the alias (unique name constraint -- blocks re-deploy if left behind)
412
+ try {
413
+ await execa('aws', [
414
+ 'kms', 'delete-alias',
415
+ '--alias-name', aliasName,
416
+ '--region', region,
417
+ ]);
418
+ }
419
+ catch { /* may not exist */ }
420
+ // Schedule the underlying key for deletion (7-day mandatory minimum)
421
+ if (keyId) {
422
+ try {
423
+ await execa('aws', [
424
+ 'kms', 'schedule-key-deletion',
425
+ '--key-id', keyId,
426
+ '--pending-window-in-days', '7',
427
+ '--region', region,
428
+ ]);
429
+ }
430
+ catch { /* key may already be pending deletion or not exist */ }
431
+ }
432
+ }
433
+ /**
434
+ * Finds KMS keys by the description the EKS module uses, and schedules them for
435
+ * deletion. Catches keys that survive after their alias is already deleted.
436
+ */
437
+ async function scheduleAwsOrphanedKmsKeys(clusterName, region) {
438
+ try {
439
+ const { stdout } = await execa('aws', [
440
+ 'kms', 'list-keys',
441
+ '--region', region,
442
+ '--query', 'Keys[].KeyId',
443
+ '--output', 'json',
444
+ ]);
445
+ const keyIds = JSON.parse(stdout);
446
+ for (const keyId of keyIds) {
447
+ try {
448
+ const { stdout: meta } = await execa('aws', [
449
+ 'kms', 'describe-key',
450
+ '--key-id', keyId,
451
+ '--region', region,
452
+ '--query', 'KeyMetadata.{State:KeyState,Desc:Description,Manager:KeyManager}',
453
+ '--output', 'json',
454
+ ]);
455
+ const info = JSON.parse(meta);
456
+ if (info.Manager === 'CUSTOMER' &&
457
+ info.State === 'Enabled' &&
458
+ info.Desc.includes(clusterName)) {
459
+ await execa('aws', [
460
+ 'kms', 'schedule-key-deletion',
461
+ '--key-id', keyId,
462
+ '--pending-window-in-days', '7',
463
+ '--region', region,
464
+ ]);
465
+ }
466
+ }
467
+ catch { /* skip individual key */ }
468
+ }
469
+ }
470
+ catch { /* skip */ }
471
+ }
472
+ async function deleteAwsLaunchTemplates(clusterName, region) {
473
+ try {
474
+ const { stdout } = await execa('aws', [
475
+ 'ec2', 'describe-launch-templates',
476
+ '--filters', `Name=tag:Environment,Values=rulebricks`,
477
+ '--region', region,
478
+ '--query', 'LaunchTemplates[].LaunchTemplateId',
479
+ '--output', 'json',
480
+ ]);
481
+ const ids = JSON.parse(stdout);
482
+ for (const id of ids) {
483
+ try {
484
+ await execa('aws', [
485
+ 'ec2', 'delete-launch-template',
486
+ '--launch-template-id', id,
487
+ '--region', region,
488
+ ]);
489
+ }
490
+ catch { /* may not exist or in use */ }
491
+ }
492
+ }
493
+ catch { /* skip */ }
494
+ }
346
495
  async function deleteAwsIamPolicy(policyName) {
347
496
  try {
348
497
  const { stdout } = await execa('aws', [
@@ -366,20 +515,31 @@ async function deleteAwsIamPolicy(policyName) {
366
515
  * Entirely best-effort: every step silently swallows errors.
367
516
  */
368
517
  async function cleanupAwsResources(clusterName, region) {
518
+ // Capture the OIDC issuer URL BEFORE deleting the cluster -- the URL uses a
519
+ // random cluster ID (not the cluster name) so we can't find it after deletion.
520
+ const oidcIssuerUrl = await getEksOidcIssuer(clusterName, region);
369
521
  // 1. EKS node groups (must be deleted before cluster)
370
522
  await deleteAwsEksNodeGroups(clusterName, region);
371
523
  // 2. EKS cluster
372
524
  await deleteAwsEksCluster(clusterName, region);
373
525
  // 3. CloudWatch log group (now safe -- cluster is gone, won't be recreated)
374
526
  await deleteAwsCloudWatchLogGroup(clusterName, region);
375
- // 4. OIDC provider (created by EKS module for IRSA)
376
- await deleteAwsOidcProvider(clusterName);
527
+ // 4. OIDC provider (matched by issuer URL captured above)
528
+ await deleteAwsOidcProvider(oidcIssuerUrl);
377
529
  // 5. IAM roles created by terraform modules
378
530
  await deleteAwsIamRole(`${clusterName}-ebs-csi`);
379
531
  await deleteAwsIamRole(`${clusterName}-external-dns`);
380
532
  await deleteAwsIamRole(`${clusterName}-vector`);
381
533
  // 6. Customer-managed IAM policies
382
534
  await deleteAwsIamPolicy(`${clusterName}-vector-s3`);
535
+ // 7. KMS key + alias (created by EKS module for envelope encryption)
536
+ await deleteAwsKmsAlias(clusterName, region);
537
+ // 8. KMS keys that lost their alias but are still Enabled (matched by description)
538
+ await scheduleAwsOrphanedKmsKeys(clusterName, region);
539
+ // 9. Launch templates (created by EKS managed node groups)
540
+ await deleteAwsLaunchTemplates(clusterName, region);
541
+ // 10. Elastic IPs (created by VPC module for NAT gateways, cost money if leaked)
542
+ await releaseAwsElasticIps(clusterName, region);
383
543
  }
384
544
  /**
385
545
  * Destroys Terraform infrastructure, then sweeps remaining cloud resources.
@@ -701,7 +701,7 @@ export declare const ProfileConfigSchema: z.ZodObject<{
701
701
  }>;
702
702
  export type ProfileConfig = z.infer<typeof ProfileConfigSchema>;
703
703
  export declare const CHANGELOG_URL = "https://rulebricks.com/docs/changelog";
704
- export declare const HELM_CHART_OCI = "oci://ghcr.io/rulebricks/charts/stack";
704
+ export declare const HELM_CHART_OCI = "oci://ghcr.io/rulebricks/helm/stack";
705
705
  export declare const DEFAULT_NAMESPACE = "rulebricks";
706
706
  export declare const LEGACY_RELEASE_NAME = "rulebricks";
707
707
  /**
@@ -603,7 +603,7 @@ export const ProfileConfigSchema = z.object({
603
603
  });
604
604
  // Constants
605
605
  export const CHANGELOG_URL = "https://rulebricks.com/docs/changelog";
606
- export const HELM_CHART_OCI = "oci://ghcr.io/rulebricks/charts/stack";
606
+ export const HELM_CHART_OCI = "oci://ghcr.io/rulebricks/helm/stack";
607
607
  // Legacy namespace/release name - kept for backwards compatibility with existing deployments
608
608
  export const DEFAULT_NAMESPACE = "rulebricks";
609
609
  export const LEGACY_RELEASE_NAME = "rulebricks";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulebricks/cli",
3
- "version": "2.1.4",
3
+ "version": "2.1.6",
4
4
  "description": "CLI for deploying and managing private Rulebricks instances",
5
5
  "type": "module",
6
6
  "bin": {