Skip to content

CNTRLPLANE-3027: Decouple all platform boot image resolvers for dual-stream support#8709

Draft
jparrill wants to merge 3 commits into
openshift:mainfrom
jparrill:CNTRLPLANE-3027
Draft

CNTRLPLANE-3027: Decouple all platform boot image resolvers for dual-stream support#8709
jparrill wants to merge 3 commits into
openshift:mainfrom
jparrill:CNTRLPLANE-3027

Conversation

@jparrill

@jparrill jparrill commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

What this PR does / why we need it

Phase 1 step 3 (all remaining platforms) of the Dual-Stream RHEL 9/10 NodePool enhancement (OCPSTRAT-3014). Applies the same decoupling pattern from CNTRLPLANE-3026 (AWS) to GCP, Azure, PowerVS, OpenStack, and KubeVirt.

Each platform's leaf boot image resolver now accepts *stream.Stream instead of *releaseinfo.ReleaseImage. Callers resolve the stream internally via StreamForName(""), preserving backward compatibility.

Changes per platform:

  • GCP: defaultNodePoolGCPImage() — accepts *stream.Stream
  • PowerVS: getPowerVSImage() — accepts *stream.Stream
  • Azure: getAzureMarketplaceMetadata() — accepts *stream.Stream
  • OpenStack: OpenstackDefaultImage(), OpenStackReleaseImage(), PrefixedClusterImageName() — accept *stream.Stream with nil guards
  • KubeVirt: defaultImage() — accepts *stream.Stream; GetImage() resolves stream once and passes to both defaultImage and openstack.OpenstackDefaultImage (fallback path)

This completes Phase 1 step 3 of the enhancement for all platforms.

Which issue(s) this PR fixes

Fixes CNTRLPLANE-3027

Dependencies (must merge first)

Special notes for your reviewer:

Checklist

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

Test plan

go test ./hypershift-operator/controllers/nodepool/... -count=1
go test ./hypershift-operator/controllers/nodepool/kubevirt/... -count=1
go test ./hypershift-operator/controllers/nodepool/openstack/... -count=1
make lint-fix  # 0 issues
make verify    # passes

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features
    • Added support for multi-stream release payloads with RHEL 9 and RHEL 10 stream selection
    • RHEL 10 is now available on OCP 5.0 and later
    • Improved stream metadata resolution across all cloud platforms (AWS, Azure, GCP, OpenStack, PowerVS, KubeVirt)

@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

openshift-ci Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with /test all

@openshift-ci-robot

openshift-ci-robot commented Jun 10, 2026

Copy link
Copy Markdown

@jparrill: This pull request references CNTRLPLANE-3027 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:

What this PR does / why we need it

Phase 1 step 3 (all remaining platforms) of the Dual-Stream RHEL 9/10 NodePool enhancement (OCPSTRAT-3014). Applies the same decoupling pattern from CNTRLPLANE-3026 (AWS) to GCP, Azure, PowerVS, OpenStack, and KubeVirt.

Each platform's leaf boot image resolver now accepts *stream.Stream instead of *releaseinfo.ReleaseImage. Callers resolve the stream internally via StreamForName(""), preserving backward compatibility.

Changes per platform:

  • GCP: DefaultNodePoolGCPImage() — exported, accepts *stream.Stream
  • PowerVS: getPowerVSImage() — accepts *stream.Stream
  • Azure: getAzureMarketplaceMetadata() — accepts *stream.Stream
  • OpenStack: OpenstackDefaultImage(), OpenStackReleaseImage(), PrefixedClusterImageName() — accept *stream.Stream
  • KubeVirt: defaultImage() — accepts *stream.Stream; GetImage() resolves stream once and passes to both defaultImage and openstack.OpenstackDefaultImage (fallback path)

Integration tests:

  • TestBootImageStreamResolution/GCP — 4 test cases covering rhel-9/rhel-10 resolution, default stream backward compat, and nil metadata error

This completes Phase 1 step 3 of the enhancement for all platforms.

Which issue(s) this PR fixes

Fixes CNTRLPLANE-3027

Dependencies (must merge first)

Checklist

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

Test plan

  • go build ./hypershift-operator/... — compiles
  • go test ./hypershift-operator/controllers/nodepool/... -count=1 — all unit tests pass
  • go test -tags=integration ./test/integration/osstreams/... -count=1 — all integration tests pass
  • make verify — lint and generation pass (only verify-git-clean expected to fail pre-merge)

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

@openshift-ci-robot openshift-ci-robot added the jira/valid-reference Indicates that this PR references a valid Jira ticket of any type. label Jun 10, 2026
@openshift-ci openshift-ci Bot added do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. do-not-merge/needs-area labels Jun 10, 2026
@coderabbitai

coderabbitai Bot commented Jun 10, 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 refactors HyperShift's nodepool controllers to support multi-stream release payloads. It extends ReleaseImage with an OSStreams field and adds StreamForName() method for selecting default or named streams. The deserialization layer is updated to parse both single-stream and multi-stream ConfigMap data. All platform-specific image-selection functions (defaultNodePoolAMI, OpenstackDefaultImage, OpenStackReleaseImage, getPowerVSImage, etc.) now accept *stream.Stream instead of *releaseinfo.ReleaseImage. Controllers resolve stream metadata explicitly via StreamForName("") before invoking these helpers. New stream resolution helpers map OCP versions and runc usage to RHEL stream identifiers (rhel-9, rhel-10). Comprehensive test fixtures and updated test suites validate single/multi-stream parsing and platform-specific image lookups.

Suggested reviewers

  • muraee
  • Nirshal

Possibly related PRs

  • openshift/hypershift#8673: Updated support/releaseinfo to use upstream stream-metadata-go types; this PR builds on that foundation by refactoring nodepool controllers to explicitly resolve and pass *stream.Stream into image-selection helpers.
