Skip to content

CNTRLPLANE-3820: add cleanleaked tool for AWS leaked resource cleanup - #8964

Merged
openshift-merge-bot[bot] merged 1 commit into
openshift:mainfrom
jparrill:aws-clean-leaked-resources
Jul 16, 2026
Merged

CNTRLPLANE-3820: add cleanleaked tool for AWS leaked resource cleanup#8964
openshift-merge-bot[bot] merged 1 commit into
openshift:mainfrom
jparrill:aws-clean-leaked-resources

Conversation

@jparrill

@jparrill jparrill commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Go tool (contrib/aws-leaked-resources/cleanleaked) that scans AWS VPCs, classifies them through safety gates, and deletes leaked CI resources with full cascade support
  • Claude Code skill (triage-leaked-infra) for interactive assessment of individual infra sets
  • Protection tags applied to all root CI resources (ci-2, ci-3, ci-metrics)

Context

On 2026-07-06, ad-hoc bash cleanup scripts accidentally deleted NAT gateways belonging to ci-2/ci-3 management clusters due to a bash IFS field-parsing bug, causing a multi-hour CI outage. This tool addresses the root causes with testable classification logic, protection tags, and safety invariants verified by unit tests.

Classification Gates

Gate Check Verdict
0 do-not-delete tag, protected VPC name, protected username PROTECTED
1 expirationDate tag or VPCE/ENI timestamp < min-age TOO_YOUNG
2 S3 OIDC discovery document exists ACTIVE
3 EC2 instances tagged with infraID ACTIVE
4 infraID does not match CI patterns UNCERTAIN
5 All gates pass LEAKED

Features

  • 3 delete modes: prompt-once, --confirm (CI-safe), --interactive (per infra set with full inventory)
  • Full cascade: ELBs → VPCE Services → VPCE → NAT → EIPs → IGW → RTB → ENI → Subnets → SG → VPC → Zones → OIDC → IAM roles
  • Retries 0/10 with 2s delay, Ctrl+C support (context propagation)
  • EIPs scoped to NAT gateways of target VPC only
  • Default SG never deleted, main RTB never deleted
  • --target for single infra set operations, --limit for batch control, --dry-run
  • Prow correlation: test type inference, HC namespace from service.ci zone, Prow deck link
  • Output: table/JSON, timestamped file reports

Cleanup Results (2026-07-08)

Metric Value
VPCs 150 → 90
VPCs deleted by cleanleaked 48
HostedZones 339 → 210
Zones deleted by cleanleaked 87
IAM roles deleted 11
EIPs released 18
LEAKED remaining 0
Quota errors resolved VPCLimitExceeded, TooManyHostedZones

Test plan

  • go build ./contrib/aws-leaked-resources/ compiles
  • go test ./contrib/aws-leaked-resources/... — all unit tests pass
  • go vet ./contrib/aws-leaked-resources/... — no issues
  • Protected VPCs (ci-2, ci-3, ci-metrics) classified as PROTECTED in all scenarios
  • do-not-delete=true tag always results in PROTECTED verdict
  • Default SG never deleted (tested)
  • EIPs scoped to NAT gateways only — never releases unrelated EIPs (tested with real-world ci-2 scenario)
  • API errors in DescribeInstances result in UNCERTAIN, not false LEAKED (tested)
  • Ctrl+C interrupts immediately during retries and NAT wait
  • --dry-run verified against real AWS — no resources modified
  • --interactive mode shows full resource inventory before prompting
  • Ran 3 delete sessions against real AWS: 48 infra sets cleaned, 0 incidents

Fixes CNTRLPLANE-3820

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Introduced the cleanleaked workflow to scan and safely delete leaked AWS HyperShift CI infrastructure with verdict-based gating and dry-run/confirmation modes.
    • Added enriched scan reporting (table and JSON) plus Prow and namespace correlation, with per-infra-set resource inventories.
    • Added an AWS CI triage guide to help determine safe-to-delete infra sets.
  • Bug Fixes
    • Improved deletion safety with dependency-aware, VPC-scoped cleanup and context-cancel-aware retry behavior.
  • Documentation
    • Added comprehensive cleanleaked documentation covering scan/delete modes, flags, and safety constraints.
  • Tests
    • Added unit tests covering scanning, Prow correlation, and deletion/protection behavior.
  • Chores
    • Added build/test automation (Makefile), updated gitignore for outputs, and expanded Codecov coverage ignores.

@openshift-merge-bot

Copy link
Copy Markdown
Contributor

Pipeline controller notification
This repo is configured to use the pipeline controller. Second-stage tests will be triggered either automatically or after lgtm label is added, depending on the repository configuration. The pipeline controller will automatically detect which contexts are required and will utilize /test Prow commands to trigger the second stage.

For optional jobs, comment /test ? to see a list of all defined jobs. To trigger manually all jobs from second stage use /pipeline required command.

This repository is configured in: LGTM mode

@openshift-ci-robot openshift-ci-robot added the jira/valid-reference Indicates that this PR references a valid Jira ticket of any type. label Jul 8, 2026
@openshift-ci-robot

openshift-ci-robot commented Jul 8, 2026

Copy link
Copy Markdown

@jparrill: This pull request references CNTRLPLANE-3820 which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the story to target the "5.0.0" version, but no target version was set.

Details

In response to this:

Summary

  • Go tool (contrib/aws-leaked-resources/cleanleaked) that scans AWS VPCs, classifies them through safety gates, and deletes leaked CI resources with full cascade support
  • Claude Code skill (triage-leaked-infra) for interactive assessment of individual infra sets
  • Protection tags applied to all root CI resources (ci-2, ci-3, ci-metrics)

Context

On 2026-07-06, ad-hoc bash cleanup scripts accidentally deleted NAT gateways belonging to ci-2/ci-3 management clusters due to a bash IFS field-parsing bug, causing a multi-hour CI outage. This tool addresses the root causes with testable classification logic, protection tags, and safety invariants verified by unit tests.

Classification Gates

Gate Check Verdict
0 do-not-delete tag, protected VPC name, protected username PROTECTED
1 expirationDate tag or VPCE/ENI timestamp < min-age TOO_YOUNG
2 S3 OIDC discovery document exists ACTIVE
3 EC2 instances tagged with infraID ACTIVE
4 infraID does not match CI patterns UNCERTAIN
5 All gates pass LEAKED

Features

  • 3 delete modes: prompt-once, --confirm (CI-safe), --interactive (per infra set with full inventory)
  • Full cascade: ELBs → VPCE Services → VPCE → NAT → EIPs → IGW → RTB → ENI → Subnets → SG → VPC → Zones → OIDC → IAM roles
  • Retries 0/10 with 2s delay, Ctrl+C support (context propagation)
  • EIPs scoped to NAT gateways of target VPC only
  • Default SG never deleted, main RTB never deleted
  • --target for single infra set operations, --limit for batch control, --dry-run
  • Prow correlation: test type inference, HC namespace from service.ci zone, Prow deck link
  • Output: table/JSON, timestamped file reports

Cleanup Results (2026-07-08)

Metric Value
VPCs 150 → 90
VPCs deleted by cleanleaked 48
HostedZones 339 → 210
Zones deleted by cleanleaked 87
IAM roles deleted 11
EIPs released 18
LEAKED remaining 0
Quota errors resolved VPCLimitExceeded, TooManyHostedZones

Test plan

  • go build ./contrib/aws-leaked-resources/ compiles
  • go test ./contrib/aws-leaked-resources/... — all unit tests pass
  • go vet ./contrib/aws-leaked-resources/... — no issues
  • Protected VPCs (ci-2, ci-3, ci-metrics) classified as PROTECTED in all scenarios
  • do-not-delete=true tag always results in PROTECTED verdict
  • Default SG never deleted (tested)
  • EIPs scoped to NAT gateways only — never releases unrelated EIPs (tested with real-world ci-2 scenario)
  • API errors in DescribeInstances result in UNCERTAIN, not false LEAKED (tested)
  • Ctrl+C interrupts immediately during retries and NAT wait
  • --dry-run verified against real AWS — no resources modified
  • --interactive mode shows full resource inventory before prompting
  • Ran 3 delete sessions against real AWS: 48 infra sets cleaned, 0 incidents

Fixes CNTRLPLANE-3820

🤖 Generated with Claude Code

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

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 the cleanleaked Go CLI for scanning and classifying leaked AWS HyperShift CI infrastructure, correlating findings with Route53 and Prow metadata, reporting results, and deleting leaked resources through an ordered VPC cleanup cascade. It adds AWS client interfaces, shared models, tests, build automation, documentation, coverage configuration, and a manual AWS triage skill.

Sequence Diagram(s)

sequenceDiagram
  participant CLI as cleanleaked CLI
  participant Scanner
  participant AWS as AWS APIs
  participant Prow as CorrelateWithProw
  participant Deleter

  CLI->>Scanner: Scan(ctx)
  Scanner->>AWS: Inspect VPCs, OIDC, S3, and EC2
  AWS-->>Scanner: Resource activity and inventory
  Scanner-->>CLI: InfraSet verdicts
  CLI->>Prow: CorrelateWithProw(sets)
  Prow->>AWS: Inspect Route53 zones and TXT records
  AWS-->>Prow: Namespace and zone metadata
  Prow-->>CLI: Prow metadata
  alt delete requested
    CLI->>Deleter: DeleteAll(leaked, mode)
    Deleter->>AWS: Delete zones, IAM resources, and VPC dependencies
    AWS-->>Deleter: Deletion results
  end
Loading

Possibly related PRs

Suggested reviewers: bryan-cox

🚥 Pre-merge checks | ✅ 4 | ❌ 7

❌ Failed checks (7 inconclusive)

Check name Status Explanation Resolution
Stable And Deterministic Test Names ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
Test Structure And Quality ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
Topology-Aware Scheduling Compatibility ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
Ipv6 And Disconnected Network Test Compatibility ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
No-Weak-Crypto ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
Container-Privileges ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
No-Sensitive-Data-In-Logs ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding the cleanleaked tool for AWS leaked resource cleanup.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@openshift-ci
openshift-ci Bot requested review from bryan-cox and cblecker July 8, 2026 16:29
@openshift-ci openshift-ci Bot added area/ai Indicates the PR includes changes related to AI - Claude agents, Cursor rules, etc. and removed do-not-merge/needs-area labels Jul 8, 2026
@openshift-ci

openshift-ci Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: jparrill

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@openshift-ci openshift-ci Bot added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label Jul 8, 2026
@jparrill

jparrill commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

Triage Skill Output Example

The triage-leaked-infra skill (.claude/skills/triage-leaked-infra/) provides interactive assessment of individual infra sets. Here's an example run against a leaked VPC:

/triage-leaked-infra 03a5c3112e6a5524d1b6-vpc

Output:

Triage: 03a5c3112e6a5524d1b6

