@@ -18,11 +18,9 @@ package resource
1818
1919import (
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.
3432const (
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.
97116func 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