@@ -18,7 +18,10 @@ package updater
1818
1919import (
2020 "context"
21+ "fmt"
22+ "reflect"
2123
24+ "github.com/go-logr/logr"
2225 "helm.sh/helm/v3/pkg/release"
2326 corev1 "k8s.io/api/core/v1"
2427 "k8s.io/apimachinery/pkg/api/errors"
@@ -33,17 +36,35 @@ import (
3336 "github.com/operator-framework/helm-operator-plugins/pkg/internal/status"
3437)
3538
36- func New (client client.Client ) Updater {
39+ func New (client client.Client , logger logr.Logger ) Updater {
40+ logger = logger .WithName ("updater" )
3741 return Updater {
3842 client : client ,
43+ logger : logger ,
3944 }
4045}
4146
4247type Updater struct {
43- isCanceled bool
44- client client.Client
45- updateFuncs []UpdateFunc
46- updateStatusFuncs []UpdateStatusFunc
48+ isCanceled bool
49+ client client.Client
50+ logger logr.Logger
51+ updateFuncs []UpdateFunc
52+ updateStatusFuncs []UpdateStatusFunc
53+ externallyManagedStatusConditions map [string ]struct {}
54+ enableAggressiveConflictResolution bool
55+ }
56+
57+ func (u * Updater ) RegisterExternallyManagedStatusConditions (conditions map [string ]struct {}) {
58+ if u .externallyManagedStatusConditions == nil {
59+ u .externallyManagedStatusConditions = make (map [string ]struct {}, len (conditions ))
60+ }
61+ for conditionType := range conditions {
62+ u .externallyManagedStatusConditions [conditionType ] = struct {}{}
63+ }
64+ }
65+
66+ func (u * Updater ) EnableAggressiveConflictResolution () {
67+ u .enableAggressiveConflictResolution = true
4768}
4869
4970type UpdateFunc func (* unstructured.Unstructured ) bool
@@ -113,7 +134,20 @@ func (u *Updater) Apply(ctx context.Context, obj *unstructured.Unstructured) err
113134 st .updateStatusObject ()
114135 obj .Object ["status" ] = st .StatusObject
115136 if err := retryOnRetryableUpdateError (backoff , func () error {
116- return u .client .Status ().Update (ctx , obj )
137+ updateErr := u .client .Status ().Update (ctx , obj )
138+ if errors .IsConflict (updateErr ) && u .enableAggressiveConflictResolution {
139+ u .logger .V (1 ).Info ("Status update conflict detected" )
140+ resolved , resolveErr := u .tryRefreshObject (ctx , obj )
141+ u .logger .V (1 ).Info ("tryRefreshObject" , "resolved" , resolved , "resolveErr" , resolveErr )
142+ if resolveErr != nil {
143+ return resolveErr
144+ }
145+ if ! resolved {
146+ return updateErr
147+ }
148+ return fmt .Errorf ("status update conflict due to externally-managed status conditions" ) // retriable error.
149+ }
150+ return updateErr
117151 }); err != nil {
118152 return err
119153 }
@@ -125,14 +159,149 @@ func (u *Updater) Apply(ctx context.Context, obj *unstructured.Unstructured) err
125159 }
126160 if needsUpdate {
127161 if err := retryOnRetryableUpdateError (backoff , func () error {
128- return u .client .Update (ctx , obj )
162+ updateErr := u .client .Update (ctx , obj )
163+ if errors .IsConflict (updateErr ) && u .enableAggressiveConflictResolution {
164+ u .logger .V (1 ).Info ("Status update conflict detected" )
165+ resolved , resolveErr := u .tryRefreshObject (ctx , obj )
166+ u .logger .V (1 ).Info ("tryRefreshObject" , "resolved" , resolved , "resolveErr" , resolveErr )
167+ if resolveErr != nil {
168+ return resolveErr
169+ }
170+ if ! resolved {
171+ return updateErr
172+ }
173+ return fmt .Errorf ("update conflict due to externally-managed status conditions" ) // retriable error.
174+ }
175+ return updateErr
129176 }); err != nil {
130177 return err
131178 }
132179 }
133180 return nil
134181}
135182
183+ // This function tries to merge the status of obj with the current version of the status on the cluster.
184+ // The unstructured obj is expected to have been modified and to have caused a conflict error during an update attempt.
185+ // If the only differences between obj and the current version are in externally managed status conditions,
186+ // those conditions are merged from the current version into obj.
187+ // Returns true if updating shall be retried with the updated obj.
188+ // Returns false if the conflict could not be resolved.
189+ func (u * Updater ) tryRefreshObject (ctx context.Context , obj * unstructured.Unstructured ) (bool , error ) {
190+ // Retrieve current version from the cluster.
191+ current := & unstructured.Unstructured {}
192+ current .SetGroupVersionKind (obj .GroupVersionKind ())
193+ objectKey := client .ObjectKeyFromObject (obj )
194+ if err := u .client .Get (ctx , objectKey , current ); err != nil {
195+ err = fmt .Errorf ("refreshing object %s/%s: %w" , objectKey .Namespace , objectKey .Name , err )
196+ return false , err
197+ }
198+
199+ if ! reflect .DeepEqual (obj .Object ["spec" ], current .Object ["spec" ]) {
200+ // Diff in object spec. Nothing we can do about it -> Fail.
201+ u .logger .V (1 ).Info ("Cluster resource cannot be updated due to spec mismatch" ,
202+ "namespace" , objectKey .Namespace , "name" , objectKey .Name , "gkv" , obj .GroupVersionKind (),
203+ )
204+ return false , nil
205+ }
206+
207+ // Merge externally managed conditions from current into object copy.
208+ objCopy := obj .DeepCopy ()
209+ u .mergeExternallyManagedConditions (objCopy , current )
210+
211+ // Overwrite metadata with the most recent in-cluster version.
212+ // This ensures we have the latest resourceVersion, annotations, labels, etc.
213+ objCopy .Object ["metadata" ] = current .Object ["metadata" ]
214+
215+ // We were able to resolve the conflict by merging external conditions.
216+ obj .Object = objCopy .Object
217+
218+ u .logger .V (1 ).Info ("Resolved update conflict by merging externally-managed status conditions" )
219+ return true , nil
220+ }
221+
222+ // mergeExternallyManagedConditions updates obj's status conditions by replacing
223+ // externally managed conditions with their values from current.
224+ // Uses current's ordering to avoid false positives in conflict detection.
225+ func (u * Updater ) mergeExternallyManagedConditions (obj , current * unstructured.Unstructured ) {
226+ objConditions := statusConditionsFromObject (obj )
227+ if objConditions == nil {
228+ return
229+ }
230+
231+ currentConditions := statusConditionsFromObject (current )
232+ if currentConditions == nil {
233+ return
234+ }
235+
236+ // Build a map of all conditions from obj (by type).
237+ objConditionsByType := make (map [string ]map [string ]interface {})
238+ for _ , cond := range objConditions {
239+ if condType , ok := cond ["type" ].(string ); ok {
240+ objConditionsByType [condType ] = cond
241+ }
242+ }
243+
244+ // Build merged conditions starting from current's ordering.
245+ mergedConditions := make ([]map [string ]interface {}, 0 , len (currentConditions ))
246+ for _ , cond := range currentConditions {
247+ condType , ok := cond ["type" ].(string )
248+ if ! ok {
249+ // Shouldn't happen.
250+ continue
251+ }
252+ if _ , isExternal := u .externallyManagedStatusConditions [condType ]; isExternal {
253+ // Keep external condition from current.
254+ mergedConditions = append (mergedConditions , cond )
255+ } else if objCond , found := objConditionsByType [condType ]; found {
256+ // Replace with non-external condition from obj.
257+ mergedConditions = append (mergedConditions , objCond )
258+ delete (objConditionsByType , condType ) // Mark as used.
259+ }
260+ // Note: If condition exists in current but not in obj (and is non-external),
261+ // we skip it.
262+ }
263+
264+ // Add any remaining non-externally managed conditions from obj that weren't in current.
265+ for condType , cond := range objConditionsByType {
266+ if _ , isExternal := u .externallyManagedStatusConditions [condType ]; isExternal {
267+ continue
268+ }
269+ mergedConditions = append (mergedConditions , cond )
270+ }
271+
272+ // Convert to []interface{} for SetNestedField
273+ mergedConditionsInterface := make ([]interface {}, len (mergedConditions ))
274+ for i , cond := range mergedConditions {
275+ mergedConditionsInterface [i ] = cond
276+ }
277+
278+ // Write the modified conditions back.
279+ _ = unstructured .SetNestedField (obj .Object , mergedConditionsInterface , "status" , "conditions" )
280+ }
281+
282+ // statusConditionsFromObject extracts status conditions from an unstructured object.
283+ // Returns nil if the conditions field is not found or is not the expected type.
284+ func statusConditionsFromObject (obj * unstructured.Unstructured ) []map [string ]interface {} {
285+ conditionsRaw , ok , _ := unstructured .NestedFieldNoCopy (obj .Object , "status" , "conditions" )
286+ if ! ok {
287+ return nil
288+ }
289+
290+ conditionsSlice , ok := conditionsRaw .([]interface {})
291+ if ! ok {
292+ return nil
293+ }
294+
295+ // Convert []interface{} to []map[string]interface{}
296+ result := make ([]map [string ]interface {}, 0 , len (conditionsSlice ))
297+ for _ , cond := range conditionsSlice {
298+ if condMap , ok := cond .(map [string ]interface {}); ok {
299+ result = append (result , condMap )
300+ }
301+ }
302+ return result
303+ }
304+
136305func RemoveFinalizer (finalizer string ) UpdateFunc {
137306 return func (obj * unstructured.Unstructured ) bool {
138307 if ! controllerutil .ContainsFinalizer (obj , finalizer ) {
0 commit comments