Skip to content

Commit f512e1e

Browse files
authored
CER: centralize status updates into big-R Reconcile method (#2200)
Signed-off-by: Joe Lanford <[email protected]>
1 parent 6e22e2b commit f512e1e

File tree

2 files changed

+81
-24
lines changed

2 files changed

+81
-24
lines changed

internal/operator-controller/controllers/clusterextension_controller.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,14 +97,14 @@ func (r *ClusterExtensionReconciler) Reconcile(ctx context.Context, req ctrl.Req
9797
l := log.FromContext(ctx).WithName("cluster-extension")
9898
ctx = log.IntoContext(ctx, l)
9999

100-
l.Info("reconcile starting")
101-
defer l.Info("reconcile ending")
102-
103100
existingExt := &ocv1.ClusterExtension{}
104101
if err := r.Get(ctx, req.NamespacedName, existingExt); err != nil {
105102
return ctrl.Result{}, client.IgnoreNotFound(err)
106103
}
107104

105+
l.Info("reconcile starting")
106+
defer l.Info("reconcile ending")
107+
108108
reconciledExt := existingExt.DeepCopy()
109109
res, reconcileErr := r.reconcile(ctx, reconciledExt)
110110

@@ -113,7 +113,7 @@ func (r *ClusterExtensionReconciler) Reconcile(ctx context.Context, req ctrl.Req
113113
updateFinalizers := !equality.Semantic.DeepEqual(existingExt.Finalizers, reconciledExt.Finalizers)
114114

115115
// If any unexpected fields have changed, panic before updating the resource
116-
unexpectedFieldsChanged := checkForUnexpectedFieldChange(*existingExt, *reconciledExt)
116+
unexpectedFieldsChanged := checkForUnexpectedClusterExtensionFieldChange(*existingExt, *reconciledExt)
117117
if unexpectedFieldsChanged {
118118
panic("spec or metadata changed by reconciler")
119119
}
@@ -158,7 +158,7 @@ func ensureAllConditionsWithReason(ext *ocv1.ClusterExtension, reason v1alpha1.C
158158
}
159159

160160
// Compare resources - ignoring status & metadata.finalizers
161-
func checkForUnexpectedFieldChange(a, b ocv1.ClusterExtension) bool {
161+
func checkForUnexpectedClusterExtensionFieldChange(a, b ocv1.ClusterExtension) bool {
162162
a.Status, b.Status = ocv1.ClusterExtensionStatus{}, ocv1.ClusterExtensionStatus{}
163163
a.Finalizers, b.Finalizers = []string{}, []string{}
164164
return !equality.Semantic.DeepEqual(a, b)

internal/operator-controller/controllers/clusterextensionrevision_controller.go

Lines changed: 76 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212

1313
appsv1 "k8s.io/api/apps/v1"
1414
corev1 "k8s.io/api/core/v1"
15+
"k8s.io/apimachinery/pkg/api/equality"
1516
"k8s.io/apimachinery/pkg/api/meta"
1617
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1718
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -67,16 +68,48 @@ func (c *ClusterExtensionRevisionReconciler) Reconcile(ctx context.Context, req
6768
l := log.FromContext(ctx).WithName("cluster-extension-revision")
6869
ctx = log.IntoContext(ctx, l)
6970

70-
rev := &ocv1.ClusterExtensionRevision{}
71-
if err := c.Client.Get(ctx, req.NamespacedName, rev); err != nil {
71+
existingRev := &ocv1.ClusterExtensionRevision{}
72+
if err := c.Client.Get(ctx, req.NamespacedName, existingRev); err != nil {
7273
return ctrl.Result{}, client.IgnoreNotFound(err)
7374
}
7475

75-
l = l.WithValues("key", req.String())
7676
l.Info("reconcile starting")
7777
defer l.Info("reconcile ending")
7878

79-
return c.reconcile(ctx, rev)
79+
reconciledRev := existingRev.DeepCopy()
80+
res, reconcileErr := c.reconcile(ctx, reconciledRev)
81+
82+
// Do checks before any Update()s, as Update() may modify the resource structure!
83+
updateStatus := !equality.Semantic.DeepEqual(existingRev.Status, reconciledRev.Status)
84+
85+
unexpectedFieldsChanged := checkForUnexpectedClusterExtensionRevisionFieldChange(*existingRev, *reconciledRev)
86+
if unexpectedFieldsChanged {
87+
panic("spec or metadata changed by reconciler")
88+
}
89+
90+
// NOTE: finalizer updates are performed during c.reconcile as patches, so that reconcile can
91+
// continue performing logic after successfully setting the finalizer. therefore we only need
92+
// to set status here.
93+
94+
if updateStatus {
95+
if err := c.Client.Status().Update(ctx, reconciledRev); err != nil {
96+
reconcileErr = errors.Join(reconcileErr, fmt.Errorf("error updating status: %v", err))
97+
}
98+
}
99+
100+
return res, reconcileErr
101+
}
102+
103+
// Compare resources - ignoring status & metadata.finalizers
104+
func checkForUnexpectedClusterExtensionRevisionFieldChange(a, b ocv1.ClusterExtensionRevision) bool {
105+
a.Status, b.Status = ocv1.ClusterExtensionRevisionStatus{}, ocv1.ClusterExtensionRevisionStatus{}
106+
107+
// when finalizers are updated during reconcile, we expect finalizers, managedFields, and resourceVersion
108+
// to be updated, so we ignore changes in these fields.
109+
a.Finalizers, b.Finalizers = []string{}, []string{}
110+
a.ManagedFields, b.ManagedFields = nil, nil
111+
a.ResourceVersion, b.ResourceVersion = "", ""
112+
return !equality.Semantic.DeepEqual(a.Spec, b.Spec)
80113
}
81114

82115
func (c *ClusterExtensionRevisionReconciler) reconcile(ctx context.Context, rev *ocv1.ClusterExtensionRevision) (ctrl.Result, error) {
@@ -98,11 +131,14 @@ func (c *ClusterExtensionRevisionReconciler) reconcile(ctx context.Context, rev
98131
Message: err.Error(),
99132
ObservedGeneration: rev.Generation,
100133
})
101-
return ctrl.Result{}, fmt.Errorf("revision teardown: %w", errors.Join(err, c.Client.Status().Update(ctx, rev)))
134+
return ctrl.Result{}, fmt.Errorf("revision teardown: %v", err)
102135
}
103136

104137
l.Info("teardown report", "report", tres.String())
105138
if !tres.IsComplete() {
139+
// TODO: If it is not complete, it seems like it would be good to update
140+
// the status in some way to tell the user that the teardown is still
141+
// in progress.
106142
return ctrl.Result{}, nil
107143
}
108144

@@ -114,9 +150,19 @@ func (c *ClusterExtensionRevisionReconciler) reconcile(ctx context.Context, rev
114150
Message: err.Error(),
115151
ObservedGeneration: rev.Generation,
116152
})
117-
return ctrl.Result{}, fmt.Errorf("free cache informers: %w", errors.Join(err, c.Client.Status().Update(ctx, rev)))
153+
return ctrl.Result{}, fmt.Errorf("error stopping informers: %v", err)
118154
}
119-
return ctrl.Result{}, c.removeFinalizer(ctx, rev, clusterExtensionRevisionTeardownFinalizer)
155+
if err := c.removeFinalizer(ctx, rev, clusterExtensionRevisionTeardownFinalizer); err != nil {
156+
meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{
157+
Type: "Available",
158+
Status: metav1.ConditionFalse,
159+
Reason: "ReconcileFailure",
160+
Message: err.Error(),
161+
ObservedGeneration: rev.Generation,
162+
})
163+
return ctrl.Result{}, fmt.Errorf("error removing teardown finalizer: %v", err)
164+
}
165+
return ctrl.Result{}, nil
120166
}
121167

122168
//
@@ -130,8 +176,9 @@ func (c *ClusterExtensionRevisionReconciler) reconcile(ctx context.Context, rev
130176
Message: err.Error(),
131177
ObservedGeneration: rev.Generation,
132178
})
133-
return ctrl.Result{}, fmt.Errorf("ensure finalizer: %w", errors.Join(err, c.Client.Status().Update(ctx, rev)))
179+
return ctrl.Result{}, fmt.Errorf("error ensuring teardown finalizer: %v", err)
134180
}
181+
135182
if err := c.establishWatch(ctx, rev, revision); err != nil {
136183
meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{
137184
Type: ocv1.ClusterExtensionRevisionTypeAvailable,
@@ -140,8 +187,9 @@ func (c *ClusterExtensionRevisionReconciler) reconcile(ctx context.Context, rev
140187
Message: err.Error(),
141188
ObservedGeneration: rev.Generation,
142189
})
143-
return ctrl.Result{}, fmt.Errorf("establish watch: %w", errors.Join(err, c.Client.Status().Update(ctx, rev)))
190+
return ctrl.Result{}, fmt.Errorf("establish watch: %v", err)
144191
}
192+
145193
rres, err := c.RevisionEngine.Reconcile(ctx, *revision, opts...)
146194
if err != nil {
147195
meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{
@@ -151,60 +199,69 @@ func (c *ClusterExtensionRevisionReconciler) reconcile(ctx context.Context, rev
151199
Message: err.Error(),
152200
ObservedGeneration: rev.Generation,
153201
})
154-
return ctrl.Result{}, fmt.Errorf("revision reconcile: %w", errors.Join(err, c.Client.Status().Update(ctx, rev)))
202+
return ctrl.Result{}, fmt.Errorf("revision reconcile: %v", err)
155203
}
156204
l.Info("reconcile report", "report", rres.String())
157205

