From 8f3db0036112b3940ea7465663f8cf1aa9b9ff9f Mon Sep 17 00:00:00 2001 From: Ankita Thomas Date: Mon, 17 Aug 2026 13:12:22 -0400 Subject: [PATCH 1/6] force node arch for opm and catalogsource pod for multiarch tests Signed-off-by: Ankita Thomas --- tests-extension/pkg/bindata/qe/bindata.go | 7 +++++ .../test/qe/specs/olmv0_custom_schema.go | 6 +++- .../qe/testdata/olm/catalogsource-image.yaml | 4 +++ .../olm/custom-schema-buildconfig.yaml | 3 ++ .../test/qe/util/olmv0util/catalog_source.go | 28 ++++++++++++++++++- .../qe/util/olmv0util/custom_schema_grpc.go | 9 ++++-- 6 files changed, 52 insertions(+), 5 deletions(-) diff --git a/tests-extension/pkg/bindata/qe/bindata.go b/tests-extension/pkg/bindata/qe/bindata.go index bbc3d2fcd2..c59290290c 100644 --- a/tests-extension/pkg/bindata/qe/bindata.go +++ b/tests-extension/pkg/bindata/qe/bindata.go @@ -510,6 +510,9 @@ objects: name: "${NAME}" namespace: "${NAMESPACE}" spec: + grpcPodConfig: + nodeSelector: + kubernetes.io/arch: "${ARCH}" image: "${ADDRESS}" secrets: - "${SECRET}" @@ -532,6 +535,7 @@ parameters: - name: SECRET - name: INTERVAL value: "10m0s" +- name: ARCH `) func testQeTestdataOlmCatalogsourceImageYamlBytes() ([]byte, error) { @@ -6952,6 +6956,8 @@ objects: name: "${NAME}" namespace: "${NAMESPACE}" spec: + nodeSelector: + kubernetes.io/arch: "${ARCH}" output: to: kind: ImageStreamTag @@ -6968,6 +6974,7 @@ parameters: - name: NAME - name: NAMESPACE - name: BASE_IMAGE +- name: ARCH `) func testQeTestdataOlmCustomSchemaBuildconfigYamlBytes() ([]byte, error) { diff --git a/tests-extension/test/qe/specs/olmv0_custom_schema.go b/tests-extension/test/qe/specs/olmv0_custom_schema.go index 3171a227b9..2819e99ee6 100644 --- a/tests-extension/test/qe/specs/olmv0_custom_schema.go +++ b/tests-extension/test/qe/specs/olmv0_custom_schema.go @@ -53,7 +53,10 @@ var _ = g.Describe("[sig-operator][Jira:OLM][OCPFeatureGate:OLMLifecycleAndCompa fbcContent, err := os.ReadFile(exutil.FixturePath("testdata", "custom-schema", "index.json")) o.Expect(err).NotTo(o.HaveOccurred()) - imageRef := olmv0util.BuildCustomCatalogImage(oc, namespace, catalogName, baseImage, fbcContent) + testArch := olmv0util.GetNodeArch(oc) + e2e.Logf("test running on architecture: %s", testArch) + + imageRef := olmv0util.BuildCustomCatalogImage(oc, namespace, catalogName, baseImage, testArch, fbcContent) e2e.Logf("built catalog image: %s", imageRef) // Register build resources for cleanup @@ -67,6 +70,7 @@ var _ = g.Describe("[sig-operator][Jira:OLM][OCPFeatureGate:OLMLifecycleAndCompa SourceType: "grpc", Address: imageRef, Template: exutil.FixturePath("testdata", "olm", "catalogsource-image.yaml"), + Arch: testArch, } catsrc.CreateWithCheck(oc, itName, dr) diff --git a/tests-extension/test/qe/testdata/olm/catalogsource-image.yaml b/tests-extension/test/qe/testdata/olm/catalogsource-image.yaml index ebb5d8624f..d5a1484d8a 100644 --- a/tests-extension/test/qe/testdata/olm/catalogsource-image.yaml +++ b/tests-extension/test/qe/testdata/olm/catalogsource-image.yaml @@ -9,6 +9,9 @@ objects: name: "${NAME}" namespace: "${NAMESPACE}" spec: + grpcPodConfig: + nodeSelector: + kubernetes.io/arch: "${ARCH}" image: "${ADDRESS}" secrets: - "${SECRET}" @@ -31,3 +34,4 @@ parameters: - name: SECRET - name: INTERVAL value: "10m0s" +- name: ARCH diff --git a/tests-extension/test/qe/testdata/olm/custom-schema-buildconfig.yaml b/tests-extension/test/qe/testdata/olm/custom-schema-buildconfig.yaml index bed1592c29..1415f72ba7 100644 --- a/tests-extension/test/qe/testdata/olm/custom-schema-buildconfig.yaml +++ b/tests-extension/test/qe/testdata/olm/custom-schema-buildconfig.yaml @@ -9,6 +9,8 @@ objects: name: "${NAME}" namespace: "${NAMESPACE}" spec: + nodeSelector: + kubernetes.io/arch: "${ARCH}" output: to: kind: ImageStreamTag @@ -25,3 +27,4 @@ parameters: - name: NAME - name: NAMESPACE - name: BASE_IMAGE +- name: ARCH diff --git a/tests-extension/test/qe/util/olmv0util/catalog_source.go b/tests-extension/test/qe/util/olmv0util/catalog_source.go index 4db214c31e..3a0f3072ef 100644 --- a/tests-extension/test/qe/util/olmv0util/catalog_source.go +++ b/tests-extension/test/qe/util/olmv0util/catalog_source.go @@ -5,7 +5,9 @@ package olmv0util import ( "context" + "encoding/json" "fmt" + "runtime" "strings" "time" @@ -33,6 +35,25 @@ type CatalogSourceDescription struct { Interval string // Update interval for the catalog source ImageTemplate string // Image template for catalog updates ClusterType string // Target cluster type (e.g., "microshift") + Arch string // Architecture for node selector +} + +func GetNodeArch(oc *exutil.CLI) string { + envOutput, err := oc.AsAdmin().WithoutNamespace().Run("get").Args( + "nodes", "-o=jsonpath={.metadata.labels}", + ).Output() + o.Expect(err).NotTo(o.HaveOccurred()) + + for _, nodeLabels := range strings.Split(envOutput, " ") { + labels := map[string]string{} + err = json.Unmarshal([]byte(nodeLabels), &labels) + o.Expect(err).NotTo(o.HaveOccurred()) + if nodeArch, ok := labels["kubernetes.io/arch"]; ok && len(nodeArch) > 0 { + return nodeArch + } + } + + return runtime.GOARCH } // Create creates a CatalogSource resource using the provided template and parameters @@ -49,6 +70,11 @@ func (catsrc *CatalogSourceDescription) Create(oc *exutil.CLI, itName string, dr catsrc.Interval = "10m0s" e2e.Logf("set interval to be 10m0s") } + + if catsrc.Arch == "" { + catsrc.Arch = GetNodeArch(oc) + e2e.Logf("set catalogsource node selector arch to %s", catsrc.Arch) + } // Choose appropriate template application function based on cluster type applyFn := ApplyResourceFromTemplate if strings.Compare(catsrc.ClusterType, "microshift") == 0 { @@ -63,7 +89,7 @@ func (catsrc *CatalogSourceDescription) Create(oc *exutil.CLI, itName string, dr err := applyFn(oc, "--ignore-unknown-parameters=true", "-f", catsrc.Template, "-p", "NAME="+catsrc.Name, "NAMESPACE="+catsrc.Namespace, "ADDRESS="+catsrc.Address, "SECRET="+catsrc.Secret, "DISPLAYNAME="+"\""+catsrc.DisplayName+"\"", "PUBLISHER="+"\""+catsrc.Publisher+"\"", "SOURCETYPE="+catsrc.SourceType, - "INTERVAL="+catsrc.Interval, "IMAGETEMPLATE="+imageTemplate) + "INTERVAL="+catsrc.Interval, "IMAGETEMPLATE="+imageTemplate, "ARCH="+catsrc.Arch) o.Expect(err).NotTo(o.HaveOccurred()) // Configure security context constraints for non-microshift clusters if strings.Compare(catsrc.ClusterType, "microshift") != 0 { diff --git a/tests-extension/test/qe/util/olmv0util/custom_schema_grpc.go b/tests-extension/test/qe/util/olmv0util/custom_schema_grpc.go index 6138cb8c1b..c19a972e87 100644 --- a/tests-extension/test/qe/util/olmv0util/custom_schema_grpc.go +++ b/tests-extension/test/qe/util/olmv0util/custom_schema_grpc.go @@ -123,7 +123,7 @@ func GetOPMBaseImage(oc *exutil.CLI) string { // BuildConfig. It creates an ImageStream and BuildConfig, starts a binary build // from the provided FBC content, and waits for completion. // Returns the internal registry image reference. -func BuildCustomCatalogImage(oc *exutil.CLI, namespace, name, baseImage string, fbcContent []byte) string { +func BuildCustomCatalogImage(oc *exutil.CLI, namespace, name, baseImage, arch string, fbcContent []byte) string { // Create ImageStream isTemplate := exutil.FixturePath("testdata", "olm", "custom-schema-imagestream.yaml") err := ApplyResourceFromTemplate(oc, "--ignore-unknown-parameters=true", "-f", isTemplate, @@ -132,11 +132,14 @@ func BuildCustomCatalogImage(oc *exutil.CLI, namespace, name, baseImage string, e2e.Logf("created ImageStream %s/%s", namespace, name) // Create BuildConfig + if arch == "" { + arch = GetNodeArch(oc) + } bcTemplate := exutil.FixturePath("testdata", "olm", "custom-schema-buildconfig.yaml") err = ApplyResourceFromTemplate(oc, "--ignore-unknown-parameters=true", "-f", bcTemplate, - "-p", "NAME="+name, "NAMESPACE="+namespace, "BASE_IMAGE="+baseImage) + "-p", "NAME="+name, "NAMESPACE="+namespace, "BASE_IMAGE="+baseImage, "ARCH="+arch) o.Expect(err).NotTo(o.HaveOccurred()) - e2e.Logf("created BuildConfig %s/%s with base image %s", namespace, name, baseImage) + e2e.Logf("created BuildConfig %s/%s with base image %s for arch %s", namespace, name, baseImage, arch) // Prepare build directory buildDir, err := os.MkdirTemp("", "custom-schema-build-") From 87797e1f60949e54eedd551e306c7389fc2db8e8 Mon Sep 17 00:00:00 2001 From: Chris Moore Date: Thu, 20 Aug 2026 12:12:34 -0400 Subject: [PATCH 2/6] OCPBUGS-104542: query node arch from worker node status Signed-off-by: Chris Moore --- .../test/qe/util/olmv0util/catalog_source.go | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/tests-extension/test/qe/util/olmv0util/catalog_source.go b/tests-extension/test/qe/util/olmv0util/catalog_source.go index 3a0f3072ef..361fa3fd88 100644 --- a/tests-extension/test/qe/util/olmv0util/catalog_source.go +++ b/tests-extension/test/qe/util/olmv0util/catalog_source.go @@ -5,7 +5,6 @@ package olmv0util import ( "context" - "encoding/json" "fmt" "runtime" "strings" @@ -38,21 +37,21 @@ type CatalogSourceDescription struct { Arch string // Architecture for node selector } +// Query architecture directly from a worker node's status. +// Use worker nodes specifically since BuildConfig and CatalogSource +// pods are scheduled on worker nodes. func GetNodeArch(oc *exutil.CLI) string { - envOutput, err := oc.AsAdmin().WithoutNamespace().Run("get").Args( - "nodes", "-o=jsonpath={.metadata.labels}", + arch, err := oc.AsAdmin().WithoutNamespace().Run("get").Args( + "nodes", + "-l", "node-role.kubernetes.io/worker", + "--field-selector", "spec.unschedulable!=true", + "-o", "jsonpath={.items[0].status.nodeInfo.architecture}", ).Output() - o.Expect(err).NotTo(o.HaveOccurred()) - - for _, nodeLabels := range strings.Split(envOutput, " ") { - labels := map[string]string{} - err = json.Unmarshal([]byte(nodeLabels), &labels) - o.Expect(err).NotTo(o.HaveOccurred()) - if nodeArch, ok := labels["kubernetes.io/arch"]; ok && len(nodeArch) > 0 { - return nodeArch - } + if err == nil && len(strings.TrimSpace(arch)) > 0 { + return strings.TrimSpace(arch) } + e2e.Logf("failed to get node architecture from cluster (%v), falling back to runtime.GOARCH (%s)", err, runtime.GOARCH) return runtime.GOARCH } From d0f598fff26ac3b590c3015cd801eca867e87f02 Mon Sep 17 00:00:00 2001 From: grokspawn Date: Tue, 25 Aug 2026 12:40:44 -0500 Subject: [PATCH 3/6] set catalogsource spec.grpcpodconfig.scc: restricted for all non-legacy cases Signed-off-by: grokspawn --- tests-extension/pkg/bindata/qe/bindata.go | 21 ++++++++++++++----- .../olm/catalogsource-image-cacheless.yaml | 1 + .../olm/catalogsource-image-extract.yaml | 1 + ...source-image-incorrect-updatestrategy.yaml | 8 ++++--- .../qe/testdata/olm/catalogsource-image.yaml | 1 + .../qe/testdata/olm/catalogsource-opm.yaml | 1 + .../qe/testdata/olm/cs-image-template.yaml | 4 +++- .../qe/testdata/olm/cs-without-interval.yaml | 4 +++- 8 files changed, 31 insertions(+), 10 deletions(-) diff --git a/tests-extension/pkg/bindata/qe/bindata.go b/tests-extension/pkg/bindata/qe/bindata.go index c59290290c..ac093de318 100644 --- a/tests-extension/pkg/bindata/qe/bindata.go +++ b/tests-extension/pkg/bindata/qe/bindata.go @@ -359,6 +359,7 @@ objects: spec: image: "${ADDRESS}" grpcPodConfig: + securityContextConfig: restricted extractContent: catalogDir: /configs secrets: @@ -412,6 +413,7 @@ objects: spec: image: "${ADDRESS}" grpcPodConfig: + securityContextConfig: restricted extractContent: cacheDir: /tmp/cache catalogDir: /configs @@ -465,15 +467,17 @@ objects: namespace: "${NAMESPACE}" spec: image: "${ADDRESS}" + grpcPodConfig: + securityContextConfig: restricted secrets: - - "${SECRET}" + - "${SECRET}" displayName: "${DISPLAYNAME}" icon: base64data: "" mediatype: "" publisher: "${PUBLISHER}" sourceType: "${SOURCETYPE}" - updateStrategy: + updateStrategy: registryPoll: {} parameters: - name: NAME @@ -482,7 +486,8 @@ parameters: - name: DISPLAYNAME - name: PUBLISHER - name: SOURCETYPE -- name: SECRET`) +- name: SECRET +`) func testQeTestdataOlmCatalogsourceImageIncorrectUpdatestrategyYamlBytes() ([]byte, error) { return _testQeTestdataOlmCatalogsourceImageIncorrectUpdatestrategyYaml, nil @@ -511,6 +516,7 @@ objects: namespace: "${NAMESPACE}" spec: grpcPodConfig: + securityContextConfig: restricted nodeSelector: kubernetes.io/arch: "${ARCH}" image: "${ADDRESS}" @@ -645,6 +651,7 @@ objects: namespace: "${NAMESPACE}" spec: grpcPodConfig: + securityContextConfig: restricted extractContent: cacheDir: /tmp/cache catalogDir: /configs @@ -6732,8 +6739,10 @@ objects: namespace: "${NAMESPACE}" spec: image: "${ADDRESS}" + grpcPodConfig: + securityContextConfig: restricted secrets: - - "${SECRET}" + - "${SECRET}" displayName: "${DISPLAYNAME}" publisher: "${PUBLISHER}" sourceType: "${SOURCETYPE}" @@ -6824,8 +6833,10 @@ objects: namespace: "${NAMESPACE}" spec: image: "${ADDRESS}" + grpcPodConfig: + securityContextConfig: restricted secrets: - - "${SECRET}" + - "${SECRET}" displayName: "${DISPLAYNAME}" publisher: "${PUBLISHER}" sourceType: "${SOURCETYPE}" diff --git a/tests-extension/test/qe/testdata/olm/catalogsource-image-cacheless.yaml b/tests-extension/test/qe/testdata/olm/catalogsource-image-cacheless.yaml index a63f50fa1b..03e8bf8fb9 100644 --- a/tests-extension/test/qe/testdata/olm/catalogsource-image-cacheless.yaml +++ b/tests-extension/test/qe/testdata/olm/catalogsource-image-cacheless.yaml @@ -11,6 +11,7 @@ objects: spec: image: "${ADDRESS}" grpcPodConfig: + securityContextConfig: restricted extractContent: catalogDir: /configs secrets: diff --git a/tests-extension/test/qe/testdata/olm/catalogsource-image-extract.yaml b/tests-extension/test/qe/testdata/olm/catalogsource-image-extract.yaml index e01c47ff2e..f359383c29 100644 --- a/tests-extension/test/qe/testdata/olm/catalogsource-image-extract.yaml +++ b/tests-extension/test/qe/testdata/olm/catalogsource-image-extract.yaml @@ -11,6 +11,7 @@ objects: spec: image: "${ADDRESS}" grpcPodConfig: + securityContextConfig: restricted extractContent: cacheDir: /tmp/cache catalogDir: /configs diff --git a/tests-extension/test/qe/testdata/olm/catalogsource-image-incorrect-updatestrategy.yaml b/tests-extension/test/qe/testdata/olm/catalogsource-image-incorrect-updatestrategy.yaml index e98530a23e..2b2ef167eb 100644 --- a/tests-extension/test/qe/testdata/olm/catalogsource-image-incorrect-updatestrategy.yaml +++ b/tests-extension/test/qe/testdata/olm/catalogsource-image-incorrect-updatestrategy.yaml @@ -10,15 +10,17 @@ objects: namespace: "${NAMESPACE}" spec: image: "${ADDRESS}" + grpcPodConfig: + securityContextConfig: restricted secrets: - - "${SECRET}" + - "${SECRET}" displayName: "${DISPLAYNAME}" icon: base64data: "" mediatype: "" publisher: "${PUBLISHER}" sourceType: "${SOURCETYPE}" - updateStrategy: + updateStrategy: registryPoll: {} parameters: - name: NAME @@ -27,4 +29,4 @@ parameters: - name: DISPLAYNAME - name: PUBLISHER - name: SOURCETYPE -- name: SECRET \ No newline at end of file +- name: SECRET diff --git a/tests-extension/test/qe/testdata/olm/catalogsource-image.yaml b/tests-extension/test/qe/testdata/olm/catalogsource-image.yaml index d5a1484d8a..4c3e1bb433 100644 --- a/tests-extension/test/qe/testdata/olm/catalogsource-image.yaml +++ b/tests-extension/test/qe/testdata/olm/catalogsource-image.yaml @@ -10,6 +10,7 @@ objects: namespace: "${NAMESPACE}" spec: grpcPodConfig: + securityContextConfig: restricted nodeSelector: kubernetes.io/arch: "${ARCH}" image: "${ADDRESS}" diff --git a/tests-extension/test/qe/testdata/olm/catalogsource-opm.yaml b/tests-extension/test/qe/testdata/olm/catalogsource-opm.yaml index dfc1edfc4e..eee5cff217 100644 --- a/tests-extension/test/qe/testdata/olm/catalogsource-opm.yaml +++ b/tests-extension/test/qe/testdata/olm/catalogsource-opm.yaml @@ -10,6 +10,7 @@ objects: namespace: "${NAMESPACE}" spec: grpcPodConfig: + securityContextConfig: restricted extractContent: cacheDir: /tmp/cache catalogDir: /configs diff --git a/tests-extension/test/qe/testdata/olm/cs-image-template.yaml b/tests-extension/test/qe/testdata/olm/cs-image-template.yaml index e08034128a..dac6362fe0 100644 --- a/tests-extension/test/qe/testdata/olm/cs-image-template.yaml +++ b/tests-extension/test/qe/testdata/olm/cs-image-template.yaml @@ -12,8 +12,10 @@ objects: namespace: "${NAMESPACE}" spec: image: "${ADDRESS}" + grpcPodConfig: + securityContextConfig: restricted secrets: - - "${SECRET}" + - "${SECRET}" displayName: "${DISPLAYNAME}" publisher: "${PUBLISHER}" sourceType: "${SOURCETYPE}" diff --git a/tests-extension/test/qe/testdata/olm/cs-without-interval.yaml b/tests-extension/test/qe/testdata/olm/cs-without-interval.yaml index 4a3f9dd0c5..5ec3e4a025 100644 --- a/tests-extension/test/qe/testdata/olm/cs-without-interval.yaml +++ b/tests-extension/test/qe/testdata/olm/cs-without-interval.yaml @@ -12,8 +12,10 @@ objects: namespace: "${NAMESPACE}" spec: image: "${ADDRESS}" + grpcPodConfig: + securityContextConfig: restricted secrets: - - "${SECRET}" + - "${SECRET}" displayName: "${DISPLAYNAME}" publisher: "${PUBLISHER}" sourceType: "${SOURCETYPE}" From 5016b1317215a416c2510a63ab59a550e8b3b080 Mon Sep 17 00:00:00 2001 From: Harald Klein Date: Wed, 28 Oct 2026 14:43:36 +0100 Subject: [PATCH 4/6] Sort incompatible CSVs in the Upgradeable condition message incompatibleOperators lists the CSVs from the informer cache. The cache returns them in map order, which changes between reconciles. skews.String() kept that order, so the same set of blocking CSVs produced different messages. conditionsEqual compares the message, so each reorder counted as a condition transition: the controller rewrote the ClusterOperator status and moved lastTransitionTime although nothing had changed. With two blocking CSVs on OCP 4.22 the message alternated between the two orders every one to two minutes. skews.String() now sorts a copy of the skews before it builds the message: skews with a property error first, as before, then by namespace and name. The same set of CSVs gives the same message in any input order, so the condition only transitions when the set changes. The regression test feeds three skews in three orders and expects one message. It fails on the previous implementation, which filled the non-error entries from the end of the slice. Fixes OCPBUGS-129618 Signed-off-by: Harald Klein Co-Authored-By: Claude --- .../operators/openshift/helpers_test.go | 17 +++++++++++ .../controller/operators/openshift/helpers.go | 30 ++++++++++++------- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/staging/operator-lifecycle-manager/pkg/controller/operators/openshift/helpers_test.go b/staging/operator-lifecycle-manager/pkg/controller/operators/openshift/helpers_test.go index 43d8c5e63f..ee81c2122c 100644 --- a/staging/operator-lifecycle-manager/pkg/controller/operators/openshift/helpers_test.go +++ b/staging/operator-lifecycle-manager/pkg/controller/operators/openshift/helpers_test.go @@ -815,3 +815,20 @@ func TestNormalizeOCPVersion(t *testing.T) { }) } } + +func TestSkewsStringIsDeterministic(t *testing.T) { + // The informer cache lists CSVs in map order. The condition message must not + // depend on that order, or every reorder counts as a condition transition. + a := skew{namespace: "openshift-logging", name: "cluster-logging.v6.6.1", maxOpenShiftVersion: "4.23"} + b := skew{namespace: "openshift-cluster-csi-drivers", name: "secrets-store-csi-driver-operator.v4.22.0", maxOpenShiftVersion: "5.0"} + invalid := skew{namespace: "openshift-logging", name: "broken.v1.0.0", err: fmt.Errorf("invalid")} + + expected := "ClusterServiceVersions blocking minor version upgrades to 4.23 or major version upgrades to 5.0:\n" + + invalid.String() + "\n" + // error skews first + b.String() + "\n" + // then by namespace/name + a.String() + + require.Equal(t, expected, skews{a, b, invalid}.String()) + require.Equal(t, expected, skews{invalid, b, a}.String()) + require.Equal(t, expected, skews{b, invalid, a}.String()) +} diff --git a/vendor/github.com/operator-framework/operator-lifecycle-manager/pkg/controller/operators/openshift/helpers.go b/vendor/github.com/operator-framework/operator-lifecycle-manager/pkg/controller/operators/openshift/helpers.go index 2e85462db2..7b7cb6cae1 100644 --- a/vendor/github.com/operator-framework/operator-lifecycle-manager/pkg/controller/operators/openshift/helpers.go +++ b/vendor/github.com/operator-framework/operator-lifecycle-manager/pkg/controller/operators/openshift/helpers.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "os" + "sort" "strings" "sync" @@ -77,18 +78,25 @@ func versionsMatch(a []configv1.OperandVersion, b []configv1.OperandVersion) boo type skews []skew func (s skews) String() string { - msg := make([]string, len(s)) - i, j := 0, len(s)-1 - for _, sk := range s { - m := sk.String() - // Partial order: error skews first - if sk.err != nil { - msg[i] = m - i++ - continue + // Sort so the message does not depend on the (unstable) order in which the + // CSVs were listed: error skews first, then by namespace/name. A different + // order would be a different Message, and so a condition transition. + sorted := make(skews, len(s)) + copy(sorted, s) + sort.SliceStable(sorted, func(i, j int) bool { + a, b := sorted[i], sorted[j] + if (a.err != nil) != (b.err != nil) { + return a.err != nil + } + if a.namespace != b.namespace { + return a.namespace < b.namespace } - msg[j] = m - j-- + return a.name < b.name + }) + + msg := make([]string, len(sorted)) + for i, sk := range sorted { + msg[i] = sk.String() } // TODO Revisit this message after 4.23 release once release schedule is determined for future possible minor versions. From 458c1a3fd867012da64a295680dbc41ab4db1b7b Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Fri, 30 Oct 2026 10:01:14 -0700 Subject: [PATCH 5/6] Revert "Hard-code version upgrade message for 4.22 upgrade paths (#3748)" This reverts commit 73a2205759278f98a4407bd0d07f166c5445a7bc (Hard-code version upgrade message for 4.22 upgrade paths, 2026-01-26, #3748). That change was an acceptable hack for 4.22 controllers, but should never have landed in the development branch, where it's currently generating output like: ClusterServiceVersions blocking minor version upgrades to 4.23 or major version upgrades to 5.0 in 5.0 clusters [1]. [1]: https://redhat.atlassian.net/browse/OPRUN-4431?focusedCommentId=17613550 --- .../pkg/controller/operators/openshift/helpers.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/vendor/github.com/operator-framework/operator-lifecycle-manager/pkg/controller/operators/openshift/helpers.go b/vendor/github.com/operator-framework/operator-lifecycle-manager/pkg/controller/operators/openshift/helpers.go index 7b7cb6cae1..2ce2f9576a 100644 --- a/vendor/github.com/operator-framework/operator-lifecycle-manager/pkg/controller/operators/openshift/helpers.go +++ b/vendor/github.com/operator-framework/operator-lifecycle-manager/pkg/controller/operators/openshift/helpers.go @@ -99,8 +99,12 @@ func (s skews) String() string { msg[i] = sk.String() } - // TODO Revisit this message after 4.23 release once release schedule is determined for future possible minor versions. - return fmt.Sprintf("ClusterServiceVersions blocking minor version upgrades to 4.23 or major version upgrades to 5.0:\n%s", strings.Join(msg, "\n")) + // it is safe to ignore the error here, with the assumption + // that we build each skew object only after verifying that the + // version string is parseable safely. + maxOCPVersion, _ := semver.ParseTolerant(s[0].maxOpenShiftVersion) + nextY := nextY(maxOCPVersion).String() + return fmt.Sprintf("ClusterServiceVersions blocking minor version upgrades to %s or higher:\n%s", nextY, strings.Join(msg, "\n")) } type skew struct { From 80b1c2821d6c64e2f5cd3c327dc86f5cb9db5e4c Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Fri, 30 Oct 2026 10:40:31 -0700 Subject: [PATCH 6/6] pkg/controller/operators/openshift: Firm skew-error handling While "it is safe to ignore the error here" would be nice, and may work for flows coming from an incompatibleOperators call, it's a brittle assumption. For example, it breaks down in TestSkewsStringIsDeterministic, which had been running skew.String() and skews.String() without either populating .err. In this commit, I'm making sure we've evaluated for errors in any code path that leads to those String() calls. This may make some code-paths slightly less efficient, by parsing the same valid skew version multiple times. But parsing a skew version isn't that expensive, so I'm not worried about a few CPU cycles there. --- .../operators/openshift/helpers_test.go | 4 +- .../controller/operators/openshift/helpers.go | 38 ++++++++++++++----- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/staging/operator-lifecycle-manager/pkg/controller/operators/openshift/helpers_test.go b/staging/operator-lifecycle-manager/pkg/controller/operators/openshift/helpers_test.go index ee81c2122c..3ac1946700 100644 --- a/staging/operator-lifecycle-manager/pkg/controller/operators/openshift/helpers_test.go +++ b/staging/operator-lifecycle-manager/pkg/controller/operators/openshift/helpers_test.go @@ -821,9 +821,9 @@ func TestSkewsStringIsDeterministic(t *testing.T) { // depend on that order, or every reorder counts as a condition transition. a := skew{namespace: "openshift-logging", name: "cluster-logging.v6.6.1", maxOpenShiftVersion: "4.23"} b := skew{namespace: "openshift-cluster-csi-drivers", name: "secrets-store-csi-driver-operator.v4.22.0", maxOpenShiftVersion: "5.0"} - invalid := skew{namespace: "openshift-logging", name: "broken.v1.0.0", err: fmt.Errorf("invalid")} + invalid := skew{namespace: "openshift-logging", name: "broken.v1.0.0"} - expected := "ClusterServiceVersions blocking minor version upgrades to 4.23 or major version upgrades to 5.0:\n" + + expected := "ClusterServiceVersions blocking minor version upgrades to 5.1 or higher:\n" + invalid.String() + "\n" + // error skews first b.String() + "\n" + // then by namespace/name a.String() diff --git a/vendor/github.com/operator-framework/operator-lifecycle-manager/pkg/controller/operators/openshift/helpers.go b/vendor/github.com/operator-framework/operator-lifecycle-manager/pkg/controller/operators/openshift/helpers.go index 2ce2f9576a..5d18bfe297 100644 --- a/vendor/github.com/operator-framework/operator-lifecycle-manager/pkg/controller/operators/openshift/helpers.go +++ b/vendor/github.com/operator-framework/operator-lifecycle-manager/pkg/controller/operators/openshift/helpers.go @@ -78,6 +78,19 @@ func versionsMatch(a []configv1.OperandVersion, b []configv1.OperandVersion) boo type skews []skew func (s skews) String() string { + // Sorting depends on a populated .err, to sort invalid skews + // first. incompatibleOperators sets .err through one call path, + // but check again here, to ensure reliability, regardless of call-chain. + var minVersion *semver.Version + for i, skew := range s { + if release, err := semver.ParseTolerant(skew.maxOpenShiftVersion); err != nil { + s[i].err = err + } else if minVersion == nil || normalizeOCPVersion(release).LT(*minVersion) { + v := normalizeOCPVersion(release) + minVersion = &v + } + } + // Sort so the message does not depend on the (unstable) order in which the // CSVs were listed: error skews first, then by namespace/name. A different // order would be a different Message, and so a condition transition. @@ -99,12 +112,11 @@ func (s skews) String() string { msg[i] = sk.String() } - // it is safe to ignore the error here, with the assumption - // that we build each skew object only after verifying that the - // version string is parseable safely. - maxOCPVersion, _ := semver.ParseTolerant(s[0].maxOpenShiftVersion) - nextY := nextY(maxOCPVersion).String() - return fmt.Sprintf("ClusterServiceVersions blocking minor version upgrades to %s or higher:\n%s", nextY, strings.Join(msg, "\n")) + if minVersion == nil { + return fmt.Sprintf("ClusterServiceVersions blocking major or minor version upgrades beyond the invalid SemVer %q (%s):\n%s", s[0].maxOpenShiftVersion, s[0].err, strings.Join(msg, "\n")) + } + nextY := nextY(*minVersion) + return fmt.Sprintf("ClusterServiceVersions blocking minor version upgrades to %d.%d or higher:\n%s", nextY.Major, nextY.Minor, strings.Join(msg, "\n")) } type skew struct { @@ -115,10 +127,17 @@ type skew struct { } func (s skew) String() string { - if s.err != nil { - return fmt.Sprintf("- %s/%s has invalid %s properties: %s", s.namespace, s.name, MaxOpenShiftVersionProperty, s.err) + // Sorting depends on a populated .err, to sort invalid skews + // first. incompatibleOperators sets .err through one call path, + // but check again here, to ensure reliability, regardless of call-chain. + if s.err == nil { + if _, err := semver.ParseTolerant(s.maxOpenShiftVersion); err != nil { + s.err = err + } else { + return fmt.Sprintf("- maximum supported OCP version for %s/%s is %s", s.namespace, s.name, s.maxOpenShiftVersion) + } } - return fmt.Sprintf("- maximum supported OCP version for %s/%s is %s", s.namespace, s.name, s.maxOpenShiftVersion) + return fmt.Sprintf("- %s/%s has invalid %s properties: %s", s.namespace, s.name, MaxOpenShiftVersionProperty, s.err) } type transientError struct { @@ -164,6 +183,7 @@ func incompatibleOperators(ctx context.Context, cli client.Client) (skews, error } max, err := maxOpenShiftVersion(&csv) if err != nil { + s.maxOpenShiftVersion = max s.err = err incompatible = append(incompatible, s) continue