┌─────┬────────────────────┬────────┬─────────────────────────────────────────────────────────────┐
│  #  │       Check        │ Result │                           Detail                            │
├─────┼────────────────────┼────────┼─────────────────────────────────────────────────────────────┤
│ 1   │ Protection tags    │ PASS   │ No do-not-delete or ci-cluster tag                          │
├─────┼────────────────────┼────────┼─────────────────────────────────────────────────────────────┤
│ 2   │ Protected VPC name │ PASS   │ Name is 03a5c3112e6a5524d1b6-vpc                            │
├─────┼────────────────────┼────────┼─────────────────────────────────────────────────────────────┤
│ 3   │ Protected user     │ PASS   │ No username match in infraID                                │
├─────┼────────────────────┼────────┼─────────────────────────────────────────────────────────────┤
│ 4   │ Age                │ PASS   │ IAM role created 2026-05-01 (68 days ago), well over 24h    │
├─────┼────────────────────┼────────┼─────────────────────────────────────────────────────────────┤
│ 5   │ CI pattern match   │ PASS   │ 20-char hex infraID → CI generic e2e                        │
├─────┼────────────────────┼────────┼─────────────────────────────────────────────────────────────┤
│ 6   │ OIDC S3 liveness   │ PASS   │ Not found in any bucket (all 3 returned 404)                │
├─────┼────────────────────┼────────┼─────────────────────────────────────────────────────────────┤
│ 7   │ EC2 instances      │ PASS   │ 0 instances                                                 │
├─────┼────────────────────┼────────┼─────────────────────────────────────────────────────────────┤
│ 8   │ red-hat-managed    │ PASS   │ No managed instances                                        │
├─────┼────────────────────┼────────┼─────────────────────────────────────────────────────────────┤
│ 9   │ Sub-resources      │ INFO   │ 1 SG (default), 2 RTBs (03a5c3112e6a5524d1b6-public + main) │
├─────┼────────────────────┼────────┼─────────────────────────────────────────────────────────────┤
│ 10  │ Route53 zones      │ INFO   │ 0 zones found                                               │
├─────┼────────────────────┼────────┼─────────────────────────────────────────────────────────────┤
│ 11  │ OIDC IAM provider  │ INFO   │ 0 orphaned providers                                        │
├─────┼────────────────────┼────────┼─────────────────────────────────────────────────────────────┤
│ 12  │ IAM roles          │ INFO   │ 1 orphaned role: 03a5c3112e6a5524d1b6 (created 2026-05-01)  │
└─────┴────────────────────┴────────┴─────────────────────────────────────────────────────────────┘

Verdict: SAFE TO DELETE

This is a leaked CI e2e infra set — 68 days old, hex infraID pattern, no protection
tags, no running instances, no OIDC document, no Route53 zones. The VPC is nearly
empty (just default SG and route tables). There's one orphaned IAM role
(03a5c3112e6a5524d1b6) to clean up alongside it.

@openshift-ci

openshift-ci Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

@jparrill: The label(s) triage/03a5c3112e6a5524d1b6-vpc cannot be applied, because the repository doesn't have them.

Details

In response to this:

Triage Skill Output Example

The triage-leaked-infra skill (.claude/skills/triage-leaked-infra/) provides interactive assessment of individual infra sets. Here's an example run against a leaked VPC:

/triage-leaked-infra 03a5c3112e6a5524d1b6-vpc

Output:

Triage: 03a5c3112e6a5524d1b6

┌─────┬────────────────────┬────────┬─────────────────────────────────────────────────────────────┐
│  #  │       Check        │ Result │                           Detail                            │
├─────┼────────────────────┼────────┼─────────────────────────────────────────────────────────────┤
│ 1   │ Protection tags    │ PASS   │ No do-not-delete or ci-cluster tag                          │
├─────┼────────────────────┼────────┼─────────────────────────────────────────────────────────────┤
│ 2   │ Protected VPC name │ PASS   │ Name is 03a5c3112e6a5524d1b6-vpc                            │
├─────┼────────────────────┼────────┼─────────────────────────────────────────────────────────────┤
│ 3   │ Protected user     │ PASS   │ No username match in infraID                                │
├─────┼────────────────────┼────────┼─────────────────────────────────────────────────────────────┤
│ 4   │ Age                │ PASS   │ IAM role created 2026-05-01 (68 days ago), well over 24h    │
├─────┼────────────────────┼────────┼─────────────────────────────────────────────────────────────┤
│ 5   │ CI pattern match   │ PASS   │ 20-char hex infraID → CI generic e2e                        │
├─────┼────────────────────┼────────┼─────────────────────────────────────────────────────────────┤
│ 6   │ OIDC S3 liveness   │ PASS   │ Not found in any bucket (all 3 returned 404)                │
├─────┼────────────────────┼────────┼─────────────────────────────────────────────────────────────┤
│ 7   │ EC2 instances      │ PASS   │ 0 instances                                                 │
├─────┼────────────────────┼────────┼─────────────────────────────────────────────────────────────┤
│ 8   │ red-hat-managed    │ PASS   │ No managed instances                                        │
├─────┼────────────────────┼────────┼─────────────────────────────────────────────────────────────┤
│ 9   │ Sub-resources      │ INFO   │ 1 SG (default), 2 RTBs (03a5c3112e6a5524d1b6-public + main) │
├─────┼────────────────────┼────────┼─────────────────────────────────────────────────────────────┤
│ 10  │ Route53 zones      │ INFO   │ 0 zones found                                               │
├─────┼────────────────────┼────────┼─────────────────────────────────────────────────────────────┤
│ 11  │ OIDC IAM provider  │ INFO   │ 0 orphaned providers                                        │
├─────┼────────────────────┼────────┼─────────────────────────────────────────────────────────────┤
│ 12  │ IAM roles          │ INFO   │ 1 orphaned role: 03a5c3112e6a5524d1b6 (created 2026-05-01)  │
└─────┴────────────────────┴────────┴─────────────────────────────────────────────────────────────┘

Verdict: SAFE TO DELETE

This is a leaked CI e2e infra set — 68 days old, hex infraID pattern, no protection
tags, no running instances, no OIDC document, no Route53 zones. The VPC is nearly
empty (just default SG and route tables). There's one orphaned IAM role
(03a5c3112e6a5524d1b6) to clean up alongside it.

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

@jparrill
jparrill force-pushed the aws-clean-leaked-resources branch from 716d5da to 2172c30 Compare July 8, 2026 16:46

@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: 7

🧹 Nitpick comments (9)
contrib/aws-leaked-resources/tags.go (1)

57-73: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Dead/misleading format entry in parseExpirationDate.

"2006-01-02T15:04+00:00" is not a valid Go time layout for a numeric offset — the stdlib only recognizes the "-0700"/"-07:00" family (which is sign-agnostic and already matches +hh:mm inputs) or the "Z..." family. Since "+00:00" isn't a special reference token, it's treated as literal text, so this format only ever matches inputs containing the literal substring "+00:00". In practice, the preceding format ("2006-01-02T15:04-07:00") already parses any ±hh:mm offset, so this entry is unreachable/dead — confirmed by TestParseExpirationDate's "short ISO with offset" case succeeding purely via the prior format. Consider removing this entry to avoid confusion.

🤖 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 `@contrib/aws-leaked-resources/tags.go` around lines 57 - 73, The
parseExpirationDate helper includes a misleading dead layout entry that can
never match numeric offsets as intended. Remove the redundant
"2006-01-02T15:04+00:00" format from the formats list in parseExpirationDate,
since the preceding "2006-01-02T15:04-07:00" already handles ±hh:mm offsets and
this extra entry only adds confusion.
contrib/aws-leaked-resources/scan.go (2)

398-444: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Duplicate AWS lookups for VPC creation time between resolveVPCCreationTime and inventorySubResources.

Both functions independently call DescribeVpcEndpoints and DescribeNetworkInterfaces and run the same "earliest timestamp wins" logic to backfill infra.VPCs[i].CreatedAt. For any VPC lacking a creation_date tag that ultimately gets classified LEAKED, these two API calls are issued twice. Consider extracting a shared helper (e.g. resolveCreatedAtFromEndpoint/resolveCreatedAtFromENI) that both call sites reuse, or have inventorySubResources reuse the already-fetched endpoint/ENI results from resolveVPCCreationTime instead of re-querying.

Also applies to: 498-511

🤖 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 `@contrib/aws-leaked-resources/scan.go` around lines 398 - 444, The VPC
creation-time backfill logic is duplicated between resolveVPCCreationTime and
inventorySubResources, causing repeated DescribeVpcEndpoints and
DescribeNetworkInterfaces calls for the same VPC. Refactor the shared “earliest
timestamp wins” logic into a helper that both resolveVPCCreationTime and
inventorySubResources reuse, or pass the already-fetched endpoint/ENI data
through so CreatedAt is resolved once. Keep the behavior centered around
infra.VPCs[i].CreatedAt and the existing EC2 lookup paths.

567-651: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

AWS API errors are silently swallowed in the inventory functions.

inventorySubResources, inventoryHostedZones, and inventoryIAMRoles all ignore errors from Describe*/List* calls without logging (e.g. inventoryHostedZones returns immediately on the first ListHostedZones error, leaving HostedZones empty for that infra set). Contrast this with checkEC2Instances (lines 381-384), which logs a WARN and reports an explicit Uncertain reason on failure. For a tool whose output drives real deletion decisions, silently under-reporting inventory on transient AWS errors (throttling, permissions) could mask resources that should have been flagged, without any signal to the operator. Consider applying the same WARN-and-note pattern here.

🤖 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 `@contrib/aws-leaked-resources/scan.go` around lines 567 - 651, AWS API
failures in the inventory helpers are currently swallowed, causing partial or
empty results without any operator signal. Update inventorySubResources,
inventoryHostedZones, and inventoryIAMRoles to handle Describe*/List* errors
like checkEC2Instances does: log a WARN with the AWS call context and error, and
record an uncertain/incomplete inventory reason on the InfraSet instead of just
returning. Use the existing helper symbols inventoryHostedZones,
inventoryIAMRoles, and inventorySubResources to add the same failure-reporting
pattern consistently.
contrib/aws-leaked-resources/clients.go (1)

13-72: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Large, producer-side interfaces conflict with the stated Go guidelines.

EC2API (~20 methods), IAMAPI (~11 methods), and friends are defined here as producer-side interfaces, whereas the repo guidelines call for small (1–2 method), consumer-defined interfaces. This is a common/pragmatic pattern for wrapping AWS SDK clients for mocking, so I'm not blocking on it, but be aware it's a guideline deviation if scrutinized later. As per coding guidelines, "Keep interfaces small and focused (generally 1–2 methods)" and "Define interfaces on the consumer side, not the producer side."

🤖 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 `@contrib/aws-leaked-resources/clients.go` around lines 13 - 72, The AWS client
interfaces in EC2API, IAMAPI, Route53API, ELBv2API, and S3API are large
producer-side mocks and should be refactored to follow the repo’s
consumer-defined, small-interface guideline. Split the methods into narrower
interfaces at the call sites that need them, and move the interface definitions
next to the consumers that use them instead of keeping one broad wrapper in
clients.go. Preserve the same method signatures, but only expose the minimal
methods each consumer actually requires.

Source: Coding guidelines

contrib/aws-leaked-resources/prow.go (1)

45-64: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefix ordering is fragile but currently correct.

InferTestType relies on knownTestPrefixes being ordered so more-specific prefixes (karpenter-upgrade-control-plane) precede shorter ones that are also prefixes (karpenter). It works today, but a future addition placed in the wrong position would silently misclassify test types with no compiler/test signal.

Consider adding a short comment above knownTestPrefixes documenting the "longer/more-specific prefix must come first" invariant, or sorting by descending length at init to make it order-independent.

🤖 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 `@contrib/aws-leaked-resources/prow.go` around lines 45 - 64, InferTestType
depends on knownTestPrefixes being ordered from more-specific to less-specific
values, so add a clear comment or similar guidance near that symbol documenting
the “longer prefix first” invariant. If you want to remove the ordering
fragility entirely, update the initialization of knownTestPrefixes so it is
sorted by descending length before InferTestType iterates over it, keeping the
existing matching behavior in isHexInfraID unchanged.
contrib/aws-leaked-resources/prow_test.go (1)

1-173: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

No test coverage for LookupNamespaceFromServiceZone, FindServiceCIZoneID, or CorrelateWithProw.

These are the only functions in prow.go that talk to Route53 and contain pagination logic — and, per the companion comment on prow.go (lines 80-111), the pagination handling there is currently missing. A table-driven test using a mock Route53API that returns a truncated first page (IsTruncated: true) followed by a second page containing the matching TXT record would have caught that gap.

