Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -96,5 +96,18 @@
"source": "openshift:payload:cluster-version-operator",
"lifecycle": "blocking",
"environmentSelector": {}
},
{
"name": "[Jira:\"Cluster Version Operator\"] cluster-version-operator should create proposals",
"labels": {
"Lifecycle:informing": {},
"Serial": {}
},
"resources": {
"isolation": {}
},
"source": "openshift:payload:cluster-version-operator",
"lifecycle": "informing",
"environmentSelector": {}
}
]
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ require (
k8s.io/klog/v2 v2.130.1
k8s.io/kube-aggregator v0.35.1
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4
sigs.k8s.io/controller-runtime v0.22.2
sigs.k8s.io/kustomize/kyaml v0.21.1
sigs.k8s.io/yaml v1.6.0
)
Expand All @@ -44,6 +45,7 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-errors/errors v1.4.2 // indirect
Expand Down Expand Up @@ -100,7 +102,6 @@ require (
gopkg.in/inf.v0 v0.9.1 // indirect
k8s.io/component-base v0.35.1 // indirect
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
sigs.k8s.io/controller-runtime v0.22.2 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/kube-storage-version-migrator v0.0.6-0.20230721195810-5c8923c5ff96 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
Expand Down
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes=
github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
Expand All @@ -25,6 +27,8 @@ github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxI
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
Expand Down Expand Up @@ -167,6 +171,10 @@ go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKr
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
Expand Down
4 changes: 4 additions & 0 deletions pkg/cvo/availableupdates.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,10 @@ func (optr *Operator) syncAvailableUpdates(ctx context.Context, config *configv1

// queue optr.sync() to update ClusterVersion status
optr.queue.Add(queueKey)
if optr.shouldEnableProposalController() {
// queue optr.proposalController.Sync() to manage proposals
optr.proposalController.Queue().Add(optr.proposalController.QueueKey())
}
return nil
}

Expand Down
29 changes: 29 additions & 0 deletions pkg/cvo/availableupdates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/blang/semver/v4"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"sigs.k8s.io/controller-runtime/pkg/client/fake"

corev1 "k8s.io/api/core/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
Expand All @@ -29,6 +30,7 @@ import (
"github.com/openshift/cluster-version-operator/pkg/clusterconditions/always"
"github.com/openshift/cluster-version-operator/pkg/clusterconditions/mock"
"github.com/openshift/cluster-version-operator/pkg/featuregates"
"github.com/openshift/cluster-version-operator/pkg/proposal"
"github.com/openshift/cluster-version-operator/pkg/risk"
riskmock "github.com/openshift/cluster-version-operator/pkg/risk/mock"
)
Expand Down Expand Up @@ -242,6 +244,15 @@ func TestSyncAvailableUpdates(t *testing.T) {
expectedAvailableUpdates.RiskConditions = map[string][]metav1.Condition{"FourFiveSix": {{Type: "Applies", Status: "True", Reason: "Match"}}}

optr.enabledCVOFeatureGates = featuregates.DefaultCvoGates("version")
optr.proposalController = proposal.NewController(func() ([]configv1.Release, []configv1.ConditionalUpdate, error) {
return nil, nil, nil
}, fake.NewClientBuilder().Build(), func(_ string) (*configv1.ClusterVersion, error) {
return &configv1.ClusterVersion{}, nil
}, func(name, namespace string) (*corev1.ConfigMap, error) {
return &corev1.ConfigMap{}, nil
}, func() string {
return optr.release.Version
})
err := optr.syncAvailableUpdates(context.Background(), cvFixture)

if err != nil {
Expand Down Expand Up @@ -331,6 +342,15 @@ func TestSyncAvailableUpdates_ConditionalUpdateRecommendedConditions(t *testing.
tc.modifyCV(cv, fixture.expectedConditionalUpdates[0])

optr.enabledCVOFeatureGates = featuregates.DefaultCvoGates("version")
optr.proposalController = proposal.NewController(func() ([]configv1.Release, []configv1.ConditionalUpdate, error) {
return nil, nil, nil
}, fake.NewClientBuilder().Build(), func(_ string) (*configv1.ClusterVersion, error) {
return &configv1.ClusterVersion{}, nil
}, func(name, namespace string) (*corev1.ConfigMap, error) {
return &corev1.ConfigMap{}, nil
}, func() string {
return optr.release.Version
})
err := optr.syncAvailableUpdates(context.Background(), cv)

if err != nil {
Expand Down Expand Up @@ -767,6 +787,15 @@ func TestSyncAvailableUpdatesDesiredUpdate(t *testing.T) {
cv := cvFixture.DeepCopy()
cv.Spec.DesiredUpdate = tt.args.desiredUpdate
optr.enabledCVOFeatureGates = featuregates.DefaultCvoGates("version")
optr.proposalController = proposal.NewController(func() ([]configv1.Release, []configv1.ConditionalUpdate, error) {
return nil, nil, nil
}, fake.NewClientBuilder().Build(), func(_ string) (*configv1.ClusterVersion, error) {
return &configv1.ClusterVersion{}, nil
}, func(name, namespace string) (*corev1.ConfigMap, error) {
return &corev1.ConfigMap{}, nil
}, func() string {
return optr.release.Version
})
if err := optr.syncAvailableUpdates(context.Background(), cv); err != nil {
t.Fatalf("syncAvailableUpdates() unexpected error: %v", err)
}
Expand Down
39 changes: 39 additions & 0 deletions pkg/cvo/cvo.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"sync"
"time"

runtimeclient "sigs.k8s.io/controller-runtime/pkg/client"

corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -53,6 +55,7 @@ import (
"github.com/openshift/cluster-version-operator/pkg/payload"
"github.com/openshift/cluster-version-operator/pkg/payload/precondition"
preconditioncv "github.com/openshift/cluster-version-operator/pkg/payload/precondition/clusterversion"
"github.com/openshift/cluster-version-operator/pkg/proposal"
"github.com/openshift/cluster-version-operator/pkg/risk"
"github.com/openshift/cluster-version-operator/pkg/risk/alert"
)
Expand Down Expand Up @@ -213,6 +216,9 @@ type Operator struct {
// risks holds update-risk source (in-cluster alerts, etc.)
// that will be aggregated into conditional update risks.
risks risk.Source

// proposalController, if enabled, watches available and conditionals updates and manage proposals for them
proposalController *proposal.Controller
}

// New returns a new cluster version operator.
Expand Down Expand Up @@ -242,6 +248,7 @@ func New(
featureSet configv1.FeatureSet,
cvoGates featuregates.CvoGateChecker,
startingEnabledManifestFeatureGates sets.Set[string],
rtClient runtimeclient.Client,
) (*Operator, error) {
eventBroadcaster := record.NewBroadcaster()
eventBroadcaster.StartLogging(klog.Infof)
Expand Down Expand Up @@ -317,6 +324,18 @@ func New(

optr.configuration = configuration.NewClusterVersionOperatorConfiguration(operatorClient, operatorInformerFactory)

optr.proposalController = proposal.NewController(func() ([]configv1.Release, []configv1.ConditionalUpdate, error) {
availableUpdates := optr.getAvailableUpdates()
if availableUpdates == nil {
return nil, nil, nil
}
return availableUpdates.Updates, availableUpdates.ConditionalUpdates, nil
}, rtClient, cvInformer.Lister().Get, func(name, namespace string) (*corev1.ConfigMap, error) {
return cmConfigManagedInformer.Lister().ConfigMaps(namespace).Get(name)
}, func() string {
Comment on lines +327 to +335
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Swap the ConfigMap callback parameters before prompt lookups break.

pkg/proposal/controller.go calls this getter as (namespace, name), but this callback interprets the arguments as (name, namespace) and then does ConfigMaps(namespace).Get(name). In practice that inverts the lookup and the proposal controller will fail to fetch its prompt ConfigMap.

🐛 Minimal fix
-	}, rtClient, cvInformer.Lister().Get, func(name, namespace string) (*corev1.ConfigMap, error) {
-		return cmConfigManagedInformer.Lister().ConfigMaps(namespace).Get(name)
+	}, rtClient, cvInformer.Lister().Get, func(namespace, name string) (*corev1.ConfigMap, error) {
+		return cmConfigManagedInformer.Lister().ConfigMaps(namespace).Get(name)
 	}, func() string {
📝 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
optr.proposalController = proposal.NewController(func() ([]configv1.Release, []configv1.ConditionalUpdate, error) {
availableUpdates := optr.getAvailableUpdates()
if availableUpdates == nil {
return nil, nil, nil
}
return availableUpdates.Updates, availableUpdates.ConditionalUpdates, nil
}, rtClient, cvInformer.Lister().Get, func(name, namespace string) (*corev1.ConfigMap, error) {
return cmConfigManagedInformer.Lister().ConfigMaps(namespace).Get(name)
}, func() string {
optr.proposalController = proposal.NewController(func() ([]configv1.Release, []configv1.ConditionalUpdate, error) {
availableUpdates := optr.getAvailableUpdates()
if availableUpdates == nil {
return nil, nil, nil
}
return availableUpdates.Updates, availableUpdates.ConditionalUpdates, nil
}, rtClient, cvInformer.Lister().Get, func(namespace, name string) (*corev1.ConfigMap, error) {
return cmConfigManagedInformer.Lister().ConfigMaps(namespace).Get(name)
}, func() string {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/cvo/cvo.go` around lines 327 - 335, The ConfigMap getter passed into
proposal.NewController has its parameters reversed: the closure is declared as
func(name, namespace string) but proposal.Controller calls it as (namespace,
name), causing inverted lookups; fix by changing the closure signature to
func(namespace, name string) (*corev1.ConfigMap, error) and ensure it calls
cmConfigManagedInformer.Lister().ConfigMaps(namespace).Get(name) with the
corrected parameter order so optr.proposalController can fetch prompt ConfigMaps
correctly.

return optr.release.Version
})

return optr, nil
}

Expand Down Expand Up @@ -470,6 +489,7 @@ func (optr *Operator) Run(runContext context.Context, shutdownContext context.Co
defer optr.availableUpdatesQueue.ShutDown()
defer optr.upgradeableQueue.ShutDown()
defer optr.configuration.Queue().ShutDown()
defer optr.proposalController.Queue().ShutDown()
stopCh := runContext.Done()

klog.Infof("Starting ClusterVersionOperator with minimum reconcile period %s", optr.minimumUpdateCheckInterval)
Expand Down Expand Up @@ -525,6 +545,19 @@ func (optr *Operator) Run(runContext context.Context, shutdownContext context.Co
klog.Infof("The ClusterVersionOperatorConfiguration feature gate is disabled or HyperShift is detected; the configuration sync routine will not run.")
}

if optr.shouldEnableProposalController() {
resultChannelCount++
go func() {
defer utilruntime.HandleCrash()
wait.UntilWithContext(runContext, func(runContext context.Context) {
optr.worker(runContext, optr.proposalController.Queue(), optr.proposalController.Sync)
}, time.Second)
resultChannel <- asyncResult{name: "proposal controller"}
}()
} else {
klog.Infof("The proposal controller is disabled.")
}

resultChannelCount++
go func() {
defer utilruntime.HandleCrash()
Expand Down Expand Up @@ -595,6 +628,7 @@ func (optr *Operator) Run(runContext context.Context, shutdownContext context.Co
optr.availableUpdatesQueue.ShutDown()
optr.upgradeableQueue.ShutDown()
optr.configuration.Queue().ShutDown()
optr.proposalController.Queue().ShutDown()
}
}

Expand Down Expand Up @@ -1191,3 +1225,8 @@ func (optr *Operator) shouldReconcileAcceptRisks() bool {
// HyperShift will be supported later if needed
return optr.enabledCVOFeatureGates.AcceptRisks() && !optr.hypershift
}

// shouldEnableProposalController returns whether the CVO should enable the proposal controller
func (optr *Operator) shouldEnableProposalController() bool {
return optr.enabledCVOFeatureGates.Proposal()
}
11 changes: 11 additions & 0 deletions pkg/cvo/cvo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/davecgh/go-spew/spew"
"github.com/google/go-cmp/cmp"
"github.com/google/uuid"
ctrlruntimefake "sigs.k8s.io/controller-runtime/pkg/client/fake"

appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
Expand Down Expand Up @@ -46,6 +47,7 @@ import (
"github.com/openshift/cluster-version-operator/pkg/featuregates"
"github.com/openshift/cluster-version-operator/pkg/internal"
"github.com/openshift/cluster-version-operator/pkg/payload"
"github.com/openshift/cluster-version-operator/pkg/proposal"
)

var (
Expand Down Expand Up @@ -2758,6 +2760,15 @@ func TestOperator_availableUpdatesSync(t *testing.T) {
old := optr.availableUpdates

ctx := context.Background()
optr.proposalController = proposal.NewController(func() ([]configv1.Release, []configv1.ConditionalUpdate, error) {
return nil, nil, nil
}, ctrlruntimefake.NewClientBuilder().Build(), func(_ string) (*configv1.ClusterVersion, error) {
return &configv1.ClusterVersion{}, nil
}, func(name, namespace string) (*corev1.ConfigMap, error) {
return &corev1.ConfigMap{}, nil
}, func() string {
return optr.release.Version
})
err := optr.availableUpdatesSync(ctx, optr.queueKey())
if err != nil && tt.wantErr == nil {
t.Fatalf("Operator.sync() unexpected error: %v", err)
Expand Down
7 changes: 7 additions & 0 deletions pkg/cvo/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,13 @@ func (optr *Operator) syncStatus(ctx context.Context, original, config *configv1
if klog.V(6).Enabled() {
klog.Infof("Apply config: %s", cmp.Diff(original, config))
}
if optr.shouldEnableProposalController() {
if len(config.Status.History) < len(original.Status.History) {
klog.V(internal.Normal).Infof("Reconciling proposals because ClusterVersion.status.history got pruned")
// queue optr.proposalController.Sync() to manage proposals
optr.proposalController.Queue().Add(optr.proposalController.QueueKey())
}
}
Comment on lines +185 to +191
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Potential nil dereference when checking pruned history.

At Line 186, original.Status.History is accessed without guarding original. This can panic in nil-original paths that syncStatus otherwise supports.

💡 Suggested fix
 	if optr.shouldEnableProposalController() {
-		if len(config.Status.History) < len(original.Status.History) {
+		originalHistoryLen := 0
+		if original != nil {
+			originalHistoryLen = len(original.Status.History)
+		}
+		if len(config.Status.History) < originalHistoryLen {
 			klog.V(internal.Normal).Infof("Reconciling proposals because ClusterVersion.status.history got pruned")
 			// queue optr.proposalController.Sync() to manage proposals
 			optr.proposalController.Queue().Add(optr.proposalController.QueueKey())
 		}
 	}
📝 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
if optr.shouldEnableProposalController() {
if len(config.Status.History) < len(original.Status.History) {
klog.V(internal.Normal).Infof("Reconciling proposals because ClusterVersion.status.history got pruned")
// queue optr.proposalController.Sync() to manage proposals
optr.proposalController.Queue().Add(optr.proposalController.QueueKey())
}
}
if optr.shouldEnableProposalController() {
originalHistoryLen := 0
if original != nil {
originalHistoryLen = len(original.Status.History)
}
if len(config.Status.History) < originalHistoryLen {
klog.V(internal.Normal).Infof("Reconciling proposals because ClusterVersion.status.history got pruned")
// queue optr.proposalController.Sync() to manage proposals
optr.proposalController.Queue().Add(optr.proposalController.QueueKey())
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/cvo/status.go` around lines 185 - 191, The code reads
original.Status.History without checking original (or original.Status), which
can panic; update the conditional in the block guarded by
optr.shouldEnableProposalController() to first ensure original is non-nil and
original.Status is non-nil (or otherwise treat original history length as 0)
before comparing lengths with config.Status.History, then only call
optr.proposalController.Queue().Add(...) when the safe length comparison
indicates pruning; reference optr.shouldEnableProposalController,
config.Status.History, original.Status.History, and
optr.proposalController.Queue().Add to locate and fix the check.

updated, err := applyClusterVersionStatus(ctx, optr.client.ConfigV1(), config, original)
optr.rememberLastUpdate(updated)
return err
Expand Down
11 changes: 8 additions & 3 deletions pkg/cvo/status_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ func TestOperator_syncFailingStatus(t *testing.T) {
Desired: configv1.Release{
Version: "4.0.1",
Image: "image/image:v4.0.1",
URL: configv1.URL("https://example.com/v4.0.1"),
URL: "https://example.com/v4.0.1",
},
VersionHash: "",
Conditions: []configv1.ClusterOperatorStatusCondition{
Expand Down Expand Up @@ -149,7 +149,7 @@ func TestOperator_syncFailingStatus(t *testing.T) {
Desired: configv1.Release{
Version: "4.0.1",
Image: "image/image:v4.0.1",
URL: configv1.URL("https://example.com/v4.0.1"),
URL: "https://example.com/v4.0.1",
},
VersionHash: "",
Conditions: []configv1.ClusterOperatorStatusCondition{
Expand Down Expand Up @@ -206,6 +206,7 @@ type fakeRiFlags struct {
statusReleaseArchitecture bool
cvoConfiguration bool
acceptRisks bool
proposal bool
}

func (f fakeRiFlags) DesiredVersion() string {
Expand All @@ -228,6 +229,10 @@ func (f fakeRiFlags) AcceptRisks() bool {
return f.acceptRisks
}

func (f fakeRiFlags) Proposal() bool {
return f.proposal
}

func TestUpdateClusterVersionStatus_FilteringMultipleErrorsForFailingCondition(t *testing.T) {
ignoreLastTransitionTime := cmpopts.IgnoreFields(configv1.ClusterOperatorStatusCondition{}, "LastTransitionTime")
type args struct {
Expand Down Expand Up @@ -1084,7 +1089,7 @@ func Test_conditionalUpdateWithRiskNamesAndRiskConditions(t *testing.T) {
desiredImage: "not-important",
availableUpdates: &availableUpdates{
RiskConditions: map[string][]metav1.Condition{
"TestAlert": []metav1.Condition{{
"TestAlert": {{
Type: "Applies",
Status: "True",
Reason: "Alert:firing",
Expand Down
1 change: 1 addition & 0 deletions pkg/featuregates/featurechangestopper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ func TestTechPreviewChangeStopper(t *testing.T) {
fg.Status = configv1.FeatureGateStatus{}
tt.startingCvoFeatureGates = CvoGates{unknownVersion: true}
}
tt.startingCvoFeatureGates.proposal = fg.Spec.FeatureSet == configv1.TechPreviewNoUpgrade

client := fakeconfigv1client.NewClientset(fg)

Expand Down
12 changes: 12 additions & 0 deletions pkg/featuregates/featuregates.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ type CvoGateChecker interface {

// AcceptRisks controls whether the CVO reconciles spec.desiredUpdate.acceptRisks.
AcceptRisks() bool

// Proposal controls whether the CVO makes proposals for updates.
Proposal() bool
}

// CvoGates contains flags that control CVO functionality gated by product feature gates. The
Expand All @@ -60,6 +63,7 @@ type CvoGates struct {
statusReleaseArchitecture bool
cvoConfiguration bool
acceptRisks bool
proposal bool
}

func (c CvoGates) DesiredVersion() string {
Expand All @@ -82,6 +86,10 @@ func (c CvoGates) AcceptRisks() bool {
return c.acceptRisks
}

func (c CvoGates) Proposal() bool {
return c.proposal
}

// DefaultCvoGates apply when actual features for given version are unknown
func DefaultCvoGates(version string) CvoGates {
return CvoGates{
Expand All @@ -90,6 +98,7 @@ func DefaultCvoGates(version string) CvoGates {
statusReleaseArchitecture: false,
cvoConfiguration: false,
acceptRisks: false,
proposal: false,
}
}

Expand All @@ -98,6 +107,9 @@ func DefaultCvoGates(version string) CvoGates {
func CvoGatesFromFeatureGate(gate *configv1.FeatureGate, version string) CvoGates {
enabledGates := DefaultCvoGates(version)

// We do not have a specific gate for the Proposal feature and use the TechPreviewNoUpgrade instead
enabledGates.proposal = gate.Spec.FeatureSet == configv1.TechPreviewNoUpgrade

for _, g := range gate.Status.FeatureGates {

if g.Version != version {
Expand Down
Loading