🚥 Pre-merge checks | ✅ 10 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Test Structure And Quality ⚠️ Warning Tests lack meaningful assertion failure messages (37/41 assertions without messages). Most lack context to help diagnose failures; table-driven test names provide some context but explicit messages... Add assertion messages to Expect() calls, e.g., g.Expect(err).ToNot(HaveOccurred(), "test: "+tc.name). Most tests pass other quality requirements (single responsibility, setup/cleanup, timeouts, consistency).
✅ Passed checks (10 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the main change: decoupling platform boot image resolvers to support dual-stream (RHEL 9/10) functionality across multiple platforms (GCP, Azure, PowerVS, OpenStack, KubeVirt).
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 Scanned all Go Ginkgo test titles (It/Describe/Context/When string literals) for dynamic patterns (%s/%d, dates/timestamps, UUID/hex suffixes, fmt.Sprintf); found 0 violations.
Topology-Aware Scheduling Compatibility ✅ Passed In the PR’s listed modified files, no new required anti-affinity (requiredDuringSchedulingIgnoredDuringExecution + maxUnavailable:0) or topologySpreadConstraints with whenUnsatisfiable:DoNotSchedul...
Ipv6 And Disconnected Network Test Compatibility ✅ Passed PR #8709 changes 24 non-e2e files; no modified/added Ginkgo e2e tests (no imports of onsi/ginkgo) and no hardcoded IPv4/external-connectivity assumptions found in changed test files.
No-Weak-Crypto ✅ Passed Scanned the PR’s listed Go/YAML files for md5/sha1/des/rc4/3des/blowfish/ECB and crypto/ imports; none found, and no obvious secret/token ==/!= comparisons were detected.
Container-Privileges ✅ Passed PR contains no container/Kubernetes manifests with privileged security configurations; CodeRabbit Container-Privileges check shows ✅ Passed.
No-Sensitive-Data-In-Logs ✅ Passed Searched nodepool + support/releaseinfo logging in PR-related files; log calls only report secret/object names and reconciliation status, not password/token/API-key/PII values.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@openshift-ci openshift-ci Bot added area/control-plane-operator Indicates the PR includes changes for the control plane operator - in an OCP release area/hypershift-operator Indicates the PR includes changes for the hypershift operator and API - outside an OCP release area/platform/aws PR/issue for AWS (AWSPlatform) platform area/platform/azure PR/issue for Azure (AzurePlatform) platform area/platform/gcp PR/issue for GCP (GCPPlatform) platform area/platform/kubevirt PR/issue for KubeVirt (KubevirtPlatform) platform and removed do-not-merge/needs-area labels Jun 10, 2026
@openshift-ci

openshift-ci Bot commented Jun 10, 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 area/platform/openstack PR/issue for OpenStack (OpenStackPlatform) platform area/platform/powervs PR/issue for PowerVS (PowerVSPlatform) platform 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 labels Jun 10, 2026
@codecov

codecov Bot commented Jun 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 54.59459% with 84 lines in your changes missing coverage. Please review.
✅ Project coverage is 41.68%. Comparing base (39f04eb) to head (ad091ee).

Files with missing lines Patch % Lines
hypershift-operator/controllers/nodepool/aws.go 13.33% 25 Missing and 1 partial ⚠️
...ypershift-operator/controllers/nodepool/powervs.go 10.52% 17 Missing ⚠️
...ershift-operator/controllers/nodepool/openstack.go 0.00% 10 Missing ⚠️
...erator/controllers/nodepool/openstack/openstack.go 65.21% 7 Missing and 1 partial ⚠️
support/releaseinfo/registryclient_provider.go 0.00% 7 Missing ⚠️
...operator/controllers/nodepool/kubevirt/kubevirt.go 25.00% 6 Missing ⚠️
hypershift-operator/controllers/nodepool/azure.go 50.00% 2 Missing and 2 partials ⚠️
hypershift-operator/controllers/nodepool/gcp.go 40.00% 2 Missing and 1 partial ⚠️
hypershift-operator/controllers/nodepool/token.go 40.00% 2 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #8709      +/-   ##
==========================================
+ Coverage   41.66%   41.68%   +0.01%     
==========================================
  Files         758      759       +1     
  Lines       93929    94066     +137     
==========================================
+ Hits        39135    39207      +72     
- Misses      52046    52110      +64     
- Partials     2748     2749       +1     
Files with missing lines Coverage Δ
...erator/controllers/nodepool/nodepool_controller.go 43.41% <100.00%> (+0.14%) ⬆️
hypershift-operator/controllers/nodepool/stream.go 100.00% <100.00%> (ø)
support/releaseinfo/deserialize.go 89.65% <100.00%> (+49.65%) ⬆️
support/releaseinfo/registry_mirror_provider.go 45.83% <100.00%> (+2.35%) ⬆️
support/releaseinfo/releaseinfo.go 51.00% <100.00%> (+5.14%) ⬆️
hypershift-operator/controllers/nodepool/gcp.go 65.77% <40.00%> (-0.74%) ⬇️
hypershift-operator/controllers/nodepool/token.go 81.81% <40.00%> (-0.73%) ⬇️
hypershift-operator/controllers/nodepool/azure.go 87.87% <50.00%> (-1.30%) ⬇️
...operator/controllers/nodepool/kubevirt/kubevirt.go 68.76% <25.00%> (-0.70%) ⬇️
support/releaseinfo/registryclient_provider.go 0.00% <0.00%> (ø)
... and 4 more
Flag Coverage Δ
cmd-support 35.04% <84.44%> (+0.08%) ⬆️
cpo-hostedcontrolplane 44.00% <ø> (ø)
cpo-other 43.45% <ø> (ø)
hypershift-operator 51.58% <45.00%> (-0.08%) ⬇️
other 31.56% <ø> (ø)

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.

@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

Caution

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

⚠️ Outside diff range comments (2)
hypershift-operator/controllers/nodepool/azure_test.go (2)

909-951: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Same test data inconsistency in Gen1/Gen2 skip tests.

Tests "skip defaulting with Gen1 imageGeneration when no marketplace metadata" (lines 909-929) and "skip defaulting with Gen2 imageGeneration when no marketplace metadata" (lines 931-951) both pass hasMarketplaceMetadata=true despite the test names claiming "no marketplace metadata".

These should likely use hasMarketplaceMetadata=false to match their stated intent.

Suggested fix
 {
     name: "skip defaulting with Gen1 imageGeneration when no marketplace metadata",
     // ...
-    releaseImage:             createMockReleaseImage("4.20.0", true),
+    releaseImage:             createMockReleaseImage("4.20.0", false),
     expectedImageType:        "",
     expectedMarketplaceImage: nil,
 },
 {
     name: "skip defaulting with Gen2 imageGeneration when no marketplace metadata",
     // ...
-    releaseImage:             createMockReleaseImage("4.20.0", true),
+    releaseImage:             createMockReleaseImage("4.20.0", false),
     expectedImageType:        "",
     expectedMarketplaceImage: nil,
 },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hypershift-operator/controllers/nodepool/azure_test.go` around lines 909 -
951, The two tests "skip defaulting with Gen1 imageGeneration when no
marketplace metadata" and "skip defaulting with Gen2 imageGeneration when no
marketplace metadata" are passing hasMarketplaceMetadata=true to
createMockReleaseImage but should simulate no marketplace metadata; update the
call to createMockReleaseImage("4.20.0", false) in those test cases so the
second argument is false, leaving the rest of the nodePool/AzureMarketplaceImage
ImageGeneration setup unchanged.

892-907: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Test data contradicts test name - appears to be a copy-paste error.

This test case passes hasMarketplaceMetadata=true but claims to test "skip defaulting when no marketplace metadata". Compare with the test at lines 992-1013 which has an identical setup (4.20.0, hasMarketplaceMetadata=true, empty AzureVMImage) but expects defaulting to occur.

Either:

  1. This test should use hasMarketplaceMetadata=false to match the test name, or
  2. The test name is wrong and expectations need updating

Additionally, the current assertion only checks when expectedImageType != "", so this test doesn't actually verify that defaults were NOT applied—it would pass even if the function applies defaults.

Suggested fix if the intent is to test "no marketplace metadata"
 {
     name: "skip defaulting when no marketplace metadata for OCP >= 4.20 (current behavior)",
     nodePool: &hyperv1.NodePool{
         // ... same ...
     },
-    releaseImage:             createMockReleaseImage("4.20.0", true),
+    releaseImage:             createMockReleaseImage("4.20.0", false),
     expectedImageType:        "",
     expectedMarketplaceImage: nil,
 },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hypershift-operator/controllers/nodepool/azure_test.go` around lines 892 -
907, The test case named "skip defaulting when no marketplace metadata for OCP
>= 4.20 (current behavior)" is passing hasMarketplaceMetadata=true but should
either set it false or be renamed and have expectations updated; update the call
to createMockReleaseImage(...) to pass hasMarketplaceMetadata=false to match the
name (or rename the test and adjust expectedImageType/expectedMarketplaceImage
to reflect defaulting), and add an explicit assertion verifying defaults were
NOT applied for AzureVMImage on the NodePool (e.g., assert expectedImageType ==
"" and expectedMarketplaceImage == nil) so the test fails if defaulting occurs;
locate the case by the test name and references to AzureVMImage,
createMockReleaseImage, expectedImageType, and expectedMarketplaceImage.
🧹 Nitpick comments (2)
hypershift-operator/controllers/nodepool/azure_test.go (1)

1100-1114: ⚡ Quick win

Test assertions don't verify that defaults were NOT applied.

When expectedImageType is empty or expectedMarketplaceImage is nil, the assertions are skipped entirely. This means tests claiming to verify "skip defaulting" behavior don't actually check that the NodePool remains unmodified.

Consider adding explicit assertions for skip scenarios.

Suggested stronger assertions
 } else {
     g.Expect(err).ToNot(HaveOccurred())
-    if tc.expectedImageType != "" {
-        g.Expect(tc.nodePool.Spec.Platform.Azure.Image.Type).To(Equal(tc.expectedImageType))
-    }
-    if tc.expectedMarketplaceImage != nil {
-        g.Expect(tc.nodePool.Spec.Platform.Azure.Image.AzureMarketplace).To(Equal(tc.expectedMarketplaceImage))
-    }
+    // Always verify image type and marketplace match expectations
+    g.Expect(tc.nodePool.Spec.Platform.Azure.Image.Type).To(Equal(tc.expectedImageType))
+    if tc.expectedMarketplaceImage == nil {
+        // For skip scenarios, verify marketplace was not set (or unchanged)
+        // Note: may need to capture original value before calling defaultAzureNodePoolImage
+        g.Expect(tc.nodePool.Spec.Platform.Azure.Image.AzureMarketplace).To(BeNil())
+    } else {
+        g.Expect(tc.nodePool.Spec.Platform.Azure.Image.AzureMarketplace).To(Equal(tc.expectedMarketplaceImage))
+    }
 }

