Skip to content

OCPBUGS-98384: fix bastion cleanup KeyPair leak by capturing infraID/region eagerly - #8982

Merged
openshift-merge-bot[bot] merged 1 commit into
openshift:mainfrom
bryan-cox:OCPBUGS-98384
Jul 20, 2026
Merged

OCPBUGS-98384: fix bastion cleanup KeyPair leak by capturing infraID/region eagerly#8982
openshift-merge-bot[bot] merged 1 commit into
openshift:mainfrom
bryan-cox:OCPBUGS-98384

Conversation

@bryan-cox

@bryan-cox bryan-cox commented Jul 10, 2026

Copy link
Copy Markdown
Member

What this PR does / why we need it:

PR #8309 (merged May 11, 2026) refactored test/e2e/util/dump/journals.go to reduce cyclomatic complexity, changing bastion cleanup from defer to t.Cleanup(). This changed the execution order so that bastion destroy now runs after the HostedCluster is already deleted from Kubernetes.

Since DestroyBastionOpts was constructed with Name/Namespace, the destroy code tried to look up the already-deleted HC to resolve infraID and region, failed with a "not found" error, and the error was silently swallowed by t.Logf. This left EC2 bastion instances, security groups, and KeyPairs orphaned in AWS on every e2e test run. The AWS KeyPair limit (5000) was hit on June 17, 2026.

Fix: Capture hc.Spec.InfraID and hc.Spec.Platform.AWS.Region at bastion creation time and pass them directly to DestroyBastionOpts, bypassing the HC lookup path entirely. Also use context.Background() in the cleanup closure to avoid the parent context being cancelled before cleanup runs.

Which issue(s) this PR fixes:

Fixes OCPBUGS-98384

Special notes for your reviewer:

DestroyBastionOpts.Run() (cmd/bastion/aws/destroy.go:87-112) supports two ways to resolve infraID/region: by looking up the HC via Name/Namespace, or by accepting InfraID/Region directly. The old test code used the lookup path, which fails when the HC is already deleted. This fix switches to passing InfraID/Region directly so no Kubernetes call is needed during cleanup.

Reported by Dan Mace.

Checklist:

  • Subject and description added to both, commit and PR.
  • Relevant issues have been referenced.
  • This change includes docs.
  • This change includes unit tests.

Summary by CodeRabbit

  • Bug Fixes
    • Improved cleanup of temporary bastion infrastructure during end-to-end test runs.
    • Cleanup now uses the correct infrastructure and region details and allows additional time for destruction to complete.

@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 10, 2026
@openshift-ci-robot

Copy link
Copy Markdown

@bryan-cox: This pull request references Jira Issue OCPBUGS-98384, which is valid. The bug has been moved to the POST state.

3 validation(s) were run on this bug
  • bug is open, matching expected state (open)
  • bug target version (5.0.0) matches configured target version for branch (5.0.0)
  • bug is in the state ASSIGNED, which is one of the valid states (NEW, ASSIGNED, POST)

The bug has been updated to refer to the pull request using the external bug tracker.

Details

In response to this:

What this PR does / why we need it:

PR #8309 (merged May 11, 2026) refactored test/e2e/util/dump/journals.go to reduce cyclomatic complexity, changing bastion cleanup from defer to t.Cleanup(). This changed the execution order so that bastion destroy now runs after the HostedCluster is already deleted from Kubernetes.

Since DestroyBastionOpts was constructed with Name/Namespace, the destroy code tried to look up the already-deleted HC to resolve infraID and region, failed with a "not found" error, and the error was silently swallowed by t.Logf. This left EC2 bastion instances, security groups, and KeyPairs orphaned in AWS on every e2e test run. The AWS KeyPair limit (5000) was hit on June 17, 2026.

Fix: Capture hc.Spec.InfraID and hc.Spec.Platform.AWS.Region at bastion creation time and pass them directly to DestroyBastionOpts, bypassing the HC lookup path entirely. Also use context.Background() in the cleanup closure to avoid the parent context being cancelled before cleanup runs.

Which issue(s) this PR fixes:

Fixes OCPBUGS-98384

Special notes for your reviewer:

DestroyBastionOpts.Run() (in cmd/bastion/aws/destroy.go:87-112) has two code paths:

  • Path A (lines 91-108): If Name is set, looks up the HC to extract infraID/region — this is the path the old code used, and it fails when the HC is already deleted.
  • Path B (lines 109-112): If Name is empty and InfraID/Region are set directly — no Kubernetes call needed. This is the path the fix uses.

