Skip to content

Commit 048560f

Browse files
committed
pkg/resource: make patching applicator consistent and deprecate update applicator
Signed-off-by: Dr. Stefan Schimanski <[email protected]>
1 parent 671b65b commit 048560f

File tree

4 files changed

+388
-94
lines changed

4 files changed

+388
-94
lines changed

pkg/resource/api.go

Lines changed: 53 additions & 40 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

@@ -33,6 +31,12 @@ import (
3331
// Error strings.
3432
const (
3533
errUpdateObject = "cannot update object"
34+
35+
// taken from k8s.io/apiserver. Not crucial to match, but for uniformity it
36+
// better should.
37+
// TODO(sttts): import from k8s.io/apiserver/pkg/registry/generic/registry when
38+
// kube has updated otel dependencies post-1.28.
39+
errOptimisticLock = "the object has been modified; please apply your changes to the latest version and try again"
3640
)
3741

3842
// An APIPatchingApplicator applies changes to an object by either creating or
@@ -50,41 +54,53 @@ func NewAPIPatchingApplicator(c client.Client) *APIPatchingApplicator {
5054
// Apply changes to the supplied object. The object will be created if it does
5155
// not exist, or patched if it does. If the object does exist, it will only be
5256
// 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")
57-
}
58-
59-
if m.GetName() == "" && m.GetGenerateName() != "" {
60-
return errors.Wrap(a.client.Create(ctx, o), "cannot create object")
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
58+
if obj.GetName() == "" && obj.GetGenerateName() != "" {
59+
return a.client.Create(ctx, obj)
6160
}
6261

63-
desired := o.DeepCopyObject()
64-
65-
err := a.client.Get(ctx, types.NamespacedName{Name: m.GetName(), Namespace: m.GetNamespace()}, o)
62+
current := obj.DeepCopyObject().(client.Object)
63+
err := a.client.Get(ctx, types.NamespacedName{Name: obj.GetName(), Namespace: obj.GetNamespace()}, current)
6664
if kerrors.IsNotFound(err) {
6765
// TODO(negz): Apply ApplyOptions here too?
68-
return errors.Wrap(a.client.Create(ctx, o), "cannot create object")
66+
return a.client.Create(ctx, obj)
6967
}
7068
if err != nil {
71-
return errors.Wrap(err, "cannot get object")
69+
return err
7270
}
7371

74-
for _, fn := range ao {
75-
if err := fn(ctx, o, desired); err != nil {
72+
// Note: this check would ideally not be necessary if the Apply signature
73+
// had a current object that we could use for the diff. But we have no
74+
// current and for consistency of the patch it matters that the object we
75+
// get above is the one that was originally used.
76+
if obj.GetResourceVersion() != "" && obj.GetResourceVersion() != current.GetResourceVersion() {
77+
gvr, err := groupResource(a.client, obj)
78+
if err != nil {
7679
return err
7780
}
81+
return kerrors.NewConflict(gvr, current.GetName(), errors.New(errOptimisticLock))
7882
}
7983

80-
// 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")
82-
}
84+
for _, fn := range ao {
85+
if err := fn(ctx, current, obj); err != nil {
86+
return errors.Wrapf(err, "apply option failed for %s", HumanReadableReference(a.client, obj))
87+
}
88+
}
8389

84-
type patch struct{ from runtime.Object }
90+
return a.client.Patch(ctx, obj, client.MergeFromWithOptions(current, client.MergeFromWithOptimisticLock{}))
91+
}
8592

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

89105
// An APIUpdatingApplicator applies changes to an object by either creating or
90106
// updating it in a Kubernetes API server.
@@ -94,40 +110,37 @@ type APIUpdatingApplicator struct {
94110

95111
// NewAPIUpdatingApplicator returns an Applicator that applies changes to an
96112
// object by either creating or updating it in a Kubernetes API server.
113+
//
114+
// Deprecated: Use NewAPIPatchingApplicator instead. The updating applicator
115+
// can lead to data-loss if the Golang types in this process are not up-to-date.
97116
func NewAPIUpdatingApplicator(c client.Client) *APIUpdatingApplicator {
98117
return &APIUpdatingApplicator{client: c}
99118
}
100119

101120
// Apply changes to the supplied object. The object will be created if it does
102121
// 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")
122+
func (a *APIUpdatingApplicator) Apply(ctx context.Context, obj client.Object, ao ...ApplyOption) error {
123+
if obj.GetName() == "" && obj.GetGenerateName() != "" {
124+
return a.client.Create(ctx, obj)
107125
}
108126

109-
if m.GetName() == "" && m.GetGenerateName() != "" {
110-
return errors.Wrap(a.client.Create(ctx, o), "cannot create object")
111-
}
112-
113-
current := o.DeepCopyObject().(client.Object)
114-
115-
err := a.client.Get(ctx, types.NamespacedName{Name: m.GetName(), Namespace: m.GetNamespace()}, current)
127+
current := obj.DeepCopyObject().(client.Object)
128+
err := a.client.Get(ctx, types.NamespacedName{Name: obj.GetName(), Namespace: obj.GetNamespace()}, current)
116129
if kerrors.IsNotFound(err) {
117130
// TODO(negz): Apply ApplyOptions here too?
118-
return errors.Wrap(a.client.Create(ctx, m), "cannot create object")
131+
return a.client.Create(ctx, obj)
119132
}
120133
if err != nil {
121-
return errors.Wrap(err, "cannot get object")
134+
return err
122135
}
123136

124137
for _, fn := range ao {
125-
if err := fn(ctx, current, m); err != nil {
126-
return err
138+
if err := fn(ctx, current, obj); err != nil {
139+
return errors.Wrapf(err, "apply option failed for %s", HumanReadableReference(a.client, obj))
127140
}
128141
}
129142

130-
return errors.Wrap(a.client.Update(ctx, m), "cannot update object")
143+
return a.client.Update(ctx, obj)
131144
}
132145

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

0 commit comments

Comments
 (0)