🤖 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 `@contrib/aws-leaked-resources/prow_test.go` around lines 1 - 173, Add
table-driven coverage in prow_test.go for LookupNamespaceFromServiceZone,
FindServiceCIZoneID, and CorrelateWithProw using a mocked Route53API. Make the
mock return a truncated first page (IsTruncated true) and a second page with the
matching TXT record so the pagination path in prow.go is exercised. Assert the
namespace/zone ID/prow correlation are found only after the second page, and
keep the tests centered around the Route53API and the three function names so
they’re easy to locate.
contrib/aws-leaked-resources/report.go (1)

12-72: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

PrintTable mixes sorting, header rendering, and detailed sub-resource dumping in one function.

The function handles sort ordering, the summary row, and then four different detail-block loops (VPCs, hosted zones, OIDC providers, IAM roles) for leaked/uncertain sets. Extracting the "print details for a leaked/uncertain set" block (lines 41-70) into a helper (e.g. printInfraDetails(w, s)) would make PrintTable easier to scan and test independently.

🤖 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 `@contrib/aws-leaked-resources/report.go` around lines 12 - 72, The PrintTable
function is doing too much by combining table sorting, summary-row rendering,
and the leaked/uncertain detail output in one block. Extract the detail-printing
section for VerdictLeaked and VerdictUncertain into a dedicated helper such as
printInfraDetails(w, s), and have PrintTable call it after writing the summary
row; keep the existing VPC, HostedZones, OIDCProviders, and IAMRoles loops
inside that helper so the main function is easier to scan and test.
contrib/aws-leaked-resources/main.go (1)

116-130: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate flag registration between scanCmd and deleteCmd.

--region, --min-age, --target, --output, and --output-dir are registered identically on both subcommands. Since they bind to the same shared cfg struct, these could be declared once as rootCmd.PersistentFlags() instead.

🤖 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 `@contrib/aws-leaked-resources/main.go` around lines 116 - 130, The flags
registered in both scanCmd and deleteCmd are duplicated even though they all
bind to the shared cfg struct. Move the common options like region, min-age,
target, output, and output-dir to rootCmd.PersistentFlags() in main.go so both
subcommands inherit them, and keep only command-specific flags on scanCmd and
deleteCmd.
contrib/aws-leaked-resources/delete.go (1)

587-754: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

printDetailedInventory duplicates the resource-listing logic already implemented in the dedicated delete* helpers.

This function re-implements its own Describe+iterate loops for ELBs, VPCEs, NATs, IGWs, subnets, RTBs, SGs, and ENIs, instead of reusing deleteLoadBalancers, deleteVPCEndpoints, etc. (in list-only mode). Keeping two independent implementations of "what belongs to this VPC" risks the two falling out of sync (e.g., a future filter added to one path but not the other).

Extract small list*(ctx, vpcID) helpers used by both the deletion cascade and this inventory printer.

🤖 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 `@contrib/aws-leaked-resources/delete.go` around lines 587 - 754,
printDetailedInventory currently duplicates the “what belongs to this VPC”
listing logic that already exists in the delete* flow, so the two paths can
drift. Refactor the resource discovery in Deleter by extracting shared list
helpers (for example listLoadBalancers, listVPCEndpoints, listNatGateways,
listInternetGateways, listSubnets, listRouteTables, listSecurityGroups, and
listNetworkInterfaces) and have both printDetailedInventory and the existing
deleteLoadBalancers/deleteVPCEndpoints/etc. reuse them. Keep the VPC-scoped
filtering and formatting centralized in those shared helpers.
🤖 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 @.claude/skills/triage-leaked-infra/SKILL.md:
- Around line 89-97: The shell examples in the triage guide are broken by inline
comments after a pipe, so they cannot be copied and run verbatim. Update the
command examples in the skill documentation so the AWS CLI snippets stand alone
as valid shell, and move explanatory notes about filtering or cross-checking
outside the command text; check the affected snippet blocks around the
describe-vpc-endpoint-service-configurations and describe-addresses examples for
consistency.

In `@contrib/aws-leaked-resources/delete.go`:
- Around line 108-120: DeleteAll currently reports deletion counts but always
returns nil, so failures in failedVPCs and failedZones never reach
runDelete/main and the CLI can exit successfully despite leaks. Update DeleteAll
to surface an error when any VPC or Zone deletions fail, using the existing
failure counters and the deletion summary logic near the end of the function, so
runDelete and main() can propagate a non-zero exit in CI-safe delete mode.
Ensure the success path still returns nil only when all tracked deletions
complete without failures.
- Around line 227-287: The regional list calls in deleteLoadBalancers and
deleteVPCEndpointServices only fetch the first page, so leaked resources can be
missed when there are multiple pages. Update these helpers to page through
DescribeLoadBalancers and DescribeVpcEndpointServiceConfigurations once per
cleanup run, collect all results, and then filter locally by VpcId or cluster
tag before calling retryDelete. Use the existing deleteLoadBalancers and
deleteVPCEndpointServices methods as the main touchpoints.

In `@contrib/aws-leaked-resources/prow.go`:
- Around line 151-178: CorrelateWithProw currently skips VerdictUncertain sets
even though PrintTable in report.go renders the TestType/Namespace/ProwLink
details for both leaked and uncertain infra. Update CorrelateWithProw to enrich
both VerdictLeaked and VerdictUncertain entries by changing the verdict filter
near the start of the loop, while keeping the existing ProwJobURL and
LookupNamespaceFromServiceZone behavior unchanged. Use the CorrelateWithProw and
PrintTable symbols to verify the correlation now feeds the report path for
uncertain sets too.
- Around line 80-111: LookupNamespaceFromServiceZone only inspects the first
ListResourceRecordSets response, so it can miss matching TXT records in larger
hosted zones. Update the Route53 scan logic in LookupNamespaceFromServiceZone to
iterate through all pages using route53.NewListResourceRecordSetsPaginator (or
equivalent manual pagination with
StartRecordName/StartRecordType/StartRecordIdentifier until IsTruncated is
false), while preserving the existing TXT filtering and extractNamespace
matching.

In `@contrib/aws-leaked-resources/report.go`:
- Around line 74-92: PrintSummary is missing the VerdictUntagged bucket from the
summary breakdown. Update PrintSummary to include an Untagged line using
counts[VerdictUntagged] and countVPCs(sets, VerdictUntagged), alongside the
existing Protected, Active, Too young, Uncertain, and Leaked entries. Keep the
output aligned with the other verdict rows so the totals remain consistent with
len(sets) and the VPC counts.

In `@contrib/aws-leaked-resources/scan.go`:
- Around line 274-293: The age gate in checkAge currently treats an undetermined
VPC age as a pass by returning empty verdicts when oldestVPC(vpcs) cannot infer
a creation time. Update checkAge to fail safe by returning VerdictUncertain with
a clear reason whenever oldestVPC is zero/unknown, matching the existing
uncertain handling used in checkEC2Instances and other ambiguous checks. Add or
update tests around checkAge/oldestVPC to cover VPC sets with no creation_date,
VPCE, or ENI attach time.

---

Nitpick comments:
In `@contrib/aws-leaked-resources/clients.go`:
- Around line 13-72: The AWS client interfaces in EC2API, IAMAPI, Route53API,
ELBv2API, and S3API are large producer-side mocks and should be refactored to
follow the repo’s consumer-defined, small-interface guideline. Split the methods
into narrower interfaces at the call sites that need them, and move the
interface definitions next to the consumers that use them instead of keeping one
broad wrapper in clients.go. Preserve the same method signatures, but only
expose the minimal methods each consumer actually requires.

In `@contrib/aws-leaked-resources/delete.go`:
- Around line 587-754: printDetailedInventory currently duplicates the “what
belongs to this VPC” listing logic that already exists in the delete* flow, so
the two paths can drift. Refactor the resource discovery in Deleter by
extracting shared list helpers (for example listLoadBalancers, listVPCEndpoints,
listNatGateways, listInternetGateways, listSubnets, listRouteTables,
listSecurityGroups, and listNetworkInterfaces) and have both
printDetailedInventory and the existing
deleteLoadBalancers/deleteVPCEndpoints/etc. reuse them. Keep the VPC-scoped
filtering and formatting centralized in those shared helpers.

In `@contrib/aws-leaked-resources/main.go`:
- Around line 116-130: The flags registered in both scanCmd and deleteCmd are
duplicated even though they all bind to the shared cfg struct. Move the common
options like region, min-age, target, output, and output-dir to
rootCmd.PersistentFlags() in main.go so both subcommands inherit them, and keep
only command-specific flags on scanCmd and deleteCmd.

In `@contrib/aws-leaked-resources/prow_test.go`:
- Around line 1-173: Add table-driven coverage in prow_test.go for
LookupNamespaceFromServiceZone, FindServiceCIZoneID, and CorrelateWithProw using
a mocked Route53API. Make the mock return a truncated first page (IsTruncated
true) and a second page with the matching TXT record so the pagination path in
prow.go is exercised. Assert the namespace/zone ID/prow correlation are found
only after the second page, and keep the tests centered around the Route53API
and the three function names so they’re easy to locate.

In `@contrib/aws-leaked-resources/prow.go`:
- Around line 45-64: InferTestType depends on knownTestPrefixes being ordered
from more-specific to less-specific values, so add a clear comment or similar
guidance near that symbol documenting the “longer prefix first” invariant. If
you want to remove the ordering fragility entirely, update the initialization of
knownTestPrefixes so it is sorted by descending length before InferTestType
iterates over it, keeping the existing matching behavior in isHexInfraID
unchanged.

In `@contrib/aws-leaked-resources/report.go`:
- Around line 12-72: The PrintTable function is doing too much by combining
table sorting, summary-row rendering, and the leaked/uncertain detail output in
one block. Extract the detail-printing section for VerdictLeaked and
VerdictUncertain into a dedicated helper such as printInfraDetails(w, s), and
have PrintTable call it after writing the summary row; keep the existing VPC,
HostedZones, OIDCProviders, and IAMRoles loops inside that helper so the main
function is easier to scan and test.

In `@contrib/aws-leaked-resources/scan.go`:
- Around line 398-444: The VPC creation-time backfill logic is duplicated
between resolveVPCCreationTime and inventorySubResources, causing repeated
DescribeVpcEndpoints and DescribeNetworkInterfaces calls for the same VPC.
Refactor the shared “earliest timestamp wins” logic into a helper that both
resolveVPCCreationTime and inventorySubResources reuse, or pass the
already-fetched endpoint/ENI data through so CreatedAt is resolved once. Keep
the behavior centered around infra.VPCs[i].CreatedAt and the existing EC2 lookup
paths.
- Around line 567-651: AWS API failures in the inventory helpers are currently
swallowed, causing partial or empty results without any operator signal. Update
inventorySubResources, inventoryHostedZones, and inventoryIAMRoles to handle
Describe*/List* errors like checkEC2Instances does: log a WARN with the AWS call
context and error, and record an uncertain/incomplete inventory reason on the
InfraSet instead of just returning. Use the existing helper symbols
inventoryHostedZones, inventoryIAMRoles, and inventorySubResources to add the
same failure-reporting pattern consistently.

In `@contrib/aws-leaked-resources/tags.go`:
- Around line 57-73: The parseExpirationDate helper includes a misleading dead
layout entry that can never match numeric offsets as intended. Remove the
redundant "2006-01-02T15:04+00:00" format from the formats list in
parseExpirationDate, since the preceding "2006-01-02T15:04-07:00" already
handles ±hh:mm offsets and this extra entry only adds confusion.
🪄 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: Repository YAML (base), Central YAML (inherited)

Review profile: CHILL

Plan: Enterprise

Run ID: 10d6af41-62b0-4df5-a271-a3aa5ef6ac90

📥 Commits

Reviewing files that changed from the base of the PR and between 8b5103a and 716d5da.

