Skip to content

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

Draft
jparrill wants to merge 9 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 9 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() — 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

Summary by CodeRabbit

  • Refactor

    • Switched image metadata handling to a standardized stream model, improving boot image resolution and error reporting across AWS, Azure, GCP, OpenStack, PowerVS and KubeVirt.
    • Added multi-stream support for deriving RHCOS/release variants.
  • Tests

    • Expanded unit and integration tests covering end-to-end stream resolution, platform image selection, and error scenarios.
    • Updated fixtures to exercise multi-stream metadata.
  • Chores

    • Added stream-metadata-go dependency.

sdminonne and others added 7 commits June 8, 2026 13:03
Replace 18 custom CoreOS stream metadata Go types with canonical
equivalents from github.com/coreos/stream-metadata-go v0.4.11.
This eliminates duplicated type definitions and gains upstream
helper methods (e.g. GetAMI(), URN()), laying groundwork for the
dual-stream RHEL NodePool feature.

Key type mappings:
- CoreOSStreamMetadata -> stream.Stream
- CoreOSArchitecture -> stream.Arch
- CoreRHCOSImage -> *rhcos.Extensions (pointer)
- CoreOSAWSImages -> *stream.AwsImage (pointer)
- CoreOSGCPImage -> *stream.GcpImage (pointer)
- CoreOSPowerVSImage -> *stream.SingleObject
- CoreOSKubevirtImages -> *stream.ContainerImage (pointer)
- HyperVGen1/HyperVGen2 -> Gen1/Gen2
- SHA256 -> Sha256, URL -> Url (PowerVS)

Pure refactor with no behavioral changes. All platform controllers
updated with nil checks for pointer-typed upstream fields.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…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

Signed-off-by: Juan Manuel Parrilla Madrid <jparrill@redhat.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Juan Manuel Parrilla Madrid <jparrill@redhat.com>
Integration test that validates the complete flow from real 5.0 EC
payload parsing through stream resolution to boot image lookup:
- Parse multi-stream ConfigMap from 5.0.0-ec.2 payload fixture
- Resolve RHEL stream via GetRHELStream for each scenario
- Look up stream-specific boot images via StreamForName
- Verify correct AWS AMI, GCP image, aarch64 AMI, and KubeVirt
  digest-ref for the resolved stream

Covers realistic 5.0+ scenarios only (implicit default, explicit
rhel-9/rhel-10, runc fallback, error cases) plus legacy 4.10
backward compatibility with real fixture data.

Signed-off-by: Juan Manuel Parrilla Madrid <jparrill@redhat.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Juan Manuel Parrilla Madrid <jparrill@redhat.com>
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.

This is Phase 1 step 3 (AWS) of the dual-stream RHEL enhancement
(OCPSTRAT-3014). The same pattern will be applied to other platform
resolvers in CNTRLPLANE-3027.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Juan Manuel Parrilla Madrid <jparrill@redhat.com>
Add TestBootImageStreamResolution with AWS subtests covering the
end-to-end flow from HostedCluster + NodePool + ReleaseImage through
ResolveAWSAMI. Tests multi-stream payload resolution, user AMI
override precedence, legacy 4.x backward compatibility, unsupported
architecture and missing region error paths. Structured with
t.Run("AWS", ...) so future providers (GCP, KubeVirt, etc.) are
added as new subtests in CNTRLPLANE-3027.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Juan Manuel Parrilla Madrid <jparrill@redhat.com>
…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
- 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) <noreply@anthropic.com>
Signed-off-by: Juan Manuel Parrilla Madrid <jparrill@redhat.com>
Extend TestBootImageStreamResolution with GCP subtests covering
multi-stream image resolution (rhel-9, rhel-10), default stream
backward compatibility, and nil metadata error path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Juan Manuel Parrilla Madrid <jparrill@redhat.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

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

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: 877834b2-ece2-44f9-b22e-5819163f5e12

📥 Commits

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

