Skip to content

MCO-2117: Allow default OSImageStream overrides#5714

Open
pablintino wants to merge 1 commit intoopenshift:mainfrom
pablintino:mco-2117
Open

MCO-2117: Allow default OSImageStream overrides#5714
pablintino wants to merge 1 commit intoopenshift:mainfrom
pablintino:mco-2117

Conversation

@pablintino
Copy link
Contributor

@pablintino pablintino commented Feb 27, 2026

- What I did

Previously the default stream was determined by matching the OS version in the stream name, without the possibility of telling the MCO which default stream to use. This change replaces that logic, allowing users to input the defaultStream they want to use and falling back to the builtin if necessary. The builtin default stream determination logic has been improved to avoid hard-coding versions and it now picks the OSImageStream that points to the images of the default OS tags in the payload ImageStream.

- How to verify it

There are two main scenarios to test.

Install a cluster with a different default set at day-zero

mkdir installer-dir
./openshift-install --dir installer-dir create manifests

# Enable TechPreview
yq -i '.spec.featureSet = "TechPreviewNoUpgrade"' installer-dir/openshift/99_feature-gate.yaml

# Create the simple bare OSImageStream with just the spec
cat << 'EOF' > installer-dir/openshift/99_osimagestream.yaml
apiVersion: machineconfiguration.openshift.io/v1alpha1
kind: OSImageStream
metadata:
  name: cluster
spec:
  defaultStream: rhel-10
EOF

# Deploy the cluster
./openshift-install --dir installer-dir create cluster