Note: For tests where the NodePool already has a partial AzureMarketplace struct set (like the Gen1/Gen2 tests), you may need to capture the original state and verify it wasn't modified, or restructure those test cases.

🤖 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 `@hypershift-operator/controllers/nodepool/azure_test.go` around lines 1100 -
1114, The test currently skips assertions when tc.expectedImageType is empty or
tc.expectedMarketplaceImage is nil, so add explicit checks that no defaults were
applied by capturing the original state of nodePool.Spec.Platform.Azure.Image
(and its AzureMarketplace sub-struct) before running the code under test and
then asserting after execution that the Image.Type and Image.AzureMarketplace
are unchanged (for nil cases assert the field stays nil/zero and for partial
structs compare equality to the captured original). Update the table-driven
cases to, for each test case, store a preCall :=
tc.nodePool.Spec.Platform.Azure.Image (or pointer) and then use
g.Expect(...).To(Equal(preCall)) / g.Expect(...).To(BeNil()) as appropriate when
tc.expectedImageType == "" or tc.expectedMarketplaceImage == nil so tests verify
"skip defaulting" precisely.
hypershift-operator/controllers/nodepool/openstack/openstack.go (1)

165-175: 💤 Low value

Consider validating that Release is non-empty.

OpenStackReleaseImage returns openStack.Release directly without checking if it's an empty string. If stream metadata contains an empty release version, this would silently propagate and result in image names like rhcos- (missing version suffix).

