-
-
Notifications
You must be signed in to change notification settings - Fork 153
Detect deleted components in affected stacks #2063
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 8 commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
7f9e3b7
updates
aknysh c7e193e
feat: detect deleted components and stacks in describe affected
aknysh 8adfc3d
Merge remote-tracking branch 'origin/main' into aknysh/update-describ…
aknysh 51eaf51
updates
aknysh fcc3124
updates
aknysh a1eabb2
updates
aknysh b77e27e
[autofix.ci] apply automated fixes
autofix-ci[bot] cfebbc2
Address CodeRabbit review feedback for deleted detection
aknysh 4abca0f
Address additional CodeRabbit review feedback
aknysh 4bf14c3
fix: address CodeRabbit review comments and fix CI test
aknysh 397f80e
fix: address CodeRabbit review - tests, docs, Windows compatibility
aknysh 4247d57
Merge branch 'main' into aknysh/update-describe-affected-9
aknysh e98ca79
Merge branch 'main' into aknysh/update-describe-affected-9
aknysh 43d241b
feat: add deleted component support to list affected command
aknysh File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,390 @@ | ||
| # PRD: Detect Deleted Components and Stacks in `describe affected` | ||
|
|
||
| **Date**: 2026-02-08 | ||
|
|
||
| ## Status | ||
|
|
||
| Implemented. | ||
|
|
||
| ## Problem Statement | ||
|
|
||
| ### Current Behavior | ||
|
|
||
| The `atmos describe affected` command detects components and stacks that have been **modified** between two Git | ||
| commits (HEAD vs. BASE). However, it does **not** detect components or stacks that have been **deleted** in HEAD | ||
| compared to BASE. | ||
|
|
||
| Currently, `describe affected` iterates over the stacks in HEAD (current branch) and compares them to BASE (target | ||
| branch). This means: | ||
|
|
||
| 1. **Components/stacks that exist only in BASE are invisible** - If a component was removed in HEAD, it won't appear in | ||
| the affected output | ||
| 2. **No way to trigger destroy workflows** - CI/CD pipelines have no automated way to know which resources need | ||
| `terraform destroy` | ||
|
|
||
| ### Impact | ||
|
|
||
| 1. **Manual destruction required**: Users must manually identify and destroy removed components | ||
| 2. **Resource leaks**: Deleted stack configurations may leave orphaned cloud resources | ||
| 3. **Incomplete CI/CD**: Pipelines can't fully automate infrastructure lifecycle | ||
| 4. **Audit gaps**: No automated tracking of what was removed | ||
|
|
||
| ## Proposed Solution | ||
|
|
||
| ### Overview | ||
|
|
||
| Extend `atmos describe affected` to detect and report: | ||
|
|
||
| 1. **Deleted components** - Components that exist in BASE but not in HEAD (within a stack that still exists) | ||
| 2. **Deleted stacks** - Entire stacks that exist in BASE but not in HEAD (all components marked as deleted) | ||
|
|
||
| ### New Affected Reasons | ||
|
|
||
| Add new affected reason values: | ||
|
|
||
| | Reason | Description | | ||
| |-----------------|-------------------------------------------------| | ||
| | `deleted` | Component was removed from a stack | | ||
| | `deleted.stack` | Entire stack was deleted (all components in it) | | ||
|
|
||
| ### New Output Fields | ||
|
|
||
| Add new fields to the affected output schema: | ||
|
|
||
| ```json | ||
| { | ||
| "component": "vpc", | ||
| "component_type": "terraform", | ||
| "stack": "dev-us-east-1", | ||
| "affected": "deleted", | ||
| "deleted": true, | ||
| "deletion_type": "component" | ||
| } | ||
| ``` | ||
|
|
||
| | Field | Type | Description | | ||
| |-----------------|---------|--------------------------------------------------- | | ||
| | `deleted` | boolean | `true` if this component/stack was deleted | | ||
| | `deletion_type` | string | Type of deletion: `component` or `stack` | | ||
|
|
||
| ### Algorithm Changes | ||
|
|
||
| **Current algorithm:** | ||
|
|
||
| ```text | ||
| for each stack in HEAD: | ||
| for each component in stack: | ||
| compare with BASE | ||
| if different: add to affected | ||
| ``` | ||
|
|
||
| **New algorithm:** | ||
|
|
||
| ```text | ||
| for each stack in HEAD: | ||
| for each component in stack: | ||
| compare with BASE | ||
| if different: add to affected | ||
|
|
||
| # NEW: Also check BASE for deletions | ||
| for each stack in BASE: | ||
| if stack not in HEAD: | ||
| add all components as deleted (deletion_type: stack) | ||
| else: | ||
| for each component in BASE stack: | ||
| if component not in HEAD stack: | ||
| add as deleted (deletion_type: component) | ||
| ``` | ||
|
|
||
| Users can filter the output using `--query` to separate modified vs deleted components. | ||
|
|
||
| ## Use Cases | ||
|
|
||
| ### Use Case 1: Component Removed from Stack | ||
|
|
||
| **Scenario**: User removes the `monitoring` component from `prod-us-east-1` stack. | ||
|
|
||
| **BASE (main branch):** | ||
|
|
||
| ```yaml | ||
| # stacks/prod/us-east-1.yaml | ||
| components: | ||
| terraform: | ||
| vpc: | ||
| vars: | ||
| cidr: "10.0.0.0/16" | ||
| monitoring: | ||
| vars: | ||
| enabled: true | ||
| ``` | ||
|
|
||
| **HEAD (PR branch):** | ||
|
|
||
| ```yaml | ||
| # stacks/prod/us-east-1.yaml | ||
| components: | ||
| terraform: | ||
| vpc: | ||
| vars: | ||
| cidr: "10.0.0.0/16" | ||
| # monitoring component removed | ||
| ``` | ||
|
|
||
| **Output:** | ||
|
|
||
| ```json | ||
| [ | ||
| { | ||
| "component": "monitoring", | ||
| "component_type": "terraform", | ||
| "stack": "prod-us-east-1", | ||
| "stack_slug": "prod-us-east-1-monitoring", | ||
| "affected": "deleted", | ||
| "deleted": true, | ||
| "deletion_type": "component" | ||
| } | ||
| ] | ||
| ``` | ||
|
|
||
| ### Use Case 2: Entire Stack Removed | ||
|
|
||
| **Scenario**: User deletes the entire `staging-us-west-2` stack. | ||
|
|
||
| **BASE:** Stack file exists at `stacks/staging/us-west-2.yaml` | ||
| **HEAD:** Stack file deleted | ||
|
|
||
| **Output:** | ||
|
|
||
| ```json | ||
| [ | ||
| { | ||
| "component": "vpc", | ||
| "component_type": "terraform", | ||
| "stack": "staging-us-west-2", | ||
| "affected": "deleted.stack", | ||
| "deleted": true, | ||
| "deletion_type": "stack" | ||
| }, | ||
| { | ||
| "component": "eks", | ||
| "component_type": "terraform", | ||
| "stack": "staging-us-west-2", | ||
| "affected": "deleted.stack", | ||
| "deleted": true, | ||
| "deletion_type": "stack" | ||
| } | ||
| ] | ||
| ``` | ||
|
|
||
| ### Use Case 3: CI/CD Pipeline Integration | ||
|
|
||
| **GitHub Actions workflow:** | ||
|
|
||
| ```yaml | ||
| jobs: | ||
| detect-changes: | ||
| runs-on: ubuntu-latest | ||
| outputs: | ||
| modified: ${{ steps.affected.outputs.modified }} | ||
| deleted: ${{ steps.affected.outputs.deleted }} | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| fetch-depth: 0 | ||
|
|
||
| - name: Detect affected | ||
| id: affected | ||
| run: | | ||
| # Get all affected components | ||
| atmos describe affected --format json > affected.json | ||
|
|
||
| # Filter modified components (for apply) | ||
| jq '[.[] | select(.deleted != true)]' affected.json > modified.json | ||
| echo "modified=$(cat modified.json | jq -c)" >> $GITHUB_OUTPUT | ||
|
|
||
| # Filter deleted components (for destroy) | ||
| jq '[.[] | select(.deleted == true)]' affected.json > deleted.json | ||
| echo "deleted=$(cat deleted.json | jq -c)" >> $GITHUB_OUTPUT | ||
|
|
||
| apply: | ||
| needs: detect-changes | ||
| if: needs.detect-changes.outputs.modified != '[]' | ||
| strategy: | ||
| matrix: | ||
| include: ${{ fromJson(needs.detect-changes.outputs.modified) }} | ||
| steps: | ||
| - run: atmos terraform apply ${{ matrix.component }} -s ${{ matrix.stack }} | ||
|
|
||
| destroy: | ||
| needs: detect-changes | ||
| if: needs.detect-changes.outputs.deleted != '[]' | ||
| strategy: | ||
| matrix: | ||
| include: ${{ fromJson(needs.detect-changes.outputs.deleted) }} | ||
| steps: | ||
| - run: atmos terraform destroy ${{ matrix.component }} -s ${{ matrix.stack }} --auto-approve | ||
| ``` | ||
aknysh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| ### Use Case 4: Query for Deleted Only | ||
|
|
||
| Users can use the `--query` flag to filter: | ||
|
|
||
| ```shell | ||
| # Get only deleted components | ||
| atmos describe affected --query '[.[] | select(.deleted == true)]' | ||
|
|
||
| # Get only modified (not deleted) components | ||
| atmos describe affected --query '[.[] | select(.deleted != true)]' | ||
|
|
||
| # Get deleted components in specific stack | ||
| atmos describe affected --query '[.[] | select(.deleted == true and .stack == "prod-us-east-1")]' | ||
| ``` | ||
|
|
||
| ## Implementation | ||
|
|
||
| ### Phase 1: Core Detection | ||
|
|
||
| 1. **Add deletion detection logic** in `internal/exec/describe_affected_utils.go`: | ||
| ```go | ||
| func detectDeletedComponents( | ||
| baseStacks map[string]any, | ||
| headStacks map[string]any, | ||
| atmosConfig *schema.AtmosConfiguration, | ||
| ) ([]schema.Affected, error) { | ||
| var deleted []schema.Affected | ||
|
|
||
| for stackName, baseStack := range baseStacks { | ||
| headStack, existsInHead := headStacks[stackName] | ||
|
|
||
| if !existsInHead { | ||
| // Entire stack deleted | ||
| // Add all components with deletion_type: "stack" | ||
| } else { | ||
| // Check for deleted components within stack | ||
| // Add missing components with deletion_type: "component" | ||
| } | ||
| } | ||
|
|
||
| return deleted, nil | ||
| } | ||
| ``` | ||
|
|
||
| 2. **Update `schema.Affected`** in `pkg/schema/schema.go`: | ||
| ```go | ||
| type Affected struct { | ||
| // ... existing fields ... | ||
| Deleted bool `json:"deleted,omitempty" yaml:"deleted,omitempty"` | ||
| DeletionType string `json:"deletion_type,omitempty" yaml:"deletion_type,omitempty"` | ||
| } | ||
| ``` | ||
|
|
||
| ### Phase 2: Documentation | ||
|
|
||
| 1. Update `website/docs/cli/commands/describe/describe-affected.mdx`: | ||
|
|
||
| - Add `deleted` and `deletion_type` to output schema | ||
| - Document `deleted` affected reason | ||
| - Add examples for deletion detection | ||
| - Add CI/CD workflow examples for destroy pipelines | ||
|
|
||
| 2. Update GitHub Actions documentation for destroy workflows | ||
|
|
||
| ### Phase 3: Testing | ||
|
|
||
| 1. Add test fixtures for: | ||
|
|
||
| - Component deleted from stack | ||
| - Entire stack deleted | ||
| - Multiple deletions | ||
| - Mixed modifications and deletions | ||
|
|
||
| 2. Add unit tests: | ||
|
|
||
| - `TestDetectDeletedComponents` | ||
| - `TestDetectDeletedStacks` | ||
| - `TestDescribeAffectedWithDeleted` | ||
|
|
||
| ## Edge Cases | ||
|
|
||
| ### 1. Abstract Components | ||
|
|
||
| Abstract components (`metadata.type: abstract`) should not be reported as deleted since they are not provisioned. | ||
|
|
||
| ### 2. Disabled Components | ||
|
|
||
| Components with `metadata.enabled: false` in BASE should still be reported if removed from HEAD (user may want to clean | ||
| up the disabled resource). | ||
|
|
||
| ### 3. Component Renamed | ||
|
|
||
| If a component is renamed (old name removed, new name added): | ||
|
|
||
| - Old name appears as `deleted` | ||
| - New name appears as modified (new component) | ||
|
|
||
| Users should handle this case in their pipelines (may need manual intervention to avoid destroying and recreating). | ||
|
|
||
| ### 4. Stack File Moved | ||
|
|
||
| If a stack file is moved (but results in the same logical stack): | ||
|
|
||
| - Need to compare by stack name, not file path | ||
| - May require additional logic to detect moves vs deletes | ||
|
|
||
| ### 5. Locked Components | ||
|
|
||
| Components with `metadata.locked: true` in BASE that are deleted in HEAD should: | ||
|
|
||
| - Still be reported as deleted (with a warning?) | ||
| - Or be excluded if `--exclude-locked` is passed? | ||
|
|
||
| **Recommendation**: Report them as deleted but add a `was_locked: true` field to alert the user. | ||
|
|
||
| ## Security Considerations | ||
|
|
||
| 1. **Destroy requires explicit action**: Atmos only reports deletions; destruction requires separate pipeline step with | ||
| explicit `terraform destroy` command | ||
| 2. **Clear identification**: Deleted components are clearly marked with `deleted: true` field | ||
| 3. **Audit trail**: Deleted components are logged with full context for audit | ||
|
|
||
| ## Success Criteria | ||
|
|
||
| 1. Deleted components are automatically detected and included in output | ||
| 2. Deleted components have clear `affected: deleted` reason and `deleted: true` field | ||
| 3. CI/CD pipelines can separate apply and destroy workflows using `--query` or jq filtering | ||
| 4. Existing workflows continue to work (new fields are additive, not breaking) | ||
| 5. Documentation covers destruction workflow patterns | ||
| 6. All edge cases are handled or documented | ||
|
|
||
| ## Future Extensions | ||
|
|
||
| ### 1. Destroy Plan Generation | ||
|
|
||
| ```shell | ||
| atmos describe affected --include-deleted --generate-destroy-plans | ||
| ``` | ||
|
|
||
| Generate `terraform plan -destroy` for each deleted component. | ||
|
|
||
| ### 2. Dependency-Aware Destruction | ||
|
|
||
| When deleting components with dependents: | ||
|
|
||
| - Warn about dependent components | ||
| - Suggest destruction order (dependents first) | ||
|
|
||
| ### 3. Soft Delete Detection | ||
|
|
||
| Detect components marked as `metadata.enabled: false` (soft delete) vs completely removed (hard delete). | ||
|
|
||
| ### 4. State Orphan Detection | ||
|
|
||
| Compare stack configuration with actual Terraform state to detect: | ||
|
|
||
| - Resources in state but not in config (orphans) | ||
| - Config without state (never applied) | ||
|
|
||
| ## References | ||
|
|
||
| - [GitHub Actions: Affected Stacks](https://atmos.tools/integrations/github-actions/affected-stacks) | ||
| - [Terraform Destroy](https://developer.hashicorp.com/terraform/cli/commands/destroy) | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.