Skip to content

Commit a0cbd80

Browse files
author
Bob Strahan
committed
feat: Add force-delete-all option to CLI delete command with comprehensive resource cleanup
1 parent 2af1713 commit a0cbd80

File tree

6 files changed

+542
-36
lines changed

6 files changed

+542
-36
lines changed

CHANGELOG.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ SPDX-License-Identifier: MIT-0
55

66
## [Unreleased]
77

8-
### Fixed
9-
10-
8+
## [0.4.2]
119

10+
### Added
1211

13-
## [0.4.2]
12+
- **IDP CLI Force Delete All Resources Option**
13+
- Added `--force-delete-all` flag to `idp-cli delete` command for comprehensive stack cleanup
14+
- **Post-CloudFormation Cleanup**: Analyzes resources after CloudFormation deletion completes to identify retained resources (DELETE_SKIPPED status)
15+
- **Use Cases**: Complete test environment cleanup, CI/CD pipelines requiring full teardown, cost optimization by removing all retained resources
1416

1517
### Changed
1618

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.4.2-wip
1+
0.4.2-wip2

docs/idp-cli.md

Lines changed: 84 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -177,14 +177,33 @@ idp-cli delete [OPTIONS]
177177
- `--stack-name` (required): CloudFormation stack name
178178
- `--force`: Skip confirmation prompt
179179
- `--empty-buckets`: Empty S3 buckets before deletion (required if buckets contain data)
180+
- `--force-delete-all`: Force delete ALL remaining resources after CloudFormation deletion (S3 buckets, CloudWatch logs, DynamoDB tables)
180181
- `--wait / --no-wait`: Wait for deletion to complete (default: wait)
181182
- `--region`: AWS region (optional)
182183

183184
**S3 Bucket Behavior:**
184-
- **LoggingBucket**: `DeletionPolicy: Retain` - Always kept
185+
- **LoggingBucket**: `DeletionPolicy: Retain` - Always kept (unless using `--force-delete-all`)
185186
- **All other buckets**: `DeletionPolicy: RetainExceptOnCreate` - Deleted if empty
186187
- CloudFormation can ONLY delete S3 buckets if they're empty
187188
- Use `--empty-buckets` to automatically empty buckets before deletion
189+
- Use `--force-delete-all` to delete ALL remaining resources after CloudFormation completes
190+
191+
**Force Delete All Behavior:**
192+
193+
The `--force-delete-all` flag performs a comprehensive cleanup AFTER CloudFormation deletion completes:
194+
195+
1. **CloudFormation Deletion Phase**: Standard stack deletion
196+
2. **Analysis Phase**: Identifies resources with DELETE_SKIPPED or retained status
197+
3. **Cleanup Phase**: Deletes remaining resources in order:
198+
- DynamoDB tables (disables PITR, then deletes)
199+
- CloudWatch Log Groups (matching stack name pattern)
200+
- S3 buckets (regular buckets first, LoggingBucket last)
201+
202+
**Resources Deleted by --force-delete-all:**
203+
- All DynamoDB tables from stack
204+
- All CloudWatch Log Groups (including nested stack logs)
205+
- All S3 buckets including LoggingBucket
206+
- Handles nested stack resources automatically
188207

189208
**Examples:**
190209

@@ -198,11 +217,14 @@ idp-cli delete --stack-name test-stack --force
198217
# Delete with automatic bucket emptying
199218
idp-cli delete --stack-name test-stack --empty-buckets --force
200219