📒 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 (9)
  • hypershift-operator/controllers/nodepool/openstack.go
  • hypershift-operator/controllers/nodepool/azure.go
  • hypershift-operator/controllers/nodepool/powervs_test.go
  • hypershift-operator/controllers/nodepool/token.go
  • hypershift-operator/controllers/nodepool/kubevirt/kubevirt_test.go
  • test/integration/osstreams/osstreams_test.go
  • support/releaseinfo/releaseinfo.go
  • hypershift-operator/controllers/nodepool/openstack/openstack_test.go
  • hypershift-operator/controllers/nodepool/powervs.go

📝 Walkthrough

Walkthrough

This PR migrates HyperShift's release image metadata handling from internal CoreOS*-based types to the external github.com/coreos/stream-metadata-go/stream library. The change enables multi-stream RHCOS release support, allowing independent version tracks (rhel-9, rhel-10) alongside defaults. All platform-specific image resolvers—AWS, Azure, GCP, OpenStack, KubeVirt, and PowerVS—are refactored to accept stream metadata objects directly instead of full ReleaseImage structs. Deserialization logic now parses both stream and streams ConfigMap keys, and ReleaseImage.StreamForName() provides unified access to default or named streams. A comprehensive 5.0 boot images fixture and integration test suite validate resolution across platforms and stream selections.

Suggested reviewers

  • sdminonne
  • devguyio
  • muraee
🚥 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 accurately describes the main change: decoupling all platform boot image resolvers from ReleaseImage to use stream.Stream directly for dual-stream support.
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 All test names are stable and deterministic. No timestamps, UUIDs, pod/node names, or random values found in any test case names. All test titles use descriptive static strings.
Test Structure And Quality ✅ Passed This PR uses standard Go testing (testing.T) with Gomega assertions, not Ginkgo framework. The custom check targets Ginkgo tests, making it inapplicable here.
Topology-Aware Scheduling Compatibility ✅ Passed PR introduces no scheduling constraints, pod affinity rules, PDBs, or topology-related configuration. Changes are purely boot image discovery metadata refactoring for dual-stream RHEL 9/10 support.
Ipv6 And Disconnected Network Test Compatibility ✅ Passed New tests in PR are standard Go tests (not Ginkgo e2e), with no IPv4 assumptions or external connectivity requirements. Check not applicable but tests pass all compatibility criteria.
No-Weak-Crypto ✅ Passed No weak crypto (MD5/SHA1/DES/RC4/3DES/Blowfish/ECB), custom implementations, or non-constant-time secret comparisons found. SHA256 used only for artifact integrity.
Container-Privileges ✅ Passed No container privilege configurations found in PR files. Only fixture ConfigMap with boot image metadata; no Kubernetes specs or security contexts introduced.
No-Sensitive-Data-In-Logs ✅ Passed Error messages in deserialize.go log boot image metadata on failure—no passwords, tokens, API keys, PII, or credentials are exposed in logging.

✏️ 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 61.86441% with 90 lines in your changes missing coverage. Please review.
✅ Project coverage is 41.57%. Comparing base (ad3da77) to head (87fe36f).
⚠️ Report is 10 commits behind head on main.

Files with missing lines Patch % Lines
...ypershift-operator/controllers/nodepool/powervs.go 28.57% 20 Missing ⚠️
...ershift-operator/controllers/nodepool/openstack.go 0.00% 17 Missing ⚠️
hypershift-operator/controllers/nodepool/aws.go 38.09% 13 Missing ⚠️
support/releaseinfo/testutils/testutils.go 0.00% 10 Missing ⚠️
...erator/controllers/nodepool/openstack/openstack.go 71.42% 7 Missing and 1 partial ⚠️
...operator/controllers/nodepool/kubevirt/kubevirt.go 60.00% 6 Missing ⚠️
hypershift-operator/controllers/nodepool/azure.go 81.48% 2 Missing and 3 partials ⚠️
hypershift-operator/controllers/nodepool/gcp.go 40.00% 2 Missing and 1 partial ⚠️
...erator/controllers/nodepool/nodepool_controller.go 78.57% 2 Missing and 1 partial ⚠️
hypershift-operator/controllers/nodepool/token.go 40.00% 2 Missing and 1 partial ⚠️
... and 1 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #8709      +/-   ##
==========================================
+ Coverage   41.49%   41.57%   +0.08%     
==========================================
  Files         756      758       +2     
  Lines       93648    93832     +184     
