Skip to content

Commit

Permalink
Experimental: reverse the logic, specify fields to be cleared
Browse files Browse the repository at this point in the history
Signed-off-by: Silvio Moioli <[email protected]>
  • Loading branch information
moio committed Oct 25, 2023
1 parent f630ef5 commit 33e6b6a
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 217 deletions.
25 changes: 17 additions & 8 deletions pkg/apply/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ type Apply interface {
WithSetOwnerReference(controller, block bool) Apply
WithIgnorePreviousApplied() Apply
WithDiffPatch(gvk schema.GroupVersionKind, namespace, name string, patch []byte) Apply
WithFastApply(dontClearFields ...string) Apply
WithFastApply() Apply
WithClearFieldExample(gvk schema.GroupVersionKind, example runtime.Object) Apply

FindOwner(obj runtime.Object) (runtime.Object, error)
PurgeOrphan(obj runtime.Object) error
Expand Down Expand Up @@ -312,12 +313,20 @@ func (a *apply) WithDiffPatch(gvk schema.GroupVersionKind, namespace, name strin
// WithFastApply configures Apply to use 2-way merging, which is less CPU intensive than the default 3-way merging.
// WithFastApply has the same behavior as Apply (including to honor strategic merge patch tags) apart from one case:
//
// When applying an object with a nil field, WithFastApply will clear the corresponding field in the cluster object.
// If that is not wanted, specify the field in dotted notation in dontClearFields, and it will be left unchanged.
// When aobject with a nil (or `omitempty` equivalent) field is applied, WithFastApply leaves the field unchanged.
//
// Instead, when applying an object with a nil field, basic Apply will decide whether to clear it or not depending on
// the **immediately previous** Apply. If the field set nil was also nil in the immediately preceding Apply, then
// it is not cleared, otherwise it is.
func (a *apply) WithFastApply(dontClearFields ...string) Apply {
return a.newDesiredSet().WithFastApply(dontClearFields...)
// (basic Apply decides whether to clear or to not clear a nil field depending on the object passed to the
// **immediately previous** Apply. If the field was also nil in the immediately preceding Apply, then it is not cleared,
// otherwise it is)
//
// If clearing on nil is necessary, use WithClearFieldExample.
func (a *apply) WithFastApply() Apply {
return a.newDesiredSet().WithFastApply()
}

// WithClearFieldExample changes the policy for clearing fields when using WithFastApply. Specifically, any non-nil
// field in obj will be cleared in the cluster object when set to nil (or `omitempty` equivalent) in the applied object.
// Default behavior is to leave the field unchanged.
func (a *apply) WithClearFieldExample(gvk schema.GroupVersionKind, example runtime.Object) Apply {
return a.newDesiredSet().WithClearFieldExample(gvk, example)
}
31 changes: 21 additions & 10 deletions pkg/apply/desiredset.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ type desiredSet struct {
createPlan bool
plan Plan

fastApply bool
dontClearFields []string
fastApply bool
clearExamples map[schema.GroupVersionKind]runtime.Object
}

func (o *desiredSet) err(err error) error {
Expand Down Expand Up @@ -243,17 +243,28 @@ func (o desiredSet) WithContext(ctx context.Context) Apply {
return o
}

// WithFastApply configures desiredSet to use 2-way merging, which is less CPU intensive than the default 3-way merging.
// WithFastApply configures Apply to use 2-way merging, which is less CPU intensive than the default 3-way merging.
// WithFastApply has the same behavior as Apply (including to honor strategic merge patch tags) apart from one case:
//
// When applying an object with a nil field, WithFastApply will clear the corresponding field in the cluster object.
// If that is not wanted, specify the field in dotted notation in dontClearFields, and it will be left unchanged.
// When aobject with a nil (or `omitempty` equivalent) field is applied, WithFastApply leaves the field unchanged.
//
// Instead, when applying an object with a nil field, basic Apply will decide whether to clear it or not depending on
// the **immediately previous** Apply. If the field set nil was also nil in the immediately preceding Apply, then
// it is not cleared, otherwise it is.
func (o desiredSet) WithFastApply(dontClearFields ...string) Apply {
// (basic Apply decides whether to clear or to not clear a nil field depending on the object passed to the
// **immediately previous** Apply. If the field was also nil in the immediately preceding Apply, then it is not cleared,
// otherwise it is)
//
// If clearing on nil is necessary, use WithClearFieldExample.
func (o desiredSet) WithFastApply() Apply {
o.fastApply = true
o.dontClearFields = dontClearFields
return o
}

// WithClearFieldExample changes the policy for clearing fields when using WithFastApply. Specifically, any non-nil
// field in obj will be cleared in the cluster object when set to nil (or `omitempty` equivalent) in the applied object.
// Default behavior is to leave the field unchanged.
func (o desiredSet) WithClearFieldExample(gvk schema.GroupVersionKind, example runtime.Object) Apply {
if o.clearExamples == nil {
o.clearExamples = map[schema.GroupVersionKind]runtime.Object{}
}
o.clearExamples[gvk] = example
return o
}
90 changes: 18 additions & 72 deletions pkg/apply/desiredset_compare.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ func emptyMaps(data map[string]interface{}, keys ...string) bool {
return true
}

func sanitizePatch(patch []byte, removeObjectSetAnnotation bool, dontClearFields []string) ([]byte, error) {
func sanitizePatch(patch []byte, removeObjectSetAnnotation bool) ([]byte, error) {
mod := false
data := map[string]interface{}{}
err := json.Unmarshal(patch, &data)
Expand Down Expand Up @@ -144,12 +144,6 @@ func sanitizePatch(patch []byte, removeObjectSetAnnotation bool, dontClearFields
}
}

// for all fields that should not be cleared, check the patch:
// if it happens to have a deletion for that field, remove it
for _, field := range dontClearFields {
mod = mod || removeDeletionsFromPatch(data, strings.Split(field, "."))
}

if emptyMaps(data, "metadata", "annotations") {
return []byte("{}"), nil
}
Expand All @@ -161,56 +155,7 @@ func sanitizePatch(patch []byte, removeObjectSetAnnotation bool, dontClearFields
return json.Marshal(data)
}

// removeDeletionsFromPatch removes deletions from JSON Merge Patch or a Strategic Merge Patch
func removeDeletionsFromPatch(data map[string]interface{}, field []string) bool {
// this is the last field in the path
if len(field) == 1 {
key := field[0]
if value, ok := data[key]; ok {
// if this is a JSON Merge Patch style deletion, remove it
// see https://datatracker.ietf.org/doc/html/rfc7386
if value == nil {
delete(data, key)
return true
}
// if this is a Strategic Merge Patch style deletion, remove it
// see https://github.com/kubernetes/community/blob/master/contributors/devel/sig-api-machinery/strategic-merge-patch.md#delete-directive
if mapValue, ok := value.(map[string]any); ok {
if patchValue, ok := mapValue["$patch"]; ok {
if stringValue, ok := patchValue.(string); ok && stringValue == "delete" {
delete(data, key)
return true
}
}
}
}
return false
}

// this is not the last field in the path
key := field[0]
if value, ok := data[key]; ok {
// next field in the path is an object, recurse
if mapValue, ok := value.(map[string]interface{}); ok {
return removeDeletionsFromPatch(mapValue, field[1:])
}
// next field in the path is a list, iterate
if listValue, ok := value.([]interface{}); ok {
for _, item := range listValue {
result := false
// list item is an object, recurse
if mapValue, ok := item.(map[string]interface{}); ok {
deletionResult := removeDeletionsFromPatch(mapValue, field[1:])
result = result || deletionResult
}
return result
}
}
}
return false
}

func applyPatch(gvk schema.GroupVersionKind, reconciler Reconciler, patcher Patcher, debugID string, ignoreOriginal, fastApply bool, dontClearFields []string, diffPatches [][]byte, oldObject, newObject runtime.Object) (bool, error) {
func applyPatch(gvk schema.GroupVersionKind, reconciler Reconciler, patcher Patcher, debugID string, ignoreOriginal, fastApply bool, clearExamples map[schema.GroupVersionKind]runtime.Object, diffPatches [][]byte, oldObject, newObject runtime.Object) (bool, error) {
oldMetadata, err := meta.Accessor(oldObject)
if err != nil {
return false, err
Expand All @@ -225,6 +170,15 @@ func applyPatch(gvk schema.GroupVersionKind, reconciler Reconciler, patcher Patc
}
}

// if clearExamples is set, use the example object as the original. In the 3-way algorithm,
// this forces the patch to remove any corresponding nil fields from the current object
if clearExample, found := clearExamples[gvk]; found {
original, err = json.Marshal(clearExample)
if err != nil {
return false, err
}
}

modified, err := getModifiedBytes(gvk, newObject, fastApply)
if err != nil {
return false, err
Expand All @@ -235,7 +189,7 @@ func applyPatch(gvk schema.GroupVersionKind, reconciler Reconciler, patcher Patc
return false, err
}

patchType, patch, err := doPatch(gvk, original, modified, current, diffPatches, fastApply)
patchType, patch, err := doPatch(gvk, original, modified, current, diffPatches)
if err != nil {
return false, errors.Wrap(err, "patch generation")
}
Expand All @@ -244,7 +198,7 @@ func applyPatch(gvk schema.GroupVersionKind, reconciler Reconciler, patcher Patc
return false, nil
}

patch, err = sanitizePatch(patch, false, dontClearFields)
patch, err = sanitizePatch(patch, false)
if err != nil {
return false, err
}
Expand Down Expand Up @@ -305,7 +259,7 @@ func (o *desiredSet) compareObjects(gvk schema.GroupVersionKind, reconciler Reco
GroupVersionKind: gvk,
}]...)

if ran, err := applyPatch(gvk, reconciler, patcher, debugID, o.ignorePreviousApplied, o.fastApply, o.dontClearFields, diffPatches, oldObject, newObject); err != nil {
if ran, err := applyPatch(gvk, reconciler, patcher, debugID, o.ignorePreviousApplied, o.fastApply, o.clearExamples, diffPatches, oldObject, newObject); err != nil {
return err
} else if !ran {
logrus.Debugf("DesiredSet - No change(2) %s %s/%s for %s", gvk, oldMetadata.GetNamespace(), oldMetadata.GetName(), debugID)
Expand Down Expand Up @@ -481,7 +435,7 @@ func stripIgnores(original, modified, current []byte, patches [][]byte) ([]byte,
}

// doPatch is adapted from "kubectl apply"
func doPatch(gvk schema.GroupVersionKind, original, modified, current []byte, diffPatch [][]byte, fastApply bool) (types.PatchType, []byte, error) {
func doPatch(gvk schema.GroupVersionKind, original, modified, current []byte, diffPatch [][]byte) (types.PatchType, []byte, error) {
var (
patchType types.PatchType
patch []byte
Expand All @@ -497,18 +451,10 @@ func doPatch(gvk schema.GroupVersionKind, original, modified, current []byte, di
return patchType, nil, err
}

if fastApply {
if patchType == types.StrategicMergePatchType {
patch, err = strategicpatch.CreateTwoWayMergePatchUsingLookupPatchMeta(current, modified, lookupPatchMeta)
} else {
patch, err = jsonpatch.CreateMergePatch(current, modified)
}
if patchType == types.StrategicMergePatchType {
patch, err = strategicpatch.CreateThreeWayMergePatch(original, modified, current, lookupPatchMeta, true)
} else {
if patchType == types.StrategicMergePatchType {
patch, err = strategicpatch.CreateThreeWayMergePatch(original, modified, current, lookupPatchMeta, true)
} else {
patch, err = jsonmergepatch.CreateThreeWayJSONMergePatch(original, modified, current)
}
patch, err = jsonmergepatch.CreateThreeWayJSONMergePatch(original, modified, current)
}

if err != nil {
Expand Down
Loading

0 comments on commit 33e6b6a

Please sign in to comment.