Skip to content

Commit da20c8e

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

File tree

5 files changed

+123
-6
lines changed

5 files changed

+123
-6
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: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,12 @@ 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"
@@ -121,6 +125,37 @@ func groupResource(c client.Client, o client.Object) (schema.GroupResource, erro
121125
return m.Resource.GroupResource(), nil
122126
}
123127

128+
// AdditiveMergePatchApplyOption returns an ApplyOption that makes
129+
// the Apply additive in the sense of a merge patch without null values. This is
130+
// the old behavior of the APIPatchingApplicator.
131+
//
132+
// This only works with a desired object of type *unstructured.Unstructured.
133+
//
134+
// Deprecated: replace with Server Side Apply.
135+
func AdditiveMergePatchApplyOption(ctx context.Context, current, desired runtime.Object) error {
136+
u, ok := desired.(*unstructured.Unstructured)
137+
if !ok {
138+
return errors.New("desired object is not an unstructured.Unstructured")
139+
}
140+
currentBytes, err := json.Marshal(current)
141+
if err != nil {
142+
return errors.Wrapf(err, "cannot marshal current %s", HumanReadableReference(nil, current))
143+
}
144+
desiredBytes, err := json.Marshal(u)
145+
if err != nil {
146+
return errors.Wrapf(err, "cannot marshal desired %s", HumanReadableReference(nil, desired))
147+
}
148+
mergedBytes, err := jsonpatch.MergePatch(currentBytes, desiredBytes)
149+
if err != nil {
150+
return errors.Wrapf(err, "cannot merge patch to %s", HumanReadableReference(nil, desired))
151+
}
152+
u.Object = nil
153+
if err = json.Unmarshal(mergedBytes, &u.Object); err != nil {
154+
return errors.Wrapf(err, "cannot unmarshal merged patch to %s", HumanReadableReference(nil, desired))
155+
}
156+
return nil
157+
}
158+
124159
// An APIUpdatingApplicator applies changes to an object by either creating or
125160
// updating it in a Kubernetes API server.
126161
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 {

pkg/resource/resource.go

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -389,19 +389,30 @@ func GetExternalTags(mg Managed) map[string]string {
389389

390390
// HumanReadableReference returns a human readable object reference like
391391
// "pod default/database", e.g. to be used in error strings.
392-
func HumanReadableReference(c client.Client, o client.Object) string {
393-
gvk, err := c.GroupVersionKindFor(o)
394-
if err != nil {
392+
//
393+
// The client is optional and can be nil. Then the kind is guessed from the
394+
// object.
395+
func HumanReadableReference(c client.Client, o runtime.Object) string {
396+
gvk := o.GetObjectKind().GroupVersionKind()
397+
if gvk.Kind == "" && c != nil {
398+
gvk, _ = c.GroupVersionKindFor(o)
399+
}
400+
if gvk.Kind == "" {
395401
gvk.Kind = fmt.Sprintf("%T", o) // best effort
396402
}
397403

398-
name := o.GetName()
404+
co, ok := o.(client.Object)
405+
if !ok {
406+
return gvk.Kind
407+
}
408+
409+
name := co.GetName()
399410
infix := ""
400-
if gn := o.GetGenerateName(); name == "" && gn != "" {
411+
if gn := co.GetGenerateName(); name == "" && gn != "" {
401412
name = gn
402413
infix = "with generated name "
403414
}
404-
if ns := o.GetNamespace(); ns != "" {
415+
if ns := co.GetNamespace(); ns != "" {
405416
return fmt.Sprintf("%s %s%s/%s", strings.ToLower(gvk.Kind), infix, ns, name)
406417
}
407418

0 commit comments

Comments
 (0)