🔧 Optional validation
 func OpenStackReleaseImage(streamMeta *stream.Stream) (string, error) {
 	arch, foundArch := streamMeta.Architectures["x86_64"]
 	if !foundArch {
 		return "", fmt.Errorf("couldn't find OS metadata for architecture %q", "x86_64")
 	}
 	openStack, exists := arch.Artifacts["openstack"]
 	if !exists {
 		return "", fmt.Errorf("couldn't find OS metadata for openstack")
 	}
+	if openStack.Release == "" {
+		return "", fmt.Errorf("openstack artifact has empty release version")
+	}
 	return openStack.Release, nil
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hypershift-operator/controllers/nodepool/openstack/openstack.go` around lines
165 - 175, The OpenStackReleaseImage function currently returns
openStack.Release without checking it's non-empty; add a validation after
retrieving openStack (from
stream.Stream->Architectures["x86_64"]->Artifacts["openstack"]) to trim and
verify openStack.Release is not empty (e.g.,
strings.TrimSpace(openStack.Release) != ""), and if empty return a descriptive
error (e.g., "openstack release is empty or missing in stream metadata") instead
of returning an empty string so callers don't receive malformed image names.
🤖 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 `@hypershift-operator/controllers/nodepool/aws.go`:
- Around line 137-141: The code calls releaseImage.StreamForName("") (and later
uses releaseImage again around the defaultNodePoolAMI call) without checking
releaseImage for nil; add a nil guard before any method call on releaseImage
(e.g., in the block that currently calls StreamForName and where
defaultNodePoolAMI is invoked) and return a clear error (fmt.Errorf or
reconcile-friendly error/condition) when releaseImage == nil so reconciliation
fails gracefully instead of panicking; reference the releaseImage variable, the
StreamForName call, and the defaultNodePoolAMI invocation when applying the
guard.

In `@hypershift-operator/controllers/nodepool/nodepool_controller_test.go`:
- Around line 574-575: The test setup currently ignores errors from
ri.StreamForName when assigning rhel9Stream and rhel10Stream; change those calls
to capture the error (e.g., rhel9Stream, err := ri.StreamForName("rhel-9") and
rhel10Stream, err := ri.StreamForName("rhel-10")) and assert/fail the test on
error (use your test helper like require.NoError/Expect/if err != nil {
t.Fatalf(...) }) so any fixture regressions surface immediately; reference the
ri variable and StreamForName function and replace the discarded-error
assignments for rhel9Stream and rhel10Stream accordingly.

In `@test/integration/osstreams/osstreams_test.go`:
- Around line 324-327: The "GCP" subtest in TestBootImageStreamResolution is
using the parent Gomega instance `g` so failures aren't tied to the subtest;
inside the `t.Run("GCP", ...)` closure, create a subtest-scoped Gomega with `g
:= NewWithT(t)` and replace uses of the parent `g.Expect(...)` (e.g. around
`multiStreamRelease.StreamForName("rhel-9")` and `StreamForName("rhel-10")`
checks for `rhel9Stream` and `rhel10Stream`) with the new `g.Expect(...)` so
assertions are attributed to the "GCP" subtest.

---

Outside diff comments:
In `@hypershift-operator/controllers/nodepool/azure_test.go`:
- Around line 909-951: The two tests "skip defaulting with Gen1 imageGeneration
when no marketplace metadata" and "skip defaulting with Gen2 imageGeneration
when no marketplace metadata" are passing hasMarketplaceMetadata=true to
createMockReleaseImage but should simulate no marketplace metadata; update the
call to createMockReleaseImage("4.20.0", false) in those test cases so the
second argument is false, leaving the rest of the nodePool/AzureMarketplaceImage
ImageGeneration setup unchanged.
- Around line 892-907: The test case named "skip defaulting when no marketplace
metadata for OCP >= 4.20 (current behavior)" is passing
hasMarketplaceMetadata=true but should either set it false or be renamed and
have expectations updated; update the call to createMockReleaseImage(...) to
pass hasMarketplaceMetadata=false to match the name (or rename the test and
adjust expectedImageType/expectedMarketplaceImage to reflect defaulting), and
add an explicit assertion verifying defaults were NOT applied for AzureVMImage
on the NodePool (e.g., assert expectedImageType == "" and
expectedMarketplaceImage == nil) so the test fails if defaulting occurs; locate
the case by the test name and references to AzureVMImage,
createMockReleaseImage, expectedImageType, and expectedMarketplaceImage.

---

Nitpick comments:
In `@hypershift-operator/controllers/nodepool/azure_test.go`:
- Around line 1100-1114: The test currently skips assertions when
tc.expectedImageType is empty or tc.expectedMarketplaceImage is nil, so add
explicit checks that no defaults were applied by capturing the original state of
nodePool.Spec.Platform.Azure.Image (and its AzureMarketplace sub-struct) before
running the code under test and then asserting after execution that the
Image.Type and Image.AzureMarketplace are unchanged (for nil cases assert the
field stays nil/zero and for partial structs compare equality to the captured
original). Update the table-driven cases to, for each test case, store a preCall
:= tc.nodePool.Spec.Platform.Azure.Image (or pointer) and then use
g.Expect(...).To(Equal(preCall)) / g.Expect(...).To(BeNil()) as appropriate when
tc.expectedImageType == "" or tc.expectedMarketplaceImage == nil so tests verify
"skip defaulting" precisely.

In `@hypershift-operator/controllers/nodepool/openstack/openstack.go`:
- Around line 165-175: The OpenStackReleaseImage function currently returns
openStack.Release without checking it's non-empty; add a validation after
retrieving openStack (from
stream.Stream->Architectures["x86_64"]->Artifacts["openstack"]) to trim and
verify openStack.Release is not empty (e.g.,
strings.TrimSpace(openStack.Release) != ""), and if empty return a descriptive
error (e.g., "openstack release is empty or missing in stream metadata") instead
of returning an empty string so callers don't receive malformed image names.
🪄 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: 143e0dcd-329f-4ebe-89a1-d75e3ccf9b62

📥 Commits

Reviewing files that changed from the base of the PR and between 832b848 and 49164dd.

⛔ Files ignored due to path filters (7)
  • go.sum is excluded by !**/*.sum
  • vendor/github.com/coreos/stream-metadata-go/LICENSE is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/coreos/stream-metadata-go/stream/artifact_utils.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/coreos/stream-metadata-go/stream/rhcos/rhcos.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/coreos/stream-metadata-go/stream/stream.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/coreos/stream-metadata-go/stream/stream_utils.go is excluded by !vendor/**, !**/vendor/**
  • vendor/modules.txt is excluded by !vendor/**, !**/vendor/**
📒 Files selected for processing (31)
  • go.mod
  • hypershift-operator/controllers/nodepool/aws.go
  • hypershift-operator/controllers/nodepool/aws_test.go
  • hypershift-operator/controllers/nodepool/azure.go
  • hypershift-operator/controllers/nodepool/azure_test.go
  • hypershift-operator/controllers/nodepool/gcp.go
  • hypershift-operator/controllers/nodepool/gcp_test.go
  • hypershift-operator/controllers/nodepool/kubevirt/kubevirt.go
  • hypershift-operator/controllers/nodepool/kubevirt/kubevirt_test.go
  • hypershift-operator/controllers/nodepool/nodepool_controller.go
  • hypershift-operator/controllers/nodepool/nodepool_controller_test.go
  • hypershift-operator/controllers/nodepool/openstack.go
  • hypershift-operator/controllers/nodepool/openstack/openstack.go
  • hypershift-operator/controllers/nodepool/openstack/openstack_test.go
  • hypershift-operator/controllers/nodepool/powervs.go
  • hypershift-operator/controllers/nodepool/stream.go
  • hypershift-operator/controllers/nodepool/stream_test.go
  • hypershift-operator/controllers/nodepool/token.go
  • hypershift-operator/controllers/nodepool/token_test.go
  • support/releaseinfo/deserialize.go
  • support/releaseinfo/deserialize_test.go
  • support/releaseinfo/fake/fake.go
  • support/releaseinfo/fixtures/5.0-installer-coreos-bootimages.yaml
  • support/releaseinfo/fixtures/fixtures.go
  • support/releaseinfo/registry_image_content_policies_test.go
  • support/releaseinfo/registry_mirror_provider.go
  • support/releaseinfo/registryclient_provider.go
  • support/releaseinfo/releaseinfo.go
  • support/releaseinfo/releaseinfo_test.go
  • support/releaseinfo/testutils/testutils.go
  • test/integration/osstreams/osstreams_test.go

Comment thread hypershift-operator/controllers/nodepool/aws.go
Comment thread hypershift-operator/controllers/nodepool/nodepool_controller_test.go Outdated
Comment on lines +324 to +327
rhel9Stream, err := multiStreamRelease.StreamForName("rhel-9")
g.Expect(err).ToNot(HaveOccurred())
rhel10Stream, err := multiStreamRelease.StreamForName("rhel-10")
g.Expect(err).ToNot(HaveOccurred())

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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify assertion scope in this file and inspect nearby context.
rg -n -C3 't\.Run\("GCP"|NewWithT\(t\)|g\.Expect\(err\)' test/integration/osstreams/osstreams_test.go

Repository: openshift/hypershift

Length of output: 4950


Use a subtest-scoped Gomega instance in the "GCP" subtest.
In test/integration/osstreams/osstreams_test.go (TestBootImageStreamResolution, t.Run("GCP", ...) around lines 323-327), the "GCP" assertions currently use the parent g (g.Expect(...)) instead of creating g := NewWithT(t) inside the "GCP" closure, so failures won’t be attributed cleanly to the right subtest.

🤖 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/integration/osstreams/osstreams_test.go` around lines 324 - 327, The
"GCP" subtest in TestBootImageStreamResolution is using the parent Gomega
instance `g` so failures aren't tied to the subtest; inside the `t.Run("GCP",
...)` closure, create a subtest-scoped Gomega with `g := NewWithT(t)` and
replace uses of the parent `g.Expect(...)` (e.g. around
`multiStreamRelease.StreamForName("rhel-9")` and `StreamForName("rhel-10")`
checks for `rhel9Stream` and `rhel10Stream`) with the new `g.Expect(...)` so
assertions are attributed to the "GCP" subtest.

Source: Coding guidelines

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

🧹 Nitpick comments (1)
hypershift-operator/controllers/nodepool/powervs_test.go (1)

121-125: ⚡ Quick win

Assert the returned image fields in success paths.

The test only verifies img != nil and region, so regressions in returned Release/Object/Url (including dot→hyphen normalization) can slip through.

Suggested test hardening
 testCases := []struct {
 	name           string
 	region         string
 	streamMeta     *stream.Stream
 	expectedRegion string
+	expectedRelease string
+	expectedObject  string
 	expectedErr    string
 }{
 	{
 		name:           "When region maps to a valid COS region it should return the image and region",
 		region:         "syd",
 		streamMeta:     basicStream,
 		expectedRegion: "au-syd",
+		expectedRelease: "9-8-20260403-0",
+		expectedObject:  "rhcos-9-8-20260403-0-ppc64le-powervs.ova.gz",
 	},
@@
 			} else {
 				g.Expect(err).ToNot(HaveOccurred())
 				g.Expect(img).ToNot(BeNil())
 				g.Expect(region).To(Equal(tc.expectedRegion))
+				g.Expect(img.Release).To(Equal(tc.expectedRelease))
+				g.Expect(img.Object).To(Equal(tc.expectedObject))
 			}
🤖 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 `@hypershift-operator/controllers/nodepool/powervs_test.go` around lines 121 -
125, The test's success branch only checks img != nil and region but must also
assert the image fields to catch regressions: add expectations on img.Release,
img.Object, and img.URL (or the exact struct field names used in
powervs_test.go) to equal the testcase's expected values (e.g.,
tc.expectedRelease, tc.expectedObject, tc.expectedURL), and include a test case
that exercises dot→hyphen normalization to validate the URL/object
transformation; update the success path where g.Expect(img).ToNot(BeNil()) and
g.Expect(region).To(Equal(tc.expectedRegion)) to also assert these image fields.
🤖 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.

Nitpick comments:
In `@hypershift-operator/controllers/nodepool/powervs_test.go`:
- Around line 121-125: The test's success branch only checks img != nil and
region but must also assert the image fields to catch regressions: add
expectations on img.Release, img.Object, and img.URL (or the exact struct field
names used in powervs_test.go) to equal the testcase's expected values (e.g.,
tc.expectedRelease, tc.expectedObject, tc.expectedURL), and include a test case
that exercises dot→hyphen normalization to validate the URL/object
transformation; update the success path where g.Expect(img).ToNot(BeNil()) and
g.Expect(region).To(Equal(tc.expectedRegion)) to also assert these image fields.

ℹ️ Review info
⚙️ Run configuration

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

Review profile: CHILL

Plan: Enterprise

Run ID: afc0063f-50be-411d-9a98-6039a8a8726a

📥 Commits

Reviewing files that changed from the base of the PR and between 49164dd and 3b8fa2a.

📒 Files selected for processing (12)
  • hypershift-operator/controllers/nodepool/azure.go
  • hypershift-operator/controllers/nodepool/kubevirt/kubevirt.go
  • hypershift-operator/controllers/nodepool/kubevirt/kubevirt_test.go
  • hypershift-operator/controllers/nodepool/openstack.go
  • hypershift-operator/controllers/nodepool/openstack/openstack.go
  • hypershift-operator/controllers/nodepool/openstack/openstack_test.go
  • hypershift-operator/controllers/nodepool/powervs.go
  • hypershift-operator/controllers/nodepool/powervs_test.go
  • hypershift-operator/controllers/nodepool/token.go
  • support/releaseinfo/releaseinfo.go
  • support/releaseinfo/releaseinfo_test.go
  • test/integration/osstreams/osstreams_test.go
🚧 Files skipped from review as they are similar to previous changes (8)
  • hypershift-operator/controllers/nodepool/token.go
  • hypershift-operator/controllers/nodepool/azure.go
  • hypershift-operator/controllers/nodepool/kubevirt/kubevirt.go
  • hypershift-operator/controllers/nodepool/openstack.go
  • support/releaseinfo/releaseinfo.go
  • hypershift-operator/controllers/nodepool/powervs.go
  • support/releaseinfo/releaseinfo_test.go
  • hypershift-operator/controllers/nodepool/openstack/openstack_test.go

@hypershift-jira-solve-ci

hypershift-jira-solve-ci Bot commented Jun 10, 2026

Copy link
Copy Markdown

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

Test Failure Analysis Complete

Job Information

  • Prow Job: Red Hat Konflux / hypershift-operator-main-enterprise-contract / hypershift-operator-main
  • Build ID: hypershift-operator-main-enterprise-contract-hl8w4
  • Second Job: Red Hat Konflux / hypershift-operator-enterprise-contract / hypershift-operator-main
  • Second Build ID: hypershift-operator-enterprise-contract-ddd9g
  • PR: #8709CNTRLPLANE-3027: Decouple all platform boot image resolvers for dual-stream support
  • Commit: 87fe36fbb4758df03d67b9a93bd05a55fccddd26
  • Snapshot: hypershift-operator-20260610-084646-000
  • Image: quay.io/redhat-user-workloads/crt-redhat-acm-tenant/hypershift-operator/hypershift-operator-main:on-pr-87fe36fbb4758df03d67b9a93bd05a55fccddd26

Test Failure Analysis

Error

Enterprise Contract verify: 254 success(es), 22 warning(s), 2 failure(s)

EC policy violation: test.no_erred_tests
  → The Task "ecosystem-cert-preflight-checks" from the build Pipeline reports a test erred

ecosystem-cert-preflight-checks TEST_OUTPUT:
  {"result":"ERROR","successes":5,"failures":2,"warnings":0,
   "note":"Task preflight is a ERROR: Refer to Tekton task logs for more information"}

Summary

Both Konflux Enterprise Contract (EC) checks failed because the ecosystem-cert-preflight-checks build task reported an ERROR result on PR #8709, whereas it reports FAILURE on all other recent PRs (#8537, #8675, #8712). The EC policy rule test.no_erred_tests produces a hard violation for any test with an ERROR result — unlike the no_failed_tests rule, it has no informative_tests waiver. On other PRs, the same task reports FAILURE (with 6 successes and 2 failures), which is waived as an informative test and only produces a warning. On PR #8709, one preflight sub-check shifted from SUCCESS to ERROR (dropping successes from 6 to 5), flipping the overall task result from FAILURE to ERROR and triggering the unwaivable EC violation. All other build tasks (Clair scan, ClamAV, Snyk SAST, RPM signature scan, etc.) produced identical results between PR #8709 and passing PRs. The control-plane-operator-enterprise-contract check passed on the same commit, confirming the EC infrastructure and policies are healthy — the issue is specific to the hypershift-operator image content.

Root Cause

The ecosystem-cert-preflight-checks Tekton task runs Red Hat certification preflight checks (HasLicense, RunAsNonRoot, HasRequiredLabels, BasedOnUBI, etc.) against the built container image. On PR #8709, one of these sub-checks that normally passes instead errored (the check itself failed to complete, rather than completing with a finding). This shifted the overall task result from FAILURE to ERROR.

The Enterprise Contract policy at policy/release/test/test.rego has two relevant rules:

  1. test.no_failed_tests — Produces a violation for tests with FAILURE result, BUT exempts tests listed in informative_tests (turning them into warnings instead). ecosystem-cert-preflight-checks is in this exemption list, which is why its FAILURE result on other PRs only produces a warning.

  2. test.no_erred_tests — Produces a violation for tests with ERROR result. This rule has no informative_tests exemption. Any ERROR is always a hard violation.

On PR #8709, the task result changed to ERROR, bypassing the informative_tests waiver and triggering the unwaivable no_erred_tests deny rule. This is most likely a flaky/intermittent preflight runner issue rather than a direct consequence of the PR's code changes, because:

  • The Containerfile, base image (ubi9-minimal), and all image labels are unchanged
  • The only image content difference is the compiled Go binary (which now includes coreos/stream-metadata-go)
  • Preflight does not typically deep-inspect compiled Go binary internals
  • The control-plane-operator image (built with the same pipeline and policies on the same commit) passed EC cleanly
  • All other scan results (Clair CVEs, ClamAV, Snyk SAST, RPM signatures) are byte-for-byte identical between PR CNTRLPLANE-3027: Decouple all platform boot image resolvers for dual-stream support #8709 and passing PRs
Recommendations
  1. Re-trigger the Konflux build — Since this is most likely a flaky ecosystem-cert-preflight-checks error, re-running the build pipeline should produce a FAILURE result (waived) instead of ERROR, allowing the EC check to pass. Comment /retest or push an empty commit to trigger a rebuild.

  2. If re-trigger still fails — Inspect the preflight task logs in the Konflux UI at the PipelineRun link to identify which specific preflight sub-check is erroring. Access the build pipeline run logs at hypershift-operator-main-on-pull-request-8nbh4 for the detailed preflight output.

  3. If consistently failing — File a Konflux/EC issue to add ecosystem-cert-preflight-checks to the erred_tests informative list, similar to how it's already in the failed_tests informative list. The current asymmetry (FAILURE is waived but ERROR is not) means a flaky preflight runner can block PRs.

  4. This failure is unrelated to the PR's code changes — The PR itself (decoupling platform boot image resolvers, adding coreos/stream-metadata-go dependency) does not introduce any issues that would cause certification preflight checks to error. The PR can proceed once the EC check passes on re-run.

Evidence
Evidence Detail
PR 8709 ecosystem-cert-preflight-checks result ERROR — 5 successes, 2 failures, 0 warnings
PR 8712 ecosystem-cert-preflight-checks result FAILURE — 6 successes, 2 failures, 0 warnings
PR 8537 ecosystem-cert-preflight-checks result FAILURE — 6 successes, 2 failures, 0 warnings
EC check total on PR 8709 254 success, 22 warning, 2 failure
EC check total on PR 8712 256 success, 22 warning, 0 failure
EC policy rule triggered test.no_erred_tests — no informative_tests waiver for ERROR results
control-plane-operator EC on same commit SUCCESS — same pipeline, same policies
Clair CVE scan Identical: 0 critical, 5 high, 22 medium, 5 low (both PRs)
ClamAV scan Identical: SUCCESS on both PRs
Snyk SAST Identical: SUCCESS on both PRs
RPM signature scan Identical: SUCCESS, all 106 RPMs signed
Coverity FAILURE on both PRs (expired license — pre-existing, waived)
sast-shell-check FAILURE on both PRs (38 findings — pre-existing, waived)
Image build pipeline ✅ All 17 tasks succeeded on PR 8709
Source: EC policy enterprise-contract/ec-policies/policy/release/test/test.rego
Source: SLSA attestation Extracted from OCI artifact at sha256-00c8699903ad...att tag

@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 `@hypershift-operator/controllers/nodepool/powervs.go`:
- Around line 169-172: When releaseImage.StreamForName("") fails, ensure you
update the NodePool condition NodePoolValidPlatformImageType to False before
returning so a stale True isn't left behind; locate the error branch after
calling releaseImage.StreamForName in powervs.go, set the NodePool's
NodePoolValidPlatformImageType condition (via whatever condition helper is used
in this controller) to False with an appropriate message including the error,
persist/update the NodePool status, then return the original fmt.Errorf("%w")
error to preserve existing error propagation.
🪄 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: bc21352a-320a-4d54-aaf4-50823e8cd30e

📥 Commits

Reviewing files that changed from the base of the PR and between 87fe36f and ce1296e.

📒 Files selected for processing (26)
  • hypershift-operator/controllers/nodepool/aws.go
  • hypershift-operator/controllers/nodepool/aws_test.go
  • hypershift-operator/controllers/nodepool/azure.go
  • hypershift-operator/controllers/nodepool/gcp.go
  • hypershift-operator/controllers/nodepool/gcp_test.go
  • hypershift-operator/controllers/nodepool/kubevirt/kubevirt.go
  • hypershift-operator/controllers/nodepool/kubevirt/kubevirt_test.go
  • hypershift-operator/controllers/nodepool/nodepool_controller.go
  • hypershift-operator/controllers/nodepool/nodepool_controller_test.go
  • hypershift-operator/controllers/nodepool/openstack.go
  • hypershift-operator/controllers/nodepool/openstack/openstack.go
  • hypershift-operator/controllers/nodepool/openstack/openstack_test.go
  • hypershift-operator/controllers/nodepool/powervs.go
  • hypershift-operator/controllers/nodepool/powervs_test.go
  • hypershift-operator/controllers/nodepool/stream.go
  • hypershift-operator/controllers/nodepool/stream_test.go
  • hypershift-operator/controllers/nodepool/token.go
  • support/releaseinfo/deserialize.go
  • support/releaseinfo/deserialize_test.go
  • support/releaseinfo/fixtures/5.0-installer-coreos-bootimages.yaml
  • support/releaseinfo/fixtures/fixtures.go
  • support/releaseinfo/registry_mirror_provider.go
  • support/releaseinfo/registryclient_provider.go
  • support/releaseinfo/releaseinfo.go
  • support/releaseinfo/releaseinfo_test.go
  • test/integration/osstreams/osstreams_test.go
🚧 Files skipped from review as they are similar to previous changes (14)
  • support/releaseinfo/registry_mirror_provider.go
  • hypershift-operator/controllers/nodepool/token.go
  • support/releaseinfo/fixtures/fixtures.go
  • hypershift-operator/controllers/nodepool/openstack.go
  • hypershift-operator/controllers/nodepool/kubevirt/kubevirt.go
  • hypershift-operator/controllers/nodepool/gcp.go
  • support/releaseinfo/deserialize.go
  • hypershift-operator/controllers/nodepool/nodepool_controller.go
  • support/releaseinfo/registryclient_provider.go
  • hypershift-operator/controllers/nodepool/azure.go
  • support/releaseinfo/fixtures/5.0-installer-coreos-bootimages.yaml
  • hypershift-operator/controllers/nodepool/openstack/openstack.go
  • test/integration/osstreams/osstreams_test.go
  • hypershift-operator/controllers/nodepool/openstack/openstack_test.go

Comment thread hypershift-operator/controllers/nodepool/powervs.go
jparrill and others added 2 commits June 12, 2026 10:37
…m resolution

Add support for parsing the new multi-stream boot image ConfigMap format
introduced in OCP 5.0 payloads. The ConfigMap now carries a "streams" key
alongside the legacy "stream" key, mapping stream names (rhel-9, rhel-10)
to per-architecture boot image metadata.

- Update DeserializeImageMetadata to parse both "streams" and "stream"
  keys, returning the parsed OSStreams map alongside the default metadata
- Add OSStreams field to ReleaseImage for holding per-stream metadata
- Add StreamForName convenience method on ReleaseImage for stream lookup
- Add GetRHELStream pure function implementing the stream resolution
  table from the dual-stream RHEL enhancement
- Add 5.0 boot image fixture extracted from 5.0.0-ec.2 release payload
- Fallback: when ConfigMap has only "streams" without "stream", populate
  StreamMetadata from the first available stream to prevent nil panics
  in platform consumers

Signed-off-by: Juan Manuel Parrilla Madrid <[email protected]>
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Signed-off-by: Juan Manuel Parrilla Madrid <[email protected]>
Refactor defaultNodePoolAMI() to accept *stream.Stream instead of
*releaseinfo.ReleaseImage, decoupling the AWS boot image resolver
from the global StreamMetadata. Callers (ResolveAWSAMI,
setAWSConditions, setKarpenterAMILabels) resolve the stream
internally via StreamForName(""), preserving backward compatibility.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Signed-off-by: Juan Manuel Parrilla Madrid <[email protected]>
…seImage

Apply the same pattern from CNTRLPLANE-3026 (AWS) to all remaining
platforms: GCP, Azure, PowerVS, OpenStack, and KubeVirt. Each leaf
function now accepts *stream.Stream instead of *releaseinfo.ReleaseImage,
and callers resolve the stream internally via StreamForName("").

- GCP: DefaultNodePoolGCPImage() exported, accepts *stream.Stream
- PowerVS: getPowerVSImage() accepts *stream.Stream
- Azure: getAzureMarketplaceMetadata() accepts *stream.Stream
- OpenStack: OpenstackDefaultImage(), OpenStackReleaseImage(),
  PrefixedClusterImageName() accept *stream.Stream with nil guards
- KubeVirt: defaultImage() accepts *stream.Stream, GetImage()
  resolves stream once and passes to both defaultImage and
  openstack.OpenstackDefaultImage (fallback path)

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Signed-off-by: Juan Manuel Parrilla Madrid <[email protected]>

@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

♻️ Duplicate comments (2)
hypershift-operator/controllers/nodepool/nodepool_controller_test.go (1)

574-575: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Check StreamForName errors in test setup.

Lines 574 and 575 discard errors, which can hide stream fixture regressions and make failures harder to diagnose.

Suggested fix
-	rhel9Stream, _ := ri.StreamForName("rhel-9")
-	rhel10Stream, _ := ri.StreamForName("rhel-10")
+	rhel9Stream, err := ri.StreamForName("rhel-9")
+	if err != nil {
+		t.Fatalf("failed to resolve rhel-9 stream: %v", err)
+	}
+	rhel10Stream, err := ri.StreamForName("rhel-10")
+	if err != nil {
+		t.Fatalf("failed to resolve rhel-10 stream: %v", err)
+	}

As per coding guidelines, "Always check errors in Go — don't ignore 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 `@hypershift-operator/controllers/nodepool/nodepool_controller_test.go` around
lines 574 - 575, The test currently ignores errors returned by StreamForName for
"rhel-9" and "rhel-10"; update the setup to check those errors (capture the
second return values) and fail the test if non-nil (e.g., use
t.Fatalf/t.Fatalf-like assert or require.NoError) so rhel9Stream and
rhel10Stream are only used when StreamForName succeeded; ensure you reference
the returned error variables from StreamForName and replace the discarded "_"
with proper error handling around rhel9Stream and rhel10Stream.

Source: Coding guidelines

hypershift-operator/controllers/nodepool/aws.go (1)

137-141: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Guard releaseImage before StreamForName to avoid panic.

Lines 137 and 368 call a method on releaseImage without a nil check. If release metadata fetch fails upstream and nil is propagated, reconciliation panics instead of returning a controlled error/condition.

Suggested fix
 func resolveAWSAMI(hostedCluster *hyperv1.HostedCluster, nodePool *hyperv1.NodePool, releaseImage *releaseinfo.ReleaseImage) (string, error) {
 	// TODO: Should the region be included in the NodePool platform information?
 	region := hostedCluster.Spec.Platform.AWS.Region
 	arch := nodePool.Spec.Arch
 	if nodePool.Spec.Platform.AWS.AMI != "" {
 		return nodePool.Spec.Platform.AWS.AMI, nil
 	}
 	// Use Windows AMI mapping when ImageType is set to Windows
 	if nodePool.Spec.Platform.AWS.ImageType == hyperv1.ImageTypeWindows {
 		ami, err := getWindowsAMI(region, arch, releaseImage)
 		if err != nil {
 			return "", fmt.Errorf("couldn't discover a Windows AMI for release image: %w", err)
 		}
 		return ami, nil
 	}
 	// Default behavior for Linux/RHCOS AMIs
+	if releaseImage == nil {
+		return "", fmt.Errorf("release image is nil")
+	}
 	streamMeta, err := releaseImage.StreamForName("")
 	if err != nil {
 		return "", fmt.Errorf("couldn't resolve stream metadata: %w", err)
 	}

And in setAWSConditions:

 		} else {
 			// Default behavior for Linux/RHCOS AMIs
+			if releaseImage == nil {
+				SetStatusCondition(&nodePool.Status.Conditions, hyperv1.NodePoolCondition{
+					Type:               hyperv1.NodePoolValidPlatformImageType,
+					Status:             corev1.ConditionFalse,
+					Reason:             hyperv1.NodePoolValidationFailedReason,
+					Message:            fmt.Sprintf("Release image metadata is nil for release image %q", nodePool.Spec.Release.Image),
+					ObservedGeneration: nodePool.Generation,
+				})
+				return fmt.Errorf("release image is nil")
+			}
 			streamMeta, err := releaseImage.StreamForName("")
🤖 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 `@hypershift-operator/controllers/nodepool/aws.go` around lines 137 - 141, The
code calls releaseImage.StreamForName("") without checking releaseImage for nil
(seen around the call to StreamForName and subsequent call to
defaultNodePoolAMI), which can cause a panic if upstream fetch failed; add a nil
guard before calling StreamForName (e.g., if releaseImage == nil return a
controlled error or set the appropriate condition) and propagate that error
instead of dereferencing nil; apply the same nil-check pattern in
setAWSConditions where releaseImage is used so both defaultNodePoolAMI and any
releaseImage-dependent logic safely handle a nil releaseImage.
🤖 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 `@hypershift-operator/controllers/nodepool/gcp.go`:
- Around line 171-175: The code calls releaseImage.StreamForName("") without
guarding releaseImage for nil; update the reconciliation logic to check if
releaseImage is nil before calling StreamForName and return a clear error (or
requeue) if it is nil. Specifically, in the block around releaseImage and the
subsequent call to StreamForName and DefaultNodePoolGCPImage, add a nil-check on
releaseImage (and optionally on streamMeta after StreamForName) and return an
informative fmt.Errorf referencing releaseImage so the caller can handle the
failure instead of panicking.

---

Duplicate comments:
In `@hypershift-operator/controllers/nodepool/aws.go`:
- Around line 137-141: The code calls releaseImage.StreamForName("") without
checking releaseImage for nil (seen around the call to StreamForName and
subsequent call to defaultNodePoolAMI), which can cause a panic if upstream
fetch failed; add a nil guard before calling StreamForName (e.g., if
releaseImage == nil return a controlled error or set the appropriate condition)
and propagate that error instead of dereferencing nil; apply the same nil-check
pattern in setAWSConditions where releaseImage is used so both
defaultNodePoolAMI and any releaseImage-dependent logic safely handle a nil
releaseImage.

In `@hypershift-operator/controllers/nodepool/nodepool_controller_test.go`:
- Around line 574-575: The test currently ignores errors returned by
StreamForName for "rhel-9" and "rhel-10"; update the setup to check those errors
(capture the second return values) and fail the test if non-nil (e.g., use
t.Fatalf/t.Fatalf-like assert or require.NoError) so rhel9Stream and
rhel10Stream are only used when StreamForName succeeded; ensure you reference
the returned error variables from StreamForName and replace the discarded "_"
with proper error handling around rhel9Stream and rhel10Stream.
🪄 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: 41c0d560-9c9d-426d-8b9b-cd8b19a9e19f

📥 Commits

Reviewing files that changed from the base of the PR and between ce1296e and d0050f1.

📒 Files selected for processing (24)
  • hypershift-operator/controllers/nodepool/aws.go
  • hypershift-operator/controllers/nodepool/azure.go
  • hypershift-operator/controllers/nodepool/gcp.go
  • hypershift-operator/controllers/nodepool/gcp_test.go
  • hypershift-operator/controllers/nodepool/kubevirt/kubevirt.go
  • hypershift-operator/controllers/nodepool/kubevirt/kubevirt_test.go
  • hypershift-operator/controllers/nodepool/nodepool_controller.go
  • hypershift-operator/controllers/nodepool/nodepool_controller_test.go
  • hypershift-operator/controllers/nodepool/openstack.go
  • hypershift-operator/controllers/nodepool/openstack/openstack.go
  • hypershift-operator/controllers/nodepool/openstack/openstack_test.go
  • hypershift-operator/controllers/nodepool/powervs.go
  • hypershift-operator/controllers/nodepool/powervs_test.go
  • hypershift-operator/controllers/nodepool/stream.go
  • hypershift-operator/controllers/nodepool/stream_test.go
  • hypershift-operator/controllers/nodepool/token.go
  • support/releaseinfo/deserialize.go
  • support/releaseinfo/deserialize_test.go
  • support/releaseinfo/fixtures/5.0-installer-coreos-bootimages.yaml
  • support/releaseinfo/fixtures/fixtures.go
  • support/releaseinfo/registry_mirror_provider.go
  • support/releaseinfo/registryclient_provider.go
  • support/releaseinfo/releaseinfo.go
  • support/releaseinfo/releaseinfo_test.go
✅ Files skipped from review due to trivial changes (1)
  • support/releaseinfo/fixtures/5.0-installer-coreos-bootimages.yaml
🚧 Files skipped from review as they are similar to previous changes (16)
  • support/releaseinfo/registry_mirror_provider.go
  • hypershift-operator/controllers/nodepool/token.go
  • hypershift-operator/controllers/nodepool/stream.go
  • support/releaseinfo/fixtures/fixtures.go
  • support/releaseinfo/registryclient_provider.go
  • hypershift-operator/controllers/nodepool/openstack.go
  • hypershift-operator/controllers/nodepool/powervs_test.go
  • support/releaseinfo/releaseinfo.go
  • hypershift-operator/controllers/nodepool/kubevirt/kubevirt.go
  • hypershift-operator/controllers/nodepool/azure.go
  • hypershift-operator/controllers/nodepool/kubevirt/kubevirt_test.go
  • hypershift-operator/controllers/nodepool/openstack/openstack_test.go
  • support/releaseinfo/releaseinfo_test.go
  • hypershift-operator/controllers/nodepool/openstack/openstack.go
  • support/releaseinfo/deserialize_test.go
  • hypershift-operator/controllers/nodepool/nodepool_controller.go

Comment on lines +171 to +175
streamMeta, err := releaseImage.StreamForName("")
if err != nil {
return "", fmt.Errorf("couldn't resolve stream metadata: %w", err)
}
image, err := DefaultNodePoolGCPImage(nodePool.Spec.Arch, streamMeta)

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.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Guard releaseImage before StreamForName to avoid panic.

Line 171 calls a method on releaseImage without a nil check. If release metadata fetch fails upstream and nil is propagated, reconciliation panics instead of returning a controlled error.

Suggested fix
 func resolveGCPImage(nodePool *hyperv1.NodePool, releaseImage *releaseinfo.ReleaseImage) (string, error) {
 	gcpPlatform := nodePool.Spec.Platform.GCP
 
 	// If user specified a custom image, use it
 	if gcpPlatform.Image != "" {
 		return gcpPlatform.Image, nil
 	}
 
 	// Resolve image from release metadata
+	if releaseImage == nil {
+		return "", fmt.Errorf("release image is nil")
+	}
 	streamMeta, err := releaseImage.StreamForName("")
 	if err != nil {
 		return "", fmt.Errorf("couldn't resolve stream metadata: %w", 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 `@hypershift-operator/controllers/nodepool/gcp.go` around lines 171 - 175, The
code calls releaseImage.StreamForName("") without guarding releaseImage for nil;
update the reconciliation logic to check if releaseImage is nil before calling
StreamForName and return a clear error (or requeue) if it is nil. Specifically,
in the block around releaseImage and the subsequent call to StreamForName and
DefaultNodePoolGCPImage, add a nil-check on releaseImage (and optionally on
streamMeta after StreamForName) and return an informative fmt.Errorf referencing
releaseImage so the caller can handle the failure instead of panicking.

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/control-plane-operator Indicates the PR includes changes for the control plane operator - in an OCP release area/hypershift-operator Indicates the PR includes changes for the hypershift operator and API - outside an OCP release area/platform/aws PR/issue for AWS (AWSPlatform) platform area/platform/azure PR/issue for Azure (AzurePlatform) platform area/platform/gcp PR/issue for GCP (GCPPlatform) platform area/platform/kubevirt PR/issue for KubeVirt (KubevirtPlatform) platform area/platform/openstack PR/issue for OpenStack (OpenStackPlatform) platform area/platform/powervs PR/issue for PowerVS (PowerVSPlatform) platform area/testing Indicates the PR includes changes for e2e testing do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. 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.

2 participants