Skip to content

Commit a645d31

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 a645d31

File tree

2 files changed

+45
-34
lines changed

2 files changed

+45
-34
lines changed

pkg/resource/api.go

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

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

2322
kerrors "k8s.io/apimachinery/pkg/api/errors"
2423
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2524
"k8s.io/apimachinery/pkg/runtime"
25+
"k8s.io/apimachinery/pkg/runtime/schema"
2626
"k8s.io/apimachinery/pkg/types"
2727
"sigs.k8s.io/controller-runtime/pkg/client"
2828

@@ -50,41 +50,54 @@ func NewAPIPatchingApplicator(c client.Client) *APIPatchingApplicator {
5050
// Apply changes to the supplied object. The object will be created if it does
5151
// not exist, or patched if it does. If the object does exist, it will only be
5252
// 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")
53+
func (a *APIPatchingApplicator) Apply(ctx context.Context, obj client.Object, ao ...ApplyOption) error {
54+
if obj.GetName() == "" && obj.GetGenerateName() != "" {
55+
return errors.Wrap(a.client.Create(ctx, obj), "cannot create object")
5756
}
5857

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)
58+
current := obj.DeepCopyObject().(client.Object)
59+
err := a.client.Get(ctx, types.NamespacedName{Name: obj.GetName(), Namespace: obj.GetNamespace()}, current)
6660
if kerrors.IsNotFound(err) {
6761
// TODO(negz): Apply ApplyOptions here too?
68-
return errors.Wrap(a.client.Create(ctx, o), "cannot create object")
62+
return errors.Wrap(a.client.Create(ctx, obj), "cannot create object")
6963
}
7064
if err != nil {
7165
return errors.Wrap(err, "cannot get object")
7266
}
7367

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

8086
// 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")
87+
return errors.Wrap(a.client.Patch(ctx, obj, client.MergeFromWithOptions(current, client.MergeFromWithOptimisticLock{})), "cannot patch object")
8288
}
8389

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) }
90+
func groupResource(c client.Client, o client.Object) (schema.GroupResource, error) {
91+
gvk, err := c.GroupVersionKindFor(o)
92+
if err != nil {
93+
return schema.GroupResource{}, errors.Wrapf(err, "cannot determine group version kind of %T", o)
94+
}
95+
m, err := c.RESTMapper().RESTMapping(gvk.GroupKind(), gvk.Version)
96+
if err != nil {
97+
return schema.GroupResource{}, errors.Wrapf(err, "cannot determine group resource of %v", gvk)
98+
}
99+
return m.Resource.GroupResource(), nil
100+
}
88101

89102
// An APIUpdatingApplicator applies changes to an object by either creating or
90103
// updating it in a Kubernetes API server.
@@ -94,40 +107,37 @@ type APIUpdatingApplicator struct {
94107

95108
// NewAPIUpdatingApplicator returns an Applicator that applies changes to an
96109
// object by either creating or updating it in a Kubernetes API server.
110+
//
111+
// Deprecated: Use NewAPIPatchingApplicator instead. The updating applicator
112+
// can lead to data-loss if the Golang types in this process are not up-to-date.
97113
func NewAPIUpdatingApplicator(c client.Client) *APIUpdatingApplicator {
98114
return &APIUpdatingApplicator{client: c}
99115
}
100116

101117
// Apply changes to the supplied object. The object will be created if it does
102118
// 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")
119+
func (a *APIUpdatingApplicator) Apply(ctx context.Context, obj client.Object, ao ...ApplyOption) error {
120+
if obj.GetName() == "" && obj.GetGenerateName() != "" {
121+
return errors.Wrap(a.client.Create(ctx, obj), "cannot create object")
111122
}
112123

113-
current := o.DeepCopyObject().(client.Object)
114-
115-
err := a.client.Get(ctx, types.NamespacedName{Name: m.GetName(), Namespace: m.GetNamespace()}, current)
124+
current := obj.DeepCopyObject().(client.Object)
125+
err := a.client.Get(ctx, types.NamespacedName{Name: obj.GetName(), Namespace: obj.GetNamespace()}, current)
116126
if kerrors.IsNotFound(err) {
117127
// TODO(negz): Apply ApplyOptions here too?
118-
return errors.Wrap(a.client.Create(ctx, m), "cannot create object")
128+
return errors.Wrap(a.client.Create(ctx, obj), "cannot create object")
119129
}
120130
if err != nil {
121131
return errors.Wrap(err, "cannot get object")
122132
}
123133

124134
for _, fn := range ao {
125-
if err := fn(ctx, current, m); err != nil {
135+
if err := fn(ctx, current, obj); err != nil {
126136
return err
127137
}
128138
}
129139

130-
return errors.Wrap(a.client.Update(ctx, m), "cannot update object")
140+
return errors.Wrap(a.client.Update(ctx, obj), "cannot update object")
131141
}
132142

133143
// 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)