# Ensure the nodes are using the expected stream
oc debug node/$(oc get nodes -l node-role.kubernetes.io/master -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release
Starting pod/pabrodri-test-xfllf-master-0-debug-d69vl ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.2 Beta (Coughlan)

oc debug node/$(oc get nodes -l node-role.kubernetes.io/worker -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release
Starting pod/pabrodri-test-xfllf-worker-a-nvh9j-debug-76blg ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.2 Beta (Coughlan)

Change the global default of a cluster running default MCPs

  1. Install a fresh OCP cluster containing this change with the default and TechPreview enabled.
  2. Wait for it to be fully stable.
  3. Ensure all pools are RHEL 9.
  4. Change the default
oc patch osimagestream cluster --type='merge' -p '{"spec":{"defaultStream":"rhel-10"}}'
  1. Wait for the pools to fully update
  2. Ensure nodes are running RHEL 10
oc debug node/$(oc get nodes -l node-role.kubernetes.io/master -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release                                                                                                      20:04:46
Starting pod/pabrodri-test-lppjx-master-0-debug-vrvgv ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.2 Beta (Coughlan)

oc debug node/$(oc get nodes -l node-role.kubernetes.io/worker -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release
Starting pod/pabrodri-test-lppjx-worker-a-thv4m-debug-plvg8 ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.2 Beta (Coughlan)

- Description for the changelog

Allow users to override the default OSImageStream via Spec.DefaultStream, falling back to the builtin default resolved from the payload ImageStream tags.

Summary by CodeRabbit

  • New Features

    • Support for user-provided OS image streams with automatic default selection (RHEL9 for 4.x, RHEL10 for 5.x+) and event-driven reconcilation when streams change.
    • Public helpers to expose builtin and spec default stream values.
  • Performance

    • Thread-safe caching of image stream retrieval to reduce repeated fetches.
  • Chores

    • Unified OS image-stream creation flow and dependency updates.
  • Tests

    • Expanded unit and e2e coverage for cluster-version parsing, default resolution, caching, create/update flows.

@openshift-ci-robot openshift-ci-robot added the jira/valid-reference Indicates that this PR references a valid Jira ticket of any type. label Feb 27, 2026
@openshift-ci-robot
Copy link
Contributor

openshift-ci-robot commented Feb 27, 2026

@pablintino: This pull request references MCO-2117 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 "4.22.0" version, but no target version was set.

Details

In response to this:

- What I did

Previously the default stream was determined by matching the OS version in the stream name, without the possibility of telling the MCO which default stream to use. This change replaces that logic, allowing users to input the defaultStream they want to use and falling back to the builtin if necessary. The builtin default stream determination logic has been improved to avoid hard-coding versions and it now picks the OSImageStream that points to the images of the default OS tags in the payload ImageStream.

- How to verify it

TBD

- Description for the changelog

Allow users to override the default OSImageStream via Spec.DefaultStream, falling back to the builtin default resolved from the payload ImageStream tags.

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 openshift-ci bot added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label Feb 27, 2026
@pablintino pablintino force-pushed the mco-2117 branch 3 times, most recently from 1a8d2b6 to 6acb9de Compare February 27, 2026 12:09
Comment on lines +497 to +511
sysCtx, err := sysCtxBuilder.Build()
if err != nil {
return nil, fmt.Errorf("could not prepare for OSImageStream inspection: %w", err)
}
defer func() {
if err := sysCtx.Cleanup(); err != nil {
klog.Warningf("Unable to clean resources after OSImageStream inspection: %s", err)
}
}()

factory := osimagestream.NewDefaultStreamSourceFactory(&osimagestream.DefaultImagesInspectorFactory{})
osImageStream, err := factory.Create(ctx, sysCtx.SysContext, osimagestream.CreateOptions{
ExistingOSImageStream: existingOSImageStream,
ReleaseImageStream: imageStream,
CliImages: &osimagestream.OSImageTuple{
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note for reviewers: This change is "this big" mostly because the removal of the thin BuildOsImageStreamBootstrap wrappers as part of this PR. The important point, from the default handling point of view is that the ExistingOSImageStream is filled with whatever the installer passed.

ReleaseImageVersionAnnotationKey = "machineconfiguration.openshift.io/release-image-version"

// BuiltinDefaultStreamAnnotationKey is the MCO fallback default stream.
BuiltinDefaultStreamAnnotationKey = "machineconfiguration.openshift.io/builtin-default-stream"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note for the reviewer: Used to store the fallback stream the MCO would use in case no default is provided by the installer/user.
Why do we need to store it? To avoid inspecting the payload over and over again during operator syncs.

Copy link
Contributor

@djoshy djoshy Mar 4, 2026

Choose a reason for hiding this comment

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

I think this is a good thing, since consulting the payload during an upgrade scenario might be problematic on a default change. How we have it setup right now should only cause a rebuild on MCO hash changes.

I wonder if we could upgrade test such a scenario to see if it behaves as we expect, though.

Copy link
Contributor

@djoshy djoshy Mar 4, 2026

Choose a reason for hiding this comment

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

Thinking about this more...as we're going to use an upgrade block to force all pools to transition to the new default stream before upgrading, the scenario described above should never happen? Sorry for the noise 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@djoshy 100% the idea is to add the EoL in a z stream before we expect the stream to dissapear. The mechanism? A simple upgrade condition. I think the main agreement we made was delete streams in major releases only, that will help too.

Comment on lines +109 to +127
sysCtx, err := sysCtxBuilder.Build()
if err != nil {
return fmt.Errorf("could not prepare for OSImageStream inspection: %w", err)
}
defer func() {
if err := sysCtx.Cleanup(); err != nil {
klog.Warningf("Unable to clean resources after OSImageStream inspection: %s", err)
}
}()

factory := osimagestream.NewDefaultStreamSourceFactory(&osimagestream.DefaultImagesInspectorFactory{})
osImageStream, err := factory.Create(
buildCtx,
sysCtx.SysContext,
osimagestream.CreateOptions{
ReleaseImage: image,
ConfigMapLister: optr.mcoCmLister,
ExistingOSImageStream: existingOSImageStream,
})
Copy link
Contributor Author

Choose a reason for hiding this comment

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

}
releaseVersion, ok := osImageStream.Annotations[ctrlcommon.ReleaseImageVersionAnnotationKey]
return !ok || releaseVersion != version.Hash
return !ok || releaseVersion != version.Hash || osimagestream.GetBuiltinDefault(osImageStream) == ""
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note for the reviewer: If the annotation is removed ensure we rebuild the CR. Shouldn't happen, but just in case an admin user removes it.

Comment on lines +54 to +65
i.cacheLock.Lock()
defer i.cacheLock.Unlock()
if i.imageStream != nil {
return i.imageStream, nil
}
is, err := i.fetchImageStream(ctx)
if err != nil {
return nil, err
}
i.imageStream = is
return is, nil
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note for the reviewer: Now we have a look at the ImageStream twice, one to fetch the streams and another one to determine the default stream. To avoid that new extra call (or future calls if we need more) I've added this simple cache mechanism.

Comment on lines -33 to +32
// CreateRuntimeSources builds an OSImageStream for runtime operation.
CreateRuntimeSources(ctx context.Context, releaseImage string, sysCtx *types.SystemContext) (*v1alpha1.OSImageStream, error)
// CreateBootstrapSources builds an OSImageStream for bootstrap operation.
CreateBootstrapSources(ctx context.Context, imageStream *imagev1.ImageStream, cliImages *OSImageTuple, sysCtx *types.SystemContext) (*v1alpha1.OSImageStream, error)
// Create builds an OSImageStream from the configured sources and options.
Create(ctx context.Context, sysCtx *types.SystemContext, opts CreateOptions) (*v1alpha1.OSImageStream, error)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note for the reviewer: With the inclusion of the default stream field the signature of these two functions started to be a mess I didn't like at all so I converged both and switch to a single arg with all the possible options as fields.

Comment on lines +690 to +696
cv, err := clientSet.ClusterVersions().Get(ctx, "version", metav1.GetOptions{})
require.NoError(t, err, "Error retrieving ClusterVersion")

payloadVersion, err := semver.ParseTolerant(cv.Status.Desired.Version)
require.NoError(t, err, "Error parsing payload version")

featureGateStatus := features.FeatureSets(payloadVersion.Major, SelfManaged, currentFeatureSet)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note for the reviewer: This is because of a breaking change in the FeatureGate's API.

@openshift-ci-robot
Copy link
Contributor

openshift-ci-robot commented Mar 2, 2026

@pablintino: This pull request references MCO-2117 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 "4.22.0" version, but no target version was set.

Details

In response to this:

- What I did

Previously the default stream was determined by matching the OS version in the stream name, without the possibility of telling the MCO which default stream to use. This change replaces that logic, allowing users to input the defaultStream they want to use and falling back to the builtin if necessary. The builtin default stream determination logic has been improved to avoid hard-coding versions and it now picks the OSImageStream that points to the images of the default OS tags in the payload ImageStream.

- How to verify it

There are two main scenarios to test.

Install a cluster with a different default set at day-zero

  1. Create the manifests using the intaller:
mkdir installer-dir
./openshift-install --dir installer-dir create manifests

# Enable TechPreview
yq -i '.spec.featureSet = "TechPreviewNoUpgrade"' installer-dir/openshift/99_feature-gate.yaml

# Create the simple bare OSImageStream with just the spec
cat << 'EOF' > installer-dir/openshift/99_osimagestream.yaml
apiVersion: machineconfiguration.openshift.io/v1alpha1
kind: OSImageStream
metadata:
 name: cluster
spec:
 defaultStream: rhel-10
EOF

# Deploy the cluster
./openshift-install --dir installer-dir create cluster

- Description for the changelog

Allow users to override the default OSImageStream via Spec.DefaultStream, falling back to the builtin default resolved from the payload ImageStream tags.

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
Copy link
Contributor

openshift-ci-robot commented Mar 2, 2026

@pablintino: This pull request references MCO-2117 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 "4.22.0" version, but no target version was set.

Details

In response to this:

- What I did

Previously the default stream was determined by matching the OS version in the stream name, without the possibility of telling the MCO which default stream to use. This change replaces that logic, allowing users to input the defaultStream they want to use and falling back to the builtin if necessary. The builtin default stream determination logic has been improved to avoid hard-coding versions and it now picks the OSImageStream that points to the images of the default OS tags in the payload ImageStream.

- How to verify it

There are two main scenarios to test.

Install a cluster with a different default set at day-zero

  1. Create the manifests using the intaller:
mkdir installer-dir
./openshift-install --dir installer-dir create manifests

# Enable TechPreview
yq -i '.spec.featureSet = "TechPreviewNoUpgrade"' installer-dir/openshift/99_feature-gate.yaml

# Create the simple bare OSImageStream with just the spec
cat << 'EOF' > installer-dir/openshift/99_osimagestream.yaml
apiVersion: machineconfiguration.openshift.io/v1alpha1
kind: OSImageStream
metadata:
 name: cluster
spec:
 defaultStream: rhel-10
EOF

# Deploy the cluster
./openshift-install --dir installer-dir create cluster

# Ensure the nodes are using the expected stream
oc debug node/$(oc get nodes -l node-role.kubernetes.io/master -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release                                                                                                               14:33:37
Starting pod/pabrodri-test-xfllf-master-0-debug-d69vl ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.1 (Coughlan)

- Description for the changelog

Allow users to override the default OSImageStream via Spec.DefaultStream, falling back to the builtin default resolved from the payload ImageStream tags.

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
Copy link
Contributor

openshift-ci-robot commented Mar 2, 2026

@pablintino: This pull request references MCO-2117 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 "4.22.0" version, but no target version was set.

Details

In response to this:

- What I did

Previously the default stream was determined by matching the OS version in the stream name, without the possibility of telling the MCO which default stream to use. This change replaces that logic, allowing users to input the defaultStream they want to use and falling back to the builtin if necessary. The builtin default stream determination logic has been improved to avoid hard-coding versions and it now picks the OSImageStream that points to the images of the default OS tags in the payload ImageStream.

- How to verify it

There are two main scenarios to test.

Install a cluster with a different default set at day-zero
  1. Create the manifests using the intaller:
mkdir installer-dir
./openshift-install --dir installer-dir create manifests

# Enable TechPreview
yq -i '.spec.featureSet = "TechPreviewNoUpgrade"' installer-dir/openshift/99_feature-gate.yaml

# Create the simple bare OSImageStream with just the spec
cat << 'EOF' > installer-dir/openshift/99_osimagestream.yaml
apiVersion: machineconfiguration.openshift.io/v1alpha1
kind: OSImageStream
metadata:
 name: cluster
spec:
 defaultStream: rhel-10
EOF

# Deploy the cluster
./openshift-install --dir installer-dir create cluster

# Ensure the nodes are using the expected stream
oc debug node/$(oc get nodes -l node-role.kubernetes.io/master -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release                                                                                                               14:33:37
Starting pod/pabrodri-test-xfllf-master-0-debug-d69vl ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.1 (Coughlan)

- Description for the changelog

Allow users to override the default OSImageStream via Spec.DefaultStream, falling back to the builtin default resolved from the payload ImageStream tags.

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
Copy link
Contributor

openshift-ci-robot commented Mar 2, 2026

@pablintino: This pull request references MCO-2117 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 "4.22.0" version, but no target version was set.

Details

In response to this:

- What I did

Previously the default stream was determined by matching the OS version in the stream name, without the possibility of telling the MCO which default stream to use. This change replaces that logic, allowing users to input the defaultStream they want to use and falling back to the builtin if necessary. The builtin default stream determination logic has been improved to avoid hard-coding versions and it now picks the OSImageStream that points to the images of the default OS tags in the payload ImageStream.

- How to verify it

There are two main scenarios to test.

Install a cluster with a different default set at day-zero

  1. Create the manifests using the intaller:
mkdir installer-dir
./openshift-install --dir installer-dir create manifests

# Enable TechPreview
yq -i '.spec.featureSet = "TechPreviewNoUpgrade"' installer-dir/openshift/99_feature-gate.yaml

# Create the simple bare OSImageStream with just the spec
cat << 'EOF' > installer-dir/openshift/99_osimagestream.yaml
apiVersion: machineconfiguration.openshift.io/v1alpha1
kind: OSImageStream
metadata:
 name: cluster
spec:
 defaultStream: rhel-10
EOF

# Deploy the cluster
./openshift-install --dir installer-dir create cluster

# Ensure the nodes are using the expected stream
oc debug node/$(oc get nodes -l node-role.kubernetes.io/master -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release                                                                                                               14:33:37
Starting pod/pabrodri-test-xfllf-master-0-debug-d69vl ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.1 (Coughlan)

Move an entire cluster, that uses MCPs with no explicit osImageStream to rhel-10 in a single move

- Description for the changelog

Allow users to override the default OSImageStream via Spec.DefaultStream, falling back to the builtin default resolved from the payload ImageStream tags.

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
Copy link
Contributor

openshift-ci-robot commented Mar 2, 2026

@pablintino: This pull request references MCO-2117 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 "4.22.0" version, but no target version was set.

Details

In response to this:

- What I did

Previously the default stream was determined by matching the OS version in the stream name, without the possibility of telling the MCO which default stream to use. This change replaces that logic, allowing users to input the defaultStream they want to use and falling back to the builtin if necessary. The builtin default stream determination logic has been improved to avoid hard-coding versions and it now picks the OSImageStream that points to the images of the default OS tags in the payload ImageStream.

- How to verify it

There are two main scenarios to test.

Install a cluster with a different default set at day-zero

  1. Create the manifests using the intaller:
mkdir installer-dir
./openshift-install --dir installer-dir create manifests

# Enable TechPreview
yq -i '.spec.featureSet = "TechPreviewNoUpgrade"' installer-dir/openshift/99_feature-gate.yaml

# Create the simple bare OSImageStream with just the spec
cat << 'EOF' > installer-dir/openshift/99_osimagestream.yaml
apiVersion: machineconfiguration.openshift.io/v1alpha1
kind: OSImageStream
metadata:
 name: cluster
spec:
 defaultStream: rhel-10
EOF

# Deploy the cluster
./openshift-install --dir installer-dir create cluster

# Ensure the nodes are using the expected stream
oc debug node/$(oc get nodes -l node-role.kubernetes.io/master -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release                                                                                                               14:33:37
Starting pod/pabrodri-test-xfllf-master-0-debug-d69vl ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.1 (Coughlan)

oc debug node/$(oc get nodes -l node-role.kubernetes.io/worker -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release                                                                                                    14:39:01
Starting pod/pabrodri-test-xfllf-worker-a-nvh9j-debug-76blg ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.1 (Coughlan)

Removing debug pod ...

Move an entire cluster, that uses MCPs with no explicit osImageStream to rhel-10 in a single move

- Description for the changelog

Allow users to override the default OSImageStream via Spec.DefaultStream, falling back to the builtin default resolved from the payload ImageStream tags.

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
Copy link
Contributor

openshift-ci-robot commented Mar 2, 2026

@pablintino: This pull request references MCO-2117 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 "4.22.0" version, but no target version was set.

Details

In response to this:

- What I did

Previously the default stream was determined by matching the OS version in the stream name, without the possibility of telling the MCO which default stream to use. This change replaces that logic, allowing users to input the defaultStream they want to use and falling back to the builtin if necessary. The builtin default stream determination logic has been improved to avoid hard-coding versions and it now picks the OSImageStream that points to the images of the default OS tags in the payload ImageStream.

- How to verify it

There are two main scenarios to test.

Install a cluster with a different default set at day-zero

  1. Create the manifests using the intaller:
mkdir installer-dir
./openshift-install --dir installer-dir create manifests

# Enable TechPreview
yq -i '.spec.featureSet = "TechPreviewNoUpgrade"' installer-dir/openshift/99_feature-gate.yaml

# Create the simple bare OSImageStream with just the spec
cat << 'EOF' > installer-dir/openshift/99_osimagestream.yaml
apiVersion: machineconfiguration.openshift.io/v1alpha1
kind: OSImageStream
metadata:
 name: cluster
spec:
 defaultStream: rhel-10
EOF

# Deploy the cluster
./openshift-install --dir installer-dir create cluster

# Ensure the nodes are using the expected stream
oc debug node/$(oc get nodes -l node-role.kubernetes.io/master -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release                                                                                                               14:33:37
Starting pod/pabrodri-test-xfllf-master-0-debug-d69vl ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.1 (Coughlan)

oc debug node/$(oc get nodes -l node-role.kubernetes.io/worker -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release                                                                                                    14:39:01
Starting pod/pabrodri-test-xfllf-worker-a-nvh9j-debug-76blg ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.1 (Coughlan)
  1. Install a fresh OCP cluster containing this change with the default and TechPreview enabled.
  2. Wait for it to be fully stable.
  3. Ensure all pools are RHEL 9.
  4. Change the default
oc patch osimagestream cluster --type='merge' -p '{"spec":{"defaultStream":"rhel-10"}}'
  1. Wait for the pools to fully update
  2. Ensure nodes are running RHEL 10
oc debug node/$(oc get nodes -l node-role.kubernetes.io/master -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release                                                                                                               14:33:37
Starting pod/pabrodri-test-xfllf-master-0-debug-d69vl ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.1 (Coughlan)

oc debug node/$(oc get nodes -l node-role.kubernetes.io/worker -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release                                                                                                    14:39:01
Starting pod/pabrodri-test-xfllf-worker-a-nvh9j-debug-76blg ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.1 (Coughlan)
####  Move an entire cluster, that uses MCPs with no explicit osImageStream to rhel-10 in a single move


**- Description for the changelog**

Allow users to override the default OSImageStream via Spec.DefaultStream, falling back to the builtin default resolved from the payload ImageStream tags.

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
Copy link
Contributor

openshift-ci-robot commented Mar 2, 2026

@pablintino: This pull request references MCO-2117 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 "4.22.0" version, but no target version was set.

Details

In response to this:

- What I did

Previously the default stream was determined by matching the OS version in the stream name, without the possibility of telling the MCO which default stream to use. This change replaces that logic, allowing users to input the defaultStream they want to use and falling back to the builtin if necessary. The builtin default stream determination logic has been improved to avoid hard-coding versions and it now picks the OSImageStream that points to the images of the default OS tags in the payload ImageStream.

- How to verify it

There are two main scenarios to test.

Install a cluster with a different default set at day-zero

mkdir installer-dir
./openshift-install --dir installer-dir create manifests

# Enable TechPreview
yq -i '.spec.featureSet = "TechPreviewNoUpgrade"' installer-dir/openshift/99_feature-gate.yaml

# Create the simple bare OSImageStream with just the spec
cat << 'EOF' > installer-dir/openshift/99_osimagestream.yaml
apiVersion: machineconfiguration.openshift.io/v1alpha1
kind: OSImageStream
metadata:
 name: cluster
spec:
 defaultStream: rhel-10
EOF

# Deploy the cluster
./openshift-install --dir installer-dir create cluster

# Ensure the nodes are using the expected stream
oc debug node/$(oc get nodes -l node-role.kubernetes.io/master -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release                                                                                                               14:33:37
Starting pod/pabrodri-test-xfllf-master-0-debug-d69vl ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.1 (Coughlan)

oc debug node/$(oc get nodes -l node-role.kubernetes.io/worker -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release                                                                                                    14:39:01
Starting pod/pabrodri-test-xfllf-worker-a-nvh9j-debug-76blg ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.1 (Coughlan)
  1. Install a fresh OCP cluster containing this change with the default and TechPreview enabled.
  2. Wait for it to be fully stable.
  3. Ensure all pools are RHEL 9.
  4. Change the default
oc patch osimagestream cluster --type='merge' -p '{"spec":{"defaultStream":"rhel-10"}}'
  1. Wait for the pools to fully update
  2. Ensure nodes are running RHEL 10
oc debug node/$(oc get nodes -l node-role.kubernetes.io/master -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release                                                                                                               14:33:37
Starting pod/pabrodri-test-xfllf-master-0-debug-d69vl ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.1 (Coughlan)

oc debug node/$(oc get nodes -l node-role.kubernetes.io/worker -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release                                                                                                    14:39:01
Starting pod/pabrodri-test-xfllf-worker-a-nvh9j-debug-76blg ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.1 (Coughlan)

Move an entire cluster, that uses MCPs with no explicit osImageStream to rhel-10 in a single move

- Description for the changelog

Allow users to override the default OSImageStream via Spec.DefaultStream, falling back to the builtin default resolved from the payload ImageStream tags.

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
Copy link
Contributor

openshift-ci-robot commented Mar 2, 2026

@pablintino: This pull request references MCO-2117 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 "4.22.0" version, but no target version was set.

Details

In response to this:

- What I did

Previously the default stream was determined by matching the OS version in the stream name, without the possibility of telling the MCO which default stream to use. This change replaces that logic, allowing users to input the defaultStream they want to use and falling back to the builtin if necessary. The builtin default stream determination logic has been improved to avoid hard-coding versions and it now picks the OSImageStream that points to the images of the default OS tags in the payload ImageStream.

- How to verify it

There are two main scenarios to test.

Install a cluster with a different default set at day-zero

mkdir installer-dir
./openshift-install --dir installer-dir create manifests

# Enable TechPreview
yq -i '.spec.featureSet = "TechPreviewNoUpgrade"' installer-dir/openshift/99_feature-gate.yaml

# Create the simple bare OSImageStream with just the spec
cat << 'EOF' > installer-dir/openshift/99_osimagestream.yaml
apiVersion: machineconfiguration.openshift.io/v1alpha1
kind: OSImageStream
metadata:
 name: cluster
spec:
 defaultStream: rhel-10
EOF

# Deploy the cluster
./openshift-install --dir installer-dir create cluster

# Ensure the nodes are using the expected stream
oc debug node/$(oc get nodes -l node-role.kubernetes.io/master -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release                                                                                                               14:33:37
Starting pod/pabrodri-test-xfllf-master-0-debug-d69vl ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.1 (Coughlan)

oc debug node/$(oc get nodes -l node-role.kubernetes.io/worker -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release                                                                                                    14:39:01
Starting pod/pabrodri-test-xfllf-worker-a-nvh9j-debug-76blg ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.1 (Coughlan)

Change the global default of a cluster running default MCPs

  1. Install a fresh OCP cluster containing this change with the default and TechPreview enabled.
  2. Wait for it to be fully stable.
  3. Ensure all pools are RHEL 9.
  4. Change the default
oc patch osimagestream cluster --type='merge' -p '{"spec":{"defaultStream":"rhel-10"}}'
  1. Wait for the pools to fully update
  2. Ensure nodes are running RHEL 10
oc debug node/$(oc get nodes -l node-role.kubernetes.io/master -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release                                                                                                               14:33:37
Starting pod/pabrodri-test-xfllf-master-0-debug-d69vl ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.1 (Coughlan)

oc debug node/$(oc get nodes -l node-role.kubernetes.io/worker -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release                                                                                                    14:39:01
Starting pod/pabrodri-test-xfllf-worker-a-nvh9j-debug-76blg ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.1 (Coughlan)

Move an entire cluster, that uses MCPs with no explicit osImageStream to rhel-10 in a single move

- Description for the changelog

Allow users to override the default OSImageStream via Spec.DefaultStream, falling back to the builtin default resolved from the payload ImageStream tags.

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
Copy link
Contributor

openshift-ci-robot commented Mar 2, 2026

@pablintino: This pull request references MCO-2117 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 "4.22.0" version, but no target version was set.

Details

In response to this:

- What I did

Previously the default stream was determined by matching the OS version in the stream name, without the possibility of telling the MCO which default stream to use. This change replaces that logic, allowing users to input the defaultStream they want to use and falling back to the builtin if necessary. The builtin default stream determination logic has been improved to avoid hard-coding versions and it now picks the OSImageStream that points to the images of the default OS tags in the payload ImageStream.

- How to verify it

There are two main scenarios to test.

Install a cluster with a different default set at day-zero

mkdir installer-dir
./openshift-install --dir installer-dir create manifests

# Enable TechPreview
yq -i '.spec.featureSet = "TechPreviewNoUpgrade"' installer-dir/openshift/99_feature-gate.yaml

# Create the simple bare OSImageStream with just the spec
cat << 'EOF' > installer-dir/openshift/99_osimagestream.yaml
apiVersion: machineconfiguration.openshift.io/v1alpha1
kind: OSImageStream
metadata:
 name: cluster
spec:
 defaultStream: rhel-10
EOF

# Deploy the cluster
./openshift-install --dir installer-dir create cluster

# Ensure the nodes are using the expected stream
oc debug node/$(oc get nodes -l node-role.kubernetes.io/master -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release                                                                                                               14:33:37
Starting pod/pabrodri-test-xfllf-master-0-debug-d69vl ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.1 (Coughlan)

oc debug node/$(oc get nodes -l node-role.kubernetes.io/worker -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release                                                                                                    14:39:01
Starting pod/pabrodri-test-xfllf-worker-a-nvh9j-debug-76blg ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.1 (Coughlan)

Change the global default of a cluster running default MCPs

  1. Install a fresh OCP cluster containing this change with the default and TechPreview enabled.
  2. Wait for it to be fully stable.
  3. Ensure all pools are RHEL 9.
  4. Change the default
oc patch osimagestream cluster --type='merge' -p '{"spec":{"defaultStream":"rhel-10"}}'
  1. Wait for the pools to fully update
  2. Ensure nodes are running RHEL 10
oc debug node/$(oc get nodes -l node-role.kubernetes.io/master -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release                                                                                                               14:33:37
Starting pod/pabrodri-test-xfllf-master-0-debug-d69vl ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.1 (Coughlan)

oc debug node/$(oc get nodes -l node-role.kubernetes.io/worker -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release                                                                                                    14:39:01
Starting pod/pabrodri-test-xfllf-worker-a-nvh9j-debug-76blg ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.1 (Coughlan)

- Description for the changelog

Allow users to override the default OSImageStream via Spec.DefaultStream, falling back to the builtin default resolved from the payload ImageStream tags.

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
Copy link
Contributor

openshift-ci-robot commented Mar 2, 2026

@pablintino: This pull request references MCO-2117 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 "4.22.0" version, but no target version was set.

Details

In response to this:

- What I did

Previously the default stream was determined by matching the OS version in the stream name, without the possibility of telling the MCO which default stream to use. This change replaces that logic, allowing users to input the defaultStream they want to use and falling back to the builtin if necessary. The builtin default stream determination logic has been improved to avoid hard-coding versions and it now picks the OSImageStream that points to the images of the default OS tags in the payload ImageStream.

- How to verify it

There are two main scenarios to test.

Install a cluster with a different default set at day-zero

mkdir installer-dir
./openshift-install --dir installer-dir create manifests

# Enable TechPreview
yq -i '.spec.featureSet = "TechPreviewNoUpgrade"' installer-dir/openshift/99_feature-gate.yaml

# Create the simple bare OSImageStream with just the spec
cat << 'EOF' > installer-dir/openshift/99_osimagestream.yaml
apiVersion: machineconfiguration.openshift.io/v1alpha1
kind: OSImageStream
metadata:
 name: cluster
spec:
 defaultStream: rhel-10
EOF

# Deploy the cluster
./openshift-install --dir installer-dir create cluster

# Ensure the nodes are using the expected stream
oc debug node/$(oc get nodes -l node-role.kubernetes.io/master -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release
Starting pod/pabrodri-test-xfllf-master-0-debug-d69vl ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.1 (Coughlan)

oc debug node/$(oc get nodes -l node-role.kubernetes.io/worker -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release
Starting pod/pabrodri-test-xfllf-worker-a-nvh9j-debug-76blg ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.1 (Coughlan)

Change the global default of a cluster running default MCPs

  1. Install a fresh OCP cluster containing this change with the default and TechPreview enabled.
  2. Wait for it to be fully stable.
  3. Ensure all pools are RHEL 9.
  4. Change the default
oc patch osimagestream cluster --type='merge' -p '{"spec":{"defaultStream":"rhel-10"}}'
  1. Wait for the pools to fully update
  2. Ensure nodes are running RHEL 10
oc debug node/$(oc get nodes -l node-role.kubernetes.io/master -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release                                                                                                      20:04:46
Starting pod/pabrodri-test-lppjx-master-0-debug-vrvgv ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.1 (Coughlan)

oc debug node/$(oc get nodes -l node-role.kubernetes.io/worker -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release
Starting pod/pabrodri-test-lppjx-worker-a-thv4m-debug-plvg8 ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.1 (Coughlan)

- Description for the changelog

Allow users to override the default OSImageStream via Spec.DefaultStream, falling back to the builtin default resolved from the payload ImageStream tags.

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.

}

func getDefaultImageStreamTag() string {
imageTag := "rhel-coreos"
Copy link
Contributor

Choose a reason for hiding this comment

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

Interesting, so when we eventually drop rhel9, would the rhel10 stream get this tag?

Copy link
Contributor

@djoshy djoshy left a comment

Choose a reason for hiding this comment

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

Overall makes sense to me, thanks for the very helpful notes.

@openshift-ci
Copy link
Contributor

openshift-ci bot commented Mar 4, 2026

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: djoshy, pablintino

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

@coderabbitai
Copy link

coderabbitai bot commented Mar 9, 2026

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

Introduces a unified OSImageStream creation flow (CreateOptions-based factory), adds thread-safe ImageStream caching, ClusterVersion utilities (GetClusterVersion/GetInstallVersion), OSImageStream informer handlers and operator wiring, a builtin-default annotation constant, and comprehensive tests and refactors across controllers and osimagestream logic.

Changes

Cohort / File(s) Summary
Dependency Updates
go.mod
Bumped github.com/openshift/api and github.com/openshift/client-go; promoted blang/semver/v4 from indirect to direct require.
Bootstrap Controller
pkg/controller/bootstrap/bootstrap.go
Plumbs existingOSImageStream through Run; fetchOSImageStream signature extended to accept existing OSImageStream; builds system context and calls factory Create; ensures osImageStream cleared when FG disabled.
Constants
pkg/controller/common/constants.go
Added exported BuiltinDefaultStreamAnnotationKey.
Render Controller
pkg/controller/render/render_controller.go
Wires OSImageStream informer; added updateOSImageStream(old, cur interface{}) to detect status changes and enqueue affected MCPs.
Internal Release Image Controller
pkg/controller/internalreleaseimage/internalreleaseimage_controller.go
Now obtains ClusterVersion via GetClusterVersion before extracting release payload image; updated error handling/message.
Operator wiring & OCP integration
pkg/operator/operator.go, pkg/operator/osimagestream_ocp.go
Operator registers OSImageStream informer when FG enabled; osimagestream_ocp refactored to decide rebuild vs update, added updateOSImageStream, use GetClusterVersion/GetInstallVersion, and call factory Create with ExistingOSImageStream.
ClusterVersion utilities & tests
pkg/osimagestream/clusterversion.go, pkg/osimagestream/clusterversion_test.go
Added GetClusterVersion(lister), changed GetReleasePayloadImage to accept a ClusterVersion, added GetInstallVersion to derive install version from ClusterVersion history; tests expanded for new APIs and history parsing.
OSImageStream helpers & tests
pkg/osimagestream/helpers.go, pkg/osimagestream/helpers_test.go
Added GetBuiltinDefault and GetOSImageStreamSpecDefault accessors with tests for nil/edge cases.
ImageStream provider, caching & tests
pkg/osimagestream/imagestream_provider.go, pkg/osimagestream/imagestream_provider_test.go, pkg/osimagestream/mocks_test.go
Added mutex-protected caching (cacheLock, cached imageStream) and fetchImageStream(ctx) helper; tests for caching and retry behavior; mock inspector tracks fetchCount.
OSImageStream factory/core logic & tests
pkg/osimagestream/osimagestream.go, pkg/osimagestream/osimagestream_test.go
Major refactor: added CreateOptions, unified Create(ctx, sysCtx, opts) API, removed cmLister from factory constructor, added StreamNameRHEL9/10, consolidated source collection/merge logic, new helpers (getBuiltinDefaultStreamName, getDefaultStreamSet, newOSImageStream), and extensive test rewrites.
E2E & integration tests
test/e2e-bootstrap/bootstrap_test.go, test/e2e-2of2/osimagestream_test.go
E2E tests updated to retrieve ClusterVersion first and derive payload major version; adjusted call to FeatureSets and to GetReleasePayloadImage(clusterVersion).

Sequence Diagram

sequenceDiagram
    participant Client
    participant BootstrapCtrl
    participant Factory
    participant Inspector
    participant Operator
    participant MCPController

    Client->>BootstrapCtrl: start bootstrap (may include existing OSImageStream)
    BootstrapCtrl->>BootstrapCtrl: determine existingOSImageStream
    alt OSImageStream feature enabled
        BootstrapCtrl->>Factory: Create(ctx, sysCtx, CreateOptions{ExistingOSImageStream, ReleaseImage/Stream, InstallVersion})
        Factory->>Inspector: fetch/inspect release / ImageStream / CLI sources
        Inspector-->>Factory: return streams & metadata
        Factory->>Factory: compute builtin default, merge sources, assemble OSImageStream
        Factory-->>BootstrapCtrl: return assembled OSImageStream
        BootstrapCtrl->>Operator: apply/create OSImageStream resource
        Operator->>MCPController: OSImageStream status update triggers informer
        MCPController->>Operator: updateOSImageStream handler enqueues affected MCPs
    else feature disabled
        BootstrapCtrl->>BootstrapCtrl: set osImageStream = nil / skip create
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 34.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: allowing users to override the default OSImageStream through a new Spec.DefaultStream field with fallback to builtin defaults.
Stable And Deterministic Test Names ✅ Passed Modified test files use Go's standard testing.T framework with deterministic test names containing no dynamic information.
Test Structure And Quality ✅ Passed Test code demonstrates good quality following Go testing patterns with table-driven testing, proper cleanup, timeout handling, and meaningful assertions.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

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

Tip

CodeRabbit can suggest fixes for GitHub Check annotations.

Configure the reviews.tools.github-checks setting to adjust the time to wait for GitHub Checks to complete.

@openshift-ci-robot
Copy link
Contributor

openshift-ci-robot commented Mar 9, 2026

@pablintino: This pull request references MCO-2117 which is a valid jira issue.

Details

In response to this:

- What I did

Previously the default stream was determined by matching the OS version in the stream name, without the possibility of telling the MCO which default stream to use. This change replaces that logic, allowing users to input the defaultStream they want to use and falling back to the builtin if necessary. The builtin default stream determination logic has been improved to avoid hard-coding versions and it now picks the OSImageStream that points to the images of the default OS tags in the payload ImageStream.

- How to verify it

There are two main scenarios to test.

Install a cluster with a different default set at day-zero

mkdir installer-dir
./openshift-install --dir installer-dir create manifests

# Enable TechPreview
yq -i '.spec.featureSet = "TechPreviewNoUpgrade"' installer-dir/openshift/99_feature-gate.yaml

# Create the simple bare OSImageStream with just the spec
cat << 'EOF' > installer-dir/openshift/99_osimagestream.yaml
apiVersion: machineconfiguration.openshift.io/v1alpha1
kind: OSImageStream
metadata:
 name: cluster
spec:
 defaultStream: rhel-10
EOF

# Deploy the cluster
./openshift-install --dir installer-dir create cluster

# Ensure the nodes are using the expected stream
oc debug node/$(oc get nodes -l node-role.kubernetes.io/master -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release
Starting pod/pabrodri-test-xfllf-master-0-debug-d69vl ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.1 (Coughlan)

oc debug node/$(oc get nodes -l node-role.kubernetes.io/worker -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release
Starting pod/pabrodri-test-xfllf-worker-a-nvh9j-debug-76blg ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.1 (Coughlan)

Change the global default of a cluster running default MCPs

  1. Install a fresh OCP cluster containing this change with the default and TechPreview enabled.
  2. Wait for it to be fully stable.
  3. Ensure all pools are RHEL 9.
  4. Change the default
oc patch osimagestream cluster --type='merge' -p '{"spec":{"defaultStream":"rhel-10"}}'
  1. Wait for the pools to fully update
  2. Ensure nodes are running RHEL 10
oc debug node/$(oc get nodes -l node-role.kubernetes.io/master -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release                                                                                                      20:04:46
Starting pod/pabrodri-test-lppjx-master-0-debug-vrvgv ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.1 (Coughlan)

oc debug node/$(oc get nodes -l node-role.kubernetes.io/worker -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release
Starting pod/pabrodri-test-lppjx-worker-a-thv4m-debug-plvg8 ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.1 (Coughlan)

- Description for the changelog

Allow users to override the default OSImageStream via Spec.DefaultStream, falling back to the builtin default resolved from the payload ImageStream tags.

Summary by CodeRabbit

Release Notes

  • New Features

  • Added support for custom OS image streams with automatic default selection based on cluster version (RHEL 9 for 4.x, RHEL 10 for 5.x+).

  • Implemented OS image stream event handling and synchronization in bootstrap workflows.

  • Performance

  • Added caching mechanism for image stream retrieval to improve efficiency.

  • Chores

  • Updated OpenShift API and client-go dependencies.

  • Refactored image stream creation with improved architecture.

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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
pkg/osimagestream/osimagestream.go (2)

201-219: Non-deterministic ordering of AvailableStreams.

maps.Values returns values in arbitrary iteration order, causing Status.AvailableStreams to have inconsistent ordering across reconciliations. This could lead to unnecessary CR updates or flaky tests.

♻️ Suggested fix: Sort streams by name for deterministic output
 func collect(ctx context.Context, sources []StreamSource) []v1alpha1.OSImageStreamSet {
 	result := make(map[string]v1alpha1.OSImageStreamSet)
 	for _, source := range sources {
 		streams, err := source.FetchStreams(ctx)
 		if err != nil {
 			// Do not return: Soft failure, best effort
 			klog.Warningf("error fetching OSImageStreams from a source: %v", err)
 		}

 		for _, stream := range streams {
 			_, exists := result[stream.Name]
 			if exists {
 				// Conflicts are allowed and we simply override the previous content
 				klog.V(4).Infof("overriding OSImageStream %s with %s %s", stream.Name, stream.OSImage, stream.OSExtensionsImage)
 			}
 			result[stream.Name] = *stream
 		}
 	}
-	return slices.Collect(maps.Values(result))
+	streams := slices.Collect(maps.Values(result))
+	slices.SortFunc(streams, func(a, b v1alpha1.OSImageStreamSet) int {
+		if a.Name < b.Name {
+			return -1
+		}
+		if a.Name > b.Name {
+			return 1
+		}
+		return 0
+	})
+	return streams
 }

Or more concisely with cmp.Compare:

slices.SortFunc(streams, func(a, b v1alpha1.OSImageStreamSet) int {
    return cmp.Compare(a.Name, b.Name)
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/osimagestream/osimagestream.go` around lines 201 - 219, The collect
function currently returns slices.Collect(maps.Values(result)) which yields
non-deterministic ordering; update collect (after building result and before
return) to convert maps.Values(result) into a slice and sort it
deterministically by OSImageStreamSet.Name (e.g. using slices.SortFunc or
cmp.Compare) so Status.AvailableStreams is stable across reconciliations; ensure
you replace the direct return with the sorted slice and keep the existing
behavior of overriding duplicates in result.

141-143: Consider adding a comment explaining the status fallback.

This logic preserves the existing Status.DefaultStream during reconciliation when the user hasn't explicitly set Spec.DefaultStream. While this is correct behavior to prevent unexpected changes to running clusters, a brief inline comment would clarify this intent for future maintainers.

📝 Suggested documentation
 	requestedDefaultStream := GetOSImageStreamSpecDefault(existingOSImageStream)
 	if requestedDefaultStream == "" && existingOSImageStream != nil && existingOSImageStream.Status.DefaultStream != "" {
-		// If the spec is empty but the CR is already populated pick whatever existed
+		// Preserve the existing default during reconciliation when no spec override is set.
+		// This prevents unexpected changes to running clusters on re-sync.
 		requestedDefaultStream = existingOSImageStream.Status.DefaultStream
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/osimagestream/osimagestream.go` around lines 141 - 143, Add a brief
inline comment above the conditional that assigns requestedDefaultStream from
existingOSImageStream.Status.DefaultStream (the block that checks
requestedDefaultStream == "" && existingOSImageStream != nil &&
existingOSImageStream.Status.DefaultStream != "") explaining this is an
intentional "status fallback" used during reconciliation: when the user has not
set Spec.DefaultStream, preserve the cluster's current DefaultStream recorded in
Status.DefaultStream to avoid changing runtime behavior; mention that this
prevents unexpected drift and is only applied when Spec is empty.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/operator/operator.go`:
- Around line 375-376: buildOSImageStream() currently calls Update() and
UpdateStatus() unconditionally when rebuildRequired() (which checks
version.Hash) returns true, causing unnecessary writes; add a semantic diff
guard before invoking Update() and UpdateStatus() so you only perform API
updates when the constructed OSImageStream object actually differs from the
existing one. Locate buildOSImageStream(), compare the desired OSImageStream
(including annotations like version.Hash and status fields) against the
fetched/original OSImageStream, and only call Update() or UpdateStatus() when
that deep/semantic comparison indicates changes; follow the same pattern used in
updateOSImageStream() and sync.go (compare resource spec/annotations/status
first) to avoid redundant writes and extra event handler firings.

In `@pkg/operator/osimagestream_ocp.go`:
- Around line 296-304: Add a unit test for osImageStreamRequiresRebuild that
constructs a v1alpha1.OSImageStream with the ReleaseImageVersionAnnotationKey
set to version.Hash but without the builtin default annotation and asserts
osImageStreamRequiresRebuild(osImageStream) returns true; target the
osImageStreamRequiresRebuild function and use the v1alpha1.OSImageStream type
and version.Hash constant so the test specifically verifies the missing builtin
default path (osimagestream.GetBuiltinDefault(...) == "") triggers a rebuild.

In `@pkg/osimagestream/clusterversion.go`:
- Around line 34-55: GetInstallVersion dereferences clusterVersion without
checking for nil; add an initial nil check in GetInstallVersion (e.g., if
clusterVersion == nil) and return a descriptive error (matching the style used
by GetReleasePayloadImage) before accessing clusterVersion.Status.History so the
function fails gracefully instead of panicking.

---

Nitpick comments:
In `@pkg/osimagestream/osimagestream.go`:
- Around line 201-219: The collect function currently returns
slices.Collect(maps.Values(result)) which yields non-deterministic ordering;
update collect (after building result and before return) to convert
maps.Values(result) into a slice and sort it deterministically by
OSImageStreamSet.Name (e.g. using slices.SortFunc or cmp.Compare) so
Status.AvailableStreams is stable across reconciliations; ensure you replace the
direct return with the sorted slice and keep the existing behavior of overriding
duplicates in result.
- Around line 141-143: Add a brief inline comment above the conditional that
assigns requestedDefaultStream from existingOSImageStream.Status.DefaultStream
(the block that checks requestedDefaultStream == "" && existingOSImageStream !=
nil && existingOSImageStream.Status.DefaultStream != "") explaining this is an
intentional "status fallback" used during reconciliation: when the user has not
set Spec.DefaultStream, preserve the cluster's current DefaultStream recorded in
Status.DefaultStream to avoid changing runtime behavior; mention that this
prevents unexpected drift and is only applied when Spec is empty.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: openshift/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9687e1fa-dd50-4264-997f-94776a8d6666

📥 Commits

Reviewing files that changed from the base of the PR and between d5dfdcf and a3ca1e5.

⛔ Files ignored due to path filters (64)
  • go.sum is excluded by !**/*.sum
  • vendor/github.com/openshift/api/.golangci.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/apiextensions/v1alpha1/types_compatibilityrequirement.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/apiextensions/v1alpha1/zz_generated.swagger_doc_generated.go is excluded by !**/vendor/**, !vendor/**, !**/zz_generated*
  • vendor/github.com/openshift/api/apps/v1/types.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/apps/v1/zz_prerelease_lifecycle_generated.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1/types_ingress.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1/zz_generated.crd-manifests/0000_10_config-operator_01_ingresses.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1/zz_generated.crd-manifests/0000_10_config-operator_01_insightsdatagathers-DevPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1/zz_generated.crd-manifests/0000_10_config-operator_01_insightsdatagathers-TechPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1/zz_generated.crd-manifests/0000_10_config-operator_01_insightsdatagathers.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1alpha1/types_cluster_monitoring.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1alpha1/types_insights.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1alpha1/zz_generated.crd-manifests/0000_10_config-operator_01_backups-CustomNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1alpha1/zz_generated.crd-manifests/0000_10_config-operator_01_backups-TechPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1alpha1/zz_generated.crd-manifests/0000_10_config-operator_01_backups.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1alpha1/zz_generated.crd-manifests/0000_10_config-operator_01_clustermonitorings-DevPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1alpha1/zz_generated.crd-manifests/0000_10_config-operator_01_clustermonitorings-TechPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1alpha1/zz_generated.crd-manifests/0000_10_config-operator_01_clustermonitorings.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1alpha1/zz_generated.crd-manifests/0000_10_config-operator_01_criocredentialproviderconfigs-DevPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1alpha1/zz_generated.crd-manifests/0000_10_config-operator_01_criocredentialproviderconfigs-TechPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1alpha1/zz_generated.crd-manifests/0000_10_config-operator_01_criocredentialproviderconfigs.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1alpha1/zz_generated.crd-manifests/0000_10_config-operator_01_insightsdatagathers-DevPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1alpha1/zz_generated.crd-manifests/0000_10_config-operator_01_insightsdatagathers-TechPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1alpha1/zz_generated.crd-manifests/0000_10_config-operator_01_insightsdatagathers.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1alpha1/zz_generated.deepcopy.go is excluded by !**/vendor/**, !vendor/**, !**/zz_generated*
  • vendor/github.com/openshift/api/config/v1alpha1/zz_generated.swagger_doc_generated.go is excluded by !**/vendor/**, !vendor/**, !**/zz_generated*
  • vendor/github.com/openshift/api/features.md is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/features/features.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/features/util.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/machine/v1/types_controlplanemachineset.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/machine/v1/zz_generated.featuregated-crd-manifests.yaml is excluded by !**/vendor/**, !vendor/**, !**/zz_generated*
  • vendor/github.com/openshift/api/machineconfiguration/v1/types.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/machineconfiguration/v1/zz_generated.crd-manifests/0000_80_machine-config_01_kubeletconfigs.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/machineconfiguration/v1/zz_generated.crd-manifests/0000_80_machine-config_01_machineconfignodes-Default.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/machineconfiguration/v1/zz_generated.crd-manifests/0000_80_machine-config_01_machineconfignodes-Hypershift-Default.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/machineconfiguration/v1/zz_generated.crd-manifests/0000_80_machine-config_01_machineconfignodes-Hypershift-OKD.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/machineconfiguration/v1/zz_generated.crd-manifests/0000_80_machine-config_01_machineconfignodes-OKD.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/machineconfiguration/v1alpha1/types_osimagestream.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/machineconfiguration/v1alpha1/zz_generated.crd-manifests/0000_80_machine-config_01_internalreleaseimages-Hypershift.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/machineconfiguration/v1alpha1/zz_generated.crd-manifests/0000_80_machine-config_01_internalreleaseimages-SelfManagedHA.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/machineconfiguration/v1alpha1/zz_generated.crd-manifests/0000_80_machine-config_01_internalreleaseimages-TechPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/machineconfiguration/v1alpha1/zz_generated.crd-manifests/0000_80_machine-config_01_osimagestreams-DevPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/machineconfiguration/v1alpha1/zz_generated.crd-manifests/0000_80_machine-config_01_osimagestreams-TechPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/machineconfiguration/v1alpha1/zz_generated.crd-manifests/0000_80_machine-config_01_osimagestreams.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/machineconfiguration/v1alpha1/zz_generated.swagger_doc_generated.go is excluded by !**/vendor/**, !vendor/**, !**/zz_generated*
  • vendor/github.com/openshift/api/operator/v1alpha1/zz_generated.crd-manifests/0000_00_cluster-version-operator_01_clusterversionoperators-DevPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/operator/v1alpha1/zz_generated.crd-manifests/0000_00_cluster-version-operator_01_clusterversionoperators-TechPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/operator/v1alpha1/zz_generated.crd-manifests/0000_00_cluster-version-operator_01_clusterversionoperators.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/operator/v1alpha1/zz_generated.crd-manifests/0000_10_etcd_01_etcdbackups-CustomNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/operator/v1alpha1/zz_generated.crd-manifests/0000_10_etcd_01_etcdbackups-TechPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/operator/v1alpha1/zz_generated.crd-manifests/0000_10_etcd_01_etcdbackups.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/operator/v1alpha1/zz_generated.crd-manifests/0000_30_cluster-api_01_clusterapis-DevPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/operator/v1alpha1/zz_generated.crd-manifests/0000_30_cluster-api_01_clusterapis-TechPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/operator/v1alpha1/zz_generated.crd-manifests/0000_30_cluster-api_01_clusterapis.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/payload-command/render/render.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/payload-command/render/write_featureset.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/client-go/config/applyconfigurations/config/v1alpha1/clustermonitoringspec.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/client-go/config/applyconfigurations/config/v1alpha1/prometheusoperatoradmissionwebhookconfig.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/client-go/config/applyconfigurations/utils.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/client-go/machineconfiguration/applyconfigurations/machineconfiguration/v1alpha1/osimagestream.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/client-go/machineconfiguration/applyconfigurations/machineconfiguration/v1alpha1/osimagestreamspec.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/client-go/machineconfiguration/applyconfigurations/utils.go is excluded by !**/vendor/**, !vendor/**
  • vendor/modules.txt is excluded by !**/vendor/**, !vendor/**
📒 Files selected for processing (17)
  • go.mod
  • pkg/controller/bootstrap/bootstrap.go
  • pkg/controller/common/constants.go
  • pkg/controller/internalreleaseimage/internalreleaseimage_controller.go
  • pkg/controller/render/render_controller.go
  • pkg/operator/operator.go
  • pkg/operator/osimagestream_ocp.go
  • pkg/osimagestream/clusterversion.go
  • pkg/osimagestream/clusterversion_test.go
  • pkg/osimagestream/helpers.go
  • pkg/osimagestream/helpers_test.go
  • pkg/osimagestream/imagestream_provider.go
  • pkg/osimagestream/imagestream_provider_test.go
  • pkg/osimagestream/mocks_test.go
  • pkg/osimagestream/osimagestream.go
  • pkg/osimagestream/osimagestream_test.go
  • test/e2e-bootstrap/bootstrap_test.go

Comment on lines +296 to +304
// osImageStreamRequiresRebuild checks if the OSImageStream needs to be created or updated.
// Returns true if osImageStream is nil, if its version annotation doesn't match the current version,
// or if the builtin default stream annotation is missing.
func osImageStreamRequiresRebuild(osImageStream *v1alpha1.OSImageStream) bool {
if osImageStream == nil {
return true
}
releaseVersion, ok := osImageStream.Annotations[ctrlcommon.ReleaseImageVersionAnnotationKey]
return !ok || releaseVersion != version.Hash
return !ok || releaseVersion != version.Hash || osimagestream.GetBuiltinDefault(osImageStream) == ""
Copy link

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
# Description: Search for existing tests covering osImageStreamRequiresRebuild

# Test: Check for test coverage of the rebuild requirement logic
rg -n "osImageStreamRequiresRebuild|RequiresRebuild" --type go

Repository: openshift/machine-config-operator

Length of output: 851


🏁 Script executed:

#!/bin/bash
# Find test files related to osimagestream
fd -e "*test.go" | xargs grep -l "osImageStreamRequiresRebuild\|osimagestream" | head -20

Repository: openshift/machine-config-operator

Length of output: 59


🏁 Script executed:

#!/bin/bash
# Check the actual implementation of osImageStreamRequiresRebuild and GetBuiltinDefault
sed -n '296,304p' pkg/operator/osimagestream_ocp.go
# Also look for more context around the function
sed -n '290,310p' pkg/operator/osimagestream_ocp.go

Repository: openshift/machine-config-operator

Length of output: 1279


🏁 Script executed:

#!/bin/bash
# Find the GetBuiltinDefault function to understand what it does
rg -n "func GetBuiltinDefault" --type go -A 5

Repository: openshift/machine-config-operator

Length of output: 459


🏁 Script executed:

#!/bin/bash
# Search for test files in the operator directory
fd -e "*test.go" pkg/operator/ | xargs grep -l "osImageStream" 2>/dev/null || echo "No matches found"

Repository: openshift/machine-config-operator

Length of output: 468


🏁 Script executed:

#!/bin/bash
# Check if there's an osimagestream_ocp_test.go file
ls -la pkg/operator/*osimage* 2>/dev/null || echo "No osimage files found in pkg/operator"

Repository: openshift/machine-config-operator

Length of output: 244


🏁 Script executed:

#!/bin/bash
# Search for any test files that might test the rebuild logic
rg -l "TestOsImageStream|TestRequiresRebuild" pkg/ --type go

Repository: openshift/machine-config-operator

Length of output: 59


🏁 Script executed:

#!/bin/bash
# Search more broadly for test files containing osImageStream references
rg "osImageStreamRequiresRebuild" --type go -l

Repository: openshift/machine-config-operator

Length of output: 110


🏁 Script executed:

#!/bin/bash
# Check the actual context where osImageStreamRequiresRebuild is called
rg -B 10 -A 10 "osImageStreamRequiresRebuild" pkg/operator/osimagestream_ocp.go | head -50

Repository: openshift/machine-config-operator

Length of output: 1725


🏁 Script executed:

#!/bin/bash
# Search for test files in pkg/operator directory properly
fd . pkg/operator -name "*test.go"

Repository: openshift/machine-config-operator

Length of output: 307


🏁 Script executed:

#!/bin/bash
# Search for all test files that might contain osImageStream references
find . -name "*_test.go" -type f | xargs grep -l "osImageStream" 2>/dev/null || echo "No test files found with osImageStream"

Repository: openshift/machine-config-operator

Length of output: 112


🏁 Script executed:

#!/bin/bash
# Check if there are test files in the entire pkg directory that test this functionality
rg -l "osImageStream" --type go | grep -i test

Repository: openshift/machine-config-operator

Length of output: 110


🏁 Script executed:

#!/bin/bash
# Look for any test files in pkg/operator
find pkg/operator -name "*test*" -type f

Repository: openshift/machine-config-operator

Length of output: 158


🏁 Script executed:

#!/bin/bash
# Check sync_test.go for osImageStreamRequiresRebuild tests
rg "osImageStreamRequiresRebuild" pkg/operator/sync_test.go -B 5 -A 10

Repository: openshift/machine-config-operator

Length of output: 59


🏁 Script executed:

#!/bin/bash
# Check if there are any tests for osImageStream in sync_test.go
rg "osImageStream" pkg/operator/sync_test.go -B 2 -A 5 | head -100

Repository: openshift/machine-config-operator

Length of output: 59


🏁 Script executed:

#!/bin/bash
# Examine helpers_test.go to see GetBuiltinDefault tests
cat pkg/osimagestream/helpers_test.go

Repository: openshift/machine-config-operator

Length of output: 5274


🏁 Script executed:

#!/bin/bash
# Read sync_test.go to see if it has any OSImageStream related tests
wc -l pkg/operator/sync_test.go

Repository: openshift/machine-config-operator

Length of output: 106


🏁 Script executed:

#!/bin/bash
# Search for any mention of osImageStream in sync_test.go
grep -n "osImageStream\|OSImageStream" pkg/operator/sync_test.go

Repository: openshift/machine-config-operator

Length of output: 59


🏁 Script executed:

#!/bin/bash
# Let's look at the structure of sync_test.go to understand what it tests
head -100 pkg/operator/sync_test.go

Repository: openshift/machine-config-operator

Length of output: 4009


🏁 Script executed:

#!/bin/bash
# Check if there are any unit tests for osImageStreamRequiresRebuild function specifically
rg "TestOsImageStreamRequiresRebuild|TestRequiresRebuild" --type go

Repository: openshift/machine-config-operator

Length of output: 59


🏁 Script executed:

#!/bin/bash
# Look at the entire sync_test.go to understand what sync functions are tested
grep -n "^func Test" pkg/operator/sync_test.go

Repository: openshift/machine-config-operator

Length of output: 240


🏁 Script executed:

#!/bin/bash
# Check if there's any test that covers the syncOSImageStream or related flow
grep -n "syncOSImageStream\|OSImageStream" pkg/operator/sync_test.go | head -20

Repository: openshift/machine-config-operator

Length of output: 59


Add test case for osImageStreamRequiresRebuild with missing builtin default annotation.

The implementation correctly triggers a rebuild when the builtin default annotation is missing (osimagestream.GetBuiltinDefault(osImageStream) == "" returns true). However, the osImageStreamRequiresRebuild function itself lacks unit test coverage. While GetBuiltinDefault is tested in pkg/osimagestream/helpers_test.go, the rebuild logic should have its own test verifying that an OSImageStream with the version annotation present but the builtin default annotation missing triggers a rebuild.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/operator/osimagestream_ocp.go` around lines 296 - 304, Add a unit test
for osImageStreamRequiresRebuild that constructs a v1alpha1.OSImageStream with
the ReleaseImageVersionAnnotationKey set to version.Hash but without the builtin
default annotation and asserts osImageStreamRequiresRebuild(osImageStream)
returns true; target the osImageStreamRequiresRebuild function and use the
v1alpha1.OSImageStream type and version.Hash constant so the test specifically
verifies the missing builtin default path (osimagestream.GetBuiltinDefault(...)
== "") triggers a rebuild.

@openshift-ci-robot
Copy link
Contributor

openshift-ci-robot commented Mar 9, 2026

@pablintino: This pull request references MCO-2117 which is a valid jira issue.

Details

In response to this:

- What I did

Previously the default stream was determined by matching the OS version in the stream name, without the possibility of telling the MCO which default stream to use. This change replaces that logic, allowing users to input the defaultStream they want to use and falling back to the builtin if necessary. The builtin default stream determination logic has been improved to avoid hard-coding versions and it now picks the OSImageStream that points to the images of the default OS tags in the payload ImageStream.

- How to verify it

There are two main scenarios to test.

Install a cluster with a different default set at day-zero

mkdir installer-dir
./openshift-install --dir installer-dir create manifests

# Enable TechPreview
yq -i '.spec.featureSet = "TechPreviewNoUpgrade"' installer-dir/openshift/99_feature-gate.yaml

# Create the simple bare OSImageStream with just the spec
cat << 'EOF' > installer-dir/openshift/99_osimagestream.yaml
apiVersion: machineconfiguration.openshift.io/v1alpha1
kind: OSImageStream
metadata:
 name: cluster
spec:
 defaultStream: rhel-10
EOF

# Deploy the cluster
./openshift-install --dir installer-dir create cluster

# Ensure the nodes are using the expected stream
oc debug node/$(oc get nodes -l node-role.kubernetes.io/master -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release
Starting pod/pabrodri-test-xfllf-master-0-debug-d69vl ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.1 (Coughlan)

oc debug node/$(oc get nodes -l node-role.kubernetes.io/worker -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release
Starting pod/pabrodri-test-xfllf-worker-a-nvh9j-debug-76blg ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.1 (Coughlan)

Change the global default of a cluster running default MCPs

  1. Install a fresh OCP cluster containing this change with the default and TechPreview enabled.
  2. Wait for it to be fully stable.
  3. Ensure all pools are RHEL 9.
  4. Change the default
oc patch osimagestream cluster --type='merge' -p '{"spec":{"defaultStream":"rhel-10"}}'
  1. Wait for the pools to fully update
  2. Ensure nodes are running RHEL 10
oc debug node/$(oc get nodes -l node-role.kubernetes.io/master -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release                                                                                                      20:04:46
Starting pod/pabrodri-test-lppjx-master-0-debug-vrvgv ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.1 (Coughlan)

oc debug node/$(oc get nodes -l node-role.kubernetes.io/worker -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release
Starting pod/pabrodri-test-lppjx-worker-a-thv4m-debug-plvg8 ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.1 (Coughlan)

- Description for the changelog

Allow users to override the default OSImageStream via Spec.DefaultStream, falling back to the builtin default resolved from the payload ImageStream tags.

Summary by CodeRabbit

  • New Features

  • Support for user-provided OS image streams with automatic default selection based on cluster install version (RHEL9 for 4.x, RHEL10 for 5.x+).

  • OS image stream event handling to trigger pool reconciliation.

  • Performance

  • Thread-safe caching of image stream retrieval to reduce repeated fetches.

  • Chores

  • Dependency updates and refactor of image-stream creation flow.

  • Tests

  • Expanded unit and e2e tests covering cluster version parsing, defaults, caching, and create/update flows.

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
Copy link
Contributor

openshift-ci-robot commented Mar 9, 2026

@pablintino: This pull request references MCO-2117 which is a valid jira issue.

Details

In response to this:

- What I did

Previously the default stream was determined by matching the OS version in the stream name, without the possibility of telling the MCO which default stream to use. This change replaces that logic, allowing users to input the defaultStream they want to use and falling back to the builtin if necessary. The builtin default stream determination logic has been improved to avoid hard-coding versions and it now picks the OSImageStream that points to the images of the default OS tags in the payload ImageStream.

- How to verify it

There are two main scenarios to test.

Install a cluster with a different default set at day-zero

mkdir installer-dir
./openshift-install --dir installer-dir create manifests

# Enable TechPreview
yq -i '.spec.featureSet = "TechPreviewNoUpgrade"' installer-dir/openshift/99_feature-gate.yaml

# Create the simple bare OSImageStream with just the spec
cat << 'EOF' > installer-dir/openshift/99_osimagestream.yaml
apiVersion: machineconfiguration.openshift.io/v1alpha1
kind: OSImageStream
metadata:
 name: cluster
spec:
 defaultStream: rhel-10
EOF

# Deploy the cluster
./openshift-install --dir installer-dir create cluster

# Ensure the nodes are using the expected stream
oc debug node/$(oc get nodes -l node-role.kubernetes.io/master -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release
Starting pod/pabrodri-test-xfllf-master-0-debug-d69vl ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.1 (Coughlan)

oc debug node/$(oc get nodes -l node-role.kubernetes.io/worker -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release
Starting pod/pabrodri-test-xfllf-worker-a-nvh9j-debug-76blg ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.1 (Coughlan)

Change the global default of a cluster running default MCPs

  1. Install a fresh OCP cluster containing this change with the default and TechPreview enabled.
  2. Wait for it to be fully stable.
  3. Ensure all pools are RHEL 9.
  4. Change the default
oc patch osimagestream cluster --type='merge' -p '{"spec":{"defaultStream":"rhel-10"}}'
  1. Wait for the pools to fully update
  2. Ensure nodes are running RHEL 10
oc debug node/$(oc get nodes -l node-role.kubernetes.io/master -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release                                                                                                      20:04:46
Starting pod/pabrodri-test-lppjx-master-0-debug-vrvgv ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.1 (Coughlan)

oc debug node/$(oc get nodes -l node-role.kubernetes.io/worker -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release
Starting pod/pabrodri-test-lppjx-worker-a-thv4m-debug-plvg8 ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.1 (Coughlan)

- Description for the changelog

Allow users to override the default OSImageStream via Spec.DefaultStream, falling back to the builtin default resolved from the payload ImageStream tags.

Summary by CodeRabbit

  • New Features

  • Support for user-provided OS image streams with automatic default selection based on install version (RHEL9 for 4.x, RHEL10 for 5.x+).

  • OS image stream event handling to trigger pool reconciliation.

  • Public helpers to expose builtin/default stream values.

  • Performance

  • Thread-safe caching of image stream retrieval to reduce repeated fetches.

  • Chores

  • Dependency updates and unified OS image-stream creation/refactor.

  • Tests

  • Expanded unit and e2e coverage for cluster-version parsing, defaults, caching, and create/update flows.

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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (2)
pkg/osimagestream/osimagestream.go (2)

141-158: ⚠️ Potential issue | 🟠 Major

Drop the sticky Status.DefaultStream fallback.

When Spec.DefaultStream is empty or no longer valid, this still pins reconciliation to the old Status.DefaultStream instead of falling back to the builtin default. That contradicts the new override contract and can keep clusters stuck on a stale default. pkg/osimagestream/osimagestream_test.go Lines 171-180 will need to follow the same fallback order once this is fixed.

Suggested fix
 func getDefaultStreamSet(streams []v1alpha1.OSImageStreamSet, builtinDefault string, existingOSImageStream *v1alpha1.OSImageStream) (string, error) {
 	streamNames := GetStreamSetsNames(streams)

 	requestedDefaultStream := GetOSImageStreamSpecDefault(existingOSImageStream)
-	if requestedDefaultStream == "" && existingOSImageStream != nil && existingOSImageStream.Status.DefaultStream != "" {
-		// If the spec is empty but the CR is already populated pick whatever existed
-		requestedDefaultStream = existingOSImageStream.Status.DefaultStream
-	} else if requestedDefaultStream == "" {
-		// No user preference: if there's only one stream just use it,
-		// otherwise fall back to the builtin default.
-		if len(streams) == 1 {
-			return streams[0].Name, nil
-		}
-		requestedDefaultStream = builtinDefault
+	if requestedDefaultStream != "" && slices.Contains(streamNames, requestedDefaultStream) {
+		return requestedDefaultStream, nil
 	}

-	if slices.Contains(streamNames, requestedDefaultStream) {
-		return requestedDefaultStream, nil
+	if len(streams) == 1 {
+		return streams[0].Name, nil
 	}

-	return "", fmt.Errorf("could not find the requested %s default stream in the list of OSImageStreams %s", requestedDefaultStream, streamNames)
+	if slices.Contains(streamNames, builtinDefault) {
+		return builtinDefault, nil
+	}
+
+	return "", fmt.Errorf("could not find a usable default stream in %v (requested=%q, builtin=%q)", streamNames, requestedDefaultStream, builtinDefault)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/osimagestream/osimagestream.go` around lines 141 - 158, The code
currently falls back to existingOSImageStream.Status.DefaultStream when
GetOSImageStreamSpecDefault(existingOSImageStream) is empty; remove that
“sticky” Status.DefaultStream fallback so an empty Spec.DefaultStream does not
pin reconciliation to a prior status. In the GetOSImageStreamSpecDefault usage
block (around requestedDefaultStream, existingOSImageStream,
Status.DefaultStream, streams, streamNames, builtinDefault) delete the branch
that assigns existingOSImageStream.Status.DefaultStream and instead treat an
empty requestedDefaultStream as "no user preference" — if len(streams)==1 return
streams[0].Name, else set requestedDefaultStream = builtinDefault; keep the
subsequent slices.Contains check and error path, and update the related tests in
pkg/osimagestream/osimagestream_test.go to expect the same fallback order.

161-188: ⚠️ Potential issue | 🟠 Major

Resolve the builtin default from payload defaults, not releaseVersion.Major().

This helper still hard-codes 4 -> rhel-9 and >=5 -> rhel-10. The PR objective says the builtin default should come from the payload ImageStream’s default OS tags, then map that image back to the matching OSImageStreamSet; otherwise the controller can annotate/select the wrong builtin default whenever payload defaults diverge from the major-version heuristic. As written, this function also cannot implement that contract because it never sees the collected stream sets. pkg/osimagestream/osimagestream_test.go Lines 915-1010 currently lock in the same old heuristic.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/osimagestream/osimagestream.go` around lines 161 - 188,
getBuiltinDefaultStreamName currently determines the builtin default purely from
releaseVersion.Major(), which ignores the payload ImageStream defaults and the
available OSImageStreamSets; update the function to use the payload
ImageStream's defaults (via ImageStreamProvider/getVersionFromImageStream or a
new call) and map the default OS tag back to the matching OSImageStreamSet to
return the appropriate StreamName (e.g., check the ImageStream's default tag or
annotation, find the OSImageStreamSet whose images/tags include that image, and
return its name); if no payload default-to-set mapping can be determined, fall
back to the previous major-version heuristic (reuse releaseVersion and return
StreamNameRHEL9 or StreamNameRHEL10).
🧹 Nitpick comments (1)
pkg/osimagestream/imagestream_provider_test.go (1)

143-203: Add one concurrent-read case for the new cache path.

These tests cover sequential retry/caching, but the production change is specifically the shared mutex-protected cache. A goroutine-based subtest asserting fetchCount == 1 would protect against duplicate fetches or racey cache population.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/osimagestream/imagestream_provider_test.go` around lines 143 - 203, Add a
concurrent-read subtest to exercise the mutex-protected cache: in
TestImageStreamProviderNetwork_CachesOnSuccess (or a new subtest within it)
spawn multiple goroutines that concurrently call provider.ReadImageStream(ctx)
after creating provider via NewImageStreamProviderNetwork and the
mockImagesInspector, wait for all to finish (e.g., sync.WaitGroup), then assert
inspector.fetchCount == 1 and that all returned pointers are the same; this
verifies the cache is populated only once under concurrency and prevents
duplicate fetches/races when mockImagesInspector.fetchCount is incremented
during fetch.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/controller/bootstrap/bootstrap.go`:
- Around line 203-205: The code currently assigns any
*mcfgv1alpha1.OSImageStream to osImageStream, allowing a non-singleton manifest
to win; change the assignment so it only consumes the cluster singleton
OSImageStream used at runtime: in the switch arm for
*mcfgv1alpha1.OSImageStream, check the object's identity (e.g.,
obj.GetNamespace() and obj.GetName() or the specific singleton name/constant the
runtime reads) and only set osImageStream = obj when it matches the expected
cluster singleton; otherwise ignore (and optionally log) the extra OSImageStream
so the bootstrap path cannot be overridden by non-singleton manifests.

In `@pkg/controller/render/render_controller.go`:
- Around line 285-290: The current early-return only compares
oldOSImageStream.Status vs curOSImageStream.Status, so changes to
spec.defaultStream are ignored; update the guard in the reconcile path (where
oldOSImageStream and curOSImageStream are compared) to also detect changes to
curOSImageStream.Spec.DefaultStream (or compare the whole Spec.DefaultStream
field) and only return early when both Status and Spec.DefaultStream are
unchanged—i.e., include a check that requeues pools when
curOSImageStream.Spec.DefaultStream != oldOSImageStream.Spec.DefaultStream (or
use reflect.DeepEqual on a struct containing Status and the DefaultStream) so
spec.defaultStream updates trigger enqueuing.

---

Duplicate comments:
In `@pkg/osimagestream/osimagestream.go`:
- Around line 141-158: The code currently falls back to
existingOSImageStream.Status.DefaultStream when
GetOSImageStreamSpecDefault(existingOSImageStream) is empty; remove that
“sticky” Status.DefaultStream fallback so an empty Spec.DefaultStream does not
pin reconciliation to a prior status. In the GetOSImageStreamSpecDefault usage
block (around requestedDefaultStream, existingOSImageStream,
Status.DefaultStream, streams, streamNames, builtinDefault) delete the branch
that assigns existingOSImageStream.Status.DefaultStream and instead treat an
empty requestedDefaultStream as "no user preference" — if len(streams)==1 return
streams[0].Name, else set requestedDefaultStream = builtinDefault; keep the
subsequent slices.Contains check and error path, and update the related tests in
pkg/osimagestream/osimagestream_test.go to expect the same fallback order.
- Around line 161-188: getBuiltinDefaultStreamName currently determines the
builtin default purely from releaseVersion.Major(), which ignores the payload
ImageStream defaults and the available OSImageStreamSets; update the function to
use the payload ImageStream's defaults (via
ImageStreamProvider/getVersionFromImageStream or a new call) and map the default
OS tag back to the matching OSImageStreamSet to return the appropriate
StreamName (e.g., check the ImageStream's default tag or annotation, find the
OSImageStreamSet whose images/tags include that image, and return its name); if
no payload default-to-set mapping can be determined, fall back to the previous
major-version heuristic (reuse releaseVersion and return StreamNameRHEL9 or
StreamNameRHEL10).

---

Nitpick comments:
In `@pkg/osimagestream/imagestream_provider_test.go`:
- Around line 143-203: Add a concurrent-read subtest to exercise the
mutex-protected cache: in TestImageStreamProviderNetwork_CachesOnSuccess (or a
new subtest within it) spawn multiple goroutines that concurrently call
provider.ReadImageStream(ctx) after creating provider via
NewImageStreamProviderNetwork and the mockImagesInspector, wait for all to
finish (e.g., sync.WaitGroup), then assert inspector.fetchCount == 1 and that
all returned pointers are the same; this verifies the cache is populated only
once under concurrency and prevents duplicate fetches/races when
mockImagesInspector.fetchCount is incremented during fetch.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: openshift/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d7b0e807-be72-4dfb-8657-3b262c0cd526

📥 Commits

Reviewing files that changed from the base of the PR and between 91027cc and c894d0f.

⛔ Files ignored due to path filters (64)
  • go.sum is excluded by !**/*.sum
  • vendor/github.com/openshift/api/.golangci.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/apiextensions/v1alpha1/types_compatibilityrequirement.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/apiextensions/v1alpha1/zz_generated.swagger_doc_generated.go is excluded by !**/vendor/**, !vendor/**, !**/zz_generated*
  • vendor/github.com/openshift/api/apps/v1/types.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/apps/v1/zz_prerelease_lifecycle_generated.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1/types_ingress.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1/zz_generated.crd-manifests/0000_10_config-operator_01_ingresses.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1/zz_generated.crd-manifests/0000_10_config-operator_01_insightsdatagathers-DevPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1/zz_generated.crd-manifests/0000_10_config-operator_01_insightsdatagathers-TechPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1/zz_generated.crd-manifests/0000_10_config-operator_01_insightsdatagathers.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1alpha1/types_cluster_monitoring.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1alpha1/types_insights.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1alpha1/zz_generated.crd-manifests/0000_10_config-operator_01_backups-CustomNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1alpha1/zz_generated.crd-manifests/0000_10_config-operator_01_backups-TechPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1alpha1/zz_generated.crd-manifests/0000_10_config-operator_01_backups.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1alpha1/zz_generated.crd-manifests/0000_10_config-operator_01_clustermonitorings-DevPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1alpha1/zz_generated.crd-manifests/0000_10_config-operator_01_clustermonitorings-TechPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1alpha1/zz_generated.crd-manifests/0000_10_config-operator_01_clustermonitorings.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1alpha1/zz_generated.crd-manifests/0000_10_config-operator_01_criocredentialproviderconfigs-DevPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1alpha1/zz_generated.crd-manifests/0000_10_config-operator_01_criocredentialproviderconfigs-TechPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1alpha1/zz_generated.crd-manifests/0000_10_config-operator_01_criocredentialproviderconfigs.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1alpha1/zz_generated.crd-manifests/0000_10_config-operator_01_insightsdatagathers-DevPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1alpha1/zz_generated.crd-manifests/0000_10_config-operator_01_insightsdatagathers-TechPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1alpha1/zz_generated.crd-manifests/0000_10_config-operator_01_insightsdatagathers.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1alpha1/zz_generated.deepcopy.go is excluded by !**/vendor/**, !vendor/**, !**/zz_generated*
  • vendor/github.com/openshift/api/config/v1alpha1/zz_generated.swagger_doc_generated.go is excluded by !**/vendor/**, !vendor/**, !**/zz_generated*
  • vendor/github.com/openshift/api/features.md is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/features/features.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/features/util.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/machine/v1/types_controlplanemachineset.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/machine/v1/zz_generated.featuregated-crd-manifests.yaml is excluded by !**/vendor/**, !vendor/**, !**/zz_generated*
  • vendor/github.com/openshift/api/machineconfiguration/v1/types.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/machineconfiguration/v1/zz_generated.crd-manifests/0000_80_machine-config_01_kubeletconfigs.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/machineconfiguration/v1/zz_generated.crd-manifests/0000_80_machine-config_01_machineconfignodes-Default.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/machineconfiguration/v1/zz_generated.crd-manifests/0000_80_machine-config_01_machineconfignodes-Hypershift-Default.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/machineconfiguration/v1/zz_generated.crd-manifests/0000_80_machine-config_01_machineconfignodes-Hypershift-OKD.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/machineconfiguration/v1/zz_generated.crd-manifests/0000_80_machine-config_01_machineconfignodes-OKD.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/machineconfiguration/v1alpha1/types_osimagestream.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/machineconfiguration/v1alpha1/zz_generated.crd-manifests/0000_80_machine-config_01_internalreleaseimages-Hypershift.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/machineconfiguration/v1alpha1/zz_generated.crd-manifests/0000_80_machine-config_01_internalreleaseimages-SelfManagedHA.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/machineconfiguration/v1alpha1/zz_generated.crd-manifests/0000_80_machine-config_01_internalreleaseimages-TechPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/machineconfiguration/v1alpha1/zz_generated.crd-manifests/0000_80_machine-config_01_osimagestreams-DevPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/machineconfiguration/v1alpha1/zz_generated.crd-manifests/0000_80_machine-config_01_osimagestreams-TechPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/machineconfiguration/v1alpha1/zz_generated.crd-manifests/0000_80_machine-config_01_osimagestreams.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/machineconfiguration/v1alpha1/zz_generated.swagger_doc_generated.go is excluded by !**/vendor/**, !vendor/**, !**/zz_generated*
  • vendor/github.com/openshift/api/operator/v1alpha1/zz_generated.crd-manifests/0000_00_cluster-version-operator_01_clusterversionoperators-DevPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/operator/v1alpha1/zz_generated.crd-manifests/0000_00_cluster-version-operator_01_clusterversionoperators-TechPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/operator/v1alpha1/zz_generated.crd-manifests/0000_00_cluster-version-operator_01_clusterversionoperators.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/operator/v1alpha1/zz_generated.crd-manifests/0000_10_etcd_01_etcdbackups-CustomNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/operator/v1alpha1/zz_generated.crd-manifests/0000_10_etcd_01_etcdbackups-TechPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/operator/v1alpha1/zz_generated.crd-manifests/0000_10_etcd_01_etcdbackups.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/operator/v1alpha1/zz_generated.crd-manifests/0000_30_cluster-api_01_clusterapis-DevPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/operator/v1alpha1/zz_generated.crd-manifests/0000_30_cluster-api_01_clusterapis-TechPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/operator/v1alpha1/zz_generated.crd-manifests/0000_30_cluster-api_01_clusterapis.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/payload-command/render/render.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/payload-command/render/write_featureset.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/client-go/config/applyconfigurations/config/v1alpha1/clustermonitoringspec.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/client-go/config/applyconfigurations/config/v1alpha1/prometheusoperatoradmissionwebhookconfig.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/client-go/config/applyconfigurations/utils.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/client-go/machineconfiguration/applyconfigurations/machineconfiguration/v1alpha1/osimagestream.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/client-go/machineconfiguration/applyconfigurations/machineconfiguration/v1alpha1/osimagestreamspec.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/client-go/machineconfiguration/applyconfigurations/utils.go is excluded by !**/vendor/**, !vendor/**
  • vendor/modules.txt is excluded by !**/vendor/**, !vendor/**
📒 Files selected for processing (18)
  • go.mod
  • pkg/controller/bootstrap/bootstrap.go
  • pkg/controller/common/constants.go
  • pkg/controller/internalreleaseimage/internalreleaseimage_controller.go
  • pkg/controller/render/render_controller.go
  • pkg/operator/operator.go
  • pkg/operator/osimagestream_ocp.go
  • pkg/osimagestream/clusterversion.go
  • pkg/osimagestream/clusterversion_test.go
  • pkg/osimagestream/helpers.go
  • pkg/osimagestream/helpers_test.go
  • pkg/osimagestream/imagestream_provider.go
  • pkg/osimagestream/imagestream_provider_test.go
  • pkg/osimagestream/mocks_test.go
  • pkg/osimagestream/osimagestream.go
  • pkg/osimagestream/osimagestream_test.go
  • test/e2e-2of2/osimagestream_test.go
  • test/e2e-bootstrap/bootstrap_test.go
🚧 Files skipped from review as they are similar to previous changes (6)
  • pkg/osimagestream/helpers.go
  • pkg/osimagestream/helpers_test.go
  • go.mod
  • pkg/osimagestream/mocks_test.go
  • test/e2e-2of2/osimagestream_test.go
  • pkg/operator/osimagestream_ocp.go

Comment on lines +203 to +205
case *mcfgv1alpha1.OSImageStream:
// If given, it's treated as user input with config such as the default stream
osImageStream = obj
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Only consume the singleton cluster OSImageStream here.

Lines 203-205 accept any OSImageStream manifest and silently let the last one win. The runtime path later reads only the singleton cluster resource, so bootstrap can apply the wrong manifest's spec.defaultStream if an extra OSImageStream is present.

Proposed fix
 			case *mcfgv1alpha1.OSImageStream:
-				// If given, it's treated as user input with config such as the default stream
-				osImageStream = obj
+				if obj.GetName() != ctrlcommon.ClusterInstanceNameOSImageStream {
+					klog.V(4).Infof("skipping OSImageStream %q; only %q is consumed during bootstrap", obj.GetName(), ctrlcommon.ClusterInstanceNameOSImageStream)
+					continue
+				}
+				if osImageStream != nil {
+					return fmt.Errorf("multiple OSImageStream manifests found for %q", ctrlcommon.ClusterInstanceNameOSImageStream)
+				}
+				// If given, it's treated as user input with config such as the default stream.
+				osImageStream = obj
📝 Committable suggestion

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

Suggested change
case *mcfgv1alpha1.OSImageStream:
// If given, it's treated as user input with config such as the default stream
osImageStream = obj
case *mcfgv1alpha1.OSImageStream:
if obj.GetName() != ctrlcommon.ClusterInstanceNameOSImageStream {
klog.V(4).Infof("skipping OSImageStream %q; only %q is consumed during bootstrap", obj.GetName(), ctrlcommon.ClusterInstanceNameOSImageStream)
continue
}
if osImageStream != nil {
return fmt.Errorf("multiple OSImageStream manifests found for %q", ctrlcommon.ClusterInstanceNameOSImageStream)
}
// If given, it's treated as user input with config such as the default stream.
osImageStream = obj
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/controller/bootstrap/bootstrap.go` around lines 203 - 205, The code
currently assigns any *mcfgv1alpha1.OSImageStream to osImageStream, allowing a
non-singleton manifest to win; change the assignment so it only consumes the
cluster singleton OSImageStream used at runtime: in the switch arm for
*mcfgv1alpha1.OSImageStream, check the object's identity (e.g.,
obj.GetNamespace() and obj.GetName() or the specific singleton name/constant the
runtime reads) and only set osImageStream = obj when it matches the expected
cluster singleton; otherwise ignore (and optionally log) the extra OSImageStream
so the bootstrap path cannot be overridden by non-singleton manifests.

Comment on lines +285 to +290
// return if status hasn't changed
// The operator components only reacts to what the OSImageStream
// publish in its status subresource.
if reflect.DeepEqual(oldOSImageStream.Status, curOSImageStream.Status) {
return
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Requeue pools when spec.defaultStream changes.

Line 288 only compares Status, so a patch to osimagestream/cluster.spec.defaultStream exits early and no pool gets enqueued. That breaks the running-cluster override flow this PR is adding, because render resolves the default stream from the OSImageStream object itself.

Proposed fix
-	// return if status hasn't changed
-	// The operator components only reacts to what the OSImageStream
-	// publish in its status subresource.
-	if reflect.DeepEqual(oldOSImageStream.Status, curOSImageStream.Status) {
+	// Return only when neither the published streams nor the selected default changed.
+	if reflect.DeepEqual(oldOSImageStream.Status, curOSImageStream.Status) &&
+		reflect.DeepEqual(oldOSImageStream.Spec, curOSImageStream.Spec) {
 		return
 	}
📝 Committable suggestion

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

Suggested change
// return if status hasn't changed
// The operator components only reacts to what the OSImageStream
// publish in its status subresource.
if reflect.DeepEqual(oldOSImageStream.Status, curOSImageStream.Status) {
return
}
// Return only when neither the published streams nor the selected default changed.
if reflect.DeepEqual(oldOSImageStream.Status, curOSImageStream.Status) &&
reflect.DeepEqual(oldOSImageStream.Spec, curOSImageStream.Spec) {
return
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/controller/render/render_controller.go` around lines 285 - 290, The
current early-return only compares oldOSImageStream.Status vs
curOSImageStream.Status, so changes to spec.defaultStream are ignored; update
the guard in the reconcile path (where oldOSImageStream and curOSImageStream are
compared) to also detect changes to curOSImageStream.Spec.DefaultStream (or
compare the whole Spec.DefaultStream field) and only return early when both
Status and Spec.DefaultStream are unchanged—i.e., include a check that requeues
pools when curOSImageStream.Spec.DefaultStream !=
oldOSImageStream.Spec.DefaultStream (or use reflect.DeepEqual on a struct
containing Status and the DefaultStream) so spec.defaultStream updates trigger
enqueuing.

@openshift-ci-robot
Copy link
Contributor

openshift-ci-robot commented Mar 9, 2026

@pablintino: This pull request references MCO-2117 which is a valid jira issue.

Details

In response to this:

- What I did

Previously the default stream was determined by matching the OS version in the stream name, without the possibility of telling the MCO which default stream to use. This change replaces that logic, allowing users to input the defaultStream they want to use and falling back to the builtin if necessary. The builtin default stream determination logic has been improved to avoid hard-coding versions and it now picks the OSImageStream that points to the images of the default OS tags in the payload ImageStream.

- How to verify it

There are two main scenarios to test.

Install a cluster with a different default set at day-zero

mkdir installer-dir
./openshift-install --dir installer-dir create manifests

# Enable TechPreview
yq -i '.spec.featureSet = "TechPreviewNoUpgrade"' installer-dir/openshift/99_feature-gate.yaml

# Create the simple bare OSImageStream with just the spec
cat << 'EOF' > installer-dir/openshift/99_osimagestream.yaml
apiVersion: machineconfiguration.openshift.io/v1alpha1
kind: OSImageStream
metadata:
 name: cluster
spec:
 defaultStream: rhel-10
EOF

# Deploy the cluster
./openshift-install --dir installer-dir create cluster

# Ensure the nodes are using the expected stream
oc debug node/$(oc get nodes -l node-role.kubernetes.io/master -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release
Starting pod/pabrodri-test-xfllf-master-0-debug-d69vl ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.1 (Coughlan)

oc debug node/$(oc get nodes -l node-role.kubernetes.io/worker -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release
Starting pod/pabrodri-test-xfllf-worker-a-nvh9j-debug-76blg ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.1 (Coughlan)

Change the global default of a cluster running default MCPs

  1. Install a fresh OCP cluster containing this change with the default and TechPreview enabled.
  2. Wait for it to be fully stable.
  3. Ensure all pools are RHEL 9.
  4. Change the default
oc patch osimagestream cluster --type='merge' -p '{"spec":{"defaultStream":"rhel-10"}}'
  1. Wait for the pools to fully update
  2. Ensure nodes are running RHEL 10
oc debug node/$(oc get nodes -l node-role.kubernetes.io/master -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release                                                                                                      20:04:46
Starting pod/pabrodri-test-lppjx-master-0-debug-vrvgv ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.1 (Coughlan)

oc debug node/$(oc get nodes -l node-role.kubernetes.io/worker -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release
Starting pod/pabrodri-test-lppjx-worker-a-thv4m-debug-plvg8 ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.1 (Coughlan)

- Description for the changelog

Allow users to override the default OSImageStream via Spec.DefaultStream, falling back to the builtin default resolved from the payload ImageStream tags.

Summary by CodeRabbit

  • New Features

  • Support for user-provided OS image streams with automatic default selection (RHEL9 for 4.x, RHEL10 for 5.x+) and event-driven reconcilation when streams change.

  • Public helpers to expose builtin and spec default stream values.

  • Performance

  • Thread-safe caching of image stream retrieval to reduce repeated fetches.

  • Chores

  • Unified OS image-stream creation flow and dependency updates.

  • Tests

  • Expanded unit and e2e coverage for cluster-version parsing, default resolution, caching, create/update flows.

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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (3)
pkg/controller/render/render_controller.go (1)

285-290: ⚠️ Potential issue | 🟠 Major

Spec-only default changes are still ignored.

Line 288 only compares Status, so a patch to osimagestream/cluster.spec.defaultStream can return early without re-enqueuing any MachineConfigPool. That breaks the running-cluster override flow this PR is adding.

Suggested fix
-	// return if status hasn't changed
-	// The operator components only reacts to what the OSImageStream
-	// publish in its status subresource.
-	if reflect.DeepEqual(oldOSImageStream.Status, curOSImageStream.Status) {
+	// Return only when neither the published streams nor the selected default changed.
+	if reflect.DeepEqual(oldOSImageStream.Status, curOSImageStream.Status) &&
+		reflect.DeepEqual(oldOSImageStream.Spec, curOSImageStream.Spec) {
 		return
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/controller/render/render_controller.go` around lines 285 - 290, The
current early-return checks only reflect.DeepEqual(oldOSImageStream.Status,
curOSImageStream.Status) so spec-only changes (like
osimagestream.spec.defaultStream) are ignored; update the condition to also
compare the spec default stream (or the relevant spec field) before returning.
Specifically, change the check to return only when both
reflect.DeepEqual(oldOSImageStream.Status, curOSImageStream.Status) and
reflect.DeepEqual(oldOSImageStream.Spec.DefaultStream,
curOSImageStream.Spec.DefaultStream) (or an equivalent comparison of the
relevant Spec field) are true so a change to Spec.DefaultStream will not be
skipped; reference oldOSImageStream, curOSImageStream, .Status and
.Spec.DefaultStream in your fix.
pkg/osimagestream/osimagestream.go (2)

161-188: ⚠️ Potential issue | 🟠 Major

Stop deriving the builtin default from the major version alone.

getBuiltinDefaultStreamName still returns rhel-9/rhel-10 purely from the release major version, and installVersion short-circuits that path before the payload ImageStream is even consulted. That does not implement the PR objective of resolving the builtin default from the payload's default OS tags, so this can still annotate/pick the wrong builtin default whenever payload defaults diverge from the hard-coded version map.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/osimagestream/osimagestream.go` around lines 161 - 188, The function
getBuiltinDefaultStreamName currently short-circuits on installVersion and then
derives the builtin default solely from releaseVersion.Major(), which ignores
the payload ImageStream's default OS tags; change the logic in
getBuiltinDefaultStreamName to first consult the payload ImageStream (via
imageStreamProvider / existing helper getVersionFromImageStream or a new helper
that reads the payload ImageStream default OS tags) to resolve the builtin
default stream name (map the payload default OS tag to
StreamNameRHEL9/StreamNameRHEL10), only falling back to using installVersion or
the parsed build releaseVersion->major mapping if the payload does not provide a
default OS tag; update references to installVersion, imageStreamProvider,
getVersionFromImageStream, and the StreamNameRHEL9/StreamNameRHEL10 constants
accordingly.

141-158: ⚠️ Potential issue | 🟠 Major

Honor the new fallback order instead of pinning Status.DefaultStream.

This still treats Status.DefaultStream as sticky when Spec.DefaultStream is empty, and it still errors when the requested spec default is no longer present. Both cases contradict the new spec -> single stream -> builtin contract and can wedge reconciliation after the available stream set changes.

Suggested fix
 func getDefaultStreamSet(streams []v1alpha1.OSImageStreamSet, builtinDefault string, existingOSImageStream *v1alpha1.OSImageStream) (string, error) {
 	streamNames := GetStreamSetsNames(streams)

 	requestedDefaultStream := GetOSImageStreamSpecDefault(existingOSImageStream)
-	if requestedDefaultStream == "" && existingOSImageStream != nil && existingOSImageStream.Status.DefaultStream != "" {
-		// If the spec is empty but the CR is already populated pick whatever existed
-		requestedDefaultStream = existingOSImageStream.Status.DefaultStream
-	} else if requestedDefaultStream == "" {
-		// No user preference: if there's only one stream just use it,
-		// otherwise fall back to the builtin default.
-		if len(streams) == 1 {
-			return streams[0].Name, nil
-		}
-		requestedDefaultStream = builtinDefault
-	}
-
-	if slices.Contains(streamNames, requestedDefaultStream) {
+	if requestedDefaultStream != "" && slices.Contains(streamNames, requestedDefaultStream) {
 		return requestedDefaultStream, nil
 	}
+
+	if len(streams) == 1 {
+		return streams[0].Name, nil
+	}
+
+	if slices.Contains(streamNames, builtinDefault) {
+		return builtinDefault, nil
+	}

 	return "", fmt.Errorf("could not find the requested %s default stream in the list of OSImageStreams %s", requestedDefaultStream, streamNames)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/osimagestream/osimagestream.go` around lines 141 - 158, The code
currently treats existingOSImageStream.Status.DefaultStream as a sticky fallback
and returns an error if the requested default (from GetOSImageStreamSpecDefault)
is missing; change the logic in the default-selection block so the fallback
order is strictly: spec -> single available stream -> builtinDefault.
Concretely, remove the branch that assigns
existingOSImageStream.Status.DefaultStream when spec is empty, always set
requestedDefaultStream = GetOSImageStreamSpecDefault(existingOSImageStream) and
if it's empty choose the single stream when len(streams)==1 else builtinDefault;
additionally, if a non-empty requestedDefaultStream is not present in
streamNames (slices.Contains), do not return an error—fall back to the
single-stream or builtinDefault using the same single-stream-first rule and then
return that value instead of erroring.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@pkg/controller/render/render_controller.go`:
- Around line 285-290: The current early-return checks only
reflect.DeepEqual(oldOSImageStream.Status, curOSImageStream.Status) so spec-only
changes (like osimagestream.spec.defaultStream) are ignored; update the
condition to also compare the spec default stream (or the relevant spec field)
before returning. Specifically, change the check to return only when both
reflect.DeepEqual(oldOSImageStream.Status, curOSImageStream.Status) and
reflect.DeepEqual(oldOSImageStream.Spec.DefaultStream,
curOSImageStream.Spec.DefaultStream) (or an equivalent comparison of the
relevant Spec field) are true so a change to Spec.DefaultStream will not be
skipped; reference oldOSImageStream, curOSImageStream, .Status and
.Spec.DefaultStream in your fix.

In `@pkg/osimagestream/osimagestream.go`:
- Around line 161-188: The function getBuiltinDefaultStreamName currently
short-circuits on installVersion and then derives the builtin default solely
from releaseVersion.Major(), which ignores the payload ImageStream's default OS
tags; change the logic in getBuiltinDefaultStreamName to first consult the
payload ImageStream (via imageStreamProvider / existing helper
getVersionFromImageStream or a new helper that reads the payload ImageStream
default OS tags) to resolve the builtin default stream name (map the payload
default OS tag to StreamNameRHEL9/StreamNameRHEL10), only falling back to using
installVersion or the parsed build releaseVersion->major mapping if the payload
does not provide a default OS tag; update references to installVersion,
imageStreamProvider, getVersionFromImageStream, and the
StreamNameRHEL9/StreamNameRHEL10 constants accordingly.
- Around line 141-158: The code currently treats
existingOSImageStream.Status.DefaultStream as a sticky fallback and returns an
error if the requested default (from GetOSImageStreamSpecDefault) is missing;
change the logic in the default-selection block so the fallback order is
strictly: spec -> single available stream -> builtinDefault. Concretely, remove
the branch that assigns existingOSImageStream.Status.DefaultStream when spec is
empty, always set requestedDefaultStream =
GetOSImageStreamSpecDefault(existingOSImageStream) and if it's empty choose the
single stream when len(streams)==1 else builtinDefault; additionally, if a
non-empty requestedDefaultStream is not present in streamNames
(slices.Contains), do not return an error—fall back to the single-stream or
builtinDefault using the same single-stream-first rule and then return that
value instead of erroring.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: openshift/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 62a86a7f-acfa-4f99-8069-1f01c3156c7a

📥 Commits

Reviewing files that changed from the base of the PR and between c894d0f and 13289ed.

⛔ Files ignored due to path filters (64)
  • go.sum is excluded by !**/*.sum
  • vendor/github.com/openshift/api/.golangci.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/apiextensions/v1alpha1/types_compatibilityrequirement.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/apiextensions/v1alpha1/zz_generated.swagger_doc_generated.go is excluded by !**/vendor/**, !vendor/**, !**/zz_generated*
  • vendor/github.com/openshift/api/apps/v1/types.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/apps/v1/zz_prerelease_lifecycle_generated.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1/types_ingress.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1/zz_generated.crd-manifests/0000_10_config-operator_01_ingresses.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1/zz_generated.crd-manifests/0000_10_config-operator_01_insightsdatagathers-DevPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1/zz_generated.crd-manifests/0000_10_config-operator_01_insightsdatagathers-TechPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1/zz_generated.crd-manifests/0000_10_config-operator_01_insightsdatagathers.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1alpha1/types_cluster_monitoring.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1alpha1/types_insights.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1alpha1/zz_generated.crd-manifests/0000_10_config-operator_01_backups-CustomNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1alpha1/zz_generated.crd-manifests/0000_10_config-operator_01_backups-TechPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1alpha1/zz_generated.crd-manifests/0000_10_config-operator_01_backups.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1alpha1/zz_generated.crd-manifests/0000_10_config-operator_01_clustermonitorings-DevPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1alpha1/zz_generated.crd-manifests/0000_10_config-operator_01_clustermonitorings-TechPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1alpha1/zz_generated.crd-manifests/0000_10_config-operator_01_clustermonitorings.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1alpha1/zz_generated.crd-manifests/0000_10_config-operator_01_criocredentialproviderconfigs-DevPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1alpha1/zz_generated.crd-manifests/0000_10_config-operator_01_criocredentialproviderconfigs-TechPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1alpha1/zz_generated.crd-manifests/0000_10_config-operator_01_criocredentialproviderconfigs.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1alpha1/zz_generated.crd-manifests/0000_10_config-operator_01_insightsdatagathers-DevPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1alpha1/zz_generated.crd-manifests/0000_10_config-operator_01_insightsdatagathers-TechPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1alpha1/zz_generated.crd-manifests/0000_10_config-operator_01_insightsdatagathers.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/config/v1alpha1/zz_generated.deepcopy.go is excluded by !**/vendor/**, !vendor/**, !**/zz_generated*
  • vendor/github.com/openshift/api/config/v1alpha1/zz_generated.swagger_doc_generated.go is excluded by !**/vendor/**, !vendor/**, !**/zz_generated*
  • vendor/github.com/openshift/api/features.md is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/features/features.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/features/util.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/machine/v1/types_controlplanemachineset.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/machine/v1/zz_generated.featuregated-crd-manifests.yaml is excluded by !**/vendor/**, !vendor/**, !**/zz_generated*
  • vendor/github.com/openshift/api/machineconfiguration/v1/types.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/machineconfiguration/v1/zz_generated.crd-manifests/0000_80_machine-config_01_kubeletconfigs.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/machineconfiguration/v1/zz_generated.crd-manifests/0000_80_machine-config_01_machineconfignodes-Default.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/machineconfiguration/v1/zz_generated.crd-manifests/0000_80_machine-config_01_machineconfignodes-Hypershift-Default.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/machineconfiguration/v1/zz_generated.crd-manifests/0000_80_machine-config_01_machineconfignodes-Hypershift-OKD.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/machineconfiguration/v1/zz_generated.crd-manifests/0000_80_machine-config_01_machineconfignodes-OKD.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/machineconfiguration/v1alpha1/types_osimagestream.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/machineconfiguration/v1alpha1/zz_generated.crd-manifests/0000_80_machine-config_01_internalreleaseimages-Hypershift.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/machineconfiguration/v1alpha1/zz_generated.crd-manifests/0000_80_machine-config_01_internalreleaseimages-SelfManagedHA.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/machineconfiguration/v1alpha1/zz_generated.crd-manifests/0000_80_machine-config_01_internalreleaseimages-TechPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/machineconfiguration/v1alpha1/zz_generated.crd-manifests/0000_80_machine-config_01_osimagestreams-DevPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/machineconfiguration/v1alpha1/zz_generated.crd-manifests/0000_80_machine-config_01_osimagestreams-TechPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/machineconfiguration/v1alpha1/zz_generated.crd-manifests/0000_80_machine-config_01_osimagestreams.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/machineconfiguration/v1alpha1/zz_generated.swagger_doc_generated.go is excluded by !**/vendor/**, !vendor/**, !**/zz_generated*
  • vendor/github.com/openshift/api/operator/v1alpha1/zz_generated.crd-manifests/0000_00_cluster-version-operator_01_clusterversionoperators-DevPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/operator/v1alpha1/zz_generated.crd-manifests/0000_00_cluster-version-operator_01_clusterversionoperators-TechPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/operator/v1alpha1/zz_generated.crd-manifests/0000_00_cluster-version-operator_01_clusterversionoperators.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/operator/v1alpha1/zz_generated.crd-manifests/0000_10_etcd_01_etcdbackups-CustomNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/operator/v1alpha1/zz_generated.crd-manifests/0000_10_etcd_01_etcdbackups-TechPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/operator/v1alpha1/zz_generated.crd-manifests/0000_10_etcd_01_etcdbackups.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/operator/v1alpha1/zz_generated.crd-manifests/0000_30_cluster-api_01_clusterapis-DevPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/operator/v1alpha1/zz_generated.crd-manifests/0000_30_cluster-api_01_clusterapis-TechPreviewNoUpgrade.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/operator/v1alpha1/zz_generated.crd-manifests/0000_30_cluster-api_01_clusterapis.crd.yaml is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/payload-command/render/render.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/api/payload-command/render/write_featureset.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/client-go/config/applyconfigurations/config/v1alpha1/clustermonitoringspec.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/client-go/config/applyconfigurations/config/v1alpha1/prometheusoperatoradmissionwebhookconfig.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/client-go/config/applyconfigurations/utils.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/client-go/machineconfiguration/applyconfigurations/machineconfiguration/v1alpha1/osimagestream.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/client-go/machineconfiguration/applyconfigurations/machineconfiguration/v1alpha1/osimagestreamspec.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/openshift/client-go/machineconfiguration/applyconfigurations/utils.go is excluded by !**/vendor/**, !vendor/**
  • vendor/modules.txt is excluded by !**/vendor/**, !vendor/**
📒 Files selected for processing (18)
  • go.mod
  • pkg/controller/bootstrap/bootstrap.go
  • pkg/controller/common/constants.go
  • pkg/controller/internalreleaseimage/internalreleaseimage_controller.go
  • pkg/controller/render/render_controller.go
  • pkg/operator/operator.go
  • pkg/operator/osimagestream_ocp.go
  • pkg/osimagestream/clusterversion.go
  • pkg/osimagestream/clusterversion_test.go
  • pkg/osimagestream/helpers.go
  • pkg/osimagestream/helpers_test.go
  • pkg/osimagestream/imagestream_provider.go
  • pkg/osimagestream/imagestream_provider_test.go
  • pkg/osimagestream/mocks_test.go
  • pkg/osimagestream/osimagestream.go
  • pkg/osimagestream/osimagestream_test.go
  • test/e2e-2of2/osimagestream_test.go
  • test/e2e-bootstrap/bootstrap_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/e2e-2of2/osimagestream_test.go

@pablintino pablintino force-pushed the mco-2117 branch 2 times, most recently from d50a73b to 6e53d54 Compare March 9, 2026 16:06
@pablintino
Copy link
Contributor Author

/test unit

@openshift-ci-robot
Copy link
Contributor

openshift-ci-robot commented Mar 9, 2026

@pablintino: This pull request references MCO-2117 which is a valid jira issue.

Details

In response to this:

- What I did

Previously the default stream was determined by matching the OS version in the stream name, without the possibility of telling the MCO which default stream to use. This change replaces that logic, allowing users to input the defaultStream they want to use and falling back to the builtin if necessary. The builtin default stream determination logic has been improved to avoid hard-coding versions and it now picks the OSImageStream that points to the images of the default OS tags in the payload ImageStream.

- How to verify it

There are two main scenarios to test.

Install a cluster with a different default set at day-zero

mkdir installer-dir
./openshift-install --dir installer-dir create manifests

# Enable TechPreview
yq -i '.spec.featureSet = "TechPreviewNoUpgrade"' installer-dir/openshift/99_feature-gate.yaml

# Create the simple bare OSImageStream with just the spec
cat << 'EOF' > installer-dir/openshift/99_osimagestream.yaml
apiVersion: machineconfiguration.openshift.io/v1alpha1
kind: OSImageStream
metadata:
 name: cluster
spec:
 defaultStream: rhel-10
EOF

# Deploy the cluster
./openshift-install --dir installer-dir create cluster

# Ensure the nodes are using the expected stream
oc debug node/$(oc get nodes -l node-role.kubernetes.io/master -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release
Starting pod/pabrodri-test-xfllf-master-0-debug-d69vl ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.2 Beta (Coughlan)

oc debug node/$(oc get nodes -l node-role.kubernetes.io/worker -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release
Starting pod/pabrodri-test-xfllf-worker-a-nvh9j-debug-76blg ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.2 Beta (Coughlan)

Change the global default of a cluster running default MCPs

  1. Install a fresh OCP cluster containing this change with the default and TechPreview enabled.
  2. Wait for it to be fully stable.
  3. Ensure all pools are RHEL 9.
  4. Change the default
oc patch osimagestream cluster --type='merge' -p '{"spec":{"defaultStream":"rhel-10"}}'
  1. Wait for the pools to fully update
  2. Ensure nodes are running RHEL 10
oc debug node/$(oc get nodes -l node-role.kubernetes.io/master -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release                                                                                                      20:04:46
Starting pod/pabrodri-test-lppjx-master-0-debug-vrvgv ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.2 Beta (Coughlan)

oc debug node/$(oc get nodes -l node-role.kubernetes.io/worker -o jsonpath='{.items[0].metadata.name}') -- chroot /host cat /etc/redhat-release
Starting pod/pabrodri-test-lppjx-worker-a-thv4m-debug-plvg8 ...
To use host binaries, run `chroot /host`. Instead, if you need to access host namespaces, run `nsenter -a -t 1`.
Red Hat Enterprise Linux release 10.2 Beta (Coughlan)

- Description for the changelog

Allow users to override the default OSImageStream via Spec.DefaultStream, falling back to the builtin default resolved from the payload ImageStream tags.

Summary by CodeRabbit

  • New Features

  • Support for user-provided OS image streams with automatic default selection (RHEL9 for 4.x, RHEL10 for 5.x+) and event-driven reconcilation when streams change.

  • Public helpers to expose builtin and spec default stream values.

  • Performance

  • Thread-safe caching of image stream retrieval to reduce repeated fetches.

  • Chores

  • Unified OS image-stream creation flow and dependency updates.

  • Tests

  • Expanded unit and e2e coverage for cluster-version parsing, default resolution, caching, create/update flows.

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.

@pablintino
Copy link
Contributor Author

/retest-required

@pablintino
Copy link
Contributor Author

/retest

@pablintino pablintino force-pushed the mco-2117 branch 2 times, most recently from e0fa169 to 3d4c2c1 Compare March 10, 2026 09:51
@pablintino
Copy link
Contributor Author

/test unit

@pablintino
Copy link
Contributor Author

/test e2e-hypershift

@pablintino
Copy link
Contributor Author

/retest-required

@pablintino pablintino force-pushed the mco-2117 branch 2 times, most recently from 4385edb to 7f3db86 Compare March 10, 2026 17:31
@pablintino
Copy link
Contributor Author

/retest-required

@pablintino
Copy link
Contributor Author

/test e2e-aws-ovn

Copy link
Member

@cheesesashimi cheesesashimi left a comment

Choose a reason for hiding this comment

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

Overall, this looks good! I do have a couple of thoughts to consider.

})
existingOSImageStream := createOptions.ExistingOSImageStream
requestedDefaultStream := GetOSImageStreamSpecDefault(existingOSImageStream)
if requestedDefaultStream == "" && existingOSImageStream != nil && existingOSImageStream.Status.DefaultStream != "" {
Copy link
Member

Choose a reason for hiding this comment

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

suggestion (non-blocking): We can make this logic a bit easier to follow and parse by dropping the else if:

if requestedDefaultStream == "" && existingOSImageStream != nil && existingOSImageStream.Status.DefaultStream != "" {
	// If the spec is empty but the CR is already populated pick whatever existed
	requestedDefaultStream = existingOSImageStream.Status.DefaultStream		
}

if requestedDefaultStream == "" {
	// No user preference: if there's only one stream just use it,
	// otherwise fall back to the builtin default.
	if len(streams) == 1 {
		return streams[0].Name, nil
	}
	builtinDefault, err := getBuiltinDefaultStreamName(createOptions.InstallVersion)
	if err != nil {
		return "", fmt.Errorf("could not determine the builtin default stream: %w", err)
	}
	requestedDefaultStream = builtinDefault
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, changed.

for _, stream := range streamNames {
if (strings.Contains(stream, "coreos-9") || strings.Contains(stream, "9-coreos") || strings.Contains(stream, "9")) && !strings.Contains(stream, "10") {
return stream, nil
// getBuiltinDefaultStreamName returns the default stream name based on the OCP version.
Copy link
Member

@cheesesashimi cheesesashimi Mar 11, 2026

Choose a reason for hiding this comment

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

issue: This will break OKD. In this particular case, it may be worth maintaining an OCP and OKD version of this function, segregated with build tags. The OKD version could be as simple as:

func getBuiltinDefaultStreamName(_ *k8sversion.Version) (string, error) {
	return streamNameCentos10, nil
}

The other way that I'm aware of to determine whether a cluster is OKD at runtime is by looking at .spec.upstream on the ClusterVersion object since that's what our e2e tests use. And I'm not sure how reliable that method actually is.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I must agree the implementation may not be obvious and/or robust. I thought as a solution for OKD the condition in https://github.com/openshift/machine-config-operator/pull/5714/changes#diff-1353921332a3d771864ad7388ab828a62e717ed7dc8f02ee13b5be6361f9c769R144-R146.
As OKD payloads may have only a single stream that piece of code should avoid hitting this function and returning the wrong stream.
I'm going to take your comment and improve this, cause it's not obvious and not reliable.

// StreamNameRHEL9 is the stream name for RHEL 9 based CoreOS images.
StreamNameRHEL9 = "rhel-9"
// StreamNameRHEL10 is the stream name for RHEL 10 based CoreOS images.
StreamNameRHEL10 = "rhel-10"
Copy link
Member

Choose a reason for hiding this comment

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

issue: We also need streamNameCentos10 = "centos-10" to maintain compatibility with OKD.

Perhaps we could also colocate this constant that with the OKD version of the getBuiltinDefaultStreamName() described below while also colocating the OCP constants with the OCP version of that function?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added.


const (
// StreamNameRHEL9 is the stream name for RHEL 9 based CoreOS images.
StreamNameRHEL9 = "rhel-9"
Copy link
Member

Choose a reason for hiding this comment

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

suggestion (non-blocking): I know that we are trying to avoid hard-coding these values because we'll have to maintain them. For now, maybe we could avoid exporting them outside of the osimagestream package by making them streamNameRHEL9 and streamNameRHEL10? That way, there's less of a chance that someone would do conditional logic on their values later.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, same thought here, but unfortunately, I think we may need to have explicit stream names :(, at least for the streams we will manage (and we will EoL at some point).
Here we have another example of why may need to have references to streams (despite I do not like the idea at all neither) https://github.com/openshift/machine-config-operator/pull/5752/changes#diff-4455c81495e83defc0ba998694bf0c729383bae72d218242f3d84671fb19229aR126

Copy link
Contributor Author

@pablintino pablintino left a comment

Choose a reason for hiding this comment

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

@cheesesashimi I've pushed a change to try to address your comments.
For the OKD vs OCP ones I've leaned towards a runtime check of version.IsSCOS() cause that approach gives me a chance to test the function, something I couldn't do using plain build tags in dedicated files, as we do not have OKD tests iiuc.

})
existingOSImageStream := createOptions.ExistingOSImageStream
requestedDefaultStream := GetOSImageStreamSpecDefault(existingOSImageStream)
if requestedDefaultStream == "" && existingOSImageStream != nil && existingOSImageStream.Status.DefaultStream != "" {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, changed.

for _, stream := range streamNames {
if (strings.Contains(stream, "coreos-9") || strings.Contains(stream, "9-coreos") || strings.Contains(stream, "9")) && !strings.Contains(stream, "10") {
return stream, nil
// getBuiltinDefaultStreamName returns the default stream name based on the OCP version.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I must agree the implementation may not be obvious and/or robust. I thought as a solution for OKD the condition in https://github.com/openshift/machine-config-operator/pull/5714/changes#diff-1353921332a3d771864ad7388ab828a62e717ed7dc8f02ee13b5be6361f9c769R144-R146.
As OKD payloads may have only a single stream that piece of code should avoid hitting this function and returning the wrong stream.
I'm going to take your comment and improve this, cause it's not obvious and not reliable.

// StreamNameRHEL9 is the stream name for RHEL 9 based CoreOS images.
StreamNameRHEL9 = "rhel-9"
// StreamNameRHEL10 is the stream name for RHEL 10 based CoreOS images.
StreamNameRHEL10 = "rhel-10"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added.


const (
// StreamNameRHEL9 is the stream name for RHEL 9 based CoreOS images.
StreamNameRHEL9 = "rhel-9"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, same thought here, but unfortunately, I think we may need to have explicit stream names :(, at least for the streams we will manage (and we will EoL at some point).
Here we have another example of why may need to have references to streams (despite I do not like the idea at all neither) https://github.com/openshift/machine-config-operator/pull/5752/changes#diff-4455c81495e83defc0ba998694bf0c729383bae72d218242f3d84671fb19229aR126

Previously the default stream was determined by matching the OS version
in the stream name, without the possibility of telling the MCO which
default stream to use. This change replaces that logic, allowing users
to input the defaultStream they want to use and falling back to the
builtin if necessary. The builtin default stream determination logic
has been improved to avoid hard-coding versions and it now picks the
OSImageStream that points to the images of the default OS tags
in the payload ImageStream.

Signed-off-by: Pablo Rodriguez Nava <git@amail.pablintino.eu>
@openshift-ci
Copy link
Contributor

openshift-ci bot commented Mar 13, 2026

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

Test name Commit Details Required Rerun command
ci/prow/e2e-aws-ovn c0e0945 link true /test e2e-aws-ovn
ci/prow/e2e-aws-ovn-upgrade c0e0945 link true /test e2e-aws-ovn-upgrade
ci/prow/e2e-hypershift c0e0945 link true /test e2e-hypershift

Full PR test history. Your PR dashboard.

Details

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

@cheesesashimi
Copy link
Member

@pablintino I agree that the version.IsSCOS() / version.IsFCOS() functions are a good way for checking this at runtime. There are two things I want to call out about these functions though:

  1. The value returned by those functions is set by Go build tags: https://github.com/openshift/machine-config-operator/blob/main/pkg/version/version_scos.go. I call this out just to clarify that although these functions are used at runtime, their values are actually set at build-time. The values can be overridden at runtime for testing purposes.
  2. Somewhat tangential, but related: In some of our e2e tests, we look at the ClusterVersion field at runtime in order to determine if the cluster we're testing against is an OKD cluster to skip certain tests altogether or to change what the expected value is. We have to do that instead of relying on those functions because of the execution context of the e2e test code. That said, we don't routinely run our e2e suite against OKD (though maybe we should).

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

5 participants