220+
# Force delete ALL remaining resources (comprehensive cleanup)
221+
idp-cli delete --stack-name test-stack --force-delete-all --force
222+
201223
# Delete without waiting
202224
idp-cli delete --stack-name test-stack --force --no-wait
203225
```
204226

205-
**What you'll see:**
227+
**What you'll see (standard deletion):**
206228
```
207229
⚠️ WARNING: Stack Deletion
208230
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -220,15 +242,70 @@ This action cannot be undone.
220242
Are you sure you want to delete this stack? [y/N]: _
221243
```
222244

245+
**What you'll see (force-delete-all):**
246+
```
247+
⚠️ WARNING: FORCE DELETE ALL RESOURCES
248+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
249+
Stack: test-stack
250+
Region: us-east-1
251+
252+
S3 Buckets:
253+
• InputBucket: 20 objects (45.3 MB)
254+
• OutputBucket: 20 objects (123.7 MB)
255+
• LoggingBucket: 5000 objects (2.3 GB)
256+
257+
⚠️ FORCE DELETE ALL will remove:
258+
• All S3 buckets (including LoggingBucket)
259+
• All CloudWatch Log Groups
260+
• All DynamoDB Tables
261+
• Any other retained resources
262+
263+
This happens AFTER CloudFormation deletion completes
264+
265+
This action cannot be undone.
266+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
267+
268+
Are you ABSOLUTELY sure you want to force delete ALL resources? [y/N]: y
269+
270+
Deleting CloudFormation stack...
271+
✓ Stack deleted successfully!
272+
273+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
274+
Starting force cleanup of retained resources...
275+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
276+
277+
Analyzing retained resources...
278+
Found 4 retained resources:
279+
• DynamoDB Tables: 0
280+
• CloudWatch Logs: 0
281+
• S3 Buckets: 3
282+
283+
⠋ Deleting S3 buckets... 3/3
284+
285+
✓ Cleanup phase complete!
286+
287+
Resources deleted:
288+
• S3 Buckets: 3
289+
- test-stack-inputbucket-abc123
290+
- test-stack-outputbucket-def456
291+
- test-stack-loggingbucket-ghi789
292+
293+
Stack 'test-stack' and all resources completely removed.
294+
```
295+
223296
**Use Cases:**
224297
- Cleanup test/development environments to avoid charges
225298
- CI/CD pipelines that provision and teardown stacks
226299
- Automated testing with temporary stack creation
227-
228-
**Note:** LoggingBucket is retained by design. To delete it manually:
229-
```bash
230-
aws s3 rb s3://<logging-bucket-name> --force
231-
```
300+
- Complete removal of failed stacks with retained resources
301+
- Cleanup of stacks with LoggingBucket and CloudWatch logs
302+
303+
**Important Notes:**
304+
- `--force-delete-all` automatically includes `--empty-buckets` behavior
305+
- Cleanup phase runs even if CloudFormation deletion fails
306+
- Includes resources from nested stacks automatically
307+
- Safe to run - only deletes resources that weren't deleted by CloudFormation
308+
- Progress bars show real-time deletion status
232309

233310
---
234311

idp_cli/idp_cli/cli.py

Lines changed: 131 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040

4141

4242
@click.group()
43-
@click.version_option(version="1.0.0")
43+
@click.version_option(version="0.4.2")
4444
def cli():
4545
"""
4646
IDP CLI - Batch document processing for IDP Accelerator
@@ -301,6 +301,11 @@ def deploy(
301301
is_flag=True,
302302
help="Empty S3 buckets before deletion (required if buckets contain data)",
303303
)
304+
@click.option(
305+
"--force-delete-all",
306+
is_flag=True,
307+
help="Force delete ALL remaining resources after CloudFormation deletion (S3 buckets, CloudWatch logs, DynamoDB tables). This cannot be undone.",
308+
)
304309
@click.option(
305310
"--wait/--no-wait",
306311
default=True,
@@ -311,6 +316,7 @@ def delete(
311316
stack_name: str,
312317
force: bool,
313318
empty_buckets: bool,
319+
force_delete_all: bool,
314320
wait: bool,
315321
region: Optional[str],
316322
):
@@ -321,6 +327,7 @@ def delete(
321327
322328
S3 buckets configured with RetainExceptOnCreate will be deleted if empty.
323329
Use --empty-buckets to automatically empty buckets before deletion.
330+
Use --force-delete-all to delete ALL remaining resources after CloudFormation deletion.
324331
325332
Examples:
326333
@@ -333,6 +340,9 @@ def delete(
333340
# Delete with automatic bucket emptying
334341
idp-cli delete --stack-name test-stack --empty-buckets --force
335342
343+
# Force delete ALL remaining resources (S3, logs, DynamoDB)
344+
idp-cli delete --stack-name test-stack --force-delete-all --force
345+
336346
# Delete without waiting for completion
337347
idp-cli delete --stack-name test-stack --force --no-wait
338348
"""
@@ -350,7 +360,10 @@ def delete(
350360

351361
# Show warning with bucket details
352362
console.print()
353-
console.print("[bold red]⚠️ WARNING: Stack Deletion[/bold red]")
363+
if force_delete_all:
364+
console.print("[bold red]⚠️ WARNING: FORCE DELETE ALL RESOURCES[/bold red]")
365+
else:
366+
console.print("[bold red]⚠️ WARNING: Stack Deletion[/bold red]")
354367
console.print("━" * 60)
355368
console.print(f"Stack: [cyan]{stack_name}[/cyan]")
356369
console.print(f"Region: {region or 'default'}")
@@ -372,12 +385,25 @@ def delete(
372385
else:
373386
console.print(f" • {logical_id}: [green]empty[/green]")
374387

375-
if has_data and not empty_buckets:
388+
if has_data and not empty_buckets and not force_delete_all:
376389
console.print()
377390
console.print("[bold red]⚠️ Buckets contain data![/bold red]")
378391
console.print("Deletion will FAIL unless you:")
379392
console.print(" 1. Use --empty-buckets flag to auto-delete data, OR")
380-
console.print(" 2. Manually empty buckets first")
393+
console.print(" 2. Use --force-delete-all to delete everything, OR")
394+
console.print(" 3. Manually empty buckets first")
395+
396+
if force_delete_all:
397+
console.print()
398+
console.print("[bold red]⚠️ FORCE DELETE ALL will remove:[/bold red]")
399+
console.print(" • All S3 buckets (including LoggingBucket)")
400+
console.print(" • All CloudWatch Log Groups")
401+
console.print(" • All DynamoDB Tables")
402+
console.print(" • Any other retained resources")
403+
console.print()
404+
console.print(
405+
"[bold yellow]This happens AFTER CloudFormation deletion completes[/bold yellow]"
406+
)
381407

382408
console.print()
383409
console.print("[bold red]This action cannot be undone.[/bold red]")
@@ -386,15 +412,22 @@ def delete(
386412

387413
# Confirmation unless --force
388414
if not force:
389-
response = click.confirm(
390-
"Are you sure you want to delete this stack?", default=False
391-
)
415+
if force_delete_all:
416+
response = click.confirm(
417+
"Are you ABSOLUTELY sure you want to force delete ALL resources?",
418+
default=False,
419+
)
420+
else:
421+
response = click.confirm(
422+
"Are you sure you want to delete this stack?", default=False
423+
)
424+
392425
if not response:
393426
console.print("[yellow]Deletion cancelled[/yellow]")
394427
return
395428

396-
# Double confirmation if --empty-buckets
397-
if empty_buckets:
429+
# Double confirmation if --empty-buckets (and not force-delete-all)
430+
if empty_buckets and not force_delete_all:
398431
console.print()
399432
console.print(
400433
"[bold red]⚠️ You are about to permanently delete all bucket data![/bold red]"
@@ -416,20 +449,11 @@ def delete(
416449
wait=wait,
417450
)
418451

419-
# Show results
452+
# Show CloudFormation deletion results
420453
if result.get("success"):
421454
console.print("\n[green]✓ Stack deleted successfully![/green]")
422455
console.print(f"Stack: {stack_name}")
423456
console.print(f"Status: {result.get('status')}")
424-
425-
# Note about LoggingBucket
426-
console.print()
427-
console.print(
428-
"[bold]Note:[/bold] LoggingBucket (if exists) is retained by design."
429-
)
430-
console.print("Delete it manually if no longer needed:")
431-
console.print(" [cyan]aws s3 rb s3://<logging-bucket-name> --force[/cyan]")
432-
console.print()
433457
else:
434458
console.print("\n[red]✗ Stack deletion failed![/red]")
435459
console.print(f"Status: {result.get('status')}")
@@ -438,10 +462,96 @@ def delete(
438462
if "bucket" in result.get("error", "").lower():
439463
console.print()
440464
console.print(
441-
"[yellow]Tip: Try again with --empty-buckets flag[/yellow]"
465+
"[yellow]Tip: Try again with --empty-buckets or --force-delete-all flag[/yellow]"
442466
)
443467

444-
sys.exit(1)
468+
if not force_delete_all:
469+
sys.exit(1)
470+
else:
471+
console.print()
472+
console.print(
473+
"[yellow]Stack deletion failed, but continuing with force cleanup...[/yellow]"
474+
)
475+
476+
# Post-deletion cleanup if --force-delete-all
477+
cleanup_result = None
478+
if force_delete_all:
479+
console.print()
480+
console.print("[bold blue]━" * 60 + "[/bold blue]")
481+
console.print(
482+
"[bold blue]Starting force cleanup of retained resources...[/bold blue]"
483+
)
484+
console.print("[bold blue]━" * 60 + "[/bold blue]")
485+
486+
try:
487+
# Use stack ID for deleted stacks (CloudFormation requires ID for deleted stacks)
488+
stack_identifier = result.get("stack_id", stack_name)
489+
cleanup_result = deployer.cleanup_retained_resources(stack_identifier)
490+
491+
# Show cleanup summary
492+
console.print()
493+
console.print("[bold green]✓ Cleanup phase complete![/bold green]")
494+
console.print()
495+
496+
total_deleted = (
497+
len(cleanup_result.get("dynamodb_deleted", []))
498+
+ len(cleanup_result.get("logs_deleted", []))
499+
+ len(cleanup_result.get("buckets_deleted", []))
500+
)
501+
502+
if total_deleted > 0:
503+
console.print("[bold]Resources deleted:[/bold]")
504+
505+
if cleanup_result.get("dynamodb_deleted"):
506+
console.print(
507+
f" • DynamoDB Tables: {len(cleanup_result['dynamodb_deleted'])}"
508+
)
509+
for table in cleanup_result["dynamodb_deleted"]:
510+
console.print(f" - {table}")
511+
512+
if cleanup_result.get("logs_deleted"):
513+
console.print(
514+
f" • CloudWatch Log Groups: {len(cleanup_result['logs_deleted'])}"
515+
)
516+
for log_group in cleanup_result["logs_deleted"]:
517+
console.print(f" - {log_group}")
518+
519+
if cleanup_result.get("buckets_deleted"):
520+
console.print(
521+
f" • S3 Buckets: {len(cleanup_result['buckets_deleted'])}"
522+
)
523+
for bucket in cleanup_result["buckets_deleted"]:
524+
console.print(f" - {bucket}")
525+
526+
if cleanup_result.get("errors"):
527+
console.print()
528+
console.print(
529+
"[bold yellow]⚠️ Some resources could not be deleted:[/bold yellow]"
530+
)
531+
for error in cleanup_result["errors"]:
532+
console.print(f" • {error['type']}: {error['resource']}")
533+
console.print(f" Error: {error['error']}")
534+
535+
console.print()
536+
537+
except Exception as e:
538+
logger.error(f"Error during cleanup: {e}", exc_info=True)
539+
console.print(f"\n[red]✗ Cleanup phase error: {e}[/red]")
540+
console.print(
541+
"[yellow]Some resources may remain - check AWS Console[/yellow]"
542+
)
543+
else:
544+
# Standard deletion without force-delete-all
545+
if result.get("success"):
546+
console.print()
547+
console.print(
548+
"[bold]Note:[/bold] LoggingBucket (if exists) is retained by design."
549+
)
550+
console.print("Delete it manually if no longer needed:")
551+
console.print(
552+
" [cyan]aws s3 rb s3://<logging-bucket-name> --force[/cyan]"
553+
)
554+
console.print()
445555

446556
except Exception as e:
447557
logger.error(f"Error deleting stack: {e}", exc_info=True)

0 commit comments

Comments
 (0)