Skip to content

feat(aws): add cleanup tool - #2157

Draft
lionello wants to merge 12 commits into
mainfrom
lio/cleanup-ai
Draft

feat(aws): add cleanup tool#2157
lionello wants to merge 12 commits into
mainfrom
lio/cleanup-ai

Conversation

@lionello

@lionello lionello commented Jun 30, 2026

Copy link
Copy Markdown
Member

Description

Add cleanup tool to AI, for cleaning up protected resources.

Linked Issues

Fixes #1041

Checklist

  • I have performed a self-review of my code
  • I have added appropriate tests
  • I have updated the Defang CLI docs and/or README to reflect my changes, if necessary

Summary by CodeRabbit

  • New Features

    • Added a cleanup command to find and handle leftover cloud resources after teardown, with interactive confirmations and non-interactive reporting.
    • Expanded cloud support for detecting and safely unblocking common AWS resource leftovers.
    • Added new AWS-aware helpers for load balancers, databases, DNS records, and container repositories.
  • Bug Fixes

    • Improved teardown error handling so orphaned resources are surfaced more reliably.
    • Updated debug feedback prompts to collect free-form comments and default to starting the assistant flow.

@lionello
lionello requested a review from jordanstephens as a code owner June 30, 2026 00:24
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0328d30d-024c-420a-b153-2b05a2f328d8

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds an orphan-resource cleanup capability for AWS BYOC deployments: a new OrphanCleaner interface, AWS helpers for ELBv2/RDS/ECR/Route53, ByocAws discovery/cleanup implementation, and a cleanup_resources agent tool with interactive prompting. It also updates debugger feedback prompting, compose-down tail error handling, stack confirmation ordering, and AWS SDK dependencies.

Changes

Orphan Cleanup Tool and AWS Implementation