158206
// Retry failing preflight checks with a flat 10s retry.
159207
// TODO: report status, backoff?
160208
if verr := rres.GetValidationError(); verr != nil {
161209
l.Info("preflight error, retrying after 10s", "err", verr.String())
210+
162211
meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{
163212
Type: ocv1.ClusterExtensionRevisionTypeAvailable,
164213
Status: metav1.ConditionFalse,
165214
Reason: ocv1.ClusterExtensionRevisionReasonRevisionValidationFailure,
166215
Message: fmt.Sprintf("revision validation error: %s", verr),
167216
ObservedGeneration: rev.Generation,
168217
})
169-
return ctrl.Result{RequeueAfter: 10 * time.Second}, c.Client.Status().Update(ctx, rev)
218+
return ctrl.Result{RequeueAfter: 10 * time.Second}, nil
170219
}
220+
171221
for i, pres := range rres.GetPhases() {
172222
if verr := pres.GetValidationError(); verr != nil {
173223
l.Info("preflight error, retrying after 10s", "err", verr.String())
224+
174225
meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{
175226
Type: ocv1.ClusterExtensionRevisionTypeAvailable,
176227
Status: metav1.ConditionFalse,
177228
Reason: ocv1.ClusterExtensionRevisionReasonPhaseValidationError,
178229
Message: fmt.Sprintf("phase %d validation error: %s", i, verr),
179230
ObservedGeneration: rev.Generation,
180231
})
181-
return ctrl.Result{RequeueAfter: 10 * time.Second}, c.Client.Status().Update(ctx, rev)
232+
return ctrl.Result{RequeueAfter: 10 * time.Second}, nil
182233
}
234+
183235
var collidingObjs []string
184236
for _, ores := range pres.GetObjects() {
185237
if ores.Action() == machinery.ActionCollision {
186238
collidingObjs = append(collidingObjs, ores.String())
187239
}
188240
}
241+
189242
if len(collidingObjs) > 0 {
190243
l.Info("object collision error, retrying after 10s", "collisions", collidingObjs)
244+
191245
meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{
192246
Type: ocv1.ClusterExtensionRevisionTypeAvailable,
193247
Status: metav1.ConditionFalse,
194248
Reason: ocv1.ClusterExtensionRevisionReasonObjectCollisions,
195249
Message: fmt.Sprintf("revision object collisions in phase %d\n%s", i, strings.Join(collidingObjs, "\n\n")),
196250
ObservedGeneration: rev.Generation,
197251
})
198-
return ctrl.Result{RequeueAfter: 10 * time.Second}, c.Client.Status().Update(ctx, rev)
252+
return ctrl.Result{RequeueAfter: 10 * time.Second}, nil
199253
}
200254
}
201255

