@@ -385,6 +385,7 @@ func (t *reconcileTarget[T]) Reconcile(ctx context.Context, component T) (bool,
385385
386386 // if item was not found, append an empty item
387387 if item == nil {
388+ // TODO: should the owner id check happen always (not only if the object is unknown to the inventory)?
388389 // fetch object (if existing)
389390 existingObject , err := t .readObject (ctx , object )
390391 if err != nil {
@@ -676,15 +677,15 @@ func (t *reconcileTarget[T]) Reconcile(ctx context.Context, component T) (bool,
676677 setAnnotation (object , t .annotationKeyOwnerId , ownerId )
677678 setAnnotation (object , t .annotationKeyDigest , item .Digest )
678679
680+ updatePolicy := getUpdatePolicy (object )
679681 if existingObject == nil {
680- if err := t .createObject (ctx , object , nil ); err != nil {
682+ if err := t .createObject (ctx , object , nil , updatePolicy ); err != nil {
681683 return false , errors .Wrapf (err , "error creating object %s" , item )
682684 }
683685 item .Phase = PhaseCreating
684686 item .Status = kstatus .InProgressStatus .String ()
685687 numUnready ++
686688 } else if existingObject .GetDeletionTimestamp ().IsZero () && existingObject .GetAnnotations ()[t .annotationKeyDigest ] != item .Digest {
687- updatePolicy := getUpdatePolicy (object )
688689 switch updatePolicy {
689690 case UpdatePolicyRecreate :
690691 // TODO: perform an additional owner id check
@@ -888,7 +889,7 @@ func (t *reconcileTarget[T]) readObject(ctx context.Context, key types.ObjectKey
888889// create object; object may be a concrete type or unstructured; in any case, type meta must be populated;
889890// createdObject is optional; if non-nil, it will be populated with the created object; the same variable can be supplied as object and createObject;
890891// if object is a crd or an api services, the reconciler's name will be added as finalizer
891- func (t * reconcileTarget [T ]) createObject (ctx context.Context , object client.Object , createdObject any ) (err error ) {
892+ func (t * reconcileTarget [T ]) createObject (ctx context.Context , object client.Object , createdObject any , updatePolicy UpdatePolicy ) (err error ) {
892893 defer func () {
893894 if err == nil && createdObject != nil {
894895 err = runtime .DefaultUnstructuredConverter .FromUnstructured (object .(* unstructured.Unstructured ).Object , createdObject )
@@ -897,6 +898,9 @@ func (t *reconcileTarget[T]) createObject(ctx context.Context, object client.Obj
897898 t .client .EventRecorder ().Event (object , corev1 .EventTypeNormal , objectReasonCreated , "Object successfully created" )
898899 }
899900 }()
901+
902+ // log := log.FromContext(ctx).WithValues("object", types.ObjectKeyToString(object))
903+
900904 data , err := runtime .DefaultUnstructuredConverter .ToUnstructured (object )
901905 if err != nil {
902906 return err
@@ -905,7 +909,18 @@ func (t *reconcileTarget[T]) createObject(ctx context.Context, object client.Obj
905909 if isCrd (object ) || isApiService (object ) {
906910 controllerutil .AddFinalizer (object , t .reconcilerName )
907911 }
908- return t .client .Create (ctx , object , client .FieldOwner (t .reconcilerName ))
912+ // note: clearing managedFields is anyway required for ssa; but also in the create (post) case it does not harm
913+ object .SetManagedFields (nil )
914+ // create the object right from the start with the right managed fields operation (Apply or Update), in order to avoid
915+ // having to patch the managed fields during future update calls
916+ switch updatePolicy {
917+ case UpdatePolicySsaMerge , UpdatePolicySsaOverride :
918+ // set the target resource version to an impossible value; this will produce a 409 conflict in case the object already exists
919+ object .SetResourceVersion ("1" )
920+ return t .client .Patch (ctx , object , client .Apply , client .FieldOwner (t .reconcilerName ))
921+ default :
922+ return t .client .Create (ctx , object , client .FieldOwner (t .reconcilerName ))
923+ }
909924}
910925
911926// update object; object may be a concrete type or unstructured; in any case, type meta must be populated;
@@ -928,11 +943,15 @@ func (t *reconcileTarget[T]) updateObject(ctx context.Context, object client.Obj
928943 t .client .EventRecorder ().Eventf (existingObject , corev1 .EventTypeWarning , objectReasonUpdateError , "Error updating object: %s" , err )
929944 }
930945 }()
931- // TODO: validate (by panic) that existingObject fits to object
946+
947+ log := log .FromContext (ctx ).WithValues ("object" , types .ObjectKeyToString (object ))
948+
949+ // TODO: validate (by panic) that existingObject fits to object (i.e. have same object key)
932950 if ! existingObject .GetDeletionTimestamp ().IsZero () {
933951 // note: we must not update objects which are in deletion (e.g. to avoid unintentionally clearing finalizers), so we want to see the panic in that case
934952 panic ("this cannot happen" )
935953 }
954+
936955 data , err := runtime .DefaultUnstructuredConverter .ToUnstructured (object )
937956 if err != nil {
938957 return err
@@ -941,6 +960,10 @@ func (t *reconcileTarget[T]) updateObject(ctx context.Context, object client.Obj
941960 if isCrd (object ) || isApiService (object ) {
942961 controllerutil .AddFinalizer (object , t .reconcilerName )
943962 }
963+ // it is allowed that target object contains a resource version (e.g. because the producing generator read it from the cluster);
964+ // otherwise, we set the resource version to the one of the existing object, in order to ensure that we do not unintentionally overwrite
965+ // a state different from the one we have read
966+ // note that the api server performs a resource version conflict check not only in case of update (put), but also for ssa (patch)
944967 if object .GetResourceVersion () == "" {
945968 object .SetResourceVersion ((existingObject .GetResourceVersion ()))
946969 }
@@ -949,13 +972,18 @@ func (t *reconcileTarget[T]) updateObject(ctx context.Context, object client.Obj
949972 // fields will not be touched
950973 object .SetManagedFields (nil )
951974 switch updatePolicy {
952- case UpdatePolicySsaMerge :
953- return t .client .Patch (ctx , object , client .Apply , client .FieldOwner (t .reconcilerName ), client .ForceOwnership )
954- case UpdatePolicySsaOverride :
955- // TODO: add ways (per reconciler, per component, per object) to configure the list of field manager (prefixes) which are reclaimed
956- if managedFields , changed , err := replaceFieldManager (existingObject .GetManagedFields (), []string {"kubectl" , "helm" }, t .reconcilerName ); err != nil {
975+ case UpdatePolicySsaMerge , UpdatePolicySsaOverride :
976+ var replacedFieldManagerPrefixes []string
977+ if updatePolicy == UpdatePolicySsaOverride {
978+ // TODO: add ways (per reconciler, per component, per object) to configure the list of field manager (prefixes) which are reclaimed
979+ replacedFieldManagerPrefixes = []string {"kubectl" , "helm" }
980+ }
981+ // note: even if replacedFieldManagerPrefixes is empty, replaceFieldManager() will reclaim fields created by us through an Update operation,
982+ // that is through a create or update call; this may be necessary, if the update policy for the object changed (globally or per-object)
983+ if managedFields , changed , err := replaceFieldManager (existingObject .GetManagedFields (), replacedFieldManagerPrefixes , t .reconcilerName ); err != nil {
957984 return err
958985 } else if changed {
986+ log .V (1 ).Info ("adjusting field managers as preparation of ssa" )
959987 gvk := object .GetObjectKind ().GroupVersionKind ()
960988 obj := & metav1.PartialObjectMetadata {
961989 TypeMeta : metav1.TypeMeta {
@@ -1002,8 +1030,10 @@ func (t *reconcileTarget[T]) deleteObject(ctx context.Context, key types.ObjectK
10021030 t .client .EventRecorder ().Eventf (existingObject , corev1 .EventTypeWarning , objectReasonDeleteError , "Error deleting object: %s" , err )
10031031 }
10041032 }()
1033+
1034+ log := log .FromContext (ctx ).WithValues ("object" , types .ObjectKeyToString (key ))
1035+
10051036 // TODO: validate (by panic) that existingObject (if present) fits to key
1006- log := log .FromContext (ctx )
10071037
10081038 object := & unstructured.Unstructured {}
10091039 object .SetGroupVersionKind (key .GetObjectKind ().GroupVersionKind ())
@@ -1039,7 +1069,7 @@ func (t *reconcileTarget[T]) deleteObject(ctx context.Context, key types.ObjectK
10391069 // note: 409 error is very likely here (because of concurrent updates happening through the api server); this is why we retry once
10401070 if err := t .client .Update (ctx , crd , client .FieldOwner (t .reconcilerName )); err != nil {
10411071 if i == 1 && apierrors .IsConflict (err ) {
1042- log .V (1 ).Info ("error while updating CustomResourcedefinition (409 conflict); doing one retry" , "name" , t . reconcilerName , " error" , err .Error ())
1072+ log .V (1 ).Info ("error while updating CustomResourcedefinition (409 conflict); doing one retry" , "error" , err .Error ())
10431073 continue
10441074 }
10451075 return err
@@ -1064,7 +1094,7 @@ func (t *reconcileTarget[T]) deleteObject(ctx context.Context, key types.ObjectK
10641094 // note: 409 error is very likely here (because of concurrent updates happening through the api server); this is why we retry once
10651095 if err := t .client .Update (ctx , apiService , client .FieldOwner (t .reconcilerName )); err != nil {
10661096 if i == 1 && apierrors .IsConflict (err ) {
1067- log .V (1 ).Info ("error while updating APIService (409 conflict); doing one retry" , "name" , t . reconcilerName , " error" , err .Error ())
1097+ log .V (1 ).Info ("error while updating APIService (409 conflict); doing one retry" , "error" , err .Error ())
10681098 continue
10691099 }
10701100 return err
0 commit comments