Skip to content

Commit

Permalink
pkg/resource: add AdditiveMergePatchApplyOption
Browse files Browse the repository at this point in the history
Signed-off-by: Dr. Stefan Schimanski <stefan.schimanski@upbound.io>
  • Loading branch information
sttts committed Sep 5, 2023
1 parent 36c1fe2 commit 9a0fe90
Show file tree
Hide file tree
Showing 4 changed files with 381 additions and 1 deletion.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.20
require (
dario.cat/mergo v1.0.0
github.com/bufbuild/buf v1.26.1
github.com/evanphx/json-patch v5.6.0+incompatible
github.com/go-logr/logr v1.2.4
github.com/google/go-cmp v0.5.9
github.com/spf13/afero v1.9.5
Expand Down
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U=
github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww=
github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
Expand Down
50 changes: 49 additions & 1 deletion pkg/resource/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,14 @@ limitations under the License.
package resource

import (
"bytes"
"context"

jsonpatch "github.com/evanphx/json-patch"
kerrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer/json"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"

Expand Down Expand Up @@ -54,7 +58,7 @@ func NewAPIPatchingApplicator(c client.Client) *APIPatchingApplicator {
// Apply changes to the supplied object. The object will be created if it does
// not exist, or patched if it does. If the object does exist, it will only be
// patched if the passed object has the same or an empty resource version.
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
func (a *APIPatchingApplicator) Apply(ctx context.Context, obj client.Object, ao ...ApplyOption) error {
if obj.GetName() == "" && obj.GetGenerateName() != "" {
return a.client.Create(ctx, obj)
}
Expand Down Expand Up @@ -102,6 +106,50 @@ func groupResource(c client.Client, o client.Object) (schema.GroupResource, erro
return m.Resource.GroupResource(), nil
}

var emptyScheme = runtime.NewScheme() // no need to recognize any types
var jsonSerializer = json.NewSerializerWithOptions(json.DefaultMetaFactory, emptyScheme, emptyScheme, json.SerializerOptions{})

// AdditiveMergePatchApplyOption returns an ApplyOption that makes
// the Apply additive in the sense of a merge patch without null values. This is
// the old behavior of the APIPatchingApplicator.
//
// This only works with a desired object of type *unstructured.Unstructured.
//
// Deprecated: replace with Server Side Apply.
func AdditiveMergePatchApplyOption(_ context.Context, current, desired runtime.Object) error {
// set GVK uniformly to the desired object to make serializer happy
currentGVK, desiredGVK := current.GetObjectKind().GroupVersionKind(), desired.GetObjectKind().GroupVersionKind()
if !desiredGVK.Empty() && currentGVK != desiredGVK {
return errors.Errorf("cannot apply %v to %v", desired.GetObjectKind().GroupVersionKind(), current.GetObjectKind().GroupVersionKind())
}
desired.GetObjectKind().SetGroupVersionKind(currentGVK)

// merge `desired` additively with `current`
var currentBytes, desiredBytes bytes.Buffer
if err := jsonSerializer.Encode(current, &currentBytes); err != nil {
return errors.Wrapf(err, "cannot marshal current %s", HumanReadableReference(nil, current))
}
if err := jsonSerializer.Encode(desired, &desiredBytes); err != nil {
return errors.Wrapf(err, "cannot marshal desired %s", HumanReadableReference(nil, desired))
}
mergedBytes, err := jsonpatch.MergePatch(currentBytes.Bytes(), desiredBytes.Bytes())
if err != nil {
return errors.Wrapf(err, "cannot merge patch to %s", HumanReadableReference(nil, desired))
}

// write merged object back to `desired`
if _, _, err := jsonSerializer.Decode(mergedBytes, nil, desired); err != nil {
return errors.Wrapf(err, "cannot unmarshal merged patch to %s", HumanReadableReference(nil, desired))
}

// restore empty GVK for typed objects
if _, isUnstructured := desired.(runtime.Unstructured); !isUnstructured {
desired.GetObjectKind().SetGroupVersionKind(schema.GroupVersionKind{})
}

return nil
}

// An APIUpdatingApplicator applies changes to an object by either creating or
// updating it in a Kubernetes API server.
type APIUpdatingApplicator struct {
Expand Down
Loading

0 comments on commit 9a0fe90

Please sign in to comment.