202256
//nolint:nestif
203257
if rres.IsComplete() {
204258
// Archive other revisions.
205259
for _, a := range previous {
206-
if err := c.Client.Patch(ctx, a, client.RawPatch(
207-
types.MergePatchType, []byte(`{"spec":{"lifecycleState":"Archived"}}`))); err != nil {
260+
patch := []byte(`{"spec":{"lifecycleState":"Archived"}}`)
261+
if err := c.Client.Patch(ctx, a, client.RawPatch(types.MergePatchType, patch)); err != nil {
262+
// TODO: It feels like an error here needs to propagate to a status _somewhere_.
263+
// Not sure the current CER makes sense? But it also feels off to set the CE
264+
// status from outside the CE reconciler.
208265
return ctrl.Result{}, fmt.Errorf("archive previous Revision: %w", err)
209266
}
210267
}
@@ -278,7 +335,7 @@ func (c *ClusterExtensionRevisionReconciler) reconcile(ctx context.Context, rev
278335
meta.RemoveStatusCondition(&rev.Status.Conditions, ocv1.TypeProgressing)
279336
}
280337

281-
return ctrl.Result{}, c.Client.Status().Update(ctx, rev)
338+
return ctrl.Result{}, nil
282339
}
283340

284341
type Sourcerer interface {
@@ -408,7 +465,7 @@ func toBoxcutterRevision(rev *ocv1.ClusterExtensionRevision) (*boxcutter.Revisio
408465
for _, specPhase := range rev.Spec.Phases {
409466
phase := boxcutter.Phase{Name: specPhase.Name}
410467
for _, specObj := range specPhase.Objects {
411-
obj := specObj.Object
468+
obj := specObj.Object.DeepCopy()
412469

413470
labels := obj.GetLabels()
414471
if labels == nil {
@@ -420,10 +477,10 @@ func toBoxcutterRevision(rev *ocv1.ClusterExtensionRevision) (*boxcutter.Revisio
420477
switch specObj.CollisionProtection {
421478
case ocv1.CollisionProtectionIfNoController, ocv1.CollisionProtectionNone:
422479
opts = append(opts, boxcutter.WithObjectReconcileOptions(
423-
&obj, boxcutter.WithCollisionProtection(specObj.CollisionProtection)))
480+
obj, boxcutter.WithCollisionProtection(specObj.CollisionProtection)))
424481
}
425482

426-
phase.Objects = append(phase.Objects, obj)
483+
phase.Objects = append(phase.Objects, *obj)
427484
}
428485
r.Phases = append(r.Phases, phase)
429486
}

0 commit comments

Comments
 (0)