cmd/
delstack/ CLI entrypoint
internal/
app/ Application layer (stack deletion orchestration)
cdk/ CDK integration (synthesis, manifest parsing)
operation/ Operators for force-deleting specific resource types
preprocessor/ Pre-deletion processing (e.g., Lambda VPC detachment)
resourcetype/ Resource type constants
io/ Logger and I/O utilities
version/ Version and revision info
pkg/
client/ AWS SDK client wrappers with interfaces for testing
e2e/
full/ E2E test environment for full resources (CDK + deploy script)
dependency/ E2E test environment for dependency graph testing
cdk_integration/ E2E test environment for `delstack cdk` subcommand testing
cdk_cross_region/ E2E test environment for `delstack cdk` cross-region deletion testing
cdk_stage/ E2E test environment for `delstack cdk` with CDK Stages (nested assemblies)
preprocessor/ E2E test environment for preprocessor testing
deletion_protection/ E2E test environment for deletion protection testing
s3_template_cfn/ E2E test environment for large CloudFormation template testing
lambda_edge/ E2E test environment for Lambda@Edge replica cleanup testing
pkg/client -> No internal dependencies (AWS SDK only)
internal/operation -> pkg/client
internal/preprocessor -> pkg/client
internal/app -> internal/operation, internal/preprocessor (NOT directly on pkg/client)
Circular dependencies are not allowed. internal/app must not depend on pkg/client directly — use operation or preprocessor as intermediaries.
make run OPT="<options>": Run delstack locallymake test: Run all testsmake mockgen: Regenerate mocksmake lint: Run linter
When adding support for a new AWS resource, first determine whether it is an Operator or a Preprocessor:
- Operator: Force-deletes resources that cause
DELETE_FAILEDduring CloudFormation stack deletion (e.g., emptying S3 buckets, deleting ECR images). See Adding a New Target Resource Type (Operator). - Preprocessor: Performs pre-deletion processing to improve deletion success rate (e.g., detaching Lambda VPC configurations). See Adding a New Preprocessor.
For a reference implementation, see PR #569 (Athena WorkGroup).
internal/resourcetype/resourcetype.go: Add resource type constant toconstblock + append toResourceTypesslicepkg/client/<service>.go(new): AWS SDK client wrapper with interface (I<Service>) and implementation.//go:generate mockgencomment on line 1. If a client file for the same AWS service already exists (e.g., addingAWS::IAM::Userwheniam.goalready hasAWS::IAM::Group), add methods to the existing interface and struct instead of creating a new file.pkg/client/<service>_mock.go: Auto-generated bymake mockgenpkg/client/<service>_test.go(new or existing): Client tests using SDK middleware mocksinternal/operation/<resource_type>.go(new, e.g.,athena_workgroup.go):Operator implementingIOperator. Addvar _ IOperator = (*XxxOperator)(nil)for compile-time type checkinternal/operation/<resource_type>_test.go(new):Operator tests using mock clientinternal/operation/operator_factory.go: AddCreate<Resource>Operator()method with SDK client initializationinternal/operation/operator_collection.go: Update in 4 places:- Create operator instance in
SetOperatorCollection() - Add
casebranch in switch statement - Append to
operatorsslice - Add row to
supportedStackResourcesDatainRaiseUnsupportedResourceError()
- Create operator instance in
internal/operation/operator_collection_test.go: Update test casesgo.mod/go.sum: Add AWS SDK service dependency (go get github.com/aws/aws-sdk-go-v2/service/<service>), then rungo mod tidyto ensure dependencies are correctly classified as direct/indirectREADME.md: Add row to "Resource Types that can be forced to delete" tablee2e/full/: See E2E Testing section
CompositePreprocessor runs preprocessors in two phases:
- Checkers: Run first. Errors are fatal and abort the entire deletion process. Used for validation that must pass before proceeding (e.g., deletion protection check). All checkers run in parallel and all errors are collected before returning.
- Modifiers: Run after all checkers pass. Errors are logged as warnings but do not stop deletion. Used for optimizations that improve deletion but are not required (e.g., Lambda VPC detachment).
When adding a new preprocessor, determine which phase it belongs to and add it to the appropriate slice (checkers or modifiers) in factory.go.
internal/preprocessor/<name>.go(new):ImplementIPreprocessorinterfaceinternal/preprocessor/<name>_test.go(new):Testsinternal/preprocessor/factory.go: Addnew<Name>FromConfig()factory function + add to thecheckersormodifiersslice inNewRecursivePreprocessorFromConfig()README.md: Add row to "Pre-deletion Processing" table
make test: Run all testsmake test_view: Run tests with coverage reportmake mockgen: Regenerate mocks (based on//go:generate mockgencomments inpkg/client/)
Tests in pkg/client/ use AWS SDK middleware to mock API calls without gomock. Instead of mocking the client interface, a middleware function is injected into the SDK's finalize stage to intercept requests and return mock responses:
withAPIOptionsFunc: func(stack *middleware.Stack) error {
return stack.Finalize.Add(
middleware.FinalizeMiddlewareFunc(
"MockName",
func(context.Context, middleware.FinalizeInput, middleware.FinalizeHandler) (middleware.FinalizeOutput, middleware.Metadata, error) {
return middleware.FinalizeOutput{
Result: &service.OperationOutput{/* mock fields */},
}, middleware.Metadata{}, nil // or error for failure cases
},
),
middleware.Before,
)
}
cfg, err := config.LoadDefaultConfig(ctx,
config.WithRegion("ap-northeast-1"),
config.WithAPIOptions([]func(*middleware.Stack) error{withAPIOptionsFunc}),
)
client := service.NewFromConfig(cfg)For pagination tests, an Initialize-stage middleware captures request parameters (e.g., NextToken) via middleware.WithStackValue(), which the Finalize-stage middleware retrieves via middleware.GetStackValue() to return different responses per page. See backup_test.go or s3_test.go for examples.
For more details on the AWS SDK for Go v2 middleware mechanism, see this article.
Tests in internal/operation/ use gomock with the mock interfaces generated by make mockgen from pkg/client/.
Deploy a test CloudFormation stack with make testgen_full, then use delstack to verify deletion works correctly. See e2e/full/README.md for details on deployment options and resource quotas.
Note: E2E tests deploy real AWS resources and incur costs. Contributors are expected to write/update E2E test code when making changes, but do not need to run them. The maintainer will verify E2E tests before merging.
When adding a new target resource type, update:
e2e/full/cdk/lib/resource/<resource>.go(new): CDK constructorNew<Resource>(scope)that creates the resource in a state that blocks deletion (e.g., ECR withEmptyOnDelete: false)e2e/full/cdk/cdk.go: Callresource.New<Resource>(stack, ...)inNewTestStack()e2e/full/deploy.go: Populate data via SDK after CDK deployment to create a state that blocks deletion (e.g., upload objects to S3, push images to ECR)
Important: The goal of E2E tests is to reproduce the
DELETE_FAILEDstate. If a dependency resource (e.g., access key, policy) is created inside the same CloudFormation stack via CDK, CloudFormation will resolve the dependency order and delete it successfully, so the stack deletion will NOT fail. To triggerDELETE_FAILED, dependency resources that block deletion must be created outside of CloudFormation (e.g., via SDK calls indeploy.go). CDK should only create the base resource itself (e.g., IAM User with no attachments), anddeploy.goshould attach the blocking dependencies via SDK after deployment.If a dependency cannot be feasibly reproduced outside of CloudFormation (e.g., MFA devices requiring TOTP code generation, FIDO/Passkey requiring physical devices), skip it in E2E tests and cover it with unit tests only.
e2e/full/go.mod: Add required AWS SDK service dependenciese2e/full/cdk/go.mod: Add CDK construct dependencies if applicablee2e/full/README.md: Add to resource list
For changes other than new target resource types (e.g., new preprocessor), consider creating a dedicated e2e/<name>/ directory instead of modifying e2e/full/. For existing logic changes, unit tests are sufficient.
When creating a new e2e/<name>/ directory, add a cdk/.gitignore that excludes cdk.context.json (CDK generates this file at deploy time with account-specific context).
make testgen_full: Deploy full resource test stackmake testgen_full_retain: Deploy full resource test stack with all RETAIN resources (for testing-foption)make testgen_large_template: Deploy large CloudFormation template (>51,200 bytes) for S3 upload testingmake testgen_dependency: Deploy CDK dependency test stacks for complex dependency graph testingmake testgen_dependency_retain: Deploy CDK dependency test stacks with RETAIN resourcesmake testgen_preprocessor: Deploy preprocessor test stacks for Lambda VPC detachment testingmake testgen_lambda_edge: Deploy Lambda@Edge test stacks for replica cleanup retry testingmake testgen_deletion_protection: Deploy deletion protection test stacks for resource-level protection check/disable testingmake testgen_deletion_protection_no_tp: Deploy deletion protection test stacks without TerminationProtection (for testing resource-level protection only)make testgen_cdk_integration: Deploy CDK integration test stacks fordelstack cdksubcommand testingmake testgen_cdk_integration_retain: Deploy CDK integration test stacks with RETAIN resourcesmake testgen_cdk_cross_region: Deploy CDK cross-region test stacks (us-east-1 + ap-northeast-1) withcrossRegionReferencesmake testgen_cdk_cross_region_retain: Deploy CDK cross-region test stacks with RETAIN resourcesmake testgen_cdk_stage: Deploy CDK Stage test stacks (nested Cloud Assemblies)make testgen_cdk_termination_protection: Deploy CDK TerminationProtection test stacksmake testgen_help: Show help for all test stack generation targets
These targets deploy test stacks and then run delstack to delete them in a single command. Each target auto-generates a unique stage name with a random suffix to avoid stack name collisions.
make e2e_full: Deploy full resources and delete with delstackmake e2e_full_retain: Deploy full RETAIN resources and force deletemake e2e_large_template: Deploy large CloudFormation template and force deletemake e2e_dependency: Deploy 6 dependency stacks and delete allmake e2e_dependency_retain: Deploy 6 dependency stacks with RETAIN and force deletemake e2e_preprocessor: Deploy preprocessor stacks and deletemake e2e_lambda_edge: Deploy Lambda@Edge stacks and delete (takes ~20 min due to replica cleanup)make e2e_deletion_protection: Deploy deletion protection stacks and force deletemake e2e_deletion_protection_no_tp: Deploy deletion protection stacks (no TP) and force deletemake e2e_cdk_integration: Deploy CDK stacks and delete withdelstack cdkmake e2e_cdk_integration_retain: Deploy CDK stacks with RETAIN and force delete withdelstack cdkmake e2e_cdk_cross_region: Deploy CDK cross-region stacks and delete withdelstack cdkmake e2e_cdk_cross_region_retain: Deploy CDK cross-region stacks with RETAIN and force deletemake e2e_cdk_stage: Deploy CDK Stage stacks and delete withdelstack cdkmake e2e_cdk_termination_protection: Deploy CDK TerminationProtection stacks and force delete withdelstack cdkmake e2e_help: Show help for all E2E test targets
Options:
STAGE=<name>: Override the auto-generated stage nameOPT="-p <profile>": Pass additional options (e.g., AWS profile)
make e2e_full # Auto-generated stage
make e2e_full STAGE=my-stage # Custom stage
make e2e_full STAGE=my-stage OPT="-p my-profile" # Custom stage + profile- Code comments in English, minimal
var _ IXxx = (*Xxx)(nil)for compile-time interface implementation check (used in bothpkg/client/andinternal/operation/)- Concurrency:
errgroup+semaphore.NewWeighted(runtime.NumCPU()) - Errors: Public methods in
pkg/clientmust wrap errors withClientError. Ininternal/operation, return client errors as-is (already wrapped); only wrap withClientErrorfor errors generated directly in the operation layer (e.g.,ctx.Done(), validation) - Idempotency: Check existence before deletion
//go:generate mockgencomment must be on line 1 of the file
- Top-level test function:
Test[ReceiverType]_[MethodName](e.g.,TestEcr_DeleteRepository,TestS3BucketOperator_DeleteS3Bucket)