Skip to content

Commit 6747cf0

Browse files
committed
pkg/resource: add AdditiveMergePatchApplyOption
Signed-off-by: Dr. Stefan Schimanski <[email protected]>
1 parent 36c1fe2 commit 6747cf0

File tree

4 files changed

+112
-1
lines changed

4 files changed

+112
-1
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.20
55
require (
66
dario.cat/mergo v1.0.0
77
github.com/bufbuild/buf v1.26.1
8+
github.com/evanphx/json-patch v5.6.0+incompatible
89
github.com/go-logr/logr v1.2.4
910
github.com/google/go-cmp v0.5.9
1011
github.com/spf13/afero v1.9.5

go.sum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y
9696
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
9797
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
9898
github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U=
99+
github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
99100
github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww=
100101
github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
101102
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=

pkg/resource/api.go

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,19 @@ package resource
1818

1919
import (
2020
"context"
21+
"encoding/json"
2122

23+
jsonpatch "github.com/evanphx/json-patch"
2224
kerrors "k8s.io/apimachinery/pkg/api/errors"
25+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
26+
"k8s.io/apimachinery/pkg/runtime"
2327
"k8s.io/apimachinery/pkg/runtime/schema"
2428
"k8s.io/apimachinery/pkg/types"
2529
"sigs.k8s.io/controller-runtime/pkg/client"
2630

2731
"github.com/crossplane/crossplane-runtime/pkg/errors"
2832
"github.com/crossplane/crossplane-runtime/pkg/meta"
33+
resourceunstructured "github.com/crossplane/crossplane-runtime/pkg/resource/unstructured"
2934
)
3035

3136
// Error strings.
@@ -54,7 +59,7 @@ func NewAPIPatchingApplicator(c client.Client) *APIPatchingApplicator {
5459
// Apply changes to the supplied object. The object will be created if it does
5560
// not exist, or patched if it does. If the object does exist, it will only be
5661
// patched if the passed object has the same or an empty resource version.
57-
func (a *APIPatchingApplicator) Apply(ctx context.Context, obj client.Object, ao ...ApplyOption) error { //nolint:gocyclo // the logic here is crucial and deserves to stay in one method
62+
func (a *APIPatchingApplicator) Apply(ctx context.Context, obj client.Object, ao ...ApplyOption) error {
5863
if obj.GetName() == "" && obj.GetGenerateName() != "" {
5964
return a.client.Create(ctx, obj)
6065
}
@@ -102,6 +107,41 @@ func groupResource(c client.Client, o client.Object) (schema.GroupResource, erro
102107
return m.Resource.GroupResource(), nil
103108
}
104109

110+
// AdditiveMergePatchApplyOption returns an ApplyOption that makes
111+
// the Apply additive in the sense of a merge patch without null values. This is
112+
// the old behavior of the APIPatchingApplicator.
113+
//
114+
// This only works with a desired object of type *unstructured.Unstructured.
115+
//
116+
// Deprecated: replace with Server Side Apply.
117+
func AdditiveMergePatchApplyOption(_ context.Context, current, desired runtime.Object) error {
118+
u, ok := desired.(*unstructured.Unstructured)
119+
if !ok {
120+
uw, ok := desired.(resourceunstructured.Wrapper)
121+
if !ok {
122+
return errors.New("desired object is not an unstructured.Unstructured")
123+
}
124+
u = uw.GetUnstructured()
125+
}
126+
currentBytes, err := json.Marshal(current)
127+
if err != nil {
128+
return errors.Wrapf(err, "cannot marshal current %s", HumanReadableReference(nil, current))
129+
}
130+
desiredBytes, err := json.Marshal(u)
131+
if err != nil {
132+
return errors.Wrapf(err, "cannot marshal desired %s", HumanReadableReference(nil, desired))
133+
}
134+
mergedBytes, err := jsonpatch.MergePatch(currentBytes, desiredBytes)
135+
if err != nil {
136+
return errors.Wrapf(err, "cannot merge patch to %s", HumanReadableReference(nil, desired))
137+
}
138+
u.Object = nil
139+
if err = json.Unmarshal(mergedBytes, &u.Object); err != nil {
140+
return errors.Wrapf(err, "cannot unmarshal merged patch to %s", HumanReadableReference(nil, desired))
141+
}
142+
return nil
143+
}
144+
105145
// An APIUpdatingApplicator applies changes to an object by either creating or
106146
// updating it in a Kubernetes API server.
107147
type APIUpdatingApplicator struct {

pkg/resource/api_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,19 @@ package resource
1818

1919
import (
2020
"context"
21+
"encoding/json"
2122
"testing"
2223

24+
jsonpatch "github.com/evanphx/json-patch"
2325
"github.com/google/go-cmp/cmp"
2426
kerrors "k8s.io/apimachinery/pkg/api/errors"
2527
"k8s.io/apimachinery/pkg/api/meta"
2628
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
29+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2730
"k8s.io/apimachinery/pkg/runtime"
2831
"k8s.io/apimachinery/pkg/runtime/schema"
2932
"sigs.k8s.io/controller-runtime/pkg/client"
33+
"sigs.k8s.io/yaml"
3034

3135
"github.com/crossplane/crossplane-runtime/pkg/errors"
3236
"github.com/crossplane/crossplane-runtime/pkg/resource/fake"
@@ -54,6 +58,14 @@ func TestAPIPatchingApplicator(t *testing.T) {
5458
fakeRESTMapper := meta.NewDefaultRESTMapper([]schema.GroupVersion{gvk.GroupVersion()})
5559
fakeRESTMapper.AddSpecific(gvk, gvr, singular, meta.RESTScopeRoot)
5660

61+
// for additive merge patch option test
62+
currentYAML := `
63+
metadata:
64+
resourceVersion: "42"
65+
a: old
66+
b: old
67+
`
68+
5769
type args struct {
5870
ctx context.Context
5971
o client.Object
@@ -221,6 +233,63 @@ func TestAPIPatchingApplicator(t *testing.T) {
221233
err: kerrors.NewConflict(schema.GroupResource{Group: "example.com", Resource: "things"}, current.GetName(), errors.New(errOptimisticLock)),
222234
},
223235
},
236+
"AdditiveMergePatch": {
237+
reason: "No error with the old additive behaviour if desired",
238+
c: &test.MockClient{
239+
MockGet: test.NewMockGetFn(nil, func(o client.Object) error {
240+
o.(*unstructured.Unstructured).Object = map[string]interface{}{}
241+
return yaml.Unmarshal([]byte(currentYAML), &o.(*unstructured.Unstructured).Object)
242+
}),
243+
MockPatch: func(_ context.Context, o client.Object, patch client.Patch, _ ...client.PatchOption) error {
244+
bs, err := patch.Data(o)
245+
if err != nil {
246+
return err
247+
}
248+
currentJSON, err := yaml.YAMLToJSON([]byte(currentYAML))
249+
if err != nil {
250+
return err
251+
}
252+
patched, err := jsonpatch.MergePatch(currentJSON, bs)
253+
if err != nil {
254+
return err
255+
}
256+
o.(*unstructured.Unstructured).Object = map[string]interface{}{}
257+
if err := json.Unmarshal(patched, &o.(*unstructured.Unstructured).Object); err != nil {
258+
return err
259+
}
260+
o.SetResourceVersion("43")
261+
return nil
262+
},
263+
MockGroupVersionKindFor: test.NewMockGroupVersionKindForFn(nil, gvk),
264+
MockRESTMapper: test.NewMockRESTMapperFn(fakeRESTMapper),
265+
},
266+
args: args{
267+
o: &unstructured.Unstructured{
268+
Object: map[string]interface{}{
269+
"kind": "Thing",
270+
"metadata": map[string]interface{}{
271+
"resourceVersion": "42",
272+
},
273+
"b": "changed",
274+
"c": "added",
275+
},
276+
},
277+
ao: []ApplyOption{AdditiveMergePatchApplyOption},
278+
},
279+
want: want{
280+
o: &unstructured.Unstructured{
281+
Object: map[string]interface{}{
282+
"kind": "Thing",
283+
"metadata": map[string]interface{}{
284+
"resourceVersion": "43",
285+
},
286+
"a": "old",
287+
"b": "changed",
288+
"c": "added",
289+
},
290+
},
291+
},
292+
},
224293
}
225294

226295
for name, tc := range cases {

0 commit comments

Comments
 (0)