==========================================
+ Hits        38855    39012     +157     
- Misses      52057    52085      +28     
+ Partials     2736     2735       -1     
Files with missing lines Coverage Δ
support/releaseinfo/deserialize.go 90.00% <100.00%> (+50.00%) ⬆️
support/releaseinfo/registry_mirror_provider.go 45.83% <100.00%> (+2.35%) ⬆️
support/releaseinfo/releaseinfo.go 56.25% <100.00%> (+10.39%) ⬆️
support/releaseinfo/registryclient_provider.go 0.00% <0.00%> (ø)
hypershift-operator/controllers/nodepool/gcp.go 65.77% <40.00%> (-0.74%) ⬇️
...erator/controllers/nodepool/nodepool_controller.go 43.47% <78.57%> (+0.34%) ⬆️
hypershift-operator/controllers/nodepool/token.go 81.81% <40.00%> (-0.73%) ⬇️
hypershift-operator/controllers/nodepool/azure.go 87.69% <81.48%> (-2.89%) ⬇️
...operator/controllers/nodepool/kubevirt/kubevirt.go 71.25% <60.00%> (+2.01%) ⬆️
...erator/controllers/nodepool/openstack/openstack.go 74.10% <71.42%> (-2.33%) ⬇️
... and 4 more

... and 7 files with indirect coverage changes

Flag Coverage Δ
cmd-support 35.00% <84.21%> (+0.12%) ⬆️
cpo-hostedcontrolplane 43.59% <ø> (+0.09%) ⬆️
cpo-other 43.17% <ø> (ø)
hypershift-operator 51.65% <51.25%> (+0.08%) ⬆️
other 31.64% <ø> (ø)

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 on lines +137 to +141
streamMeta, err := releaseImage.StreamForName("")
if err != nil {
return "", fmt.Errorf("couldn't resolve stream metadata: %w", err)
}
ami, err := defaultNodePoolAMI(region, 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 | 🟠 Major | ⚡ Quick win

Guard releaseImage before StreamForName to avoid panic.

Line 137 and Line 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) {
@@
 	// 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)
 	}
@@
 		} 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("")
 			if err != nil {

Also applies to: 368-379

🤖 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("") (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.

Comment on lines +574 to +575
rhel9Stream, _ := ri.StreamForName("rhel-9")
rhel10Stream, _ := ri.StreamForName("rhel-10")

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

Check StreamForName errors in test setup.

Line 574 and Line 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 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.

Source: Coding guidelines

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

jparrill and others added 2 commits June 10, 2026 09:46
…tream

Add nil guards for streamMeta in getPowerVSImage, OpenstackDefaultImage,
OpenStackReleaseImage, PrefixedClusterImageName, and defaultImage
(kubevirt) matching the AWS/GCP pattern.

Add NodePoolValidPlatformImageType status conditions for stream
resolution failures in setPowerVSconditions and setOpenStackConditions,
matching setAWSConditions.

Replace manual architecture switch in Azure with hyperv1.ArchAliases.

Move GetRHELStream from controllers/nodepool to support/releaseinfo
to fix the dependency direction for Phase 2 consumers.

Add unit tests for getPowerVSImage, getImageRegion, and kubevirt
defaultImage error paths.

Signed-off-by: Juan Manuel Parrilla Madrid <jparrill@redhat.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Update integration test imports to reference GetRHELStream from
support/releaseinfo instead of controllers/nodepool, fixing the
dependency direction.

Signed-off-by: Juan Manuel Parrilla Madrid <jparrill@redhat.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@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

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.

3 participants