Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions install/0000_90_cluster-version-operator_02_servicemonitor.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
31 changes: 28 additions & 3 deletions pkg/cvo/availableupdates.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 == "") {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
needsConditionalUpdateEval := false
preserveCacheOnFailure = true
for _, conditionalUpdate := range optrAvailableUpdates.ConditionalUpdates {
Expand Down Expand Up @@ -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.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
func isOKDRelease(version string) bool {
v, err := semver.Parse(version)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

along with this check i would also make sure this operator itself has been built for OKD - we pass in TAGS=SCOS to build OKD. we can check if that is present like other components have

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

With that tag in place, why check the release version at all? Can't you just switch on the tag to figure out which default URI to use?

@jatinsu jatinsu Sep 2, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This tag was never put in place for the CVO. This was an intentional decision since we didn't want to diverge OKD CVO too much from OCP CVO, hence why the release version check is put in place

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)
Expand Down
23 changes: 23 additions & 0 deletions pkg/cvo/availableupdates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
}
}
25 changes: 25 additions & 0 deletions pkg/cvo/legacy_update_service.go
Original file line number Diff line number Diff line change
@@ -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.",
})
}
85 changes: 85 additions & 0 deletions pkg/cvo/legacy_update_service_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
1 change: 1 addition & 0 deletions pkg/cvo/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
4 changes: 4 additions & 0 deletions pkg/internal/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down