Skip to content

Latest commit

 

History

History
216 lines (168 loc) · 14.1 KB

File metadata and controls

216 lines (168 loc) · 14.1 KB

Contributing to delstack

Project Structure

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

Package Dependency Rules

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.

Development Setup

  • make run OPT="<options>": Run delstack locally
  • make test: Run all tests
  • make mockgen: Regenerate mocks
  • make lint: Run linter

Adding New Resource Support

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_FAILED during 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.

Adding a New Target Resource Type (Operator)

For a reference implementation, see PR #569 (Athena WorkGroup).

  1. internal/resourcetype/resourcetype.go: Add resource type constant to const block + append to ResourceTypes slice
  2. pkg/client/<service>.go (new): AWS SDK client wrapper with interface (I<Service>) and implementation. //go:generate mockgen comment on line 1. If a client file for the same AWS service already exists (e.g., adding AWS::IAM::User when iam.go already has AWS::IAM::Group), add methods to the existing interface and struct instead of creating a new file.
  3. pkg/client/<service>_mock.go: Auto-generated by make mockgen
  4. pkg/client/<service>_test.go (new or existing): Client tests using SDK middleware mocks
  5. internal/operation/<resource_type>.go (new, e.g., athena_workgroup.go):Operator implementing IOperator. Add var _ IOperator = (*XxxOperator)(nil) for compile-time type check
  6. internal/operation/<resource_type>_test.go (new):Operator tests using mock client
  7. internal/operation/operator_factory.go: Add Create<Resource>Operator() method with SDK client initialization
  8. internal/operation/operator_collection.go: Update in 4 places:
    • Create operator instance in SetOperatorCollection()
    • Add case branch in switch statement
    • Append to operators slice
    • Add row to supportedStackResourcesData in RaiseUnsupportedResourceError()
  9. internal/operation/operator_collection_test.go: Update test cases
  10. go.mod / go.sum: Add AWS SDK service dependency (go get github.com/aws/aws-sdk-go-v2/service/<service>), then run go mod tidy to ensure dependencies are correctly classified as direct/indirect
  11. README.md: Add row to "Resource Types that can be forced to delete" table
  12. e2e/full/: See E2E Testing section

Adding a New Preprocessor

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.

  1. internal/preprocessor/<name>.go (new):Implement IPreprocessor interface
  2. internal/preprocessor/<name>_test.go (new):Tests
  3. internal/preprocessor/factory.go: Add new<Name>FromConfig() factory function + add to the checkers or modifiers slice in NewRecursivePreprocessorFromConfig()
  4. README.md: Add row to "Pre-deletion Processing" table

Testing

Unit Tests

  • make test: Run all tests
  • make test_view: Run tests with coverage report
  • make mockgen: Regenerate mocks (based on //go:generate mockgen comments in pkg/client/)

pkg/client/ tests:SDK Middleware Mocks

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.

internal/operation/ tests:gomock

Tests in internal/operation/ use gomock with the mock interfaces generated by make mockgen from pkg/client/.

E2E Testing (e2e/full/)

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:

  1. e2e/full/cdk/lib/resource/<resource>.go (new): CDK constructor New<Resource>(scope) that creates the resource in a state that blocks deletion (e.g., ECR with EmptyOnDelete: false)
  2. e2e/full/cdk/cdk.go: Call resource.New<Resource>(stack, ...) in NewTestStack()
  3. 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_FAILED state. 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 trigger DELETE_FAILED, dependency resources that block deletion must be created outside of CloudFormation (e.g., via SDK calls in deploy.go). CDK should only create the base resource itself (e.g., IAM User with no attachments), and deploy.go should 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.

  1. e2e/full/go.mod: Add required AWS SDK service dependencies
  2. e2e/full/cdk/go.mod: Add CDK construct dependencies if applicable
  3. e2e/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).

Other E2E Test Environments

  • make testgen_full: Deploy full resource test stack
  • make testgen_full_retain: Deploy full resource test stack with all RETAIN resources (for testing -f option)
  • make testgen_large_template: Deploy large CloudFormation template (>51,200 bytes) for S3 upload testing
  • make testgen_dependency: Deploy CDK dependency test stacks for complex dependency graph testing
  • make testgen_dependency_retain: Deploy CDK dependency test stacks with RETAIN resources
  • make testgen_preprocessor: Deploy preprocessor test stacks for Lambda VPC detachment testing
  • make testgen_lambda_edge: Deploy Lambda@Edge test stacks for replica cleanup retry testing
  • make testgen_deletion_protection: Deploy deletion protection test stacks for resource-level protection check/disable testing
  • make 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 for delstack cdk subcommand testing
  • make testgen_cdk_integration_retain: Deploy CDK integration test stacks with RETAIN resources
  • make testgen_cdk_cross_region: Deploy CDK cross-region test stacks (us-east-1 + ap-northeast-1) with crossRegionReferences
  • make testgen_cdk_cross_region_retain: Deploy CDK cross-region test stacks with RETAIN resources
  • make testgen_cdk_stage: Deploy CDK Stage test stacks (nested Cloud Assemblies)
  • make testgen_cdk_termination_protection: Deploy CDK TerminationProtection test stacks
  • make testgen_help: Show help for all test stack generation targets

E2E Combined Targets (testgen + delstack run)

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 delstack
  • make e2e_full_retain: Deploy full RETAIN resources and force delete
  • make e2e_large_template: Deploy large CloudFormation template and force delete
  • make e2e_dependency: Deploy 6 dependency stacks and delete all
  • make e2e_dependency_retain: Deploy 6 dependency stacks with RETAIN and force delete
  • make e2e_preprocessor: Deploy preprocessor stacks and delete
  • make 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 delete
  • make e2e_deletion_protection_no_tp: Deploy deletion protection stacks (no TP) and force delete
  • make e2e_cdk_integration: Deploy CDK stacks and delete with delstack cdk
  • make e2e_cdk_integration_retain: Deploy CDK stacks with RETAIN and force delete with delstack cdk
  • make e2e_cdk_cross_region: Deploy CDK cross-region stacks and delete with delstack cdk
  • make e2e_cdk_cross_region_retain: Deploy CDK cross-region stacks with RETAIN and force delete
  • make e2e_cdk_stage: Deploy CDK Stage stacks and delete with delstack cdk
  • make e2e_cdk_termination_protection: Deploy CDK TerminationProtection stacks and force delete with delstack cdk
  • make e2e_help: Show help for all E2E test targets

Options:

  • STAGE=<name>: Override the auto-generated stage name
  • OPT="-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 Style & Conventions

  • Code comments in English, minimal
  • var _ IXxx = (*Xxx)(nil) for compile-time interface implementation check (used in both pkg/client/ and internal/operation/)
  • Concurrency: errgroup + semaphore.NewWeighted(runtime.NumCPU())
  • Errors: Public methods in pkg/client must wrap errors with ClientError. In internal/operation, return client errors as-is (already wrapped); only wrap with ClientError for errors generated directly in the operation layer (e.g., ctx.Done(), validation)
  • Idempotency: Check existence before deletion
  • //go:generate mockgen comment must be on line 1 of the file

Test Naming Conventions

  • Top-level test function: Test[ReceiverType]_[MethodName] (e.g., TestEcr_DeleteRepository, TestS3BucketOperator_DeleteS3Bucket)