Skip to content

Commit 33cc8ed

Browse files
committed
feat: block openshift upgrades for incompatible operators
- Refactor OpenShift ClusterOperator monitoring for olm-operator into an independent controller - Set the OLM operator's ClusterOperator to upgradeable=false whenever incompatible operators, installed by OLM, exist on cluster; effectively blocking OpenShift upgrades Signed-off-by: Nick Hale <[email protected]>
1 parent 44a00ee commit 33cc8ed

File tree

12 files changed

+1665
-6
lines changed

12 files changed

+1665
-6
lines changed

cmd/olm/main.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@ import (
1616
"github.com/sirupsen/logrus"
1717
"github.com/spf13/pflag"
1818
v1 "k8s.io/api/core/v1"
19-
2019
"k8s.io/klog"
20+
ctrl "sigs.k8s.io/controller-runtime"
2121

2222
"github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/versioned"
2323
"github.com/operator-framework/operator-lifecycle-manager/pkg/controller/operators/olm"
24+
"github.com/operator-framework/operator-lifecycle-manager/pkg/controller/operators/openshift"
2425
"github.com/operator-framework/operator-lifecycle-manager/pkg/feature"
2526
"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/filemonitor"
2627
"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorclient"
@@ -223,7 +224,24 @@ func main() {
223224
<-op.Ready()
224225

225226
if *writeStatusName != "" {
226-
operatorstatus.MonitorClusterStatus(*writeStatusName, op.AtLevel(), ctx.Done(), opClient, configClient, crClient)
227+
reconciler, err := openshift.NewClusterOperatorReconciler(
228+
openshift.WithClient(mgr.GetClient()),
229+
openshift.WithScheme(mgr.GetScheme()),
230+
openshift.WithLog(ctrl.Log.WithName("controllers").WithName("clusteroperator")),
231+
openshift.WithName(*writeStatusName),
232+
openshift.WithNamespace(*namespace),
233+
openshift.WithSyncChannel(op.AtLevel()),
234+
openshift.WithOLMOperator(),
235+
)
236+
if err != nil {
237+
logger.WithError(err).Fatalf("error configuring openshift integration")
238+
return
239+
}
240+
241+
if err := reconciler.SetupWithManager(mgr); err != nil {
242+
logger.WithError(err).Fatalf("error configuring openshift integration")
243+
return
244+
}
227245
}
228246

229247
if *writePackageServerStatusName != "" {

cmd/olm/manager.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,8 @@ func Manager(ctx context.Context, debug bool) (ctrl.Manager, error) {
4747
return nil, err
4848
}
4949

50-
// Setup a new controller to reconcile Operators
51-
setupLog.Info("configuring controller")
5250
if feature.Gate.Enabled(feature.OperatorLifecycleManagerV1) {
51+
// Setup a new controller to reconcile Operators
5352
operatorReconciler, err := operators.NewOperatorReconciler(
5453
mgr.GetClient(),
5554
ctrl.Log.WithName("controllers").WithName("operator"),
@@ -77,7 +76,6 @@ func Manager(ctx context.Context, debug bool) (ctrl.Manager, error) {
7776
}
7877

7978
}
80-
8179
setupLog.Info("manager configured")
8280

8381
return mgr, nil
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package openshift
2+
3+
import (
4+
"context"
5+
6+
configv1 "github.com/openshift/api/config/v1"
7+
utilerrors "k8s.io/apimachinery/pkg/util/errors"
8+
)
9+
10+
func NewClusterOperator(name string) *ClusterOperator {
11+
co := &ClusterOperator{ClusterOperator: &configv1.ClusterOperator{}}
12+
co.SetName(name)
13+
return co
14+
}
15+
16+
type ClusterOperator struct {
17+
*configv1.ClusterOperator
18+
}
19+
20+
func (c *ClusterOperator) GetOperatorVersion() string {
21+
for _, v := range c.Status.Versions {
22+
if v.Name == "operator" {
23+
return v.Version
24+
}
25+
}
26+
27+
return ""
28+
}
29+
30+
func (c *ClusterOperator) GetCondition(conditionType configv1.ClusterStatusConditionType) *configv1.ClusterOperatorStatusCondition {
31+
for _, cond := range c.Status.Conditions {
32+
if cond.Type == conditionType {
33+
return &cond
34+
}
35+
}
36+
37+
return nil
38+
}
39+
40+
func (c *ClusterOperator) SetCondition(condition *configv1.ClusterOperatorStatusCondition) {
41+
// Filter dups
42+
conditions := []configv1.ClusterOperatorStatusCondition{}
43+
for _, c := range c.Status.Conditions {
44+
if c.Type != condition.Type {
45+
conditions = append(conditions, c)
46+
}
47+
}
48+
49+
c.Status.Conditions = append(conditions, *condition)
50+
}
51+
52+
type Mutator interface {
53+
Mutate(context.Context, *ClusterOperator) error
54+
}
55+
56+
type MutateFunc func(context.Context, *ClusterOperator) error
57+
58+
func (m MutateFunc) Mutate(ctx context.Context, co *ClusterOperator) error {
59+
return m(ctx, co)
60+
}
61+
62+
type SerialMutations []Mutator
63+
64+
func (s SerialMutations) Mutate(ctx context.Context, co *ClusterOperator) error {
65+
var errs []error
66+
for _, m := range s {
67+
if err := m.Mutate(ctx, co); err != nil {
68+
errs = append(errs, err)
69+
}
70+
}
71+
72+
return utilerrors.NewAggregate(errs)
73+
}
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
package openshift
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"reflect"
7+
8+
configv1 "github.com/openshift/api/config/v1"
9+
apierrors "k8s.io/apimachinery/pkg/api/errors"
10+
"k8s.io/apimachinery/pkg/runtime"
11+
utilerrors "k8s.io/apimachinery/pkg/util/errors"
12+
ctrl "sigs.k8s.io/controller-runtime"
13+
"sigs.k8s.io/controller-runtime/pkg/builder"
14+
"sigs.k8s.io/controller-runtime/pkg/handler"
15+
"sigs.k8s.io/controller-runtime/pkg/reconcile"
16+
"sigs.k8s.io/controller-runtime/pkg/source"
17+
18+
operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1"
19+
olmversion "github.com/operator-framework/operator-lifecycle-manager/pkg/version"
20+
)
21+
22+
var (
23+
localSchemeBuilder = runtime.NewSchemeBuilder(
24+
configv1.AddToScheme,
25+
operatorsv1alpha1.AddToScheme,
26+
)
27+
28+
// AddToScheme adds all types necessary for the controller to operate.
29+
AddToScheme = localSchemeBuilder.AddToScheme
30+
)
31+
32+
type ClusterOperatorReconciler struct {
33+
*ReconcilerConfig
34+
35+
delayRequeue reconcile.Result
36+
mutate MutateFunc
37+
syncTracker *SyncTracker
38+
co *ClusterOperator
39+
}
40+
41+
func NewClusterOperatorReconciler(opts ...ReconcilerOption) (*ClusterOperatorReconciler, error) {
42+
config := new(ReconcilerConfig)
43+
config.apply(opts)
44+
if err := config.complete(); err != nil {
45+
return nil, err
46+
}
47+
48+
co := NewClusterOperator(config.Name)
49+
r := &ClusterOperatorReconciler{
50+
ReconcilerConfig: config,
51+
delayRequeue: reconcile.Result{RequeueAfter: config.RequeueDelay},
52+
co: co,
53+
syncTracker: NewSyncTracker(config.SyncCh, co),
54+
}
55+
56+
var mutations SerialMutations
57+
if config.Mutator != nil {
58+
mutations = append(mutations, config.Mutator)
59+
}
60+
mutations = append(mutations,
61+
MutateFunc(r.setVersions),
62+
MutateFunc(r.setProgressing),
63+
MutateFunc(r.setAvailable),
64+
MutateFunc(r.setDegraded),
65+
MutateFunc(r.setUpgradeable),
66+
)
67+
r.mutate = mutations.Mutate
68+
69+
return r, nil
70+
}
71+
72+
func (r *ClusterOperatorReconciler) SetupWithManager(mgr ctrl.Manager) error {
73+
if err := mgr.Add(r.syncTracker); err != nil {
74+
return fmt.Errorf("failed to add %T to manager: %s", r.syncTracker, err)
75+
}
76+
77+
bldr := ctrl.NewControllerManagedBy(mgr).
78+
For(&configv1.ClusterOperator{}, builder.WithPredicates(watchName(&r.Name))).
79+
Watches(&source.Channel{Source: r.syncTracker.Events()}, &handler.EnqueueRequestForObject{})
80+
81+
return r.TweakBuilder(bldr).Complete(r)
82+
}
83+
84+
func (r *ClusterOperatorReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
85+
log := r.Log.WithValues("request", req)
86+
noRequeue := reconcile.Result{}
87+
if req.NamespacedName.Name != r.co.GetName() {
88+
// Throw away requests to reconcile all ClusterOperators but our own
89+
// These should already be filtered by controller-runtime by this point
90+
return noRequeue, nil
91+
}
92+
93+
// Get the ClusterOperator
94+
in := &configv1.ClusterOperator{}
95+
if err := r.Client.Get(ctx, req.NamespacedName, in); err != nil {
96+
if apierrors.IsNotFound(err) {
97+
// The ClusterOperator is missing, let's create it
98+
stripObject(r.co.ClusterOperator)
99+
err = r.Client.Create(ctx, r.co.ClusterOperator)
100+
}
101+
102+
// Transient error or successful creation, requeue
103+
return r.delayRequeue, err
104+
}
105+
106+
r.co.ClusterOperator = in.DeepCopy()
107+
108+
var errs []error
109+
res := reconcile.Result{}
110+
if err := r.mutate(ctx, r.co); err != nil {
111+
// Transitive error, requeue
112+
log.Error(err, "Error mutating ClusterOperator")
113+
errs = append(errs, err)
114+
res = r.delayRequeue
115+
}
116+
117+
if !reflect.DeepEqual(r.co.Status, in.Status) {
118+
// Status change detected, update
119+
if err := r.Client.Status().Update(ctx, r.co.ClusterOperator); err != nil {
120+
// Transitive error, requeue
121+
errs = append(errs, err)
122+
res = r.delayRequeue
123+
}
124+
}
125+
126+
return res, utilerrors.NewAggregate(errs)
127+
}
128+
129+
func (r *ClusterOperatorReconciler) setVersions(_ context.Context, co *ClusterOperator) error {
130+
// If we've successfully synced, we know our operator is working properly, so we can update the version
131+
if r.syncTracker.SuccessfulSyncs() > 0 && !reflect.DeepEqual(co.Status.Versions, r.TargetVersions) {
132+
co.Status.Versions = r.TargetVersions
133+
}
134+
135+
return nil
136+
}
137+
138+
func (r *ClusterOperatorReconciler) setProgressing(_ context.Context, co *ClusterOperator) error {
139+
desired := &configv1.ClusterOperatorStatusCondition{
140+
Type: configv1.OperatorProgressing,
141+
LastTransitionTime: r.Now(),
142+
}
143+
144+
if r.syncTracker.SuccessfulSyncs() > 0 && reflect.DeepEqual(co.Status.Versions, r.TargetVersions) {
145+
desired.Status = configv1.ConditionFalse
146+
desired.Message = fmt.Sprintf("Deployed %s", olmversion.OLMVersion)
147+
} else {
148+
desired.Status = configv1.ConditionTrue
149+
desired.Message = fmt.Sprintf("Waiting to see update %s succeed", olmversion.OLMVersion)
150+
}
151+
152+
current := co.GetCondition(configv1.OperatorProgressing)
153+
if conditionsEqual(current, desired) { // Comparison ignores lastUpdated
154+
return nil
155+
}
156+
157+
co.SetCondition(desired)
158+
159+
return nil
160+
}
161+
162+
func (r *ClusterOperatorReconciler) setAvailable(_ context.Context, co *ClusterOperator) error {
163+
desired := &configv1.ClusterOperatorStatusCondition{
164+
Type: configv1.OperatorAvailable,
165+
LastTransitionTime: r.Now(),
166+
}
167+
168+
if r.syncTracker.SuccessfulSyncs() > 0 && reflect.DeepEqual(co.Status.Versions, r.TargetVersions) {
169+
desired.Status = configv1.ConditionTrue
170+
} else {
171+
desired.Status = configv1.ConditionFalse
172+
}
173+
174+
current := co.GetCondition(configv1.OperatorAvailable)
175+
if conditionsEqual(current, desired) { // Comparison ignores lastUpdated
176+
return nil
177+
}
178+
179+
co.SetCondition(desired)
180+
181+
return nil
182+
}
183+
184+
func (r *ClusterOperatorReconciler) setDegraded(_ context.Context, co *ClusterOperator) error {
185+
desired := &configv1.ClusterOperatorStatusCondition{
186+
Type: configv1.OperatorDegraded,
187+
LastTransitionTime: r.Now(),
188+
}
189+
190+
if r.syncTracker.SuccessfulSyncs() > 0 && reflect.DeepEqual(co.Status.Versions, r.TargetVersions) {
191+
desired.Status = configv1.ConditionFalse
192+
} else {
193+
desired.Status = configv1.ConditionTrue
194+
desired.Message = "Waiting for updates to take effect"
195+
}
196+
197+
current := co.GetCondition(configv1.OperatorDegraded)
198+
if conditionsEqual(current, desired) { // Comparison ignores lastUpdated
199+
return nil
200+
}
201+
202+
co.SetCondition(desired)
203+
204+
return nil
205+
}
206+
207+
const (
208+
IncompatibleOperatorsInstalled = "IncompatibleOperatorsInstalled"
209+
)
210+
211+
func (r *ClusterOperatorReconciler) setUpgradeable(ctx context.Context, co *ClusterOperator) error {
212+
desired := &configv1.ClusterOperatorStatusCondition{
213+
Type: configv1.OperatorUpgradeable,
214+
Status: configv1.ConditionTrue,
215+
LastTransitionTime: r.Now(),
216+
}
217+
218+
// Set upgradeable=false if (either/or):
219+
// 1. OLM currently upgrading (takes priorty in the status message)
220+
// 2. Operators currently installed that are incompatible with the next OCP minor version
221+
if r.syncTracker.SuccessfulSyncs() < 1 || !reflect.DeepEqual(co.Status.Versions, r.TargetVersions) {
222+
// OLM is still upgrading
223+
desired.Status = configv1.ConditionFalse
224+
desired.Message = "Waiting for updates to take effect"
225+
} else {
226+
incompatible, err := incompatibleOperators(ctx, r.Client)
227+
if err != nil {
228+
return err
229+
}
230+
231+
if len(incompatible) > 0 {
232+
// Some incompatible operator is installed
233+
desired.Status = configv1.ConditionFalse
234+
desired.Reason = IncompatibleOperatorsInstalled
235+
desired.Message = incompatible.String() // TODO: Truncate message to field length
236+
}
237+
}
238+
239+
current := co.GetCondition(configv1.OperatorUpgradeable)
240+
if conditionsEqual(current, desired) { // Comparison ignores lastUpdated
241+
return nil
242+
}
243+
244+
co.SetCondition(desired)
245+
246+
return nil
247+
}

0 commit comments

Comments
 (0)