📒 Files selected for processing (15)
  • .claude/skills/triage-leaked-infra/SKILL.md
  • contrib/aws-leaked-resources/.gitignore
  • contrib/aws-leaked-resources/Makefile
  • contrib/aws-leaked-resources/README.md
  • contrib/aws-leaked-resources/clients.go
  • contrib/aws-leaked-resources/delete.go
  • contrib/aws-leaked-resources/delete_test.go
  • contrib/aws-leaked-resources/main.go
  • contrib/aws-leaked-resources/prow.go
  • contrib/aws-leaked-resources/prow_test.go
  • contrib/aws-leaked-resources/report.go
  • contrib/aws-leaked-resources/scan.go
  • contrib/aws-leaked-resources/scan_test.go
  • contrib/aws-leaked-resources/tags.go
  • contrib/aws-leaked-resources/types.go

Comment thread .claude/skills/triage-leaked-infra/SKILL.md
Comment thread contrib/aws-leaked-resources/delete.go
Comment thread contrib/aws-leaked-resources/delete.go
Comment thread contrib/aws-leaked-resources/prow.go
Comment thread contrib/aws-leaked-resources/prow.go
Comment thread contrib/aws-leaked-resources/report.go
Comment thread contrib/aws-leaked-resources/scan.go
@jparrill
jparrill force-pushed the aws-clean-leaked-resources branch from 2172c30 to 2649352 Compare July 8, 2026 16:49

@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.

♻️ Duplicate comments (1)
.claude/skills/triage-leaked-infra/SKILL.md (1)

89-97: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Broken shell snippets still present despite prior "addressed" note.

Lines 89 and 96 still pipe into a trailing # comment, which makes the pipeline truncate at the comment and produces invalid/incomplete shell (the AWS CLI output is piped into nothing runnable). This matches a previously raised concern that was marked as addressed, but the current file content shows the same issue unresolved.

🔧 Proposed fix
-aws ec2 describe-vpc-endpoint-service-configurations --region us-east-1 --output json | # filter by kubernetes.io/cluster/<infraID> tag
+aws ec2 describe-vpc-endpoint-service-configurations --region us-east-1 --output json
+# filter by kubernetes.io/cluster/<infraID> tag
@@
-aws ec2 describe-addresses --region us-east-1 --filters "Name=domain,Values=vpc" --output text | # cross-check with NAT gateway EIPs
+aws ec2 describe-addresses --region us-east-1 --filters "Name=domain,Values=vpc" --output text
+# cross-check with NAT gateway EIPs

As per path instructions, .claude files are high risk and should be reviewed with a security and malware lens; although this is a documentation-only issue, it should be fixed so the snippets remain copy/paste-safe.

🤖 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 @.claude/skills/triage-leaked-infra/SKILL.md around lines 89 - 97, The shell
snippets in the triage guide still contain trailing inline comments after a
pipe, which breaks the command flow and leaves incomplete copy/pasteable AWS CLI
examples. Update the affected snippets in SKILL.md so the comment is moved onto
its own line or removed, preserving valid pipelines in the examples around the
VPC endpoint, NAT gateway, and address checks. Keep the commands in the same
section and verify the snippets remain runnable as written.

Source: Path instructions

🧹 Nitpick comments (1)
.claude/skills/triage-leaked-infra/SKILL.md (1)

36-38: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Hardcoded username allowlist will go stale.

The protected-user list is a hardcoded set of individual usernames embedded directly in the skill doc. As team membership changes, this list requires manual doc edits to stay accurate, and it's easy to forget. Consider sourcing this from a shared, more discoverable location (e.g., a small config file or a well-known Slack/JIRA group) referenced by the skill instead of inlining names in markdown.

🤖 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 @.claude/skills/triage-leaked-infra/SKILL.md around lines 36 - 38, The
protected-user check in the triage skill is using a hardcoded username allowlist
that will drift over time. Update the skill guidance where it currently
enumerates usernames to reference a shared source of truth instead, such as a
small config file or an approved group identifier, and make the infraID check in
the skill point to that external list. Keep the existing FAIL behavior for
matches, but ensure the usernames themselves are no longer embedded directly in
the markdown.
🤖 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.

Duplicate comments:
In @.claude/skills/triage-leaked-infra/SKILL.md:
- Around line 89-97: The shell snippets in the triage guide still contain
trailing inline comments after a pipe, which breaks the command flow and leaves
incomplete copy/pasteable AWS CLI examples. Update the affected snippets in
SKILL.md so the comment is moved onto its own line or removed, preserving valid
pipelines in the examples around the VPC endpoint, NAT gateway, and address
checks. Keep the commands in the same section and verify the snippets remain
runnable as written.

---

Nitpick comments:
In @.claude/skills/triage-leaked-infra/SKILL.md:
- Around line 36-38: The protected-user check in the triage skill is using a
hardcoded username allowlist that will drift over time. Update the skill
guidance where it currently enumerates usernames to reference a shared source of
truth instead, such as a small config file or an approved group identifier, and
make the infraID check in the skill point to that external list. Keep the
existing FAIL behavior for matches, but ensure the usernames themselves are no
longer embedded directly in the markdown.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited)

Review profile: CHILL

Plan: Enterprise

Run ID: 83375943-372c-4040-8e02-8beeb707e0f3

📥 Commits

Reviewing files that changed from the base of the PR and between 716d5da and 2649352.

📒 Files selected for processing (15)
  • .claude/skills/triage-leaked-infra/SKILL.md
  • contrib/aws-leaked-resources/.gitignore
  • contrib/aws-leaked-resources/Makefile
  • contrib/aws-leaked-resources/README.md
  • contrib/aws-leaked-resources/clients.go
  • contrib/aws-leaked-resources/delete.go
  • contrib/aws-leaked-resources/delete_test.go
  • contrib/aws-leaked-resources/main.go
  • contrib/aws-leaked-resources/prow.go
  • contrib/aws-leaked-resources/prow_test.go
  • contrib/aws-leaked-resources/report.go
  • contrib/aws-leaked-resources/scan.go
  • contrib/aws-leaked-resources/scan_test.go
  • contrib/aws-leaked-resources/tags.go
  • contrib/aws-leaked-resources/types.go
✅ Files skipped from review due to trivial changes (2)
  • contrib/aws-leaked-resources/.gitignore
  • contrib/aws-leaked-resources/README.md
🚧 Files skipped from review as they are similar to previous changes (11)
  • contrib/aws-leaked-resources/prow_test.go
  • contrib/aws-leaked-resources/report.go
  • contrib/aws-leaked-resources/clients.go
  • contrib/aws-leaked-resources/prow.go
  • contrib/aws-leaked-resources/tags.go
  • contrib/aws-leaked-resources/types.go
  • contrib/aws-leaked-resources/main.go
  • contrib/aws-leaked-resources/scan.go
  • contrib/aws-leaked-resources/scan_test.go
  • contrib/aws-leaked-resources/delete.go
  • contrib/aws-leaked-resources/delete_test.go

@cblecker

cblecker commented Jul 9, 2026

Copy link
Copy Markdown
Member

/uncc

@openshift-ci
openshift-ci Bot removed the request for review from cblecker July 9, 2026 00:30
@sdminonne

Copy link
Copy Markdown
Contributor

/cc @sdminonne

@openshift-ci
openshift-ci Bot requested a review from sdminonne July 13, 2026 08:48

@sdminonne sdminonne 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.

@jparrill may you have a look and let me know?

Comment thread contrib/aws-leaked-resources/delete.go Outdated
return
}

out, err := d.ELBv2.DescribeLoadBalancers(ctx, &elbv2.DescribeLoadBalancersInput{})

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.

Hmm, if I'm understanding correctly we may need to add a paginator here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added — both deleteLoadBalancers and inventorySubResources now paginate through all pages. Also added a per-scan cache in Scanner so the scan path fetches LBs once per run.

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.

Yeah. Thanks it paginates only for VPCs right? All the other subresources are not. Guess is fine in practice or at least for resources we handle at the moment but it may change in the future (if we decide to handle different resources in other ways).

Comment thread contrib/aws-leaked-resources/scan.go Outdated
}

if s.ELBv2 != nil {
if out, err := s.ELBv2.DescribeLoadBalancers(ctx, &elbv2.DescribeLoadBalancersInput{}); err == 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.

Paginator here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed — paginated DescribeVpcEndpointServiceConfigurations here and in delete.go.

Comment thread contrib/aws-leaked-resources/prow.go Outdated
// Input: "heritage=external-dns,...,external-dns/resource=route/e2e-clusters-7c84g-node-pool-78vcg/kube-apiserver"
// Output: "e2e-clusters-7c84g-node-pool-78vcg"
func extractNamespace(txtValue string) string {
re := regexp.MustCompile(`resource=route/([^/]+)/`)

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.

Can we put in a var this regexp? I know it won't be executed many times though :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done — moved to a package-level var namespaceFromRouteRe = regexp.MustCompile(...).

Comment thread contrib/aws-leaked-resources/prow.go
Comment thread contrib/aws-leaked-resources/scan.go
} else {
fmt.Fprintf(os.Stderr, " LEAKED: %s — inventorying sub-resources...\n", infraID)
}
s.inventorySubResources(ctx, &infraSet)

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.

This is called for one infraID, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, inventorySubResources is called once per InfraSet (= one infraID). It iterates over the VPCs in that set and counts their sub-resources.

VerdictUncertain Verdict = "UNCERTAIN"
)

type InfraSet struct {

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.

To be honest the InfraSet name is a little bit misleading, since it can contain only one infraID but it's `nit

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Acknowledged — InfraSet groups all AWS resources (VPCs, hosted zones, OIDC providers, IAM roles) that share the same infraID. Think of it as the set of infrastructure belonging to one cluster deployment. The name is accurate since it's a set of resources, not a set of infraIDs. That said, if it causes confusion during reviews I'm open to renaming in a follow-up.

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.

InfraInfo? InfraData? It may come from my past but a Set is an ensemble of things. Anyway. A follow-up is good too.

Comment thread contrib/aws-leaked-resources/scan.go Outdated
}