Layer / File(s) Summary
OrphanCleaner contract
src/pkg/cli/client/cleanup.go
Defines OrphanResource struct and OrphanCleaner interface with DiscoverOrphans and CleanupOrphan methods.
AWS cloud helpers
src/pkg/clouds/aws/elbv2.go, .../rds.go, .../ecr.go, .../route53.go, .../*_test.go, src/go.mod
Adds paginated AWS API helpers to find/modify load balancers, DB instances, ECR repositories/images, and Route53 record sets, with unit tests and AWS SDK version bumps.
ByocAws orphan discovery and cleanup
src/pkg/cli/client/byoc/aws/byoc.go, .../cleanup.go, .../domain_test.go
Adds an orphans map field and implements DiscoverOrphans/CleanupOrphan for ALB, RDS, ECR, and Route53 resource categories, plus a test stub for ChangeResourceRecordSets.
cleanup_resources agent tool
src/pkg/agent/tools/cleanup.go, .../cleanup_test.go, .../tools.go
Implements HandleCleanupTool (client/provider setup, capability gate, discovery, interactive/non-interactive reporting) and registers it as a tool with tests.
Debugger prompt and compose down trigger
src/pkg/debug/debug.go, .../debug_test.go, src/cmd/cli/command/compose.go
Changes feedback prompt from boolean confirm to text input, sets explicit confirm default, updates mock survey handling, and triggers debugger flow on compose down tail failures.
Stack confirmation order
src/pkg/cli/stacks.go, .../stacks_test.go
Reorders RequestEnum confirmation choices from yes/no to no/yes.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant CleanupTool as HandleCleanupTool
  participant ByocAws
  participant AWS as AWS APIs
  participant EC as ElicitationController

  User->>CleanupTool: invoke cleanup_resources
  CleanupTool->>ByocAws: DiscoverOrphans(projectName)
  ByocAws->>AWS: scan ALB/RDS/ECR/Route53 by prefix
  AWS-->>ByocAws: matching resources
  ByocAws-->>CleanupTool: []OrphanResource

  alt no orphans found
    CleanupTool-->>User: "no blocking leftovers"
  else orphans found, non-interactive
    CleanupTool-->>User: report + rerun instructions
  else orphans found, interactive
    loop each orphan
      CleanupTool->>EC: RequestEnum(yes/no)
      EC-->>User: prompt
      User-->>EC: answer
      EC-->>CleanupTool: choice
      alt yes
        CleanupTool->>ByocAws: CleanupOrphan(resource)
        ByocAws->>AWS: unblock resource (disable protection / delete images / delete record)
        AWS-->>ByocAws: result
      end
    end
    CleanupTool-->>User: summary + "run defang down" guidance
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • DefangLabs/defang#1713: The new cleanup_resources tool relies on elicitations.Controller.IsSupported() for interactive vs non-interactive behavior, introduced in this related PR.
  • DefangLabs/defang#1742: Both PRs modify handleComposeUpErr/makeComposeDownCmd error handling in src/cmd/cli/command/compose.go.
  • DefangLabs/defang#1919: Both PRs touch the same handleComposeUpErr area in src/cmd/cli/command/compose.go.

Suggested reviewers

  • jordanstephens
  • edwardrf

A rabbit dug through stacks of cloud debris,
Found orphaned ALBs stuck on tippy-toes 🐰
"Run cleanup_resources!" the rabbit decrees,
Records and repos, away the leaf blows~
One hop, one prompt, one tidy goodbye! 🍃

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed The PR adds AWS orphan cleanup tooling for resources left after defang down, satisfying #1041's request for a way to remove protected stateful resources.
Out of Scope Changes check ✅ Passed The changes stay focused on cleanup/orphan handling and supporting AWS/debug/test code, with no clearly unrelated feature work.
Title check ✅ Passed The title clearly and concisely identifies the main change: adding an AWS cleanup tool.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch lio/cleanup-ai

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (2)
src/pkg/agent/tools/cleanup_test.go (1)

76-121: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a CleanupOrphan failure case to the table.

This suite never exercises cleanupErr, so the new failed-cleanup reporting path in HandleCleanupTool can regress without a test catching it. Add a case that returns cleanupErr and assert both the failed item output and the final failed counter.

Example table entry
 		{
 			name:      "discovery error is surfaced",
 			provider:  &mockCleanerProvider{discoverErr: errors.New("boom")},
 			expectErr: "failed to discover leftover resources: boom",
 		},
+		{
+			name:           "cleanup error is reported",
+			provider:       &mockCleanerProvider{orphans: twoOrphans[:1], cleanupErr: errors.New("cannot clean")},
+			confirm:        "yes",
+			expectContains: []string{"failed: cannot clean", "0 cleaned, 0 skipped, 1 failed"},
+			expectCleaned:  nil,
+		},
 	}

As per coding guidelines, "Use table-driven tests for multiple scenarios" and "Add tests for new behavior and important failure modes."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pkg/agent/tools/cleanup_test.go` around lines 76 - 121, Add a
table-driven failure case in cleanup_test.go for the CleanupOrphan path that
returns cleanupErr from the mock cleaner provider. Update the test table around
HandleCleanupTool to include a scenario with orphans plus a failing cleanup, and
assert both the failed item is reported in the output and the final summary
shows the expected failed count. Use the existing mockCleanerProvider,
cleanupErr, and expectContains/expectCleaned patterns so the new case covers the
regression path without changing other scenarios.

Source: Coding guidelines

src/pkg/clouds/aws/elbv2.go (1)

25-27: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Wrap AWS errors with operation context.

These helpers currently return bare SDK errors, so downstream cleanup output loses whether listing load balancers or toggling deletion protection failed. Please wrap the original error with the operation and target identifier while preserving the provider detail. As per coding guidelines, "Preserve cloud-provider error detail while wrapping errors with operation context in cloud SDK wrappers."

Also applies to: 49-55

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pkg/clouds/aws/elbv2.go` around lines 25 - 27, The AWS ELBv2 helper calls
are returning bare SDK errors, so add operation context while preserving the
original provider error detail in the error path. Update the relevant wrapper
functions in elbv2.go, including the DescribeLoadBalancers call and the
deletion-protection-related helper mentioned in the comment, to wrap returned
errors with the operation name and target identifier using their existing
function names and parameters. Keep the underlying AWS error attached so
downstream cleanup can still surface the provider-specific failure.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/cmd/cli/command/compose.go`:
- Around line 485-500: Skip initializing the debugger in the non-interactive
failure path. In the compose command’s error handling block, guard the call to
debug.NewDebugger so it only runs when interactive prompting is possible, since
handleTailAndMonitorErr already exits early for global.NonInteractive. Keep the
existing deploymentErr return flow, but avoid the agent.New/paid-tier lookup
work and the “Failed to initialize debugger” warning in CI/non-interactive runs.

In `@src/pkg/agent/tools/cleanup.go`:
- Around line 32-35: The cleanup flow is ignoring the error from
loader.ProjectWorkingDir and falling back to an empty working directory, which
can send stacks.NewManager down the wrong project context. Update the code in
cleanup.go so the ProjectWorkingDir call is checked and any failure is returned
immediately as a wrapped error before creating the stack manager. Keep the fix
localized around ProjectWorkingDir and stacks.NewManager so failures are
deterministic and not masked by the empty string fallback.

In `@src/pkg/cli/client/byoc/aws/cleanup.go`:
- Around line 78-80: `DiscoverOrphans` is swallowing category lookup failures
and can return an empty orphan list with no error, which makes
`HandleCleanupTool` report a false “No leftover resources found”. Update
`DiscoverOrphans` to track errors from the AWS lookup helpers (for example the
`aws.FindLoadBalancersByPrefix` / other category lookups at the referenced
blocks) and, if nothing was discovered successfully, return a wrapped aggregated
error instead of nil; only return nil when at least one discovery path
succeeded. Keep the existing `term.Warnf` logging, but make sure the final
return reflects total discovery failure so `HandleCleanupTool` can surface it.

In `@src/pkg/cli/client/byoc/aws/domain_test.go`:
- Around line 40-43: The r53Mock.ChangeResourceRecordSets stub currently returns
nil, nil, which can hide unexpected DNS mutation calls during tests. Update this
mock to fail fast in the test path by panicking or otherwise recording and
asserting the request when ChangeResourceRecordSets is invoked, so accidental
cleanup mutations are surfaced immediately. Use the ChangeResourceRecordSets
method on r53Mock as the place to make this behavior explicit.

In `@src/pkg/clouds/aws/ecr.go`:
- Around line 61-65: The BatchDeleteImage call in the ECR helper currently
treats any HTTP 200 as success, which can hide per-image failures. Update the
deletion flow in the BatchDeleteImage-related helper to inspect the returned
BatchDeleteImageOutput.Failures after the svc.BatchDeleteImage call, and return
an error when that slice is non-empty. Keep the check alongside the existing
error handling so the helper only reports success when all requested images were
actually deleted.

In `@src/pkg/debug/debug.go`:
- Around line 90-104: The prompt-default lookup in isPaidAccount is performing a
live WhoAmI network call and can block NewDebugger on the full command context;
make that remote side effect explicit and bounded. Rename or wrap the helper so
the call site in NewDebugger clearly signals network I/O, and use a short-lived
context with timeout/deadline for the Fabric lookup instead of the long command
context. Keep the fallback behavior to false in the error path and preserve the
existing term.Debug logging around the whoami failure.

---

Nitpick comments:
In `@src/pkg/agent/tools/cleanup_test.go`:
- Around line 76-121: Add a table-driven failure case in cleanup_test.go for the
CleanupOrphan path that returns cleanupErr from the mock cleaner provider.
Update the test table around HandleCleanupTool to include a scenario with
orphans plus a failing cleanup, and assert both the failed item is reported in
the output and the final summary shows the expected failed count. Use the
existing mockCleanerProvider, cleanupErr, and expectContains/expectCleaned
patterns so the new case covers the regression path without changing other
scenarios.

In `@src/pkg/clouds/aws/elbv2.go`:
- Around line 25-27: The AWS ELBv2 helper calls are returning bare SDK errors,
so add operation context while preserving the original provider error detail in
the error path. Update the relevant wrapper functions in elbv2.go, including the
DescribeLoadBalancers call and the deletion-protection-related helper mentioned
in the comment, to wrap returned errors with the operation name and target
identifier using their existing function names and parameters. Keep the
underlying AWS error attached so downstream cleanup can still surface the
provider-specific failure.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 133488ad-b0e1-4d3e-b56c-950cf8fbc213

📥 Commits

Reviewing files that changed from the base of the PR and between a6ec064 and 800c2ee.

⛔ Files ignored due to path filters (1)
  • src/go.sum is excluded by !**/*.sum
