@rulebricks/cli 2.1.4 → 2.1.5

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.
@@ -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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulebricks/cli",
3
- "version": "2.1.4",
3
+ "version": "2.1.5",
4
4
  "description": "CLI for deploying and managing private Rulebricks instances",
5
5
  "type": "module",
6
6
  "bin": {