Skip to content

Commit 8ddd288

Browse files
committed
pkg/resource: make patching applicator consistent and deprecate update applicator
Signed-off-by: Dr. Stefan Schimanski <[email protected]>
1 parent 05b5813 commit 8ddd288

File tree

2 files changed

+37
-20
lines changed

2 files changed

+37
-20
lines changed

pkg/resource/api.go

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

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

2322
corev1 "k8s.io/api/core/v1"
2423
kerrors "k8s.io/apimachinery/pkg/api/errors"
2524
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2625
"k8s.io/apimachinery/pkg/runtime"
26+
"k8s.io/apimachinery/pkg/runtime/schema"
2727
"k8s.io/apimachinery/pkg/types"
2828
"sigs.k8s.io/controller-runtime/pkg/client"
2929

@@ -121,41 +121,54 @@ func NewAPIPatchingApplicator(c client.Client) *APIPatchingApplicator {
121121
// Apply changes to the supplied object. The object will be created if it does
122122
// not exist, or patched if it does. If the object does exist, it will only be
123123
// patched if the passed object has the same or an empty resource version.
124-
func (a *APIPatchingApplicator) Apply(ctx context.Context, o client.Object, ao ...ApplyOption) error {
125-
m, ok := o.(metav1.Object)
126-
if !ok {
127-
return errors.New("cannot access object metadata")
124+
func (a *APIPatchingApplicator) Apply(ctx context.Context, obj client.Object, ao ...ApplyOption) error {
125+
if obj.GetName() == "" && obj.GetGenerateName() != "" {
126+
return errors.Wrap(a.client.Create(ctx, obj), "cannot create object")
128127
}
129128

130-
if m.GetName() == "" && m.GetGenerateName() != "" {
131-
return errors.Wrap(a.client.Create(ctx, o), "cannot create object")
132-
}
133-
134-
desired := o.DeepCopyObject()
135-
136-
err := a.client.Get(ctx, types.NamespacedName{Name: m.GetName(), Namespace: m.GetNamespace()}, o)
129+
current := obj.DeepCopyObject().(client.Object)
130+
err := a.client.Get(ctx, types.NamespacedName{Name: obj.GetName(), Namespace: obj.GetNamespace()}, current)
137131
if kerrors.IsNotFound(err) {
138132
// TODO(negz): Apply ApplyOptions here too?
139-
return errors.Wrap(a.client.Create(ctx, o), "cannot create object")
133+
return errors.Wrap(a.client.Create(ctx, obj), "cannot create object")
140134
}
141135
if err != nil {
142136
return errors.Wrap(err, "cannot get object")
143137
}
144138

139+
// Note: this check would ideally not be necessary if the Apply signature
140+
// had a current object that we could us for the diff. But we have no
141+
// current and for consistency of the patch it matter that the object we get
142+
// above is the one that was originally used.
143+
if obj.GetResourceVersion() != "" && obj.GetResourceVersion() != current.GetResourceVersion() {
144+
gvr, err := groupResource(a.client, obj)
145+
if err != nil {
146+
return err
147+
}
148+
return kerrors.NewConflict(gvr, current.GetName(), errors.New("resource version does not match"))
149+
}
150+
145151
for _, fn := range ao {
146-
if err := fn(ctx, o, desired); err != nil {
152+
if err := fn(ctx, current, obj); err != nil {
147153
return err
148154
}
149155
}
150156

151157
// TODO(negz): Allow callers to override the kind of patch used.
152-
return errors.Wrap(a.client.Patch(ctx, o, &patch{desired}), "cannot patch object")
158+
return errors.Wrap(a.client.Patch(ctx, obj, client.MergeFromWithOptions(current, client.MergeFromWithOptimisticLock{})), "cannot patch object")
153159
}
154160

155-
type patch struct{ from runtime.Object }
156-
157-
func (p *patch) Type() types.PatchType { return types.MergePatchType }
158-
func (p *patch) Data(_ client.Object) ([]byte, error) { return json.Marshal(p.from) }
161+
func groupResource(c client.Client, o client.Object) (schema.GroupResource, error) {
162+
gvk, err := c.GroupVersionKindFor(o)
163+
if err != nil {
164+
return schema.GroupResource{}, errors.Wrapf(err, "cannot determine group version kind of %T", o)
165+
}
166+
m, err := c.RESTMapper().RESTMapping(gvk.GroupKind(), gvk.Version)
167+
if err != nil {
168+
return schema.GroupResource{}, errors.Wrapf(err, "cannot determine group resource of %v", gvk)
169+
}
170+
return m.Resource.GroupResource(), nil
171+
}
159172

160173
// An APIUpdatingApplicator applies changes to an object by either creating or
161174
// updating it in a Kubernetes API server.
@@ -165,6 +178,9 @@ type APIUpdatingApplicator struct {
165178

166179
// NewAPIUpdatingApplicator returns an Applicator that applies changes to an
167180
// object by either creating or updating it in a Kubernetes API server.
181+
//
182+
// Deprecated: Use NewAPIPatchingApplicator instead. The updating applicator
183+
// can lead to data-loss if the Golang types in this process are not up-to-date.
168184
func NewAPIUpdatingApplicator(c client.Client) *APIUpdatingApplicator {
169185
return &APIUpdatingApplicator{client: c}
170186
}

pkg/resource/resource.go

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

234-
// An Applicator applies changes to an object.
234+
// An Applicator applies changes to an object. The passed object is expected to
235+
// be complete, i.e. not partial.
235236
type Applicator interface {
236237
Apply(context.Context, client.Object, ...ApplyOption) error
237238
}

0 commit comments

Comments
 (0)