Skip to content

Commit f8daa6d

Browse files
authored
fix field owner management in case of mixed update/apply operations (#82)
1 parent 385401b commit f8daa6d

File tree

2 files changed

+48
-14
lines changed

2 files changed

+48
-14
lines changed

pkg/component/ssa.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,11 @@ func replaceFieldManager(managedFields []metav1.ManagedFieldsEntry, managerPrefi
3535
if entry == managerEntry {
3636
continue
3737
}
38-
if !slices.Any(managerPrefixes, func(s string) bool { return strings.HasPrefix(entry.Manager, s) }) || entry.Subresource != "" {
38+
if entry.Subresource != "" {
39+
entries = append(entries, entry)
40+
continue
41+
}
42+
if entry.Manager != manager && !slices.Any(managerPrefixes, func(s string) bool { return strings.HasPrefix(entry.Manager, s) }) {
3943
entries = append(entries, entry)
4044
continue
4145
}

pkg/component/target.go

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)