Skip to content

Commit 94a1a05

Browse files
committed
pkg/resource: make patching applicator consistent and deprecate update applicator
Signed-off-by: Dr. Stefan Schimanski <[email protected]>
1 parent 64edbb0 commit 94a1a05

File tree

2 files changed

+45
-36
lines changed

2 files changed

+45
-36
lines changed

pkg/resource/api.go

Lines changed: 43 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,9 @@ package resource
1818

1919
import (
2020
"context"
21-
"encoding/json"
2221

2322
kerrors "k8s.io/apimachinery/pkg/api/errors"
24-
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
25-
"k8s.io/apimachinery/pkg/runtime"
23+
"k8s.io/apimachinery/pkg/runtime/schema"
2624
"k8s.io/apimachinery/pkg/types"
2725
"sigs.k8s.io/controller-runtime/pkg/client"
2826

@@ -50,41 +48,54 @@ func NewAPIPatchingApplicator(c client.Client) *APIPatchingApplicator {
5048
// Apply changes to the supplied object. The object will be created if it does
5149
// not exist, or patched if it does. If the object does exist, it will only be
5250
// patched if the passed object has the same or an empty resource version.
53-
func (a *APIPatchingApplicator) Apply(ctx context.Context, o client.Object, ao ...ApplyOption) error {
54-
m, ok := o.(metav1.Object)
55-
if !ok {
56-
return errors.New("cannot access object metadata")
51+
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
52+
if obj.GetName() == "" && obj.GetGenerateName() != "" {
53+
return errors.Wrap(a.client.Create(ctx, obj), "cannot create object")
5754
}
5855

59-
if m.GetName() == "" && m.GetGenerateName() != "" {
60-
return errors.Wrap(a.client.Create(ctx, o), "cannot create object")
61-
}
62-
63-
desired := o.DeepCopyObject()
64-
65-
err := a.client.Get(ctx, types.NamespacedName{Name: m.GetName(), Namespace: m.GetNamespace()}, o)
56+
current := obj.DeepCopyObject().(client.Object)
57+
err := a.client.Get(ctx, types.NamespacedName{Name: obj.GetName(), Namespace: obj.GetNamespace()}, current)
6658
if kerrors.IsNotFound(err) {
6759
// TODO(negz): Apply ApplyOptions here too?
68-
return errors.Wrap(a.client.Create(ctx, o), "cannot create object")
60+
return errors.Wrap(a.client.Create(ctx, obj), "cannot create object")
6961
}
7062
if err != nil {
7163
return errors.Wrap(err, "cannot get object")
7264
}
7365

66+
// Note: this check would ideally not be necessary if the Apply signature
67+
// had a current object that we could us for the diff. But we have no
68+
// current and for consistency of the patch it matters that the object we
69+
// get above is the one that was originally used.
70+
if obj.GetResourceVersion() != "" && obj.GetResourceVersion() != current.GetResourceVersion() {
71+
gvr, err := groupResource(a.client, obj)
72+
if err != nil {
73+
return err
74+
}
75+
return kerrors.NewConflict(gvr, current.GetName(), errors.New("resource version does not match"))
76+
}
77+
7478
for _, fn := range ao {
75-
if err := fn(ctx, o, desired); err != nil {
79+
if err := fn(ctx, current, obj); err != nil {
7680
return err
7781
}
7882
}
7983

8084
// TODO(negz): Allow callers to override the kind of patch used.
81-
return errors.Wrap(a.client.Patch(ctx, o, &patch{desired}), "cannot patch object")
85+
return errors.Wrap(a.client.Patch(ctx, obj, client.MergeFromWithOptions(current, client.MergeFromWithOptimisticLock{})), "cannot patch object")
8286
}
8387

84-
type patch struct{ from runtime.Object }
85-
86-
func (p *patch) Type() types.PatchType { return types.MergePatchType }
87-
func (p *patch) Data(_ client.Object) ([]byte, error) { return json.Marshal(p.from) }
88+
func groupResource(c client.Client, o client.Object) (schema.GroupResource, error) {
89+
gvk, err := c.GroupVersionKindFor(o)
90+
if err != nil {
91+
return schema.GroupResource{}, errors.Wrapf(err, "cannot determine group version kind of %T", o)
92+
}
93+
m, err := c.RESTMapper().RESTMapping(gvk.GroupKind(), gvk.Version)
94+
if err != nil {
95+
return schema.GroupResource{}, errors.Wrapf(err, "cannot determine group resource of %v", gvk)
96+
}
97+
return m.Resource.GroupResource(), nil
98+
}
8899

89100
// An APIUpdatingApplicator applies changes to an object by either creating or
90101
// updating it in a Kubernetes API server.
@@ -94,40 +105,37 @@ type APIUpdatingApplicator struct {
94105

95106
// NewAPIUpdatingApplicator returns an Applicator that applies changes to an
96107
// object by either creating or updating it in a Kubernetes API server.
108+
//
109+
// Deprecated: Use NewAPIPatchingApplicator instead. The updating applicator
110+
// can lead to data-loss if the Golang types in this process are not up-to-date.
97111
func NewAPIUpdatingApplicator(c client.Client) *APIUpdatingApplicator {
98112
return &APIUpdatingApplicator{client: c}
99113
}
100114

101115
// Apply changes to the supplied object. The object will be created if it does
102116
// not exist, or updated if it does.
103-
func (a *APIUpdatingApplicator) Apply(ctx context.Context, o client.Object, ao ...ApplyOption) error {
104-
m, ok := o.(Object)
105-
if !ok {
106-
return errors.New("cannot access object metadata")
107-
}
108-
109-
if m.GetName() == "" && m.GetGenerateName() != "" {
110-
return errors.Wrap(a.client.Create(ctx, o), "cannot create object")
117+
func (a *APIUpdatingApplicator) Apply(ctx context.Context, obj client.Object, ao ...ApplyOption) error {
118+
if obj.GetName() == "" && obj.GetGenerateName() != "" {
119+
return errors.Wrap(a.client.Create(ctx, obj), "cannot create object")
111120
}
112121

113-
current := o.DeepCopyObject().(client.Object)
114-
115-
err := a.client.Get(ctx, types.NamespacedName{Name: m.GetName(), Namespace: m.GetNamespace()}, current)
122+
current := obj.DeepCopyObject().(client.Object)
123+
err := a.client.Get(ctx, types.NamespacedName{Name: obj.GetName(), Namespace: obj.GetNamespace()}, current)
116124
if kerrors.IsNotFound(err) {
117125
// TODO(negz): Apply ApplyOptions here too?
118-
return errors.Wrap(a.client.Create(ctx, m), "cannot create object")
126+
return errors.Wrap(a.client.Create(ctx, obj), "cannot create object")
119127
}
120128
if err != nil {
121129
return errors.Wrap(err, "cannot get object")
122130
}
123131

124132
for _, fn := range ao {
125-
if err := fn(ctx, current, m); err != nil {
133+
if err := fn(ctx, current, obj); err != nil {
126134
return err
127135
}
128136
}
129137

130-
return errors.Wrap(a.client.Update(ctx, m), "cannot update object")
138+
return errors.Wrap(a.client.Update(ctx, obj), "cannot update object")
131139
}
132140

133141
// An APIFinalizer adds and removes finalizers to and from a resource.

pkg/resource/resource.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,8 @@ func IsConditionTrue(c xpv1.Condition) bool {
202202
return c.Status == corev1.ConditionTrue
203203
}
204204

205-
// An Applicator applies changes to an object.
205+
// An Applicator applies changes to an object. The passed object is expected to
206+
// be complete, i.e. not partial.
206207
type Applicator interface {
207208
Apply(context.Context, client.Object, ...ApplyOption) error
208209
}

0 commit comments

Comments
 (0)