📒 Files selected for processing (21)
  • src/cmd/cli/command/compose.go
  • src/go.mod
  • src/pkg/agent/tools/cleanup.go
  • src/pkg/agent/tools/cleanup_test.go
  • src/pkg/agent/tools/tools.go
  • src/pkg/cli/client/byoc/aws/byoc.go
  • src/pkg/cli/client/byoc/aws/cleanup.go
  • src/pkg/cli/client/byoc/aws/domain_test.go
  • src/pkg/cli/client/cleanup.go
  • src/pkg/cli/stacks.go
  • src/pkg/cli/stacks_test.go
  • src/pkg/clouds/aws/ecr.go
  • src/pkg/clouds/aws/ecr_test.go
  • src/pkg/clouds/aws/elbv2.go
  • src/pkg/clouds/aws/elbv2_test.go
  • src/pkg/clouds/aws/rds.go
  • src/pkg/clouds/aws/rds_test.go
  • src/pkg/clouds/aws/route53.go
  • src/pkg/clouds/aws/route53_cleanup_test.go
  • src/pkg/debug/debug.go
  • src/pkg/utils.go

Comment thread src/cmd/cli/command/compose.go Outdated
Comment thread src/pkg/agent/tools/cleanup.go Outdated
Comment thread src/pkg/cli/client/byoc/aws/cleanup.go
Comment on lines +40 to +43
func (r r53Mock) ChangeResourceRecordSets(ctx context.Context, params *route53.ChangeResourceRecordSetsInput, optFns ...func(*route53.Options)) (*route53.ChangeResourceRecordSetsOutput, error) {
// TODO: implement if needed
return nil, nil
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fail fast on unexpected DNS mutation calls in this mock.

Returning nil, nil here makes any accidental ChangeResourceRecordSets call look successful, so tests can miss a broken cleanup path. Panic or record/assert the request instead. Based on learnings, in Go test files under this repo it's acceptable for mocks to panic to surface issues quickly during tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pkg/cli/client/byoc/aws/domain_test.go` around lines 40 - 43, The
r53Mock.ChangeResourceRecordSets stub currently returns nil, nil, which can hide
unexpected DNS mutation calls during tests. Update this mock to fail fast in the
test path by panicking or otherwise recording and asserting the request when
ChangeResourceRecordSets is invoked, so accidental cleanup mutations are
surfaced immediately. Use the ChangeResourceRecordSets method on r53Mock as the
place to make this behavior explicit.

Source: Learnings

Comment thread src/pkg/clouds/aws/ecr.go Outdated
Comment thread src/pkg/debug/debug.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/pkg/debug/debug_test.go (1)

27-36: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Mock now silently no-ops instead of failing fast, and the new feedback flow has no test coverage.

AskOne previously presumably failed loudly on an unexpected response type; the ok-guarded check now silently skips setting *string responses (used by the new promptForFeedback), so a wiring bug there would go unnoticed. Based on learnings, mocks in this repo should panic on type mismatches rather than add defensive handling, since panics surface issues fast.

Additionally, no test exercises promptForFeedback's new string-based flow or the "Feedback Prompt Answered" tracking property added in debug.go.

♻️ Suggested mock + test addition
 type mockSurveyor struct {
-	response bool
+	response       bool
+	feedbackResponse string
 }

 func (s *mockSurveyor) AskOne(q survey.Prompt, response interface{}, opts ...survey.AskOpt) error {
-	if boolptr, ok := response.(*bool); ok {
-		*boolptr = s.response
+	switch v := response.(type) {
+	case *bool:
+		*v = s.response
+	case *string:
+		*v = s.feedbackResponse
+	default:
+		panic(fmt.Sprintf("unexpected response type %T", response))
 	}
 	return nil
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pkg/debug/debug_test.go` around lines 27 - 36, Update mockSurveyor.AskOne
in debug_test.go to fail fast on unexpected response types instead of silently
ignoring them, so the mock panics or otherwise clearly errors when the passed
response is not the expected type. Then add test coverage for promptForFeedback
in debug.go using the mockSurveyor to exercise the new string-based prompt flow
and verify the "Feedback Prompt Answered" tracking property is recorded.

Sources: Path instructions, Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/pkg/debug/debug.go`:
- Around line 132-139: The feedback tracking in promptAndTrackDebugSession is
now sending raw free-text from promptForFeedback to analytics, which should be
avoided. Update the event emitted via track.Evt for “Feedback Prompt Answered”
to stop forwarding the full feedback string, and instead either track only that
feedback was provided or pass a truncated/redacted value before calling
track.Track. Keep the failure path and the promptForFeedback flow intact, and
make the change wherever this feedback-answer tracking is duplicated.

---

Outside diff comments:
In `@src/pkg/debug/debug_test.go`:
- Around line 27-36: Update mockSurveyor.AskOne in debug_test.go to fail fast on
unexpected response types instead of silently ignoring them, so the mock panics
or otherwise clearly errors when the passed response is not the expected type.
Then add test coverage for promptForFeedback in debug.go using the mockSurveyor
to exercise the new string-based prompt flow and verify the "Feedback Prompt
Answered" tracking property is recorded.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a0bde84f-f4da-488c-97c1-24e68df7467a

📥 Commits

Reviewing files that changed from the base of the PR and between 5df6607 and b5c9e07.

📒 Files selected for processing (4)
  • src/cmd/cli/command/compose.go
  • src/pkg/cli/client/byoc/aws/cleanup.go
  • src/pkg/debug/debug.go
  • src/pkg/debug/debug_test.go
💤 Files with no reviewable changes (1)
  • src/pkg/cli/client/byoc/aws/cleanup.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/cmd/cli/command/compose.go

Comment thread src/pkg/debug/debug.go Outdated
@defangdevs

Copy link
Copy Markdown
Contributor

Slimmed this PR down to pure cleanup: the debugger commits (switch AI debugger to default true, feedback prompt refactor) and the compose-down debugger hook moved to PR 2158, which now targets main directly. What remains here: the cleanup_resources AI tool, orphan discovery (ELB/RDS/Route53/ECR), the internal-zones record fix, and the vendorHash bump for the new AWS SDK modules. The two PRs are now independent — no file overlap — and can merge in either order.

@lionello
lionello marked this pull request as draft August 14, 2026 17:57
@lionello lionello changed the title feat(aws): add cleanup tool to AI feat(aws): add cleanup tool Aug 18, 2026
@lionello

Copy link
Copy Markdown
Member Author

What about Azure/GCP? @defangdevs

@defangdevs

Copy link
Copy Markdown
Contributor

Checked the pulumi-defang providers. AWS is the only one where teardown can get stuck on a resource:

  • ALB/RDS deletion protection, non-empty ECR repos, and leftover Route53 records can all block pulumi destroy outright. That is unpredictable per-resource state, so it needs a tool like this one to find and clear it.

GCP and Azure retain some resources too, but by design, not as a side effect:

  • GCP Cloud SQL has an equivalent deletion-protection recipe flag (provider/defanggcp/gcp/recipe.go). It defaults to false, but if a user sets it, defang down would hit the same stuck-teardown failure as AWS RDS. That looks like a real gap.
  • GCP's VPC network/subnet, VPC peering, and Artifact Registry always use RetainOnDelete(true) on purpose (reused across deploys), so Pulumi never attempts to delete them. Nothing for this tool to unblock there.
  • Azure's Key Vault and resource group are retained the same way, on purpose (the RG hosts the Pulumi state storage too, per the comment in provider/defangazure/project.go). Also not a blocking-teardown case.

So the only concrete follow-up I see is GCP Cloud SQL deletion protection, if you want it covered. Want that added to this PR, or should I file a follow-up issue for it?

@lionello

Copy link
Copy Markdown
Member Author

point of cleanup is to clean up 100%:

  • azure: rg (implies keyvault)
  • gcp: sql …

Adds OrphanCleaner support for Azure: the project resource group is
deliberately retained on `defang down` to preserve the project's Key
Vault, so DiscoverOrphans reports it and CleanupOrphan deletes it
(which cascades to the vault too). Generalizes the tool's gating
message and description now that it's no longer AWS-only.
@defangdevs

Copy link
Copy Markdown
Contributor

Pushed Azure support (459b625): cleanup_resources now finds the project's resource group if it still exists after defang down, and deletes it — which also deletes the Key Vault inside it, so "implies keyvault" needed no separate action. Generalized the tool's gating message and description since it's no longer AWS-only.

GCP Cloud SQL needs more thought before I write it. In pulumi-defang, the Cloud SQL instance (and the Cloud Run service, for that matter) is created with no explicit name and no labels — Pulumi auto-names it from the bare compose service name plus a random suffix (provider/defanggcp/gcp/cloudsql.go line 143). AWS/Azure orphan discovery works by deriving the expected resource name from project+stack and checking if it exists. GCP resources carry nothing that ties them back to a specific project+stack, so if the same GCP project ever hosts more than one defang project/stack (nothing stops that today — GCP_PROJECT_ID is one shared config value, same as an AWS account), a prefix match could hit the wrong stack's database.

Two ways to unblock this safely:

  1. Add userLabels (project, stack) to the Cloud SQL instance and Cloud Run service in pulumi-defang, then filter on those instead of guessing from the name. Correct fix, but touches the provider repo.
  2. If in practice one GCP project is always dedicated to a single defang project+stack, I can skip project-name filtering entirely and just list Cloud SQL instances with deletion protection enabled in the configured project — but I'd rather confirm that's actually the operational assumption before shipping something that operates on "everything in the project" for a live database.

Let me know which way you want it, and I'll follow up with the GCP piece.

@defangdevs

Copy link
Copy Markdown
Contributor

Picking up your three points on this branch. Plan below before I write anything.

1. GCP cleanup lands here, not in a stacked PR

Merged main into this branch first (no conflicts) — the branch was 17 behind, and I need the down-failure debugger hook that is already on main (compose.go:520) for point 3.

New src/pkg/cli/client/byoc/gcp/cleanup.go implementing client.OrphanCleaner, alongside the AWS and Azure ones. No new dependency: google.golang.org/api/compute/v1 is already used by pkg/clouds/gcp/compute.go, and it covers networks, subnetworks, instance templates, addresses and networks.removePeering.

Discovery cannot use labels for the two resources that matter. cd/program/gcp.go:22 sets DefaultLabels with defang-project/defang-stack on every resource, which would have been ideal — but GCP networks and subnetworks have no labels field at all. So the candidate filter is the deterministic name: the Pulumi logical name is projectName+"-vpc" (provider/defanggcp/gcp/gcp.go:110) where projectName is cf.Name, the compose project name, giving physical <project>-vpc-<7hex>. That matches the leaked html-css-js-vpc-e99e23a exactly.

A name prefix alone is not safe enough, because the VPC name has no stack in it — two stacks of one compose project produce identically-prefixed VPCs, and picking wrong would delete a live network. So the name is only the candidate filter, and the safety check is the real invariant, read from GCP: a candidate is reported only if nothing is still attached to it (no Cloud Run service, instance, or forwarding rule on its subnets). A VPC with nothing attached is safe to delete no matter which stack it came from; one with attachments is skipped with the reason shown.

Order matters here, unlike AWS. DiscoverOrphans returns resources in the order they must be removed — instance templates, then peering via removePeering, then the reserved range, then subnets, then the network — and I will document that on the interface, since the existing caller already iterates in slice order.

The 1-2h Cloud Run IP-release window gets named explicitly: a subnet delete returning resourceInUseByAnotherResource is reported as "Cloud Run is still releasing IP addresses for this subnet; retry in 1-2 hours", not as a raw API error.

2. Cobra command, not only MCP

defang cleanup. To avoid two copies of the confirm-and-report loop, I will lift the loop out of HandleCleanupTool into pkg/cli and have both the cobra command and the MCP tool call it. The tool keeps its current behaviour, including the report-only path when elicitation is unavailable.

3. A failed teardown points the AI at the cleanup tool

The hook already exists on main and the agent already registers cleanup_resources via CollectDefangTools. What is missing is that the prompt is written for a deployment: buildDeploymentDebugPrompt opens with "An error occurred while deploying this project", which tells the model nothing about orphaned resources. I will mark the operation as a teardown in debug.DebugConfig and have the prompt say resources may have been left behind and that cleanup_resources exists. A failed action from defang cleanup itself will offer the debugger the same way.

Two things I could not verify

  • gcloud credentials in my sandbox have expired, so I could not confirm the live naming against defang-playground-dev. The html-css-js-vpc-e99e23a sample supports <project>-vpc-<hex>, but the defang-html-css-js-newprovidergcp-* networks in my earlier inventory on pulumi-defang#183 do not fit that shape and may be legacy-CD leftovers. If you run gcloud auth login for me I will settle it; otherwise the attachment check is what keeps a wrong guess harmless.
  • Per pulumi-defang#462's analysis the peering reserved range is not retained (vpc_peering.go:28), so its delete already fails during down while the retained connection holds it. I have not confirmed that against a real Postgres-stack run. If it holds, the flow ends in "run defang down again" to clear the range from state — the same ending the AWS path already has.

…aware debugger

Three additions to the cleanup capability, all on this branch rather than
stacked on it.

**GCP networking cleanup.** The Pulumi program creates the VPC network,
subnet, service networking connection and MIG instance templates with
RetainOnDelete, so a successful `down` drops them from Pulumi state but
not from GCP. The leaked networks eventually exhaust the project's
NETWORKS quota, which is what blocked the new-provider GCP sanity test
(pulumi-defang#183).

New byoc/gcp/cleanup.go implements client.OrphanCleaner beside the AWS
and Azure ones, and pkg/clouds/gcp/network.go holds the compute calls.
No new dependency: google.golang.org/api/compute/v1 was already used by
GetInstanceGroupManagerLabels and covers everything needed, including
networks.removePeering.

Two design points worth knowing:

- Discovery cannot use labels. cd/program/gcp.go sets defang-project and
  defang-stack as DefaultLabels on every resource, but GCP networks and
  subnetworks have no labels field, so the deterministic name is the only
  handle: Pulumi auto-naming of the logical name projectName+"-vpc" gives
  <project>-vpc-<hex>.

- That name carries no stack, so two stacks of one compose project share
  a prefix and a prefix match alone must never authorise a delete.
  DiscoverOrphans therefore checks GCP for live attachments -- instances,
  forwarding rules, and Cloud Run services with Direct VPC egress -- and
  skips any network that has one, or whose usage could not be
  established. This is what makes a wrong name guess harmless.

Orphans are returned in the order GCP requires (instance templates,
peering, reserved range, subnet, network) because unlike the AWS
resources these have hard dependencies. The interface documents that the
order is significant.

The one expected failure is explained rather than dumped: Cloud Run holds
a subnet's IP addresses for 1-2 hours after the last service using it is
deleted, so a subnet delete rejected with resourceInUseByAnotherResource
reports when to come back instead of a raw API error.

**A cleanup command.** `defang cleanup`, with --dry-run and --yes. The
confirm-and-report loop moves out of HandleCleanupTool into
pkg/cli.Cleanup so the command and the cleanup_resources tool share one
implementation and cannot drift.

**A teardown-aware debugger.** The hook already existed: a failed `down`
prompts the debugger, and the agent already registers cleanup_resources.
But the prompt opened with "An error occurred while deploying this
project", which tells the model nothing about orphaned resources.
DebugConfig now carries the operation, and a teardown or cleanup failure
says resources were probably left behind and to use cleanup_resources.
A failed `defang cleanup` offers the debugger the same way, via
DebugCleanupError.

Tests: the shared loop (order preservation, dry run, the non-interactive
report-only path, --yes, skip and failure counts, a broken confirmation
transport), GCP discovery and ordering against a mock driver including
every in-use veto, and the compute helpers (self-link comparison, region
extraction, async operation error mapping).

Not covered: the compute and Cloud Run API calls themselves, and a real
subnet delete inside the release window. Those need a GCP down on a stack
with Postgres plus the ~2h wait. The naming is also unconfirmed against a
live project -- gcloud credentials in my sandbox had expired -- which is
why the attachment check, not the name, is what keeps this safe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3WmpdY3zc555sNdkY9dzQ
@defangdevs

Copy link
Copy Markdown
Contributor

Pushed c25ab8db. All three points are on this branch. go build ./..., go vet ./... and go test -short ./... are green locally, and go.mod is untouched, so the vendorHash in pkgs/defang/cli.nix still holds.

1. GCP networking cleanup

src/pkg/cli/client/byoc/gcp/cleanup.go implements client.OrphanCleaner beside the AWS and Azure ones; src/pkg/clouds/gcp/network.go holds the compute calls. No new dependencygoogle.golang.org/api/compute/v1 was already there for GetInstanceGroupManagerLabels and covers everything, networks.removePeering included.

Two things came out of the reading that are worth knowing:

Labels cannot be used for discovery, which surprised me. cd/program/gcp.go:22 sets defang-project and defang-stack as DefaultLabels on every resource, which would have been ideal — but GCP networks and subnetworks have no labels field at all. So the handle is the deterministic name: Pulumi auto-names the logical projectName+"-vpc", giving <project>-vpc-<hex>, which matches the leaked html-css-js-vpc-e99e23a exactly.

That name has no stack in it, so two stacks of one compose project share a prefix, and a prefix match alone must never authorise a delete. DiscoverOrphans asks GCP what is still attached — instances, forwarding rules, and Cloud Run services with Direct VPC egress — and skips any network with a live attachment, or whose usage could not be established. That check, not the name, is what makes this safe.

Orphans come back in the order GCP requires — instance templates, peering, reserved range, subnet, network — because unlike the AWS resources these have hard dependencies. The interface now documents that the order is significant.

The one expected failure is explained rather than dumped: a subnet delete rejected with resourceInUseByAnotherResource reports that Cloud Run releases the IPs 1-2 hours after the last service using it is deleted, and to re-run then.

2. Cobra command

defang cleanup, with --dry-run and -y/--yes. The confirm-and-report loop moved out of HandleCleanupTool into pkg/cli.Cleanup, so the command and the cleanup_resources tool share one implementation and cannot drift. The tool keeps its existing behaviour, including the report-only path when elicitation is unavailable; --yes covers CI, where there is nobody to confirm.

3. Teardown failure points the AI at the tool

The hook was already on main and the agent already registers cleanup_resources, so the gap was only the wording: buildDeploymentDebugPrompt opened with "An error occurred while deploying this project". DebugConfig now carries the operation, and a teardown or cleanup failure tells the model that resources were probably left behind and to use cleanup_resources before retrying the destroy. Confirmed the trigger really fires on GCP: a failed CD build returns client.ErrDeploymentFailed from clouds/gcp/cloudbuild.go:207, which is what handleTailAndMonitorErr gates on. A failed defang cleanup offers the debugger the same way through the new DebugCleanupError.

What is not covered

Unit tests cover the shared loop (order preservation, dry run, the non-interactive report-only path, --yes, skip and failure counts, a broken confirmation transport), GCP discovery and ordering against a mock driver including every in-use veto, and the compute helpers.

Not covered, and it needs a real run: the compute and Cloud Run API calls themselves, and a subnet delete inside the release window — that wants a GCP down on a stack with Postgres plus the ~2h wait. The naming is also still unconfirmed against a live project, since gcloud credentials in my sandbox had expired.

One consequence worth deciding on: if this is the cleanup path we want, then pulumi-defang#462 is the alternative to it rather than a complement, and shipping both would leave two teardown paths for the same resources. I have left 462 as a draft.

defangdevs and others added 3 commits August 20, 2026 17:31
Four merges landed on main after the previous merge: #2225, #2226, #2224
(buildkit bump) and #2221. The conflicts were all dependency state, none
in the cleanup code:

- src/go.mod, src/go.sum: this branch carried AWS SDK pins from when it
  was opened in June, main has newer ones. Resolved to main's versions
  and re-ran `go mod tidy`, which re-added the ecr, elbv2 and rds modules
  this branch needs at their current versions.
- pkgs/defang/cli.nix: took main's vendorHash as the closer starting
  point. The Update vendorHash step in go.yml corrects and commits it if
  the tidy moved it.

src/pkg/cli/client/byoc/aws/byoc.go merged cleanly.

`go build ./...` and `go test -short ./...` green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3WmpdY3zc555sNdkY9dzQ
…p errors

Removes the `isPaidAccount`/`AutoApprove` gate as requested in review. The
WhoAmI round-trip is gone from the post-failure path, and no caller auto-runs
the AI debugger in CI anymore: the non-interactive branches print the debug
hint and skip building the debugger entirely, so `agent.New` is no longer paid
for where nothing can prompt.

Also from review:

- agent cleanup tool: return the ProjectWorkingDir error instead of resolving
  the project against an empty directory.
- AWS DiscoverOrphans: collect per-category failures and return them when
  nothing was discovered, so a permissions or throttling failure is no longer
  reported as "no leftover resources found".
- ECR DeleteImages: BatchDeleteImage answers 200 with a per-image Failures
  list, so check it; one undeleted image still blocks the repository.
- debug feedback: cap the free-form survey answer sent to analytics.
- route53 test mock: panic on unexpected ChangeResourceRecordSets rather than
  reporting success.
- cover the CleanupOrphan failure path and the ECR per-image failure path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3WmpdY3zc555sNdkY9dzQ
@defangdevs

Copy link
Copy Markdown
Contributor

Addressed the review findings in 73c407f.

The gate is gone, as you said. isPaidAccount/AutoApprove are removed, so the WhoAmI round-trip no longer sits in the post-failure path. One behavior change to confirm: no account auto-runs the AI debugger in CI anymore. Previously paid accounts did. The non-interactive branches now print the debug hint and skip debug.NewDebugger entirely, which also fixes the finding about paying for agent.New where nothing can prompt. An explicit defang debug still works non-interactively — it proceeds without asking, since the caller already decided.

The rest:

  • agent cleanup tool — returns the ProjectWorkingDir error instead of resolving the project against "".
  • AWS DiscoverOrphans — per-category failures are collected and returned when nothing was discovered, so a permissions or throttling failure no longer reports "No leftover resources found". A partial failure still warns and reports what it did find.
  • ECR DeleteImages — checks BatchDeleteImageOutput.Failures; BatchDeleteImage answers 200 with per-image failures, and one undeleted image still blocks the repository.
  • debug feedback — the free-form answer is capped at 1 KiB before tracking. Kept the content rather than dropping it: the prompt's help text tells the user their answer is sent to Defang, so the cap is there for an accidental log paste, not to gut the signal. Say the word if you would rather track only that feedback was given.
  • route53 test mockChangeResourceRecordSets panics on an unexpected call. Left DeleteReusableDelegationSet returning success: createUsableDelegationSet legitimately calls it on its rollback path, and panicking there broke TestPrepareDomainDelegation.
  • tests — added the CleanupOrphan failure case to the table, plus coverage for the ECR per-image failure path.

go test -short ./... passes; gofmt clean; no new lint findings in the touched files.

Note this review was from June 30 — the findings all still applied to today's head.

main gained #2228 (compose up must exit non-zero when the AI debugger runs),
which touches the same functions as the review fixes on this branch.

Kept main's fix: the debugger runs for its side effect and handleComposeUpErr
returns the original error. Its regression test survives the reconciliation and
still fails with "got <nil>" if the fix is reverted.

Reconciled the removal of the paid-tier gate with that test's needs. The gate is
gone, so the debugger is no longer built at all when there is nobody to prompt,
but #2228 needs it injectable to test how the caller handles its result. The
handlers now take a debuggerFactory that is only called once the caller knows it
will use one, which satisfies both: no agent connection in CI, and a stub in
tests. NewDebuggerForTest loses its defaultPermission argument.

TestHandleComposeUpErrWithoutAutoApprove becomes
TestHandleComposeUpErrNonInteractiveSkipsTheDebugger: with no tier gate, the
thing worth asserting is that CI never builds a debugger, and still gets the
deployment error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3WmpdY3zc555sNdkY9dzQ
@defangdevs

Copy link
Copy Markdown
Contributor

Merged main and reconciled with #2228, which landed while I was pushing the review fixes and rewrites the same functions.

Kept the #2228 fix. The debugger runs for its side effect and handleComposeUpErr returns the original error. I re-verified the regression test after reworking it: reverting the one-line fix still fails it with expected the original deployment error, got <nil> — the reported symptom for issue 2227.

The collision, and how it is resolved. #2228 needs the debugger injectable so the test can control what it returns. My first pass built it inside the handler, which would have made that test impossible — it would have removed coverage of a bug that just reached production. The handlers now take a debuggerFactory that is only called once the caller knows it will use one:

  • CI never builds a debugger, so there is no agent.New and no Fabric round-trip to reach a prompt that cannot happen — the original review finding.
  • Tests inject a stub, so the exit-code contract stays covered.

NewDebuggerForTest loses its defaultPermission argument along with the gate. TestHandleComposeUpErrWithoutAutoApprove became TestHandleComposeUpErrNonInteractiveSkipsTheDebugger: with no tier gate there is no free-vs-paid split left to assert, so it now asserts the stronger property — CI must not even build a debugger, and must still return the deployment error.

One consequence of #2228 reading against the gate removal worth stating plainly: that PR's description notes the false green "hits exactly the paid accounts whose CI runs the debugger unattended". With the gate gone, no account runs the debugger unattended in CI, so that path no longer exists at all. The exit-code fix still matters for the interactive case, which is why I kept it.

go test -short ./... passes on the merge; gofmt clean.

DiscoverOrphans looked for "<project>-vpc", which is the legacy CD's naming.
The current CD sets pulumi:autonaming to
"<lower(prefix)>-${project}-${stack}-${name}-${hex(7)}", so the network is
actually named "defang-<project>-<stack>-vpc-<hex7>" and never matched — the
tool reported no orphans while the leaked VPC was sitting right there.

Found by a live up/down cycle against defang-playground-dev, which left
"defang-cdtest-min-local1-vpc-be420a3" standing and undiscovered.

Match both shapes: the legacy networks are the ones most likely to have leaked
already, and the current one is every network from here on. Deduplicate, since
offering the same network twice would 404 on the second pass. The extra stack
segment also narrows the current prefix, so it can no longer reach a sibling
stack of the same project.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3WmpdY3zc555sNdkY9dzQ
@defangdevs

Copy link
Copy Markdown
Contributor

Live GCP up/down cycle: one bug found and fixed, and the design confirmed

Ran a real cycle against defang-playground-dev, driving the CD binary directly (built from DefangLabs/pulumi-defang#473, which removes the GCP RetainOnDeletes so the down fails instead of leaking). No Fabric in the loop.

Bug: discovery never matched a current network

networkNamePrefix looked for <project>-vpc. That is the legacy CD's naming. The current CD sets pulumi:autonaming to <lower(prefix)>-${project}-${stack}-${name}-${hex(7)} (cd/config.go), so the network is really named defang-<project>-<stack>-vpc-<hex7>.

The live cycle left defang-cdtest-min-local1-vpc-be420a3 standing, and DiscoverOrphans reported "no orphans found" — the exact networks this feature exists to clean up were invisible to it.

Fixed in fe873de: match both shapes and deduplicate. The legacy networks are the ones most likely to have leaked already; the current shape is every network from here on. The extra stack segment also narrows the current prefix so it cannot reach a sibling stack of the same project. After the fix, the same project discovers both resources in the right order.

The rest of the design is confirmed against real GCP

up created 36 resources in 3m9s. down then deleted 31 and failed on exactly two:

error: The subnetwork resource 'defang-cdtest-min-local1-subnet-d759bdc' is already
being used by 'addresses/serverless-ipv4-1787416136122560055', resourceInUseByAnotherResource

So the down fails non-zero, which is what puts the debugger in front of the user, and both resources stay in Pulumi state, which is what lets a later down finish the job.

The cleanup tool then behaves as designed: it finds both, attempts them in order, and annotates both failures with the release-window explanation and the instruction to re-run defang cleanup.

What actually holds the subnet, and why nothing can force it

The blocker is a regional address with purpose: SERVERLESS, serverless-ipv4-1787416136122560055, whose subnetwork field points at our subnet. It cannot be deleted:

ERROR: The address resource '.../addresses/serverless-ipv4-...' is already being used by
'//serverless.googleapis.com/projects/.../addressReservations/serverless-ipv4-...'

serverless.googleapis.com exposes no public addressReservations API (v1, v1alpha1 and v1beta1 all 404), so there is no way to release it early. This confirms the 1-2 hour Cloud Run window empirically rather than from the docs alone, and it settles that no retry inside a command can ever cover it.

One improvement worth considering: annotateCleanupError explains the window generically. Naming the serverless-ipv4-* address that is actually holding the subnet would make the message diagnosable rather than merely reassuring.

Not yet covered

The debugger hand-off itself — down fails → debugger starts → debugger selects the cleanup tool — still needs a Fabric login to exercise, so it is verified by unit tests only. The second defang cleanup after the window passes is also still to run.

An earlier logical name for the network was "<project>-vpc" rather than "vpc",
so the autonaming pattern produced "defang-<project>-<stack>-<project>-vpc-<hex7>"
— the project name twice. Those networks exist in live projects today, side by
side with the current shape and the legacy CD's; all three were observed in
defang-playground-dev.

Networks from older code are exactly the ones most likely to have leaked
already, so missing this shape would miss much of the backlog the cleanup
command exists to clear.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3WmpdY3zc555sNdkY9dzQ
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Consider how to delete stateful prod resources from CLI

2 participants