diff --git a/pkg/controller/status/controller.go b/pkg/controller/status/controller.go index 34c8af51f..12194ea22 100644 --- a/pkg/controller/status/controller.go +++ b/pkg/controller/status/controller.go @@ -13,6 +13,7 @@ import ( "golang.org/x/time/rate" + "github.com/blang/semver/v4" configv1 "github.com/openshift/api/config/v1" configv1client "github.com/openshift/client-go/config/clientset/versioned/typed/config/v1" "k8s.io/apimachinery/pkg/api/errors" @@ -181,15 +182,28 @@ func (c *Controller) merge(clusterOperator *configv1.ClusterOperator) *configv1. c.updateControllerConditions(cs, isInitializing) c.updateControllerConditionsByStatus(cs, isInitializing) - // all status conditions from conditions to cluster operator - clusterOperator.Status.Conditions = cs.entries() + if releaseVersion := os.Getenv("RELEASE_VERSION"); len(releaseVersion) > 0 { + setProgressing, err := shouldSetProgressingCondition(releaseVersion, clusterOperator.Status.Versions) + if err != nil { + klog.Errorf("failed checking openshift release version: %s with err: %v", releaseVersion, err) + } - if release := os.Getenv("RELEASE_VERSION"); len(release) > 0 { clusterOperator.Status.Versions = []configv1.OperandVersion{ - {Name: "operator", Version: release}, + {Name: "operator", Version: releaseVersion}, + } + + if setProgressing { + cs.setCondition( + configv1.OperatorProgressing, + configv1.ConditionTrue, + "Openshift Upgrade", + "Cluster version is updated") } } + // all status conditions from conditions to cluster operator + clusterOperator.Status.Conditions = cs.entries() + reported := Reported{LastReportTime: metav1.Time{Time: c.LastReportedTime()}} if data, err := json.Marshal(reported); err != nil { klog.Errorf("Unable to marshal status extension: %v", err) @@ -199,6 +213,34 @@ func (c *Controller) merge(clusterOperator *configv1.ClusterOperator) *configv1. return clusterOperator } +// shouldSetProgressingCondition checks if the openshift version was changed and decides whether we should +// switch the Progressing condition to true or not. We should do that only if the major or minor version +// is changed and ignore the patch version. +func shouldSetProgressingCondition(newVersion string, clusterOperatorVersions []configv1.OperandVersion) (bool, error) { + newVersionParsed, err := semver.Parse(newVersion) + if err != nil { + return false, err + } + + // Skip initial run, the condition is set there + if len(clusterOperatorVersions) == 0 { + return false, nil + } + + for _, cov := range clusterOperatorVersions { + covParsed, err := semver.Parse(cov.Version) + if err != nil { + return false, err + } + + // Change Progressing condition only on major or minor version update + if newVersionParsed.Major != covParsed.Major || newVersionParsed.Minor != covParsed.Minor { + return true, nil + } + } + return false, nil +} + // calculate the current controller status based on its given sources func (c *Controller) currentControllerStatus() (allReady bool) { //nolint: gocyclo var errorReason string diff --git a/pkg/controller/status/controller_test.go b/pkg/controller/status/controller_test.go index ec19761ac..adfd9253b 100644 --- a/pkg/controller/status/controller_test.go +++ b/pkg/controller/status/controller_test.go @@ -253,6 +253,75 @@ func Test_updatingConditionsFromDegradedToDisabled(t *testing.T) { assert.Equal(t, disabledCondition, getConditionByType(updatedCO.Status.Conditions, OperatorDisabled)) } +func Test_shouldSetProgressingCondition(t *testing.T) { + tests := []struct { + name string + newVersion string + clusterOperatorVersions []configv1.OperandVersion + expectedShouldUpdate bool + expectError bool + }{ + { + name: "Invalid new version returns error", + newVersion: "invalid-version", + clusterOperatorVersions: []configv1.OperandVersion{{Name: "operator", Version: "4.21.0-0.nightly-2026-01-07-204315"}}, + expectedShouldUpdate: false, + expectError: true, + }, + { + name: "Empty clusterOperatorVersions returns false", + newVersion: "4.21.0-0.nightly-2026-01-07-204315", + clusterOperatorVersions: []configv1.OperandVersion{}, + expectedShouldUpdate: false, + expectError: false, + }, + { + name: "Major version change triggers update", + newVersion: "5.21.0-0.nightly-2026-01-07-204315", + clusterOperatorVersions: []configv1.OperandVersion{{Name: "operator", Version: "4.21.0-0.nightly-2026-01-07-204315"}}, + expectedShouldUpdate: true, + expectError: false, + }, + { + name: "Minor version change triggers update", + newVersion: "4.22.0-0.nightly-2026-01-07-204315", + clusterOperatorVersions: []configv1.OperandVersion{{Name: "operator", Version: "4.21.0-0.nightly-2026-01-07-204315"}}, + expectedShouldUpdate: true, + expectError: false, + }, + { + name: "Patch version change does not trigger update", + newVersion: "4.21.1-0.nightly-2026-01-07-204315", + clusterOperatorVersions: []configv1.OperandVersion{{Name: "operator", Version: "4.21.0-0.nightly-2026-01-07-204315"}}, + expectedShouldUpdate: false, + expectError: false, + }, + { + name: "Invalid existing version returns error", + newVersion: "4.21.0-0.nightly-2026-01-07-204315", + clusterOperatorVersions: []configv1.OperandVersion{{Name: "operator", Version: "invalid"}}, + expectedShouldUpdate: false, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + shouldUpdate, err := shouldSetProgressingCondition(tt.newVersion, tt.clusterOperatorVersions) + + if tt.expectError { + assert.Error(t, err, "Expected an error but got nil") + } else { + assert.NoError(t, err, "Expected no error but got: %v", err) + } + + assert.Equal(t, tt.expectedShouldUpdate, shouldUpdate, + "shouldUpdateVersion(%q, %v) = %v, want %v", + tt.newVersion, tt.clusterOperatorVersions, shouldUpdate, tt.expectedShouldUpdate) + }) + } +} + func getConditionByType(conditions []configv1.ClusterOperatorStatusCondition, ctype configv1.ClusterStatusConditionType, ) *configv1.ClusterOperatorStatusCondition {