diff --git a/pkg/apply/apply.go b/pkg/apply/apply.go index 304271ce..2541dcdb 100644 --- a/pkg/apply/apply.go +++ b/pkg/apply/apply.go @@ -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 @@ -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) } diff --git a/pkg/apply/desiredset.go b/pkg/apply/desiredset.go index d69a9b5d..cef1d098 100644 --- a/pkg/apply/desiredset.go +++ b/pkg/apply/desiredset.go @@ -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 { @@ -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 } diff --git a/pkg/apply/desiredset_compare.go b/pkg/apply/desiredset_compare.go index 1a2e029e..fe644710 100644 --- a/pkg/apply/desiredset_compare.go +++ b/pkg/apply/desiredset_compare.go @@ -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) @@ -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 } @@ -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 @@ -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 @@ -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") } @@ -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 } @@ -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) @@ -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 @@ -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 { diff --git a/pkg/apply/desiredset_compare_test.go b/pkg/apply/desiredset_compare_test.go index 8c57e771..e93277fd 100644 --- a/pkg/apply/desiredset_compare_test.go +++ b/pkg/apply/desiredset_compare_test.go @@ -6,6 +6,7 @@ import ( "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/json" @@ -138,7 +139,7 @@ func Test_doPatchJSONMergePatch3way(t *testing.T) { for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { - patchType, patch, err := doPatch(tt.args.gvk, tt.args.original, tt.args.modified, tt.args.current, [][]byte{}, false) + patchType, patch, err := doPatch(tt.args.gvk, tt.args.original, tt.args.modified, tt.args.current, [][]byte{}) if !tt.wantErr(t, err, fmt.Sprintf("doPatch(%v, %v, %v, %v)", tt.args.gvk, tt.args.original, tt.args.modified, tt.args.current)) { return } @@ -296,7 +297,7 @@ func Test_doPatchStrategicMergePatch3way(t *testing.T) { for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { - patchType, patch, err := doPatch(tt.args.gvk, tt.args.original, tt.args.modified, tt.args.current, [][]byte{}, false) + patchType, patch, err := doPatch(tt.args.gvk, tt.args.original, tt.args.modified, tt.args.current, [][]byte{}) if !tt.wantErr(t, err, fmt.Sprintf("doPatch(%v, %v, %v, %v)", tt.args.gvk, tt.args.original, tt.args.modified, tt.args.current)) { return } @@ -309,6 +310,7 @@ func Test_doPatchStrategicMergePatch3way(t *testing.T) { func Test_doPatchJSONMergePatch2way(t *testing.T) { type args struct { gvk schema.GroupVersionKind + original []byte modified []byte current []byte } @@ -394,13 +396,25 @@ func Test_doPatchJSONMergePatch2way(t *testing.T) { wantErr: assert.NoError, }, { - name: "2wayObjectKeyNotInModifiedAndInCurrentThenRemoveKey", + name: "2wayObjectKeyNotInModifiedAndInCurrentThenDoNothing", args: args{ gvk: testCRDGVK, modified: toTestCRDBytes(map[string]any{}, t), current: toTestCRDBytes(map[string]any{"one": "one"}, t), }, patchType: types.MergePatchType, + patch: []byte(`{}`), + wantErr: assert.NoError, + }, + { + name: "2wayObjectKeyNotInModifiedAndInCurrentWithClearExampleThenRemoveKey", + args: args{ + gvk: testCRDGVK, + original: toTestCRDBytes(map[string]any{"one": "clearme"}, t), + modified: toTestCRDBytes(map[string]any{}, t), + current: toTestCRDBytes(map[string]any{"one": "one"}, t), + }, + patchType: types.MergePatchType, patch: []byte(`{"data":{"one":null}}`), wantErr: assert.NoError, }, @@ -408,7 +422,7 @@ func Test_doPatchJSONMergePatch2way(t *testing.T) { for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { - patchType, patch, err := doPatch(tt.args.gvk, []byte{}, tt.args.modified, tt.args.current, [][]byte{}, true) + patchType, patch, err := doPatch(tt.args.gvk, tt.args.original, tt.args.modified, tt.args.current, [][]byte{}) if !tt.wantErr(t, err, fmt.Sprintf("doPatch(%v, %v, %v, %v)", tt.args.gvk, []byte{}, tt.args.modified, tt.args.current)) { return } @@ -421,6 +435,7 @@ func Test_doPatchJSONMergePatch2way(t *testing.T) { func Test_doPatchStrategicMergePatch2way(t *testing.T) { type args struct { gvk schema.GroupVersionKind + original []byte modified []byte current []byte } @@ -507,13 +522,25 @@ func Test_doPatchStrategicMergePatch2way(t *testing.T) { wantErr: assert.NoError, }, { - name: "2wayObjectKeyNotInModifiedAndInCurrentThenRemoveKey", + name: "2wayObjectKeyNotInModifiedAndInCurrentThenDoNothing", args: args{ gvk: configMapGVK, modified: toConfigMapBytes(map[string]string{}, t), current: toConfigMapBytes(map[string]string{"one": "one"}, t), }, patchType: types.StrategicMergePatchType, + patch: []byte(`{}`), + wantErr: assert.NoError, + }, + { + name: "2wayObjectKeyNotInModifiedAndInCurrentThenRemove", + args: args{ + gvk: configMapGVK, + original: toConfigMapBytes(map[string]string{"one": "clearme"}, t), + modified: toConfigMapBytes(map[string]string{}, t), + current: toConfigMapBytes(map[string]string{"one": "one", "two": "two"}, t), + }, + patchType: types.StrategicMergePatchType, patch: []byte(`{"data":null}`), wantErr: assert.NoError, }, @@ -531,14 +558,14 @@ func Test_doPatchStrategicMergePatch2way(t *testing.T) { }, t), }, patchType: types.StrategicMergePatchType, - patch: []byte(`{"spec":{"$setElementOrder/volumes":[{"name":"two"},{"name":"three"}],"volumes":[{"$retainKeys":["name"],"hostPath":null,"name":"two"},{"hostPath":{"path":"I am new"},"name":"three"},{"$patch":"delete","name":"four"}]}}`), + patch: []byte(`{"spec":{"$setElementOrder/volumes":[{"name":"two"},{"name":"three"}],"volumes":[{"$retainKeys":["name"],"name":"two"},{"hostPath":{"path":"I am new"},"name":"three"}]}}`), wantErr: assert.NoError, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { - patchType, patch, err := doPatch(tt.args.gvk, []byte{}, tt.args.modified, tt.args.current, [][]byte{}, true) + patchType, patch, err := doPatch(tt.args.gvk, tt.args.original, tt.args.modified, tt.args.current, [][]byte{}) if !tt.wantErr(t, err, fmt.Sprintf("doPatch(%v, %v, %v, %v)", tt.args.gvk, []byte{}, tt.args.modified, tt.args.current)) { return } @@ -552,7 +579,6 @@ func Test_sanitizePatch(t *testing.T) { type args struct { patch []byte removeObjectSetAnnotation bool - dontClearFields []string } tests := []struct { name string @@ -565,7 +591,6 @@ func Test_sanitizePatch(t *testing.T) { args: args{ patch: []byte(`{}`), removeObjectSetAnnotation: false, - dontClearFields: []string{}, }, want: []byte(`{}`), wantErr: assert.NoError, @@ -575,7 +600,6 @@ func Test_sanitizePatch(t *testing.T) { args: args{ patch: []byte(`{1: "one"}`), removeObjectSetAnnotation: false, - dontClearFields: []string{}, }, want: nil, wantErr: assert.Error, @@ -585,7 +609,6 @@ func Test_sanitizePatch(t *testing.T) { args: args{ patch: []byte(`{"kind": "patched", "apiVersion": "patched", "status": "patched", "metadata": {"creationTimestamp": "patched", "preserve": "this"}, "preserve": "this too"}`), removeObjectSetAnnotation: false, - dontClearFields: []string{}, }, want: []byte(`{"metadata":{"preserve":"this"},"preserve":"this too"}`), wantErr: assert.NoError, @@ -595,7 +618,6 @@ func Test_sanitizePatch(t *testing.T) { args: args{ patch: []byte(`{"metadata": {"annotations": {"objectset.rio.cattle.io/test": "delete me"}}}`), removeObjectSetAnnotation: true, - dontClearFields: []string{}, }, want: []byte(`{}`), wantErr: assert.NoError, @@ -605,35 +627,14 @@ func Test_sanitizePatch(t *testing.T) { args: args{ patch: []byte(`{"metadata": {"annotations": {"objectset.rio.cattle.io/test": "do not delete me"}}}`), removeObjectSetAnnotation: false, - dontClearFields: []string{}, }, want: []byte(`{"metadata": {"annotations": {"objectset.rio.cattle.io/test": "do not delete me"}}}`), wantErr: assert.NoError, }, - { - name: "RemoveJSONPatchDeletions", - args: args{ - patch: []byte(`{"a":{"b":[{"c":[{"d":null},{"e":null},{"f":"leave f alone"}]}]},"z":"leave z alone"}`), - removeObjectSetAnnotation: true, - dontClearFields: []string{"a.b.c.d", "a.b.c.f"}, - }, - want: []byte(`{"a":{"b":[{"c":[{},{"e":null},{"f":"leave f alone"}]}]},"z":"leave z alone"}`), - wantErr: assert.NoError, - }, - { - name: "RemoveStrategicPatchDeletions", - args: args{ - patch: []byte(`{"a":{"b":[{"c":[{"d":{"$patch": "delete"}},{"e":{"$patch": "delete"}},{"f":"leave f alone"}]}]},"z":"leave z alone"}`), - removeObjectSetAnnotation: true, - dontClearFields: []string{"a.b.c.d", "a.b.c.f"}, - }, - want: []byte(`{"a":{"b":[{"c":[{},{"e":{"$patch":"delete"}},{"f":"leave f alone"}]}]},"z":"leave z alone"}`), - wantErr: assert.NoError, - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := sanitizePatch(tt.args.patch, tt.args.removeObjectSetAnnotation, tt.args.dontClearFields) + got, err := sanitizePatch(tt.args.patch, tt.args.removeObjectSetAnnotation) if !tt.wantErr(t, err, fmt.Sprintf("sanitizePatch(%v, %v)", tt.args.patch, tt.args.removeObjectSetAnnotation)) { return } @@ -642,98 +643,6 @@ func Test_sanitizePatch(t *testing.T) { } } -func Test_removeDeletionsFromPatch(t *testing.T) { - type args struct { - data string - field []string - } - tests := []struct { - name string - args args - modified bool - modifiedData string - }{ - { - name: "JSONMergePatchNonExistingField", - args: args{ - data: `{"a":"z","c": {"f": null}}`, - field: []string{"containers"}, - }, - modifiedData: `{"a":"z","c": {"f": null}}`, - }, - { - name: "JSONMergePatchNonDeleteField", - args: args{ - data: `{"a":"z","c": {"f": null}}`, - field: []string{"a"}, - }, - modifiedData: `{"a":"z","c": {"f": null}}`, - }, - { - name: "JSONMergePatchDeleteField", - args: args{ - data: `{"a":"z","c": {"f": null}}`, - field: []string{"c", "f"}, - }, - modified: true, - modifiedData: `{"a":"z","c": {}}`, - }, - { - name: "JSONMergePatchDeleteFromArrayField", - args: args{ - data: `{"a":"z","c": [{"f": null}, {"g": null}]}`, - field: []string{"c", "f"}, - }, - modified: true, - modifiedData: `{"a":"z","c": [{},{"g": null}]}`, - }, - - { - name: "JSONStrategicMergePatchNonExistingField", - args: args{ - data: `{"a":"z","c": {"f": {"$patch": "delete"}}}`, - field: []string{"containers"}, - }, - modifiedData: `{"a":"z","c": {"f": {"$patch": "delete"}}}`, - }, - { - name: "JSONStrategicMergePatchNonDeleteField", - args: args{ - data: `{"a":"z","c": {"f": {"$patch": "delete"}}}`, - field: []string{"a"}, - }, - modifiedData: `{"a":"z","c": {"f": {"$patch": "delete"}}}`, - }, - { - name: "JSONStrategicMergePatchDeleteField", - args: args{ - data: `{"a":"z","c": {"f": {"$patch": "delete"}}}`, - field: []string{"c", "f"}, - }, - modified: true, - modifiedData: `{"a":"z","c": {}}`, - }, - { - name: "JSONStrategicMergePatchDeleteFromArrayField", - args: args{ - data: `{"a":"z","c": [{"f": {"$patch": "delete"}}, {"g": {"$patch": "delete"}}]}`, - field: []string{"c", "f"}, - }, - modified: true, - modifiedData: `{"a":"z","c": [{},{"g": {"$patch": "delete"}}]}`, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - data := toMap(tt.args.data, t) - assert.Equalf(t, tt.modified, removeDeletionsFromPatch(data, tt.args.field), "removeDeletionsFromPatch(%v, %v)", tt.args.data, tt.args.field) - - modifiedData := toMap(tt.modifiedData, t) - assert.Equalf(t, modifiedData, data, "removeDeletionsFromPatch(%v, %v)", tt.args.data, tt.args.field) - }) - } -} - // Utilities // testCRDGVK is the GVK of a CustomResourceDefinition, which uses MergePatchType (because it is not registered) @@ -815,3 +724,47 @@ func toPodBytes(volumes []v1.Volume, t *testing.T) []byte { } return toBytes(obj, t) } + +func Test_applyPatch(t *testing.T) { + type args struct { + gvk schema.GroupVersionKind + reconciler Reconciler + patcher Patcher + debugID string + ignoreOriginal bool + fastApply bool + clearExamples map[schema.GroupVersionKind]runtime.Object + diffPatches [][]byte + oldObject runtime.Object + newObject runtime.Object + } + tests := []struct { + name string + args args + want bool + wantErr assert.ErrorAssertionFunc + }{ + { + name: "EmptyPatch", + args: args{ + gvk: configMapGVK, + patcher: nil, + fastApply: false, + clearExamples: nil, + oldObject: nil, + newObject: nil, + }, + want: true, + wantErr: assert.NoError, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := applyPatch(tt.args.gvk, tt.args.reconciler, tt.args.patcher, tt.args.debugID, tt.args.ignoreOriginal, tt.args.fastApply, tt.args.clearExamples, tt.args.diffPatches, tt.args.oldObject, tt.args.newObject) + if !tt.wantErr(t, err, fmt.Sprintf("applyPatch(%v, %v, %v, %v, %v, %v, %v, %v, %v, %v)", tt.args.gvk, tt.args.reconciler, tt.args.patcher, tt.args.debugID, tt.args.ignoreOriginal, tt.args.fastApply, tt.args.clearExamples, tt.args.diffPatches, tt.args.oldObject, tt.args.newObject)) { + return + } + assert.Equalf(t, tt.want, got, "applyPatch(%v, %v, %v, %v, %v, %v, %v, %v, %v, %v)", tt.args.gvk, tt.args.reconciler, tt.args.patcher, tt.args.debugID, tt.args.ignoreOriginal, tt.args.fastApply, tt.args.clearExamples, tt.args.diffPatches, tt.args.oldObject, tt.args.newObject) + }) + } +} diff --git a/pkg/apply/desiredset_process.go b/pkg/apply/desiredset_process.go index 22a33cf0..e609f4a4 100644 --- a/pkg/apply/desiredset_process.go +++ b/pkg/apply/desiredset_process.go @@ -268,7 +268,7 @@ func (o *desiredSet) process(debugID string, set labels.Selector, gvk schema.Gro reconciler = nil patcher = func(namespace, name string, pt types2.PatchType, data []byte) (runtime.Object, error) { - data, err := sanitizePatch(data, true, o.dontClearFields) + data, err := sanitizePatch(data, true) if err != nil { return nil, err }