if s.ELBv2 != nil {
if out, err := s.ELBv2.DescribeLoadBalancers(ctx, &elbv2.DescribeLoadBalancersInput{}); err == 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.

Here we describe all account LB, right so for each potentially leaked infraID we goes over all the LBs... May we cache it somhow?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch — fixed. Added a per-scan LB cache to Scanner that fetches all LBs once (with pagination) and filters locally per VPC. No more N×DescribeLoadBalancers calls.

Comment thread contrib/aws-leaked-resources/delete.go Outdated
natEIPs := d.collectNATGatewayEIPs(ctx, vpcID)
if natDeleted := d.deleteNATGateways(ctx, vpcID); natDeleted > 0 {
fmt.Fprintf(os.Stderr, " Waiting for NAT gateways to release ENIs...\n")
if err := sleepWithContext(ctx, 15*time.Second); err != 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.

Not sure 15secs. couldn't we implement retry wit exponential backoff? I won't block on this since I'm not totally sure neither. Just a question

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good question. The 15s sleep was a grace period for NAT gateways to release their ENIs (async AWS behavior), not a retry. That said, a blind sleep is fragile. I've replaced it with a polling loop: check every 5s whether NATs are still in deleting state, up to 12 retries (60s max). Fixed interval, no exponential backoff — this isn't a k8s controller and these AWS calls are cheap and on-demand.

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.

SGTM it was just to think about this and sharing rationale. TY!

RoleName: aws.String(roleName),
}); err == nil {
for _, prof := range profOut.InstanceProfiles {
_, _ = d.IAM.RemoveRoleFromInstanceProfile(ctx, &iam.RemoveRoleFromInstanceProfileInput{

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.

OK but we should delete the instance profile too, in case of no error we should invoke DeleteInstanceProfile.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch — added DeleteInstanceProfile call right after RemoveRoleFromInstanceProfile so the orphaned profile doesn't linger.

@jparrill
jparrill force-pushed the aws-clean-leaked-resources branch from 2649352 to 4f1efd0 Compare July 13, 2026 12:48
@codecov

codecov Bot commented Jul 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 44.11%. Comparing base (3e6ecb5) to head (40eb21c).
⚠️ Report is 103 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #8964      +/-   ##
==========================================
+ Coverage   43.37%   44.11%   +0.73%     
==========================================
  Files         771      772       +1     
  Lines       95718    96227     +509     
==========================================
+ Hits        41520    42449     +929     
+ Misses      51313    50832     -481     
- Partials     2885     2946      +61     

see 52 files with indirect coverage changes

Flag Coverage Δ
cmd-support 38.24% <ø> (+1.36%) ⬆️
cpo-hostedcontrolplane 46.17% <ø> (+0.95%) ⬆️
cpo-other 45.13% <ø> (+0.03%) ⬆️
hypershift-operator 54.09% <ø> (+0.43%) ⬆️
other 32.12% <ø> (+0.03%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@jparrill
jparrill force-pushed the aws-clean-leaked-resources branch from 4f1efd0 to ee69d33 Compare July 13, 2026 13:39

@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: 3

🧹 Nitpick comments (2)
contrib/aws-leaked-resources/scan.go (2)

74-105: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

--limit selects a nondeterministic subset of leaked sets.

Scan ranges over grouped (a map), so iteration order is random. With Config.Limit > 0, the first Limit leaked sets encountered are inventoried and the rest are dropped (Line 103-105), meaning repeated runs report/act on different infra sets. For a repeatable cleanup tool, consider iterating over sorted infra IDs so --limit is deterministic.

🤖 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 `@contrib/aws-leaked-resources/scan.go` around lines 74 - 105, Make the
iteration that drives Scan’s limit handling deterministic by extracting the
infra IDs from grouped, sorting them, and iterating in that sorted order instead
of ranging over the map directly. Preserve the existing per-set processing and
--limit behavior while ensuring the same infra IDs are selected on every run.

630-662: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Cache account-wide IAM role and hosted-zone listings like ELBs. Both functions run once per leaked infra set (Lines 147-148), and each paginates the entire account (ListRoles / ListHostedZones) from scratch every time. With many leaked sets this is O(leaked × total-roles) and O(leaked × total-zones) API calls — the exact N× fan-out that was already fixed for load balancers via lbCache/fetchAllLoadBalancers. Fetch each collection once and filter locally per infra set.

  • contrib/aws-leaked-resources/scan.go#L630-L662: add a rolesCache (fetched once, paginated) to Scanner and filter by suffix/infraID locally instead of calling ListRoles per infra set.
  • contrib/aws-leaked-resources/scan.go#L578-L611: add a zonesCache (fetched once, paginated) to Scanner and run ZoneMatchesInfra locally instead of calling ListHostedZones per infra set.
🤖 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 `@contrib/aws-leaked-resources/scan.go` around lines 630 - 662, Update
contrib/aws-leaked-resources/scan.go:630-662 around Scanner.inventoryIAMRoles to
add a Scanner-level rolesCache, fetch and paginate ListRoles once, then filter
cached roles by suffix and infraID locally for each infra set instead of calling
ListRoles per set. Also update contrib/aws-leaked-resources/scan.go:578-611
around the hosted-zone inventory function to add and reuse a zonesCache
populated once through paginated ListHostedZones calls, applying
ZoneMatchesInfra locally for each infra set.
🤖 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 @.claude/skills/triage-leaked-infra/SKILL.md:
- Around line 17-19: Update the identifier extraction flow to fully validate
user-provided VPC IDs and infraIDs against their required formats before
constructing AWS CLI commands. Store validated values in shell variables and
quote those variables in every command interpolation, including both
VPC-to-infraID and infraID-to-VPC lookups.
- Around line 61-65: Update the OIDC liveness check instructions for the aws
s3api head-object commands so only an explicit 404 is classified as PASS;
classify 200 as FAIL and all other errors, including AccessDenied, throttling,
wrong-region, or transport failures, as UNKNOWN. Ensure UNKNOWN blocks deletion
rather than being treated as evidence that the object is absent.

In `@contrib/aws-leaked-resources/Makefile`:
- Around line 7-10: Define one root-relative BINDIR for the aws-leaked-resources
artifact directory, then update the build and clean targets to create, write to,
echo, and remove that same directory. Replace the separate ./bin creation and
hard-coded $(REPO_ROOT)/contrib/aws-leaked-resources/bin references while
preserving the existing binary name.

---

Nitpick comments:
In `@contrib/aws-leaked-resources/scan.go`:
- Around line 74-105: Make the iteration that drives Scan’s limit handling
deterministic by extracting the infra IDs from grouped, sorting them, and
iterating in that sorted order instead of ranging over the map directly.
Preserve the existing per-set processing and --limit behavior while ensuring the
same infra IDs are selected on every run.
- Around line 630-662: Update contrib/aws-leaked-resources/scan.go:630-662
around Scanner.inventoryIAMRoles to add a Scanner-level rolesCache, fetch and
paginate ListRoles once, then filter cached roles by suffix and infraID locally
for each infra set instead of calling ListRoles per set. Also update
contrib/aws-leaked-resources/scan.go:578-611 around the hosted-zone inventory
function to add and reuse a zonesCache populated once through paginated
ListHostedZones calls, applying ZoneMatchesInfra locally for each infra set.
🪄 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: Repository YAML (base), Central YAML (inherited)

Review profile: CHILL

Plan: Enterprise

Run ID: 8094bf45-ae9e-4139-90f5-1ea285ffeeb7

📥 Commits

Reviewing files that changed from the base of the PR and between 2649352 and ee69d33.

📒 Files selected for processing (15)
  • .claude/skills/triage-leaked-infra/SKILL.md
  • contrib/aws-leaked-resources/.gitignore
  • contrib/aws-leaked-resources/Makefile
  • contrib/aws-leaked-resources/README.md
  • contrib/aws-leaked-resources/clients.go
  • contrib/aws-leaked-resources/delete.go
  • contrib/aws-leaked-resources/delete_test.go
  • contrib/aws-leaked-resources/main.go
  • contrib/aws-leaked-resources/prow.go
  • contrib/aws-leaked-resources/prow_test.go
  • contrib/aws-leaked-resources/report.go
  • contrib/aws-leaked-resources/scan.go
  • contrib/aws-leaked-resources/scan_test.go
  • contrib/aws-leaked-resources/tags.go
  • contrib/aws-leaked-resources/types.go
🚧 Files skipped from review as they are similar to previous changes (10)
  • contrib/aws-leaked-resources/.gitignore
  • contrib/aws-leaked-resources/types.go
  • contrib/aws-leaked-resources/clients.go
  • contrib/aws-leaked-resources/report.go
  • contrib/aws-leaked-resources/main.go
  • contrib/aws-leaked-resources/prow.go
  • contrib/aws-leaked-resources/delete.go
  • contrib/aws-leaked-resources/scan_test.go
  • contrib/aws-leaked-resources/delete_test.go
  • contrib/aws-leaked-resources/README.md

Comment on lines +17 to +19
Extract the **infraID** and **VPC ID** from the input. If only one is given, derive the other:
- VPC ID → `aws ec2 describe-vpcs --vpc-ids <VPC> --query 'Vpcs[0].Tags'` → extract infraID from `kubernetes.io/cluster/<infraID>` tag or `Name` tag (strip `-vpc` suffix)
- infraID → `aws ec2 describe-vpcs --filters "Name=tag:Name,Values=<infraID>-vpc"` → get VPC ID

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Validate identifiers before interpolating them into shell commands.

Lines 17–19 accept user-provided values that are then copied into AWS CLI snippets. A crafted value can inject shell syntax and execute commands under the operator’s credentials. Require full-format allow-list validation for VPC IDs and infraIDs, then use quoted shell variables in every command.

🤖 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 @.claude/skills/triage-leaked-infra/SKILL.md around lines 17 - 19, Update the
identifier extraction flow to fully validate user-provided VPC IDs and infraIDs
against their required formats before constructing AWS CLI commands. Store
validated values in shell variables and quote those variables in every command
interpolation, including both VPC-to-infraID and infraID-to-VPC lookups.

Source: Path instructions

Comment on lines +61 to +65
aws s3api head-object --bucket hypershift-ci-oidc --key "<infraID>/.well-known/openid-configuration" --region us-east-1
aws s3api head-object --bucket hypershift-ci-2-oidc --key "<infraID>/.well-known/openid-configuration" --region us-east-1
aws s3api head-object --bucket hypershift-ci-3-oidc --key "<infraID>/.well-known/openid-configuration" --region us-east-1
```
If any returns 200 → **FAIL** (cluster still active). If all return 404/error → **PASS**.

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
# Compare a known existing OIDC key and a known-absent key. Both commands must
# have their AWS error code captured; a non-zero exit alone is not "not found".
aws s3api head-object --bucket hypershift-ci-oidc \
  --key "<known-existing-infraID>/.well-known/openid-configuration" \
  --region us-east-1

aws s3api head-object --bucket hypershift-ci-oidc \
  --key "__known-absent-key__" \
  --region us-east-1

Repository: openshift/hypershift

Length of output: 236


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

# Inspect the target file around the cited lines.
file=".claude/skills/triage-leaked-infra/SKILL.md"
wc -l "$file"
sed -n '45,85p' "$file"

Repository: openshift/hypershift

Length of output: 2793


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

file=".claude/skills/triage-leaked-infra/SKILL.md"
nl -ba "$file" | sed -n '58,68p'

Repository: openshift/hypershift

Length of output: 198


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path('.claude/skills/triage-leaked-infra/SKILL.md')
lines = p.read_text().splitlines()
for i in range(58, 69):
    if 1 <= i <= len(lines):
        print(f"{i}: {lines[i-1]}")
PY

Repository: openshift/hypershift

Length of output: 725


Treat only 404 as PASS in the OIDC liveness check. head-object failures like AccessDenied, throttling, wrong-region, or transport errors can mask an active cluster; classify them as UNKNOWN and block deletion unless the bucket returns a real 404.

🤖 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 @.claude/skills/triage-leaked-infra/SKILL.md around lines 61 - 65, Update the
OIDC liveness check instructions for the aws s3api head-object commands so only
an explicit 404 is classified as PASS; classify 200 as FAIL and all other
errors, including AccessDenied, throttling, wrong-region, or transport failures,
as UNKNOWN. Ensure UNKNOWN blocks deletion rather than being treated as evidence
that the object is absent.

Source: Path instructions

Comment on lines +7 to +10
build:
@mkdir -p bin
cd $(REPO_ROOT) && go build -o $(PKG)/bin/$(BINARY) $(PKG)
@echo "Built: bin/$(BINARY)"

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 | 🟠 Major | ⚡ Quick win

Use one absolute artifact directory for build and clean.

Lines 8–10 create ./bin but build to $(REPO_ROOT)/contrib/aws-leaked-resources/bin/cleanleaked; from the repository root, that parent directory does not exist, so make build fails. Line 19 also removes the wrong directory. Define a single root-relative BINDIR and use it consistently.

Proposed fix
 BINARY := cleanleaked
 REPO_ROOT := $(shell git rev-parse --show-toplevel)
 PKG := ./contrib/aws-leaked-resources
+BINDIR := $(REPO_ROOT)/bin
 
 build:
-	`@mkdir` -p bin
-	cd $(REPO_ROOT) && go build -o $(PKG)/bin/$(BINARY) $(PKG)
-	`@echo` "Built: bin/$(BINARY)"
+	`@mkdir` -p $(BINDIR)
+	cd $(REPO_ROOT) && go build -o $(BINDIR)/$(BINARY) $(PKG)
+	`@echo` "Built: $(BINDIR)/$(BINARY)"
 
 clean:
-	rm -rf bin
+	rm -rf $(BINDIR)

Also applies to: 18-19

🤖 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 `@contrib/aws-leaked-resources/Makefile` around lines 7 - 10, Define one
root-relative BINDIR for the aws-leaked-resources artifact directory, then
update the build and clean targets to create, write to, echo, and remove that
same directory. Replace the separate ./bin creation and hard-coded
$(REPO_ROOT)/contrib/aws-leaked-resources/bin references while preserving the
existing binary name.

@jparrill
jparrill force-pushed the aws-clean-leaked-resources branch from ee69d33 to 0ec5cc6 Compare July 14, 2026 16:24

@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: 2

♻️ Duplicate comments (4)
.claude/skills/triage-leaked-infra/SKILL.md (3)

17-19: ⚠️ Potential issue | 🟠 Major

Validate identifiers before interpolating them into shell commands.

User-provided VPC IDs and infraIDs are inserted into AWS CLI snippets without strict format validation or shell-safe variables. Require allow-list validation, assign validated values to variables, and quote every interpolation.

As per path instructions, .claude files are high risk and require security-focused review.

🤖 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 @.claude/skills/triage-leaked-infra/SKILL.md around lines 17 - 19, Validate
the extracted VPC ID and infraID against strict allow-lists before using them in
AWS CLI commands, then assign the validated values to shell variables. Update
the VPC lookup and infraID lookup commands to quote every variable interpolation
and avoid embedding unvalidated input directly in command strings.

Source: Path instructions


61-65: ⚠️ Potential issue | 🟠 Major

Fail closed on OIDC liveness errors.

Only an explicit 404 should be a PASS; 200 must be a FAIL, and AccessDenied, throttling, wrong-region, or transport errors must be UNKNOWN and block deletion. Treating every error as absence can hide an active cluster.

As per path instructions, .claude files are high risk and require security-focused review.

🤖 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 @.claude/skills/triage-leaked-infra/SKILL.md around lines 61 - 65, Update the
OIDC liveness checks in the triage procedure so only an explicit 404 is treated
as PASS and a 200 as FAIL; classify AccessDenied, throttling, wrong-region, and
transport errors as UNKNOWN and block deletion. Revise the documented
command-result handling around the three aws s3api head-object checks to fail
closed rather than treating all errors as absence.

Source: Path instructions


89-96: ⚠️ Potential issue | 🟡 Minor

Keep the shell examples executable.

The pipelines on Line 89 and Line 96 end at inline comments, so these commands cannot be copied and run verbatim. Move the explanatory text outside the commands or implement the intended filters.

As per path instructions, .claude files are high risk and require security-focused review.

🤖 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 @.claude/skills/triage-leaked-infra/SKILL.md around lines 89 - 96, Update the
AWS CLI examples in the triage-leaked-infra instructions so every command
remains executable when copied verbatim. Remove the inline comments after the
pipelines on the endpoint-service and address commands, placing their
explanatory text outside the commands or replacing it with valid filtering
logic; preserve the intended Kubernetes tag filtering and NAT gateway EIP
cross-check guidance.

Source: Path instructions

contrib/aws-leaked-resources/Makefile (1)

7-10: ⚠️ Potential issue | 🟠 Major

Use one artifact directory for build and clean.

The build creates ./bin but writes to $(PKG)/bin/cleanleaked, whose parent is not created from the repository root. make build therefore fails, and clean removes the wrong directory. Define one BINDIR and use it consistently.

Also applies to: 18-19

🤖 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 `@contrib/aws-leaked-resources/Makefile` around lines 7 - 10, Update the
Makefile build and clean targets to define a single BINDIR artifact directory,
create that directory before building, and write the binary there. Replace both
hardcoded or inconsistent bin paths in build and clean with BINDIR so they
operate on the same artifact location.
🤖 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 @.claude/skills/triage-leaked-infra/SKILL.md:
- Line 126: Update the fenced report example in the triage-leaked-infra skill
documentation to specify the markdown language identifier, changing the opening
fence to use markdown while preserving the example content.
- Around line 149-157: Update the verdict rules to prevent any UNKNOWN
deletion-gating check among checks 1–8 from producing SAFE TO DELETE. Restrict
the existing CI-pattern/age exception to cases where all required safety checks
PASS, or explicitly identify only non-gating informational checks that may
remain UNKNOWN; preserve DO NOT DELETE for any FAIL and human review for
unresolved UNKNOWN states.

---

Duplicate comments:
In @.claude/skills/triage-leaked-infra/SKILL.md:
- Around line 17-19: Validate the extracted VPC ID and infraID against strict
allow-lists before using them in AWS CLI commands, then assign the validated
values to shell variables. Update the VPC lookup and infraID lookup commands to
quote every variable interpolation and avoid embedding unvalidated input
directly in command strings.
- Around line 61-65: Update the OIDC liveness checks in the triage procedure so
only an explicit 404 is treated as PASS and a 200 as FAIL; classify
AccessDenied, throttling, wrong-region, and transport errors as UNKNOWN and
block deletion. Revise the documented command-result handling around the three
aws s3api head-object checks to fail closed rather than treating all errors as
absence.
- Around line 89-96: Update the AWS CLI examples in the triage-leaked-infra
instructions so every command remains executable when copied verbatim. Remove
the inline comments after the pipelines on the endpoint-service and address
commands, placing their explanatory text outside the commands or replacing it
with valid filtering logic; preserve the intended Kubernetes tag filtering and
NAT gateway EIP cross-check guidance.

In `@contrib/aws-leaked-resources/Makefile`:
- Around line 7-10: Update the Makefile build and clean targets to define a
single BINDIR artifact directory, create that directory before building, and
write the binary there. Replace both hardcoded or inconsistent bin paths in
build and clean with BINDIR so they operate on the same artifact location.
🪄 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: Repository YAML (base), Central YAML (inherited)

Review profile: CHILL

Plan: Enterprise

Run ID: 4ee96c95-9db3-44f1-81b7-d5776667e6de

📥 Commits

Reviewing files that changed from the base of the PR and between ee69d33 and 0ec5cc6.

📒 Files selected for processing (15)
  • .claude/skills/triage-leaked-infra/SKILL.md
  • contrib/aws-leaked-resources/.gitignore
  • contrib/aws-leaked-resources/Makefile
  • contrib/aws-leaked-resources/README.md
  • contrib/aws-leaked-resources/clients.go
  • contrib/aws-leaked-resources/delete.go
  • contrib/aws-leaked-resources/delete_test.go
  • contrib/aws-leaked-resources/main.go
  • contrib/aws-leaked-resources/prow.go
  • contrib/aws-leaked-resources/prow_test.go
  • contrib/aws-leaked-resources/report.go
  • contrib/aws-leaked-resources/scan.go
  • contrib/aws-leaked-resources/scan_test.go
  • contrib/aws-leaked-resources/tags.go
  • contrib/aws-leaked-resources/types.go
🚧 Files skipped from review as they are similar to previous changes (13)
  • contrib/aws-leaked-resources/.gitignore
  • contrib/aws-leaked-resources/clients.go
  • contrib/aws-leaked-resources/report.go
  • contrib/aws-leaked-resources/README.md
  • contrib/aws-leaked-resources/types.go
  • contrib/aws-leaked-resources/prow.go
  • contrib/aws-leaked-resources/delete.go
  • contrib/aws-leaked-resources/main.go
  • contrib/aws-leaked-resources/tags.go
  • contrib/aws-leaked-resources/prow_test.go
  • contrib/aws-leaked-resources/scan_test.go
  • contrib/aws-leaked-resources/delete_test.go
  • contrib/aws-leaked-resources/scan.go


Present the report in this format:

```

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add a language tag to the fenced report example.

The code fence starting on Line 126 has no language identifier and triggers markdownlint MD040; mark it as ```markdown.

🧰 Tools
🪛 markdownlint-cli2 (0.23.0)

[warning] 126-126: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 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 @.claude/skills/triage-leaked-infra/SKILL.md at line 126, Update the fenced
report example in the triage-leaked-infra skill documentation to specify the
markdown language identifier, changing the opening fence to use markdown while
preserving the example content.

Source: Linters/SAST tools

Comment on lines +149 to +157
## Verdict rules

These are non-negotiable:

- Any **FAIL** → verdict is **DO NOT DELETE** (explain which check failed and why)
- All checks **PASS** and CI pattern matches → **SAFE TO DELETE**
- All checks **PASS** but CI pattern is **UNKNOWN** → **UNCERTAIN** (could be a developer cluster)
- If CI pattern is **PASS** and age/expiration is **PASS** (older than 24h or expired), remaining UNKNOWN checks do not block → **SAFE TO DELETE** (a confirmed CI resource older than 24h with no OIDC, no instances, and no protection tags is leaked)
- If CI pattern is **UNKNOWN** and ANY check is **UNKNOWN** → **UNCERTAIN — REQUIRES HUMAN DECISION**

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not let UNKNOWN safety checks produce SAFE TO DELETE.

Line 156 permits deletion when checks remain UNKNOWN. A failed protection-tag, OIDC, or instance query could therefore be mistaken for an absent resource. Require PASS for all deletion-gating checks (1-8), or explicitly limit this exception to informational checks only.

As per path instructions, .claude files are high risk and require security-focused review.

🤖 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 @.claude/skills/triage-leaked-infra/SKILL.md around lines 149 - 157, Update
the verdict rules to prevent any UNKNOWN deletion-gating check among checks 1–8
from producing SAFE TO DELETE. Restrict the existing CI-pattern/age exception to
cases where all required safety checks PASS, or explicitly identify only
non-gating informational checks that may remain UNKNOWN; preserve DO NOT DELETE
for any FAIL and human review for unresolved UNKNOWN states.

Source: Path instructions

@hypershift-jira-solve-ci

hypershift-jira-solve-ci Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Test Failure Analysis Complete

Job Information

  • Prow Job: codecov/patch (Codecov GitHub check)
  • Build ID: Check run 87145109025 on commit 0ec5cc6
  • PR: #8964CNTRLPLANE-3820: add cleanleaked tool for AWS leaked resource cleanup
  • Branch: aws-clean-leaked-resources

Test Failure Analysis

Error

codecov/patch: 40.83% of diff hit (target 43.37%)
  → 1,023 lines in the diff are missing test coverage

Summary

The codecov/patch check failed because only 40.83% of the 1,729 new executable lines added by this PR are covered by tests, falling 2.54 percentage points short of the 43.37% project target. The PR introduces an entirely new standalone tool under contrib/aws-leaked-resources/ with 8 Go source files. Two files — main.go (253 lines, 0% covered) and report.go (135 lines, 0% covered) — have zero test coverage, and delete.go (41% covered, 372 lines missing) collectively drag overall diff coverage below the threshold. Approximately 44 additional lines of test coverage would be sufficient to pass the check.

Root Cause

The PR adds 2,586 new source lines across 8 Go files and 2,325 test lines across 3 test files. While the test-to-source ratio is healthy (89.9%), the tests concentrate on certain files while leaving others completely untested:

File Patch Coverage Missing Lines Why Uncovered
main.go 0.00% 253 lines CLI entrypoint: cobra command setup, flag parsing, AWS session init — no tests at all
report.go 0.00% 135 lines Report formatting (table, JSON, file output) — pure logic functions but completely untested
delete.go 41.04% 319 missing + 53 partials AWS resource deletion functions — only mock-path happy branches covered; real API call sequences and error-handling paths untested
scan.go 57.90% 171 missing + 34 partials AWS scanning — mock-based tests cover main paths but many AWS API interaction branches and error conditions are not exercised
prow.go 54.54% 43 missing + 2 partials Prow job URL parsing and Route53 hosted zone scanning — some branches untested
tags.go 86.31% 11 missing + 2 partials Tag parsing — mostly covered, a few edge cases missing
types.go 100% 0 Fully covered
clients.go N/A 0 Interface definitions only — no executable lines to cover

The aggregate result: 706 lines hit / 1,729 executable lines = 40.83%, which is 2.54 percentage points below the 43.37% project-wide target that codecov enforces as a patch requirement. The gap is approximately 44 additional lines needing test coverage to cross the threshold.

The two zero-coverage files (main.go and report.go) account for 388 of the 1,023 uncovered lines (38% of the gap). These are the lowest-hanging fruit because they contain pure logic functions (CLI command construction, table formatting, JSON output) that require no AWS mocking to test.

Recommendations
  1. Quickest fix — add tests for report.go (0% → ~50% covers the gap):

    • report.go contains pure formatting functions (printTable, printJSON, writeReport) with no AWS dependencies — straightforward table-driven tests.
    • Covering ~67 of its 135 lines would push overall diff coverage above 43.37%.
  2. Alternative — add basic smoke tests for main.go:

    • Test cobra command tree structure, flag defaults, and flag validation.
    • These tests are easy to write and cover many lines quickly since command setup is linear code.
  3. For thorough coverage — extend delete_test.go:

    • Add test cases for error-handling branches: deletion retry failures, partial failures, concurrent deletion edge cases.
    • Even 44 more lines hit across any combination of uncovered files would cross the threshold.
  4. Pre-push verification:

    make test           # Run tests with coverage
    make fmt verify     # Ensure formatting is correct
Evidence
Evidence Detail
Codecov check result 40.83% of diff hit (target 43.37%) — check conclusion: FAILURE
Lines missing coverage 1,023 lines across 6 files in contrib/aws-leaked-resources/
Largest coverage gaps main.go at 0% (253 lines) and report.go at 0% (135 lines) — 388 of 1,023 uncovered
Coverage shortfall 2.54 percentage points below target; ~44 additional covered lines needed to pass
Total new source lines 2,586 lines across 8 .go files
Total new test lines 2,325 lines across 3 _test.go files
Test files present delete_test.go (810 lines), scan_test.go (1,227 lines), prow_test.go (288 lines)
Files with no test file main.go, report.go, clients.go (interface-only), tags.go (partially covered via other tests)
Base commit 3e6ecb5 (main) — project coverage 43.37%
Head commit 0ec5cc6 — project coverage 44.05% (+0.68% overall, but patch threshold not met)
Codecov bot comment PR #8964 comment confirms per-file breakdown

@sdminonne

Copy link
Copy Markdown
Contributor

/lgtm

@openshift-ci openshift-ci Bot added the lgtm Indicates that a PR is ready to be merged. label Jul 15, 2026
@openshift-merge-bot

Copy link
Copy Markdown
Contributor

Pipeline controller notification

No second-stage tests were triggered for this PR.

This can happen when:

  • The changed files don't match any pipeline_run_if_changed patterns
  • All files match pipeline_skip_if_only_changed patterns
  • No pipeline-controlled jobs are defined for the main branch

Use /test ? to see all available tests.

@jparrill
jparrill force-pushed the aws-clean-leaked-resources branch from 0ec5cc6 to 7942e1d Compare July 15, 2026 14:25
@openshift-ci openshift-ci Bot removed the lgtm Indicates that a PR is ready to be merged. label Jul 15, 2026

@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: 9

🤖 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 @.claude/skills/triage-leaked-infra/SKILL.md:
- Around line 96-97: Update the Elastic IP inventory command in the triage
workflow to scope results to the target VPC by filtering through its NAT gateway
or network-interface associations, rather than listing all account-wide
VPC-domain addresses. Preserve the existing cross-check with NAT gateway EIPs
and report only allocations associated with the target VPC.
- Line 3: Restrict the SKILL.md description trigger to AWS VPC IDs and
HyperShift infraIDs, removing the broad “any AWS resource” wording. Keep
activation for cleanleaked output, VPC safety checks, and LEAKED or UNCERTAIN
infra verdicts, without implying support for IAM roles, hosted zones, or
unrelated resources.
- Around line 18-19: Update the VPC discovery commands for both the VPC ID →
infraID and infraID → VPC ID flows to explicitly use AWS region us-east-1, and
add the same region option to the protection-tag query used by the safety
checks. Ensure every related AWS CLI command in this discovery and protection
flow is pinned to us-east-1 rather than relying on the operator’s default
region.

In `@contrib/aws-leaked-resources/delete.go`:
- Around line 47-50: Update the deletion flow around the infra-set loop and
final failure-counter check to return ctx.Err() when cancellation occurs, rather
than breaking and returning nil. Check ctx.Err() immediately before the final
failure evaluation, while preserving the existing cancellation message and
failure handling for non-canceled runs.
- Around line 77-94: Update the cleanup logic surrounding deleteOIDCProvider and
deleteIAMRole to track failed deletion counts, incrementing the appropriate
counter whenever either operation returns an error. Include those OIDC-provider
and IAM-role failures in the final result returned by the enclosing cleanup
method, while preserving the existing logging and success counters.
- Around line 395-403: Update collectNATGatewayEIPs and the related cleanup
paths at the referenced symbols to stop discarding discovery, detach, revoke,
release, and IAM deletion errors. Propagate or aggregate each failure through
deleteVPC and deleteIAMRole, wrapping returned errors with operation-specific
context while preserving successful cleanup behavior and ensuring EIP and
instance-profile failures reach the caller.
- Around line 594-627: Update deleteHostedZone to avoid accumulating all record
deletions into one ChangeResourceRecordSets request. Build and submit quota-safe
batches while iterating records, keeping each batch within both 1,000
ResourceRecord elements and 32,000 total value characters, and reuse the
existing retryDelete behavior for each batch. Preserve skipping NS and SOA
records and ensure the final partial batch is deleted.
- Around line 126-130: Update retryDelete to accept the caller’s context and
pass that ctx to retryDeleteWithCtx instead of creating context.Background(),
preserving cancellation during retry delays and attempts.
- Around line 296-300: Update the retryDelete callbacks in DeleteVpcEndpoints
and DeleteVpcEndpointServiceConfigurations to inspect each EC2 response’s
Unsuccessful entries and return an error containing the failed resource IDs when
any exist, even if the request error is nil. Preserve transport-error handling
and allow retryDelete to retry these partial failures.
🪄 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: Repository YAML (base), Central YAML (inherited)

Review profile: CHILL

Plan: Enterprise

Run ID: a5dff954-99d6-4b89-8712-733f6057c1cf

📥 Commits

Reviewing files that changed from the base of the PR and between 0ec5cc6 and 7942e1d.

📒 Files selected for processing (16)
  • .claude/skills/triage-leaked-infra/SKILL.md
  • codecov.yml
  • contrib/aws-leaked-resources/.gitignore
  • contrib/aws-leaked-resources/Makefile
  • contrib/aws-leaked-resources/README.md
  • contrib/aws-leaked-resources/clients.go
  • contrib/aws-leaked-resources/delete.go
  • contrib/aws-leaked-resources/delete_test.go
  • contrib/aws-leaked-resources/main.go
  • contrib/aws-leaked-resources/prow.go
  • contrib/aws-leaked-resources/prow_test.go
  • contrib/aws-leaked-resources/report.go
  • contrib/aws-leaked-resources/scan.go
  • contrib/aws-leaked-resources/scan_test.go
  • contrib/aws-leaked-resources/tags.go
  • contrib/aws-leaked-resources/types.go
🚧 Files skipped from review as they are similar to previous changes (11)
  • contrib/aws-leaked-resources/clients.go
  • contrib/aws-leaked-resources/prow.go
  • contrib/aws-leaked-resources/prow_test.go
  • contrib/aws-leaked-resources/README.md
  • contrib/aws-leaked-resources/tags.go
  • contrib/aws-leaked-resources/types.go
  • contrib/aws-leaked-resources/main.go
  • contrib/aws-leaked-resources/delete_test.go
  • contrib/aws-leaked-resources/report.go
  • contrib/aws-leaked-resources/scan.go
  • contrib/aws-leaked-resources/scan_test.go

Comment thread .claude/skills/triage-leaked-infra/SKILL.md
Comment on lines +18 to +19
- VPC ID → `aws ec2 describe-vpcs --vpc-ids <VPC> --query 'Vpcs[0].Tags'` → extract infraID from `kubernetes.io/cluster/<infraID>` tag or `Name` tag (strip `-vpc` suffix)
- infraID → `aws ec2 describe-vpcs --filters "Name=tag:Name,Values=<infraID>-vpc"` → get VPC ID

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

Pin VPC discovery and protection checks to us-east-1.

These commands omit --region us-east-1 despite the instruction on Line 23. If the operator’s AWS CLI default is another region, discovery can fail or select a different VPC before safety checks run. Add the explicit region to both derivation commands and the protection-tag query.

Also applies to: 26-28

🤖 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 @.claude/skills/triage-leaked-infra/SKILL.md around lines 18 - 19, Update the
VPC discovery commands for both the VPC ID → infraID and infraID → VPC ID flows
to explicitly use AWS region us-east-1, and add the same region option to the
protection-tag query used by the safety checks. Ensure every related AWS CLI
command in this discovery and protection flow is pinned to us-east-1 rather than
relying on the operator’s default region.

Comment on lines +96 to +97
aws ec2 describe-addresses --region us-east-1 --filters "Name=domain,Values=vpc" --output text | # cross-check with NAT gateway EIPs
```

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

Scope Elastic IP inventory to the target VPC.

describe-addresses with only domain=vpc returns account-wide EIPs, so unrelated allocations can be attributed to <VPC>. Filter through the target VPC’s NAT gateway or network-interface associations before reporting them.

🤖 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 @.claude/skills/triage-leaked-infra/SKILL.md around lines 96 - 97, Update the
Elastic IP inventory command in the triage workflow to scope results to the
target VPC by filtering through its NAT gateway or network-interface
associations, rather than listing all account-wide VPC-domain addresses.
Preserve the existing cross-check with NAT gateway EIPs and report only
allocations associated with the target VPC.

Comment on lines +47 to +50
if err := ctx.Err(); err != nil {
fmt.Fprintf(os.Stderr, "\nCanceled.\n")
break
}

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 | 🟠 Major | ⚡ Quick win

Return cancellation instead of reporting success.

Cancellation between infra sets breaks the loop with zero failures, then returns nil. Check ctx.Err() before the final failure-counter check.

Proposed fix
+	if err := ctx.Err(); err != nil {
+		return fmt.Errorf("deletion canceled: %w", err)
+	}
 	if failedVPCs > 0 || failedZones > 0 {

As per path instructions, “context.Context for cancellation and timeouts.”

Also applies to: 119-123

🤖 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 `@contrib/aws-leaked-resources/delete.go` around lines 47 - 50, Update the
deletion flow around the infra-set loop and final failure-counter check to
return ctx.Err() when cancellation occurs, rather than breaking and returning
nil. Check ctx.Err() immediately before the final failure evaluation, while
preserving the existing cancellation message and failure handling for
non-canceled runs.

Source: Path instructions

Comment on lines +77 to +94
for _, oidcARN := range infra.OIDCProviders {
if err := d.deleteOIDCProvider(ctx, oidcARN); err != nil {
fmt.Fprintf(os.Stderr, " FAILED OIDC provider %s: %v\n", oidcARN, err)
} else {
fmt.Fprintf(os.Stderr, " Deleted OIDC provider: %s\n", oidcARN)
deletedOIDC++
}
}

for _, roleName := range infra.IAMRoles {
if err := ctx.Err(); err != nil {
break
}
if err := d.deleteIAMRole(ctx, roleName); err != nil {
fmt.Fprintf(os.Stderr, " FAILED IAM role %s: %v\n", roleName, err)
} else {
fmt.Fprintf(os.Stderr, " Deleted IAM role: %s\n", roleName)
}

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 | 🟠 Major | ⚡ Quick win

Include OIDC and IAM failures in the final result.

Failed OIDC-provider and IAM-role deletions are logged but never affect the returned error. Track their failure counts so automation cannot report successful cleanup while these resources remain.

Also applies to: 108-120

🤖 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 `@contrib/aws-leaked-resources/delete.go` around lines 77 - 94, Update the
cleanup logic surrounding deleteOIDCProvider and deleteIAMRole to track failed
deletion counts, incrementing the appropriate counter whenever either operation
returns an error. Include those OIDC-provider and IAM-role failures in the final
result returned by the enclosing cleanup method, while preserving the existing
logging and success counters.

Comment thread contrib/aws-leaked-resources/delete.go Outdated
Comment on lines +296 to +300
if len(serviceIDs) > 0 {
if err := retryDelete(fmt.Sprintf("VPCE services (%d)", len(serviceIDs)), func() error {
_, err := d.EC2.DeleteVpcEndpointServiceConfigurations(ctx, &ec2.DeleteVpcEndpointServiceConfigurationsInput{ServiceIds: serviceIDs})
return err
}); err != 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 | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -C4 'DeleteVpcEndpoints|DeleteVpcEndpointServiceConfigurations|Unsuccessful' contrib/aws-leaked-resources

Repository: openshift/hypershift

Length of output: 2058


🌐 Web query:

AWS EC2 DeleteVpcEndpoints Unsuccessful response field DeleteVpcEndpointServiceConfigurations Unsuccessful API reference

💡 Result:

Both the AWS DeleteVpcEndpoints and DeleteVpcEndpointServiceConfigurations API operations include an Unsuccessful response field [1][2]. The Unsuccessful field is a list containing information about resources that were not successfully deleted during the batch request [2][3]. Each entry in this list typically includes a ResourceId, identifying the specific resource that failed to process, and an Error structure containing a Code and Message detailing why the operation failed for that specific item [2]. In AWS SDKs and CLI tools, this field allows developers to handle partial failures [4][5][2]. For example, in many SDKs, you can use methods such as hasUnsuccessful to determine if the service returned any failure information in the response, allowing you to distinguish between an empty list (successful batch) and a situation where the service returned no data for this field [6][7]. If a command is partially successful or unsuccessful, the Unsuccessful list is populated; if the entire operation is successful, the returned list is empty [4][3].

Citations:


Treat partial EC2 batch failures as errors.
DeleteVpcEndpoints and DeleteVpcEndpointServiceConfigurations can return per-item failures in Unsuccessful even when the request error is nil, so returning only the transport error treats partial deletions as success. Inspect the response and retry or surface the failed IDs.

🤖 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 `@contrib/aws-leaked-resources/delete.go` around lines 296 - 300, Update the
retryDelete callbacks in DeleteVpcEndpoints and
DeleteVpcEndpointServiceConfigurations to inspect each EC2 response’s
Unsuccessful entries and return an error containing the failed resource IDs when
any exist, even if the request error is nil. Preserve transport-error handling
and allow retryDelete to retry these partial failures.

Comment on lines +395 to +403
func (d *Deleter) collectNATGatewayEIPs(ctx context.Context, vpcID string) []string {
out, err := d.EC2.DescribeNatGateways(ctx, &ec2.DescribeNatGatewaysInput{
Filter: []ec2types.Filter{
{Name: aws.String("vpc-id"), Values: []string{vpcID}},
},
})
if err != nil {
return 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 | 🟠 Major | 🏗️ Heavy lift

Propagate dependency-cleanup errors instead of discarding them.

These paths suppress discovery, detach, revoke, release, and IAM deletion errors. Most critically, a failed EIP lookup/release can leave a billable allocation after the VPC disappears, and a failed instance-profile deletion can leave an orphan after the role is successfully deleted.

Return or aggregate these errors through deleteVPC and deleteIAMRole rather than only logging them.

As per coding guidelines, “Always check errors — don’t ignore them” and “Wrap errors with context when rethrowing.”

Also applies to: 419-431, 450-455, 488-491, 516-521, 568-575, 819-857

🤖 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 `@contrib/aws-leaked-resources/delete.go` around lines 395 - 403, Update
collectNATGatewayEIPs and the related cleanup paths at the referenced symbols to
stop discarding discovery, detach, revoke, release, and IAM deletion errors.
Propagate or aggregate each failure through deleteVPC and deleteIAMRole,
wrapping returned errors with operation-specific context while preserving
successful cleanup behavior and ensuring EIP and instance-profile failures reach
the caller.

Source: Coding guidelines

Comment on lines +594 to +627
func (d *Deleter) deleteHostedZone(ctx context.Context, zoneID, zoneName string) error {
var changes []route53types.Change
input := &route53.ListResourceRecordSetsInput{
HostedZoneId: aws.String(zoneID),
}
for {
rrOut, err := d.Route53.ListResourceRecordSets(ctx, input)
if err != nil {
return err
}

for _, rr := range rrOut.ResourceRecordSets {
if rr.Type == route53types.RRTypeNs || rr.Type == route53types.RRTypeSoa {
continue
}
changes = append(changes, route53types.Change{
Action: route53types.ChangeActionDelete, ResourceRecordSet: &rr,
})
}

if !rrOut.IsTruncated {
break
}
input.StartRecordName = rrOut.NextRecordName
input.StartRecordType = rrOut.NextRecordType
input.StartRecordIdentifier = rrOut.NextRecordIdentifier
}

if len(changes) > 0 {
if err := retryDelete("records in "+zoneName, func() error {
_, err := d.Route53.ChangeResourceRecordSets(ctx, &route53.ChangeResourceRecordSetsInput{
HostedZoneId: aws.String(zoneID),
ChangeBatch: &route53types.ChangeBatch{Changes: changes},
})

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 | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -C4 'ChangeResourceRecordSets|ResourceRecordSets|1000|32000' contrib/aws-leaked-resources

Repository: openshift/hypershift

Length of output: 2058


🌐 Web query:

AWS Route53 ChangeResourceRecordSets 1000 ResourceRecord elements 32000 characters limit

💡 Result:

When using the AWS Route53 ChangeResourceRecordSets API, your request is subject to two specific constraints regarding the size and number of resource records [1]: 1. Maximum ResourceRecord Elements: A single ChangeResourceRecordSets request cannot contain more than 1,000 ResourceRecord elements in total [1]. This limit includes all alias records present in the request [1]. 2. Maximum Character Count: The total length of all Value elements combined within a single request must not exceed 32,000 characters [1]. Additionally, it is important to note that for individual DNS records, the Value string itself cannot exceed 4,000 characters [2][3]. If your request exceeds these thresholds, the API will reject the operation; you should structure your updates to remain within these limits by batching your changes accordingly [1].

Citations:


Split Route53 deletions into quota-safe batches. A single ChangeResourceRecordSets request is capped at 1,000 ResourceRecord elements and 32,000 total value characters, so accumulating an entire hosted zone here can fail for large or multi-value zones. Batch by both limits before deleting the zone.

🤖 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 `@contrib/aws-leaked-resources/delete.go` around lines 594 - 627, Update
deleteHostedZone to avoid accumulating all record deletions into one
ChangeResourceRecordSets request. Build and submit quota-safe batches while
iterating records, keeping each batch within both 1,000 ResourceRecord elements
and 32,000 total value characters, and reuse the existing retryDelete behavior
for each batch. Preserve skipping NS and SOA records and ensure the final
partial batch is deleted.

@jparrill

Copy link
Copy Markdown
Contributor Author

/acknowledge-critical-fixes-only

@csrwng csrwng 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.

Nice tool — safety gates and cascade ordering look solid. A few nits below, nothing blocking.

Comment thread contrib/aws-leaked-resources/delete.go Outdated
// retryDelete executes fn up to maxRetries times with retryDelay between attempts.
// Respects context cancellation between retries so Ctrl+C works immediately.
func retryDelete(desc string, fn func() error) error {
return retryDeleteWithCtx(context.Background(), desc, fn)

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.

nit: retryDelete wraps retryDeleteWithCtx with context.Background(), so the retry sleep (line 143) won't wake up on Ctrl+C — it blocks for the full 2s delay. The actual AWS API calls inside fn() do use the real ctx from their closure, so cancellation eventually propagates, but the gap contradicts the stated "Ctrl+C interrupts immediately during retries" behavior.

All callers in deleteVPC and its sub-methods (deleteLoadBalancers, deleteNATGateways, etc.) use retryDelete instead of retryDeleteWithCtx with the proper context. Consider removing the retryDelete wrapper and passing ctx through everywhere.

Suggested change
return retryDeleteWithCtx(context.Background(), desc, fn)
func retryDelete(ctx context.Context, desc string, fn func() error) error {
return retryDeleteWithCtx(ctx, desc, fn)
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done — removed the retryDelete wrapper entirely. The function now takes ctx directly, and all 18 callers updated. Ctrl+C interrupts retry sleeps immediately.

Comment thread contrib/aws-leaked-resources/scan.go Outdated
return "", ""
}

func (s *Scanner) s3DocExists(ctx context.Context, bucket, _, infraID string) bool {

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.

nit: the region parameter is named _ and unused — the S3 client is already initialized with the config region. Dead parameter.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done — removed the unused region parameter from s3DocExists and updated both callers.

Comment thread contrib/aws-leaked-resources/types.go Outdated
const (
VerdictProtected Verdict = "PROTECTED"
VerdictTooYoung Verdict = "TOO_YOUNG"
VerdictUntagged Verdict = "UNTAGGED"

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.

nit: VerdictUntagged is defined here but no code path in scan.go ever assigns it. It's referenced in report sorting/summary output, but never produced. Dead code — either remove it or add the gate that produces it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done — removed VerdictUntagged from types.go, the sort order in report.go, and the summary line in PrintSummary.

@jparrill

Copy link
Copy Markdown
Contributor Author

/acknowledge-critical-fixes-only

Add a standalone Go tool under contrib/aws-leaked-resources/ that scans
for and optionally deletes leaked AWS infrastructure from CI e2e test
runs. The tool identifies VPCs, hosted zones, OIDC providers, and IAM
roles left behind by failed or interrupted HyperShift CI jobs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Juan Manuel Parrilla Madrid <jparrill@redhat.com>
@jparrill
jparrill force-pushed the aws-clean-leaked-resources branch from 7942e1d to 40eb21c Compare July 16, 2026 10:22
@openshift-ci

openshift-ci Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

@jparrill: all tests passed!

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

@csrwng

csrwng commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

/lgtm

@openshift-ci openshift-ci Bot added the lgtm Indicates that a PR is ready to be merged. label Jul 16, 2026
@openshift-merge-bot

Copy link
Copy Markdown
Contributor

Pipeline controller notification

No second-stage tests were triggered for this PR.

This can happen when:

  • The changed files don't match any pipeline_run_if_changed patterns
  • All files match pipeline_skip_if_only_changed patterns
  • No pipeline-controlled jobs are defined for the main branch

Use /test ? to see all available tests.

@jparrill

Copy link
Copy Markdown
Contributor Author

/verified by @jparrill

The AWS Cleanup (the right one) was done via this utility.

@openshift-ci-robot openshift-ci-robot added the verified Signifies that the PR passed pre-merge verification criteria label Jul 16, 2026
@openshift-ci-robot

Copy link
Copy Markdown

@jparrill: This PR has been marked as verified by @jparrill.

Details

In response to this:

/verified by @jparrill

The AWS Cleanup (the right one) was done via this utility.

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@jparrill

Copy link
Copy Markdown
Contributor Author

/label acknowledge-critical-fixes-only

@openshift-ci openshift-ci Bot added the acknowledge-critical-fixes-only Indicates if the issuer of the label is OK with the policy. label Jul 16, 2026
@openshift-merge-bot
openshift-merge-bot Bot merged commit 0121785 into openshift:main Jul 16, 2026
31 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

acknowledge-critical-fixes-only Indicates if the issuer of the label is OK with the policy. approved Indicates a PR has been approved by an approver from all required OWNERS files. area/ai Indicates the PR includes changes related to AI - Claude agents, Cursor rules, etc. jira/valid-reference Indicates that this PR references a valid Jira ticket of any type. lgtm Indicates that a PR is ready to be merged. verified Signifies that the PR passed pre-merge verification criteria

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants