Skip to content

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

Open
bryan-cox wants to merge 1 commit into
openshift:mainfrom
bryan-cox:OCPBUGS-98384
Open

OCPBUGS-98384: fix bastion cleanup KeyPair leak by capturing infraID/region eagerly#8982
bryan-cox wants to merge 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.

…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>
@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
📝 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() rather than the setup context. Other setup and return behavior remains unchanged.

🚥 Pre-merge checks | ✅ 10 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Test Structure And Quality ⚠️ Warning Cleanup now uses unbounded context.Background(), and DestroyBastionOpts.Run can retry indefinitely on retryable AWS errors. Bound cleanup with context.WithTimeout(context.Background(), ) and pass that ctx to destroy.Run.
✅ Passed checks (10 passed)
Check name Status Explanation
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 touched code is bastion cleanup in journals.go; it contains no Ginkgo test titles or dynamic name strings.
Topology-Aware Scheduling Compatibility ✅ Passed Only bastion cleanup in a test helper changed; no scheduling constraints, node selectors, affinity, replicas, or topology logic were added.
Ipv6 And Disconnected Network Test Compatibility ✅ Passed Only helper cleanup in journals.go changed; no new Ginkgo test bodies or IPv4/external-connectivity assumptions were added.
No-Weak-Crypto ✅ Passed Diff only changes bastion cleanup to use infraID/region and context.Background(); no weak-crypto, custom crypto, or secret/token comparison code was added.
Container-Privileges ✅ Passed Only test/e2e/util/dump/journals.go changed; no container/K8s manifests or privilege/securityContext settings were added.
No-Sensitive-Data-In-Logs ✅ Passed The PR only changes bastion cleanup inputs/context; it adds no new log calls and the existing error log in that path doesn't emit secrets or PII.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main fix: bastion cleanup leak prevention by eagerly capturing infraID and region.
✨ 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 on lines 166 to 175
t.Cleanup(func() {
destroyBastion := bastionaws.DestroyBastionOpts{
Namespace: hc.Namespace,
Name: hc.Name,
InfraID: infraID,
Region: region,
AWSCredentialsFile: awsCreds,
}
if err := destroyBastion.Run(ctx, zapr.NewLoggerWithOptions(destroyLogger)); err != nil {
if err := destroyBastion.Run(context.Background(), zapr.NewLoggerWithOptions(destroyLogger)); err != nil {
t.Logf("error destroying bastion: %v", err)
}
})

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Unbounded context.Background() risks hanging cleanup indefinitely.

DestroyBastionOpts.Run uses the context passed here for wait.PollUntilContextCancel(ctx, 5*time.Second, true, ...), retrying on any retryable AWS error. With a bare context.Background() there is no deadline, so a persistently-retryable AWS error (throttling, transient network issue) will retry forever and can hang the test process instead of just this one cleanup step. Consider bounding cleanup with a fixed timeout instead of an unbounded background context — this still protects cleanup from the parent test context being canceled, but caps worst-case blocking time.

♻️ Proposed bounded cleanup
 	t.Cleanup(func() {
+		destroyCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
+		defer cancel()
 		destroyBastion := bastionaws.DestroyBastionOpts{
 			InfraID:            infraID,
 			Region:             region,
 			AWSCredentialsFile: awsCreds,
 		}
-		if err := destroyBastion.Run(context.Background(), zapr.NewLoggerWithOptions(destroyLogger)); err != nil {
+		if err := destroyBastion.Run(destroyCtx, zapr.NewLoggerWithOptions(destroyLogger)); err != nil {
 			t.Logf("error destroying bastion: %v", err)
 		}
 	})
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
t.Cleanup(func() {
destroyBastion := bastionaws.DestroyBastionOpts{
Namespace: hc.Namespace,
Name: hc.Name,
InfraID: infraID,
Region: region,
AWSCredentialsFile: awsCreds,
}
if err := destroyBastion.Run(ctx, zapr.NewLoggerWithOptions(destroyLogger)); err != nil {
if err := destroyBastion.Run(context.Background(), zapr.NewLoggerWithOptions(destroyLogger)); err != nil {
t.Logf("error destroying bastion: %v", err)
}
})
t.Cleanup(func() {
destroyCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
destroyBastion := bastionaws.DestroyBastionOpts{
InfraID: infraID,
Region: region,
AWSCredentialsFile: awsCreds,
}
if err := destroyBastion.Run(destroyCtx, zapr.NewLoggerWithOptions(destroyLogger)); err != nil {
t.Logf("error destroying bastion: %v", err)
}
})
🤖 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 `@test/e2e/util/dump/journals.go` around lines 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.

@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.67%. Comparing base (e708f6f) to head (25ab781).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #8982   +/-   ##
=======================================
  Coverage   43.67%   43.67%           
=======================================
  Files         771      771           
  Lines       95840    95840           
=======================================
  Hits        41862    41862           
  Misses      51067    51067           
  Partials     2911     2911           
Flag Coverage Δ
cmd-support 37.22% <ø> (ø)
cpo-hostedcontrolplane 45.91% <ø> (ø)
cpo-other 45.11% <ø> (ø)
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.

@openshift-ci

openshift-ci Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

@bryan-cox: The following test failed, say /retest to rerun all failed tests or /retest-required to rerun all mandatory failed tests:

Test name Commit Details Required Rerun command
ci/prow/e2e-aws 25ab781 link true /test e2e-aws

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.

@cwbotbot

Copy link
Copy Markdown

Test Results

e2e-aws

@hypershift-jira-solve-ci

Copy link
Copy Markdown
Contributor

Now I can see the full picture clearly. The build process:

  1. Successfully pre-pulled both base images (sha256:0ba6... = builder image, sha256:9b63... = base-rhel9)
  2. Started the actual Dockerfile build
  3. At STEP 1/6: FROM ... AS builder, it tried to pull the builder image again
  4. This second pull got a 503 Service Unavailable from quay-proxy.ci.openshift.org

The build-log.txt interleaves output, so the actual error message was lost between the build output and the next INFO line. But the builds.json logSnippet confirms: received unexpected HTTP status: 503 Service Unavailable.

This is a CI infrastructure issue — a transient 503 from the image registry proxy, not related to the PR's code changes at all. The PR only modifies bastion cleanup logic (KeyPair leak), and the failure happened during the image build phase before any test code ran.

Now I have all the evidence I need. Let me produce the final report:

Test Failure Analysis Complete

Job Information

Test Failure Analysis

Error

error: build error: creating build container: copying system blob: received unexpected HTTP status: 503 Service Unavailable

Summary

The hypershift-amd64 image build (using Dockerfile.control-plane) failed during the first FROM stage while pulling the builder base image quay-proxy.ci.openshift.org/openshift/ci@sha256:0ba6... (ocp/builder:rhel-9-golang-1.25-openshift-4.23). The CI image registry proxy returned a 503 Service Unavailable error. This is a transient CI infrastructure failure completely unrelated to the PR's code changes. All other image builds (hypershift-operator, hypershift-tests, hypershift-cli, src) succeeded. The e2e test step never ran because the build-phase failure blocked it.

Root Cause

The root cause is a transient 503 Service Unavailable error from quay-proxy.ci.openshift.org during the Dockerfile build phase.

The build process pre-pulled both base images successfully (the builder golang image and the base-rhel9 runtime image). However, when the actual buildah/podman Dockerfile build started executing [1/2] STEP 1/6: FROM quay-proxy.ci.openshift.org/openshift/ci@sha256:0ba6... AS builder, it attempted to pull the same builder image again from the registry proxy and received a 503 response. This is a transient registry infrastructure issue — the same image had been successfully pulled moments earlier during the pre-pull phase.

Key observations confirming this is infrastructure-related:

  1. The same base image (sha256:0ba6074920a61c2643dd65183dce96b1cfe0316849de798144e02e0f253bed49) was pre-pulled successfully just seconds before the Dockerfile build stage failed on it.
  2. All four other image builds (hypershift-operator-amd64, hypershift-tests-amd64, hypershift-cli-amd64, src-amd64) completed successfully, including hypershift-operator which uses the same golang builder image via Dockerfile.
  3. The PR changes (fix bastion cleanup KeyPair leak by capturing infraID/region eagerly) are purely Go code changes to bastion cleanup logic — they do not modify any Dockerfile, build configuration, or dependencies.
  4. The build failed after only 77 seconds (1m17s), at the very first FROM instruction — no compilation or test code was ever executed.
Recommendations
  1. Retest the PR — This is a transient CI infrastructure failure. Simply re-trigger the e2e-aws job with /retest or /test e2e-aws. The failure is not caused by any code change in the PR.
  2. No code changes needed — The PR's changes to bastion cleanup logic are completely unrelated to this build failure. The failure occurred before any code from the PR was compiled or tested.
  3. If the failure recurs — Check the status of quay-proxy.ci.openshift.org and the CI build cluster's image registry. Repeated 503 errors may indicate a broader registry outage. Consult the #forum-ocp-crt or #announce-testplatform Slack channels for known infrastructure issues.
Evidence
Evidence Detail
Failed build hypershift-amd64 using Dockerfile.control-plane
Error message received unexpected HTTP status: 503 Service Unavailable
Failed at [1/2] STEP 1/6: FROM quay-proxy.ci.openshift.org/openshift/ci@sha256:0ba6... (pulling builder base image)
Build duration 77 seconds (1m17s) — failed before any Go compilation
Registry quay-proxy.ci.openshift.org returned 503
Image pulled ocp/builder:rhel-9-golang-1.25-openshift-4.23 (sha256:0ba6074920a61c2643dd65183dce96b1cfe0316849de798144e02e0f253bed49)
Pre-pull succeeded Same image was successfully pre-pulled at 12:06:33 before the build stage failed at 12:06:38
Other builds hypershift-operator-amd64 ✅, hypershift-tests-amd64 ✅, hypershift-cli-amd64 ✅, src-amd64
Build cluster build01 (console.build01.ci.openshift.org), namespace ci-op-twcr4p5t
Build node ip-10-0-186-104.ec2.internal
Scheduling delay 178s (pod was pending for ~3 min waiting for a node — normal for CI)
JUnit failure Build image hypershift from the repository: "error occurred handling build hypershift-amd64: the build hypershift-amd64 failed after 1m17s with reason DockerBuildFailed"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants