@@ -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
82115func (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
284341type 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