@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.
- package/dist/lib/terraform.js +164 -4
- package/package.json +1 -1
package/dist/lib/terraform.js
CHANGED
|
@@ -265,7 +265,32 @@ async function deleteAwsCloudWatchLogGroup(clusterName, region) {
|
|
|
265
265
|
}
|
|
266
266
|
catch { /* may not exist */ }
|
|
267
267
|
}
|
|
268
|
-
|
|
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
|
|
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 (
|
|
376
|
-
await deleteAwsOidcProvider(
|
|
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.
|