From 0ab8a0b3ddf2e46187c9cb0a887e1c490935d4ab Mon Sep 17 00:00:00 2001 From: Jatin Suri Date: Mon, 31 Aug 2026 14:24:34 -0400 Subject: [PATCH 1/2] Add OKD cincinnati as the default update service for OKD --- pkg/cvo/availableupdates.go | 31 ++++++++++++++++++++++++++++--- pkg/cvo/availableupdates_test.go | 23 +++++++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/pkg/cvo/availableupdates.go b/pkg/cvo/availableupdates.go index d05239e0d6..c26ccd6683 100644 --- a/pkg/cvo/availableupdates.go +++ b/pkg/cvo/availableupdates.go @@ -32,6 +32,7 @@ import ( const noArchitecture string = "NoArchitecture" const noChannel string = "NoChannel" const defaultUpdateService string = "https://api.openshift.com/api/upgrades_info/v1/graph" +const defaultOKDUpdateService string = "https://updates.okd.io/api/updates/graph" // syncAvailableUpdates attempts to retrieve the latest updates and update the status of the ClusterVersion // object. It will set the RetrievedUpdates condition. Updates are only checked if it has been more than @@ -48,8 +49,13 @@ func (optr *Operator) syncAvailableUpdates(ctx context.Context, config *configv1 updateServiceSource = "ClusterVersion spec.upstream" } else { usedDefaultUpdateService = true - updateService = defaultUpdateService - updateServiceSource = "the operator's default update service" + if isOKDRelease(optr.release.Version) { + updateService = defaultOKDUpdateService + updateServiceSource = "the operator's default OKD update service" + } else { + updateService = defaultUpdateService + updateServiceSource = "the operator's default update service" + } } channel := config.Spec.Channel @@ -85,7 +91,7 @@ func (optr *Operator) syncAvailableUpdates(ctx context.Context, config *configv1 } else if !optrAvailableUpdates.RecentlyAttempted(optr.minimumUpdateCheckInterval) { klog.V(2).Infof("Retrieving available updates again, because more than %s has elapsed since last attempt at %s", optr.minimumUpdateCheckInterval, optrAvailableUpdates.LastAttempt.Format(time.RFC3339)) preserveCacheOnFailure = true - } else if updateService == optrAvailableUpdates.UpdateService || (updateService == defaultUpdateService && optrAvailableUpdates.UpdateService == "") { + } else if updateService == optrAvailableUpdates.UpdateService || (usedDefaultUpdateService && optrAvailableUpdates.UpdateService == "") { needsConditionalUpdateEval := false preserveCacheOnFailure = true for _, conditionalUpdate := range optrAvailableUpdates.ConditionalUpdates { @@ -452,6 +458,25 @@ func loadRiskVersions(conditionalUpdates []configv1.ConditionalUpdate) map[strin return riskVersions } +// isOKDRelease returns true when the given release version string identifies an +// OKD release. OKD releases embed an "okd" identifier in the semantic version +// pre-release segment (for example "4.19.0-0.okd-2024-01-06-084517" or +// "4.22.0-0.okd-scos-nightly-2025-..."), while OCP releases do not embed this +// identifier. It is used to select the appropriate default update service. +func isOKDRelease(version string) bool { + v, err := semver.Parse(version) + if err != nil { + klog.V(2).Infof("Unable to parse release version %q to determine whether this is an OKD cluster: %v", version, err) + return false + } + for _, pre := range v.Pre { + if strings.HasPrefix(pre.VersionStr, "okd") { + return true + } + } + return false +} + func (optr *Operator) getDesiredArchitecture(update *configv1.Update) string { if update != nil && len(update.Architecture) > 0 { return string(update.Architecture) diff --git a/pkg/cvo/availableupdates_test.go b/pkg/cvo/availableupdates_test.go index 1f20de720f..0918ef1305 100644 --- a/pkg/cvo/availableupdates_test.go +++ b/pkg/cvo/availableupdates_test.go @@ -1280,3 +1280,26 @@ func TestOperator_syncAvailableUpdates_noticeResolvedAlertsQuickly(t *testing.T) t.Errorf("syncAvailableUpdates mismatch (-want +got):\n%s", diff) } } + +func Test_isOKDRelease(t *testing.T) { + tests := []struct { + name string + version string + want bool + }{ + {name: "OKD FCOS release", version: "4.19.0-0.okd-2024-01-06-084517", want: true}, + {name: "OKD SCOS nightly", version: "4.22.0-0.okd-scos-nightly-2025-01-01-000000", want: true}, + {name: "OKD minimal", version: "4.1.0-0.okd-0", want: true}, + {name: "OCP GA release", version: "4.18.0", want: false}, + {name: "OCP nightly", version: "4.18.0-0.nightly-2025-01-01-000000", want: false}, + {name: "empty version", version: "", want: false}, + {name: "non-semver version", version: "not-a-version", want: false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isOKDRelease(tt.version); got != tt.want { + t.Errorf("isOKDRelease(%q) = %v, want %v", tt.version, got, tt.want) + } + }) + } +} From a80ae40db1293bb445455f3a60372e98c812281d Mon Sep 17 00:00:00 2001 From: Jatin Suri Date: Wed, 2 Sep 2026 13:50:38 -0400 Subject: [PATCH 2/2] Warn OKD users if they are on a old update service url --- ...er-version-operator_02_servicemonitor.yaml | 11 +++ pkg/cvo/legacy_update_service.go | 25 ++++++ pkg/cvo/legacy_update_service_test.go | 85 +++++++++++++++++++ pkg/cvo/status.go | 1 + pkg/internal/constants.go | 4 + 5 files changed, 126 insertions(+) create mode 100644 pkg/cvo/legacy_update_service.go create mode 100644 pkg/cvo/legacy_update_service_test.go diff --git a/install/0000_90_cluster-version-operator_02_servicemonitor.yaml b/install/0000_90_cluster-version-operator_02_servicemonitor.yaml index 35831cef1c..2385b14423 100644 --- a/install/0000_90_cluster-version-operator_02_servicemonitor.yaml +++ b/install/0000_90_cluster-version-operator_02_servicemonitor.yaml @@ -74,6 +74,17 @@ spec: sum by (channel, namespace, upstream) (cluster_version_available_updates) > 0 labels: severity: info + - alert: ClusterVersionOperatorLegacyUpdateService + annotations: + summary: ClusterVersion is configured to use a legacy update service. + description: ClusterVersion spec.upstream points to a legacy update service. At an appropriate time, clear spec.upstream to use the CVO's default update service, or configure another update service. + expr: | + max by (namespace, name) + ( + cluster_operator_conditions{name="version", condition="LegacyUpdateService", endpoint="metrics"} == 1 + ) + labels: + severity: info - alert: ClusterReleaseNotAccepted annotations: summary: The desired cluster release has not been accepted for at least an hour. diff --git a/pkg/cvo/legacy_update_service.go b/pkg/cvo/legacy_update_service.go new file mode 100644 index 0000000000..8f4c3bf05a --- /dev/null +++ b/pkg/cvo/legacy_update_service.go @@ -0,0 +1,25 @@ +package cvo + +import ( + configv1 "github.com/openshift/api/config/v1" + + "github.com/openshift/cluster-version-operator/lib/resourcemerge" + "github.com/openshift/cluster-version-operator/pkg/internal" +) + +const legacyOKDUpdateService = "https://amd64.origin.releases.ci.openshift.org/graph" + +// UpdateOKDLegacyUpdateServiceCondition updates the status condition that warns about the legacy OKD update service. +func UpdateOKDLegacyUpdateServiceCondition(status *configv1.ClusterVersionStatus, upstream configv1.URL) { + if string(upstream) != legacyOKDUpdateService { + resourcemerge.RemoveOperatorStatusCondition(&status.Conditions, internal.ClusterVersionOKDLegacyUpdateService) + return + } + + resourcemerge.SetOperatorStatusCondition(&status.Conditions, configv1.ClusterOperatorStatusCondition{ + Type: internal.ClusterVersionOKDLegacyUpdateService, + Status: configv1.ConditionTrue, + Reason: "LegacyUpstreamConfigured", + Message: "ClusterVersion spec.upstream is set to the legacy OKD update service " + legacyOKDUpdateService + ". Clear spec.upstream to use the OKD Cincinnati update service.", + }) +} diff --git a/pkg/cvo/legacy_update_service_test.go b/pkg/cvo/legacy_update_service_test.go new file mode 100644 index 0000000000..7c28c82162 --- /dev/null +++ b/pkg/cvo/legacy_update_service_test.go @@ -0,0 +1,85 @@ +package cvo + +import ( + "testing" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + configv1 "github.com/openshift/api/config/v1" + + "github.com/openshift/cluster-version-operator/lib/resourcemerge" + "github.com/openshift/cluster-version-operator/pkg/internal" +) + +func TestUpdateOKDLegacyUpdateServiceCondition(t *testing.T) { + originalTransitionTime := metav1.NewTime(time.Unix(1, 0)) + tests := []struct { + name string + upstream configv1.URL + conditions []configv1.ClusterOperatorStatusCondition + want bool + wantPreservedTransition bool + }{ + { + name: "legacy update service", + upstream: legacyOKDUpdateService, + want: true, + }, + { + name: "custom update service", + upstream: "https://example.com/graph", + }, + { + name: "legacy URL with trailing slash does not match", + upstream: legacyOKDUpdateService + "/", + }, + { + name: "default update service", + }, + { + name: "resolved warning is removed", + upstream: "https://example.com/graph", + conditions: []configv1.ClusterOperatorStatusCondition{{ + Type: internal.ClusterVersionOKDLegacyUpdateService, + Status: configv1.ConditionTrue, + Reason: "LegacyUpstreamConfigured", + LastTransitionTime: originalTransitionTime, + }}, + }, + { + name: "unchanged warning preserves transition time", + upstream: legacyOKDUpdateService, + conditions: []configv1.ClusterOperatorStatusCondition{{ + Type: internal.ClusterVersionOKDLegacyUpdateService, + Status: configv1.ConditionTrue, + Reason: "LegacyUpstreamConfigured", + LastTransitionTime: originalTransitionTime, + }}, + want: true, + wantPreservedTransition: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status := &configv1.ClusterVersionStatus{Conditions: tt.conditions} + UpdateOKDLegacyUpdateServiceCondition(status, tt.upstream) + + condition := resourcemerge.FindOperatorStatusCondition(status.Conditions, internal.ClusterVersionOKDLegacyUpdateService) + if tt.want { + if condition == nil { + t.Fatal("LegacyUpdateService condition is missing") + } + if condition.Status != configv1.ConditionTrue || condition.Reason != "LegacyUpstreamConfigured" || condition.Message == "" { + t.Fatalf("unexpected LegacyUpdateService condition: %#v", condition) + } + if tt.wantPreservedTransition && condition.LastTransitionTime != originalTransitionTime { + t.Fatalf("lastTransitionTime = %v, want %v", condition.LastTransitionTime, originalTransitionTime) + } + } else if condition != nil { + t.Fatalf("unexpected LegacyUpdateService condition: %#v", condition) + } + }) + } +} diff --git a/pkg/cvo/status.go b/pkg/cvo/status.go index 8167d4ce47..859b9b3e86 100644 --- a/pkg/cvo/status.go +++ b/pkg/cvo/status.go @@ -175,6 +175,7 @@ func (optr *Operator) syncStatus(ctx context.Context, original, config *configv1 } updateClusterVersionStatus(ctx, &config.Status, status, optr.release, optr.conditionRegistry, optr.getAvailableUpdates, optr.upgradeable, optr.enabledCVOFeatureGates, validationErrs, optr.shouldReconcileAcceptRisks) + UpdateOKDLegacyUpdateServiceCondition(&config.Status, config.Spec.Upstream) if klog.V(6).Enabled() { klog.Infof("Apply config: %s", cmp.Diff(original, config)) diff --git a/pkg/internal/constants.go b/pkg/internal/constants.go index 568e038dbc..58bc3a5d97 100644 --- a/pkg/internal/constants.go +++ b/pkg/internal/constants.go @@ -70,6 +70,10 @@ const ( // enabled capabilities. ImplicitlyEnabledCapabilities configv1.ClusterStatusConditionType = "ImplicitlyEnabledCapabilities" + // ClusterVersionOKDLegacyUpdateService is True when spec.upstream selects the + // legacy OKD update service, which should be replaced or cleared. + ClusterVersionOKDLegacyUpdateService configv1.ClusterStatusConditionType = "LegacyUpdateService" + // UpgradeableAdminAckRequired is False if there is API removed from the Kubernetes API server which requires admin // consideration, and thus update to the next minor or major version is blocked. UpgradeableAdminAckRequired configv1.ClusterStatusConditionType = "UpgradeableAdminAckRequired"