Reported by Dan Mace.

Checklist:

  • Subject and description added to both, commit and PR.
  • Relevant issues have been referenced.
  • This change includes docs.
  • This change includes unit tests.

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.

@openshift-ci-robot openshift-ci-robot added the jira/valid-bug Indicates that a referenced Jira bug is valid for the branch this PR is targeting. label Jul 10, 2026
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

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

Review profile: CHILL

Plan: Enterprise

Run ID: c6ce8a36-b224-4913-b9a5-2b6b167a2403

📥 Commits

Reviewing files that changed from the base of the PR and between 25ab781 and bc4e87a.

📒 Files selected for processing (1)
  • test/e2e/util/dump/journals.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/e2e/util/dump/journals.go

📝 Walkthrough

Walkthrough

The setupBastion cleanup callback now derives the bastion infrastructure ID and region from hc.Spec. It passes these values through bastionaws.DestroyBastionOpts instead of the hosted cluster namespace and name, and executes destruction with context.Background() and a five-minute timeout.

Suggested reviewers: nirshal, sdminonne

🚥 Pre-merge checks | ✅ 11
✅ Passed checks (11 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main fix: bastion cleanup now captures infraID and region eagerly to prevent KeyPair leakage.
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.
Stable And Deterministic Test Names ✅ Passed The only changed file is a helper utility, and it contains no Ginkgo It/Describe/Context/When titles at all.
Test Structure And Quality ✅ Passed No Ginkgo spec changes; the helper cleanup now uses explicit timeout/background and direct infraID/region, matching repo cleanup patterns.
Topology-Aware Scheduling Compatibility ✅ Passed Only test/e2e bastion cleanup logic changed; no manifests, controllers, or scheduling constraints were added.
Ipv6 And Disconnected Network Test Compatibility ✅ Passed The PR only changes bastion cleanup in a helper; it adds no new Ginkgo tests and introduces no IPv4-only or external connectivity assumptions.
No-Weak-Crypto ✅ Passed Touched code only changes bastion cleanup context/options; no MD5/SHA1/DES/RC4/3DES/Blowfish/ECB, custom crypto, or secret comparisons found.
Container-Privileges ✅ Passed Only test/e2e/util/dump/journals.go changed; no container/K8s manifests or privilege settings were added.
No-Sensitive-Data-In-Logs ✅ Passed The change only passes infraID/region into cleanup and uses existing error logging; no passwords, tokens, PII, hostnames, or customer data are logged.
✨ 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 added area/testing Indicates the PR includes changes for e2e testing and removed do-not-merge/needs-area labels Jul 10, 2026
@openshift-ci
openshift-ci Bot requested review from Nirshal and sdminonne July 10, 2026 11:56
@openshift-ci

openshift-ci Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: bryan-cox

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 10, 2026
@openshift-ci-robot

Copy link
Copy Markdown

@bryan-cox: This pull request references Jira Issue OCPBUGS-98384, which is valid.

3 validation(s) were run on this bug
  • bug is open, matching expected state (open)
  • bug target version (5.0.0) matches configured target version for branch (5.0.0)
  • bug is in the state POST, which is one of the valid states (NEW, ASSIGNED, POST)
Details

In response to this:

What this PR does / why we need it:

PR #8309 (merged May 11, 2026) refactored test/e2e/util/dump/journals.go to reduce cyclomatic complexity, changing bastion cleanup from defer to t.Cleanup(). This changed the execution order so that bastion destroy now runs after the HostedCluster is already deleted from Kubernetes.

Since DestroyBastionOpts was constructed with Name/Namespace, the destroy code tried to look up the already-deleted HC to resolve infraID and region, failed with a "not found" error, and the error was silently swallowed by t.Logf. This left EC2 bastion instances, security groups, and KeyPairs orphaned in AWS on every e2e test run. The AWS KeyPair limit (5000) was hit on June 17, 2026.

Fix: Capture hc.Spec.InfraID and hc.Spec.Platform.AWS.Region at bastion creation time and pass them directly to DestroyBastionOpts, bypassing the HC lookup path entirely. Also use context.Background() in the cleanup closure to avoid the parent context being cancelled before cleanup runs.

Which issue(s) this PR fixes:

Fixes OCPBUGS-98384

Special notes for your reviewer:

DestroyBastionOpts.Run() (in cmd/bastion/aws/destroy.go:87-112) has two code paths:

  • Path A (lines 91-108): If Name is set, looks up the HC to extract infraID/region — this is the path the old code used, and it fails when the HC is already deleted.
  • Path B (lines 109-112): If Name is empty and InfraID/Region are set directly — no Kubernetes call needed. This is the path the fix uses.

Reported by Dan Mace.

Checklist:

  • Subject and description added to both, commit and PR.
  • Relevant issues have been referenced.
  • This change includes docs.
  • This change includes unit tests.

Summary by CodeRabbit

  • Bug Fixes
  • Improved bastion cleanup reliability during end-to-end test teardown.
  • Cleanup now uses the infrastructure’s configured identifier and region, helping ensure the correct bastion resources are removed.
  • Teardown can complete independently of the original request context.

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.

@bryan-cox

Copy link
Copy Markdown
Member Author

/test e2e-aws

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@test/e2e/util/dump/journals.go`:
- Around line 166-175: Bound the bastion cleanup context instead of passing
context.Background() directly: create a child context with a fixed timeout
inside the t.Cleanup callback, defer its cancellation, and pass it to
destroyBastion.Run. Preserve independence from the parent test context while
ensuring cleanup cannot retry indefinitely.
🪄 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: 2a700cb1-f11c-4537-aab8-40bf7d50bf98

📥 Commits

Reviewing files that changed from the base of the PR and between e708f6f and 25ab781.

📒 Files selected for processing (1)
  • test/e2e/util/dump/journals.go

Comment thread test/e2e/util/dump/journals.go
@codecov

codecov Bot commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 43.79%. Comparing base (e708f6f) to head (bc4e87a).
⚠️ Report is 96 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #8982      +/-   ##
==========================================
+ Coverage   43.67%   43.79%   +0.11%     
==========================================
  Files         771      772       +1     
  Lines       95840    96037     +197     
==========================================
+ Hits        41862    42061     +199     
+ Misses      51067    51061       -6     
- Partials     2911     2915       +4     

see 20 files with indirect coverage changes

Flag Coverage Δ
cmd-support 37.43% <ø> (+0.20%) ⬆️
cpo-hostedcontrolplane 45.91% <ø> (ø)
cpo-other 45.11% <ø> (ø)
hypershift-operator 54.07% <ø> (+0.15%) ⬆️
other 32.08% <ø> (ø)

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.

@cwbotbot

cwbotbot commented Jul 10, 2026

Copy link
Copy Markdown

Test Results

e2e-aws

e2e-aks

@bryan-cox

Copy link
Copy Markdown
Member Author

/retest

…Pair leak

The refactor in PR openshift#8309 changed bastion cleanup from defer to
t.Cleanup(), which runs after the HostedCluster is already deleted.
Since DestroyBastionOpts used Name/Namespace to look up the HC for
infraID/region, the lookup fails silently and the bastion EC2
instance, security group, and KeyPair are leaked on every test run.

Capture infraID and region at bastion creation time and pass them
directly to DestroyBastionOpts. Also use context.Background() in
the cleanup closure to avoid context cancellation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@bryan-cox

Copy link
Copy Markdown
Member Author

/test e2e-aws

1 similar comment
@bryan-cox

Copy link
Copy Markdown
Member Author

/test e2e-aws

@bryan-cox

Copy link
Copy Markdown
Member Author

/retest

@hypershift-jira-solve-ci

Copy link
Copy Markdown
Contributor

Test Failure Analysis Complete

Job Information

Test Failure Analysis

Error

TestKarpenterUpgradeControlPlane/ValidateHostedCluster/EnsureNoCrashingPods (0.05s)
    util.go:829: Container etcd in pod etcd-0 has a restartCount > 0 (1)

Summary

The sole failure is TestKarpenterUpgradeControlPlane/ValidateHostedCluster/EnsureNoCrashingPods, which detected that the etcd container in pod etcd-0 had restartCount=1 after a control plane upgrade from a previous release to the latest. The EnsureNoCrashingPods check has zero restart tolerance for etcd (it is not in the podCrashTolerations allowlist), so a single restart causes a hard t.Errorf failure. This is a pre-existing flake unrelated to PR #8982, which only modifies test/e2e/util/dump/journals.go (bastion cleanup logic). The 13 other tests that ran EnsureNoCrashingPods all passed, including the non-Karpenter TestUpgradeControlPlane which performs the same upgrade flow. The etcd restart is a transient artifact of the HA control plane rollout during the Karpenter upgrade test's specific timing window.

Root Cause

The TestKarpenterUpgradeControlPlane test creates a HighlyAvailable HostedCluster with the previous OCP release, then upgrades it to the latest release. During this control plane upgrade, the etcd StatefulSet pods are rolled out sequentially. The etcd-0 pod experienced exactly 1 restart (restartCount=1) during/after the upgrade rollout.

After the test function completes, the framework's ValidateHostedCluster hook runs EnsureNoCrashingPods, which iterates all pods in the hosted control plane namespace and fails any container with restartCount > crashToleration. The etcd component has no entry in podCrashTolerations (defined in test/e2e/util/util.go:119-150), so its default toleration is 0. A single restart therefore causes a hard test failure.

The non-Karpenter TestUpgradeControlPlane test performs an identical control plane upgrade flow and its EnsureNoCrashingPods check PASSED — indicating etcd restarts during upgrades are not deterministic but rather depend on timing. The non-upgrade TestKarpenter also passed its crash check, confirming this is upgrade-specific.

This failure is completely unrelated to PR #8982. The PR changes only the setupBastion function in test/e2e/util/dump/journals.go, switching from HostedCluster name/namespace lookup (which fails when the HC is already deleted) to passing pre-captured infraID/region directly. This code path is only executed during bastion teardown for journal dumping and has no interaction with the etcd StatefulSet, control plane upgrade, or pod restart detection.

Additionally, the KeyPairLimitExceeded errors visible in the logs (fixture.go:417: Failed to dump machine journals; this is nonfatal: failed to ensure bastion keypair: ... api error KeyPairLimitExceeded: Maximum of 5000 keypairs reached) are explicitly marked as nonfatal and are the very problem PR #8982 aims to fix — leaked keypairs accumulating over time.

Recommendations
  1. Retry / Merge: This failure is a pre-existing flake unrelated to PR OCPBUGS-98384: fix bastion cleanup KeyPair leak by capturing infraID/region eagerly #8982's changes. The PR should be safe to merge. Consider retriggering the job to confirm.

  2. Add etcd crash toleration for upgrade tests: The podCrashTolerations map in test/e2e/util/util.go:119-150 should be updated to allow 1 restart for etcd containers, at least during upgrade tests. During a HighlyAvailable control plane upgrade, a single etcd pod restart is expected behavior as the StatefulSet rolls out new pod versions. Other components already have similar tolerations (e.g., aws-ebs-csi-driver-controller: 1, network-node-identity: 1).

  3. File a tracking issue: Track this flake under a JIRA ticket (similar to how OCPBUGS-30068 is tracked for cloud-credential-operator or OCPBUGS-78539 for dns-operator) to add a proper toleration for etcd during upgrade tests.

Evidence
Evidence Detail
Failing test TestKarpenterUpgradeControlPlane/ValidateHostedCluster/EnsureNoCrashingPods
Failure message Container etcd in pod etcd-0 has a restartCount > 0 (1) (util.go:829)
Test type Control plane upgrade with Karpenter (HA, upgrades from previous → latest release)
etcd crash toleration 0 (not in podCrashTolerations map, default applies)
Non-Karpenter upgrade test TestUpgradeControlPlane/ValidateHostedCluster/EnsureNoCrashingPodsPASSED
Non-upgrade Karpenter test TestKarpenter/ValidateHostedCluster/EnsureNoCrashingPodsPASSED
Other 13 EnsureNoCrashingPods checks All PASSED
PR #8982 files changed test/e2e/util/dump/journals.go only (+8/-3 lines)
PR scope Bastion cleanup: switches from HC name/namespace lookup to pre-captured infraID/region
Total test results 622 tests, 30 skipped, 3 failures (all same test chain)
HostedCluster e2e-clusters-54l5m/karpenter-upgrade-control-plane-zwbn8
KeyPairLimitExceeded (nonfatal) Seen 4 times in logs — this is the bug PR #8982 fixes, unrelated to the test failure

@bryan-cox

Copy link
Copy Markdown
Member Author

/retest

I cleaned up all the leaked keypairs so hopefully the karpenter test can pass now.

@bryan-cox

Copy link
Copy Markdown
Member Author

/verify by e2e

Job: pull-ci-openshift-hypershift-main-e2e-aws/2077722929948266496

Only TestKarpenter and TestKarpenterUpgradeControlPlane exercise the bastion path (PublicOnly=false). All other tests use public IPs and skip bastion entirely.

TestKarpenter — keypair created and cleaned up:

create:  {"msg":"Created key pair","id":"key-09fa036eb67b366e0","name":"karpenter-dbqlr-bastion"}
destroy: {"msg":"Deleted keypair","id":"key-09fa036eb67b366e0","name":"karpenter-dbqlr-bastion"}

TestKarpenterUpgradeControlPlane — keypair created and cleaned up:

create:  {"msg":"Created key pair","id":"key-01c6a91c733938ad8","name":"karpenter-upgrade-control-plane-sssqw-bastion"}
destroy: {"msg":"Deleted keypair","id":"key-01c6a91c733938ad8","name":"karpenter-upgrade-control-plane-sssqw-bastion"}

Both keypair IDs match between create and destroy — no leak.

Baseline comparison (build 2077075393017286656, before keypair cleanup): destroy-bastion.log files were 0 bytes because the CI account had 4,996 orphaned *-bastion keypairs (limit 5,000), causing KeyPairLimitExceeded during bastion creation. Since creation failed before t.Cleanup was registered, cleanup never ran. After purging the orphaned keypairs, this run shows the fix working end-to-end.

infraID := hc.Spec.InfraID
region := hc.Spec.Platform.AWS.Region
t.Cleanup(func() {
destroyCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)

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.

I need to look through the code again to see how context management has evolved, but is there no secondary context being managed that could be used here instead of background + local arbitrary timeout?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

No, there's no secondary context managed in the e2e framework that's available here. The ctx parameter to setupBastion is the test context, which may already be canceled when t.Cleanup fires — specifically, the cleanup teardown path at hypershift_framework.go:334 passes context.Background() explicitly. The testContext (process-wide, only canceled on SIGINT/SIGTERM) would work in principle, but it's not accessible from setupBastion and threading it through the call chain would change the signatures of DumpJournals, newClusterDumper, and teardownHostedCluster.

context.Background() with a bounded timeout is the established pattern for cleanup operations in this codebase — the integration tests use the same approach (e.g. test/integration/framework/run.go:45,83), and test/e2e/util/aws.go:121,395 both use context.WithTimeout(context.Background(), 2*time.Minute) in cleanup paths. The 5-minute timeout here accounts for the bastion destroy needing to sequentially delete the EC2 instance, security group, and keypair.


AI-assisted response via Claude Code

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.

Prow will issue a SIGTERM which drives cancellation, and then a SIGKILL after the grace period, so I don't see an opportunity to drive a secondary cancellation within the cleanup... that said, it raises the question why use a timeout at all? Could let the cleanup block forever until it either finishes or Prow kills the whole process, no arbitrary timeout required?

https://docs.ci.openshift.org/architecture/timeouts/#handling-sigterm-in-a-test-process

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good point — the testContext signal handler at e2e_test.go:220-224 does cancel on SIGTERM, so Prow's kill chain would propagate. But testContext isn't accessible from setupBastion without threading it through DumpJournalsnewClusterDumperteardownHostedCluster.

A bare context.Background() without timeout would work if this cleanup were the only thing running, but t.Cleanup functions fire LIFO — if the bastion AWS call hangs (network partition, throttled API), it blocks all subsequent cleanups from running until Prow SIGKILLs the process. The 5-minute bound ensures other cleanup functions (HC destroy, namespace deletion) still get a chance to execute within Prow's grace period.

That said, 5 minutes is generous — the destroy typically completes in under 30 seconds (delete instance, delete SG, delete keypair). Happy to adjust the timeout if you think a shorter bound makes more sense.


AI-assisted response via Claude Code

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.

How many functions are in the cleanup stack? What is a "fair" timeout to allocate to each? How would you compute that statically or at runtime? If this timeout is exceeded and it falls through to the next, you still leaked. Is it better to try each one until pass or overall timeout, or give arbitrary inconsistent timeouts to everything in the stack? etc. etc.

Not sure what is the right answer. If our best current approach is limited to arbitrary timeout assignments, I guess I would err on the side of shorter to improve the chance of other items in the stack executing and ensuring we're logging leaks. Maybe take a look at the current grace timeouts in the prow config to see what budget we're working with.

Post-hoc detection / backstopping is probably our best bet in any case as we'll never be able to account for the orchestrator SIGTERM'ing us in any case, so as long as the things we're creating are being appropriately tagged and/or isolated in accounts, risk of cleanup issues like this can be reduced

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

You're right that there's no principled way to statically budget timeouts across the cleanup stack — that's a real framework gap worth tracking separately.

For this PR though, the primary fix is the eager capture of infraID/region (lines 165-166), which directly prevents the KeyPair leak. The timeout is a secondary defensive measure, consistent with existing patterns in the codebase:

  • aws.go:121PutRolePolicy cleanup: context.WithTimeout(context.Background(), 2*time.Minute)
  • aws.go:395CreateCapacityReservation cleanup: context.WithTimeout(context.Background(), 2*time.Minute)

The main framework teardown (hypershift_framework.go:334) uses bare context.Background() with no timeout at all, so there's no existing budget system to integrate with.

Happy to shorten this to 2 minutes to match the existing cleanup timeout pattern and improve the odds for subsequent cleanup items, but I think the broader cleanup orchestration work (managed cleanup context with budget allocation) is a separate enhancement. The immediate priority is stopping the keypair leaks on every Karpenter test run.

Want me to drop to 2 minutes, or is 5 minutes acceptable as-is?


AI-assisted response via Claude Code

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.

Your call, this isn't blocking feedback

@sdminonne

Copy link
Copy Markdown
Contributor

/cc @sdminonne

@ironcladlou

Copy link
Copy Markdown
Contributor

/lgtm
/hold
if you want what's here just remove the hold, if you want to change the timeout somehow I'll re-tag

@openshift-ci openshift-ci Bot added the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Jul 16, 2026
@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

Tests from second stage were triggered manually. Pipeline can be controlled only manually, until HEAD changes. Use command to trigger second stage.

@bryan-cox

Copy link
Copy Markdown
Member Author

/hold cancel

Will take what's here for now to stop the keypair bleeding.

@openshift-ci openshift-ci Bot removed the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Jul 16, 2026
@bryan-cox

Copy link
Copy Markdown
Member Author

/pipeline required

@openshift-merge-bot

Copy link
Copy Markdown
Contributor

Scheduling tests matching the pipeline_run_if_changed or not excluded by pipeline_skip_if_only_changed parameters:
/test e2e-aks-4-22
/test e2e-aws-4-22
/test e2e-aks
/test e2e-aws
/test e2e-aws-upgrade-hypershift-operator
/test e2e-azure-v2-self-managed
/test e2e-kubevirt-aws-ovn-reduced
/test e2e-v2-aws
/test e2e-v2-gke

@bryan-cox

Copy link
Copy Markdown
Member Author

/verified by e2e

Job: pull-ci-openshift-hypershift-main-e2e-aws/2077722929948266496

Only TestKarpenter and TestKarpenterUpgradeControlPlane exercise the bastion path (PublicOnly=false). All other tests use public IPs and skip bastion entirely.

TestKarpenter — keypair created and cleaned up:

create: {"msg":"Created key pair","id":"key-09fa036eb67b366e0","name":"karpenter-dbqlr-bastion"}
destroy: {"msg":"Deleted keypair","id":"key-09fa036eb67b366e0","name":"karpenter-dbqlr-bastion"}
TestKarpenterUpgradeControlPlane — keypair created and cleaned up:

create: {"msg":"Created key pair","id":"key-01c6a91c733938ad8","name":"karpenter-upgrade-control-plane-sssqw-bastion"}
destroy: {"msg":"Deleted keypair","id":"key-01c6a91c733938ad8","name":"karpenter-upgrade-control-plane-sssqw-bastion"}
Both keypair IDs match between create and destroy — no leak.

Baseline comparison (build 2077075393017286656, before keypair cleanup): destroy-bastion.log files were 0 bytes because the CI account had 4,996 orphaned *-bastion keypairs (limit 5,000), causing KeyPairLimitExceeded during bastion creation. Since creation failed before t.Cleanup was registered, cleanup never ran. After purging the orphaned keypairs, this run shows the fix working end-to-end.

@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

@bryan-cox: This PR has been marked as verified by e2e.

Details

In response to this:

/verified by e2e

Job: pull-ci-openshift-hypershift-main-e2e-aws/2077722929948266496

Only TestKarpenter and TestKarpenterUpgradeControlPlane exercise the bastion path (PublicOnly=false). All other tests use public IPs and skip bastion entirely.

TestKarpenter — keypair created and cleaned up:

create: {"msg":"Created key pair","id":"key-09fa036eb67b366e0","name":"karpenter-dbqlr-bastion"}
destroy: {"msg":"Deleted keypair","id":"key-09fa036eb67b366e0","name":"karpenter-dbqlr-bastion"}
TestKarpenterUpgradeControlPlane — keypair created and cleaned up:

create: {"msg":"Created key pair","id":"key-01c6a91c733938ad8","name":"karpenter-upgrade-control-plane-sssqw-bastion"}
destroy: {"msg":"Deleted keypair","id":"key-01c6a91c733938ad8","name":"karpenter-upgrade-control-plane-sssqw-bastion"}
Both keypair IDs match between create and destroy — no leak.

Baseline comparison (build 2077075393017286656, before keypair cleanup): destroy-bastion.log files were 0 bytes because the CI account had 4,996 orphaned *-bastion keypairs (limit 5,000), causing KeyPairLimitExceeded during bastion creation. Since creation failed before t.Cleanup was registered, cleanup never ran. After purging the orphaned keypairs, this run shows the fix working end-to-end.

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.

@bryan-cox

Copy link
Copy Markdown
Member Author

/retest

1 similar comment
@bryan-cox

Copy link
Copy Markdown
Member Author

/retest

@sdminonne

Copy link
Copy Markdown
Contributor

/test e2e-aws-4-22

@enxebre enxebre added the acknowledge-critical-fixes-only Indicates if the issuer of the label is OK with the policy. label Jul 20, 2026
@openshift-ci

openshift-ci Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

@bryan-cox: 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.

@openshift-merge-bot
openshift-merge-bot Bot merged commit 84697a2 into openshift:main Jul 20, 2026
41 checks passed
@openshift-ci-robot

Copy link
Copy Markdown

@bryan-cox: Jira Issue Verification Checks: Jira Issue OCPBUGS-98384
✔️ This pull request was pre-merge verified.
✔️ All associated pull requests have merged.
✔️ All associated, merged pull requests were pre-merge verified.

Jira Issue OCPBUGS-98384 has been moved to the MODIFIED state and will move to the VERIFIED state when the change is available in an accepted nightly payload. 🕓

Details

In response to this:

What this PR does / why we need it:

PR #8309 (merged May 11, 2026) refactored test/e2e/util/dump/journals.go to reduce cyclomatic complexity, changing bastion cleanup from defer to t.Cleanup(). This changed the execution order so that bastion destroy now runs after the HostedCluster is already deleted from Kubernetes.

Since DestroyBastionOpts was constructed with Name/Namespace, the destroy code tried to look up the already-deleted HC to resolve infraID and region, failed with a "not found" error, and the error was silently swallowed by t.Logf. This left EC2 bastion instances, security groups, and KeyPairs orphaned in AWS on every e2e test run. The AWS KeyPair limit (5000) was hit on June 17, 2026.

Fix: Capture hc.Spec.InfraID and hc.Spec.Platform.AWS.Region at bastion creation time and pass them directly to DestroyBastionOpts, bypassing the HC lookup path entirely. Also use context.Background() in the cleanup closure to avoid the parent context being cancelled before cleanup runs.

Which issue(s) this PR fixes:

Fixes OCPBUGS-98384

Special notes for your reviewer:

DestroyBastionOpts.Run() (cmd/bastion/aws/destroy.go:87-112) supports two ways to resolve infraID/region: by looking up the HC via Name/Namespace, or by accepting InfraID/Region directly. The old test code used the lookup path, which fails when the HC is already deleted. This fix switches to passing InfraID/Region directly so no Kubernetes call is needed during cleanup.

Reported by Dan Mace.

Checklist:

  • Subject and description added to both, commit and PR.
  • Relevant issues have been referenced.
  • This change includes docs.
  • This change includes unit tests.

Summary by CodeRabbit

  • Bug Fixes
  • Improved cleanup of temporary bastion infrastructure during end-to-end test runs.
  • Cleanup now uses the correct infrastructure and region details and allows additional time for destruction to complete.

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.

@bryan-cox
bryan-cox deleted the OCPBUGS-98384 branch July 20, 2026 11:50
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/testing Indicates the PR includes changes for e2e testing jira/valid-bug Indicates that a referenced Jira bug is valid for the branch this PR is targeting. 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.

6 participants