@@ -137,11 +137,17 @@ func (t versionedTracker) update(gvr schema.GroupVersionResource, obj runtime.Ob
137137 if err != nil {
138138 return err
139139 }
140- obj , err = t .updateObject (gvr , obj , ns , isStatus , deleting , opts .DryRun )
140+ obj , needsCreate , err : = t .updateObject (gvr , gvk , obj , ns , isStatus , deleting , allowsCreateOnUpdate ( gvk ) , opts .DryRun )
141141 if err != nil {
142142 return err
143143 }
144- if obj == nil {
144+
145+ if needsCreate {
146+ opts := metav1.CreateOptions {DryRun : opts .DryRun , FieldManager : opts .FieldManager }
147+ return t .Create (gvr , obj , ns , opts )
148+ }
149+
150+ if obj == nil { // Object was deleted in updateObject
145151 return nil
146152 }
147153
@@ -158,72 +164,94 @@ func (t versionedTracker) Patch(gvr schema.GroupVersionResource, obj runtime.Obj
158164 return err
159165 }
160166
167+ gvk , err := apiutil .GVKForObject (obj , t .scheme )
168+ if err != nil {
169+ return err
170+ }
171+
161172 // We apply patches using a client-go reaction that ends up calling the trackers Patch. As we can't change
162173 // that reaction, we use the callstack to figure out if this originated from the status client.
163174 isStatus := bytes .Contains (debug .Stack (), []byte ("sigs.k8s.io/controller-runtime/pkg/client/fake.(*fakeSubResourceClient).statusPatch" ))
164175
165- obj , err = t .updateObject (gvr , obj , ns , isStatus , false , patchOptions .DryRun )
176+ obj , needsCreate , err : = t .updateObject (gvr , gvk , obj , ns , isStatus , false , allowsCreateOnUpdate ( gvk ) , patchOptions .DryRun )
166177 if err != nil {
167178 return err
168179 }
169- if obj == nil {
180+ if needsCreate {
181+ opts := metav1.CreateOptions {DryRun : patchOptions .DryRun , FieldManager : patchOptions .FieldManager }
182+ return t .Create (gvr , obj , ns , opts )
183+ }
184+
185+ if obj == nil { // Object was deleted in updateObject
170186 return nil
171187 }
172188
173189 return t .upstream .Patch (gvr , obj , ns , patchOptions )
174190}
175191
176- func (t versionedTracker ) updateObject (gvr schema.GroupVersionResource , obj runtime.Object , ns string , isStatus , deleting bool , dryRun []string ) (runtime.Object , error ) {
192+ // updateObject performs a number of validations and changes before the
193+ // related to object updates, such as checking and updating the resourceVersion.
194+ func (t versionedTracker ) updateObject (
195+ gvr schema.GroupVersionResource ,
196+ gvk schema.GroupVersionKind ,
197+ obj runtime.Object ,
198+ ns string ,
199+ isStatus bool ,
200+ deleting bool ,
201+ allowCreateOnUpdate bool ,
202+ dryRun []string ,
203+ ) (result runtime.Object , needsCreate bool , _ error ) {
177204 accessor , err := meta .Accessor (obj )
178205 if err != nil {
179- return nil , fmt .Errorf ("failed to get accessor for object: %w" , err )
206+ return nil , false , fmt .Errorf ("failed to get accessor for object: %w" , err )
180207 }
181208
182209 if accessor .GetName () == "" {
183- gvk , _ := apiutil .GVKForObject (obj , t .scheme )
184- return nil , apierrors .NewInvalid (
210+ return nil , false , apierrors .NewInvalid (
185211 gvk .GroupKind (),
186212 accessor .GetName (),
187213 field.ErrorList {field .Required (field .NewPath ("metadata.name" ), "name is required" )})
188214 }
189215
190- gvk , err := apiutil .GVKForObject (obj , t .scheme )
191- if err != nil {
192- return nil , err
193- }
194-
195216 oldObject , err := t .Get (gvr , ns , accessor .GetName ())
196217 if err != nil {
197218 // If the resource is not found and the resource allows create on update, issue a
198219 // create instead.
199- if apierrors .IsNotFound (err ) && allowsCreateOnUpdate (gvk ) {
200- return nil , t .Create (gvr , obj , ns )
220+ if apierrors .IsNotFound (err ) && allowCreateOnUpdate {
221+ // Pass this info to the caller rather than create, because in the SSA case it
222+ // must be created by calling Apply in the upstream tracker, not Create.
223+ // This is because SSA considers Apply and Non-Apply operations to be different
224+ // even then they use the same fieldManager. This behavior is also observable
225+ // with a real Kubernetes apiserver.
226+ //
227+ // Ref https://kubernetes.slack.com/archives/C0EG7JC6T/p1757868204458989?thread_ts=1757808656.002569&cid=C0EG7JC6T
228+ return obj , true , nil
201229 }
202- return nil , err
230+ return obj , false , err
203231 }
204232
205233 if t .withStatusSubresource .Has (gvk ) {
206234 if isStatus { // copy everything but status and metadata.ResourceVersion from original object
207235 if err := copyStatusFrom (obj , oldObject ); err != nil {
208- return nil , fmt .Errorf ("failed to copy non-status field for object with status subresouce: %w" , err )
236+ return nil , false , fmt .Errorf ("failed to copy non-status field for object with status subresouce: %w" , err )
209237 }
210238 passedRV := accessor .GetResourceVersion ()
211239 if err := copyFrom (oldObject , obj ); err != nil {
212- return nil , fmt .Errorf ("failed to restore non-status fields: %w" , err )
240+ return nil , false , fmt .Errorf ("failed to restore non-status fields: %w" , err )
213241 }
214242 accessor .SetResourceVersion (passedRV )
215243 } else { // copy status from original object
216244 if err := copyStatusFrom (oldObject , obj ); err != nil {
217- return nil , fmt .Errorf ("failed to copy the status for object with status subresource: %w" , err )
245+ return nil , false , fmt .Errorf ("failed to copy the status for object with status subresource: %w" , err )
218246 }
219247 }
220248 } else if isStatus {
221- return nil , apierrors .NewNotFound (gvr .GroupResource (), accessor .GetName ())
249+ return nil , false , apierrors .NewNotFound (gvr .GroupResource (), accessor .GetName ())
222250 }
223251
224252 oldAccessor , err := meta .Accessor (oldObject )
225253 if err != nil {
226- return nil , err
254+ return nil , false , err
227255 }
228256
229257 // If the new object does not have the resource version set and it allows unconditional update,
@@ -246,29 +274,54 @@ func (t versionedTracker) updateObject(gvr schema.GroupVersionResource, obj runt
246274 }
247275
248276 if accessor .GetResourceVersion () != oldAccessor .GetResourceVersion () {
249- return nil , apierrors .NewConflict (gvr .GroupResource (), accessor .GetName (), errors .New ("object was modified" ))
277+ return nil , false , apierrors .NewConflict (gvr .GroupResource (), accessor .GetName (), errors .New ("object was modified" ))
250278 }
251279 if oldAccessor .GetResourceVersion () == "" {
252280 oldAccessor .SetResourceVersion ("0" )
253281 }
254282 intResourceVersion , err := strconv .ParseUint (oldAccessor .GetResourceVersion (), 10 , 64 )
255283 if err != nil {
256- return nil , fmt .Errorf ("can not convert resourceVersion %q to int: %w" , oldAccessor .GetResourceVersion (), err )
284+ return nil , false , fmt .Errorf ("can not convert resourceVersion %q to int: %w" , oldAccessor .GetResourceVersion (), err )
257285 }
258286 intResourceVersion ++
259287 accessor .SetResourceVersion (strconv .FormatUint (intResourceVersion , 10 ))
260288
261289 if ! deleting && ! deletionTimestampEqual (accessor , oldAccessor ) {
262- return nil , fmt .Errorf ("error: Unable to edit %s: metadata.deletionTimestamp field is immutable" , accessor .GetName ())
290+ return nil , false , fmt .Errorf ("error: Unable to edit %s: metadata.deletionTimestamp field is immutable" , accessor .GetName ())
263291 }
264292
265293 if ! accessor .GetDeletionTimestamp ().IsZero () && len (accessor .GetFinalizers ()) == 0 {
266- return nil , t .Delete (gvr , accessor .GetNamespace (), accessor .GetName (), metav1.DeleteOptions {DryRun : dryRun })
294+ return nil , false , t .Delete (gvr , accessor .GetNamespace (), accessor .GetName (), metav1.DeleteOptions {DryRun : dryRun })
267295 }
268- return convertFromUnstructuredIfNecessary (t .scheme , obj )
296+
297+ obj , err = convertFromUnstructuredIfNecessary (t .scheme , obj )
298+ return obj , false , err
269299}
270300
271301func (t versionedTracker ) Apply (gvr schema.GroupVersionResource , applyConfiguration runtime.Object , ns string , opts ... metav1.PatchOptions ) error {
302+ patchOptions , err := getSingleOrZeroOptions (opts )
303+ if err != nil {
304+ return err
305+ }
306+ gvk , err := apiutil .GVKForObject (applyConfiguration , t .scheme )
307+ if err != nil {
308+ return err
309+ }
310+ applyConfiguration , needsCreate , err := t .updateObject (gvr , gvk , applyConfiguration , ns , false , false , true , patchOptions .DryRun )
311+ if err != nil {
312+ return err
313+ }
314+
315+ if needsCreate && t .withStatusSubresource .Has (gvk ) {
316+ if err := copyStatusFrom (& unstructured.Unstructured {}, applyConfiguration ); err != nil {
317+ return err
318+ }
319+ }
320+
321+ if applyConfiguration == nil { // Object was deleted in updateObject
322+ return nil
323+ }
324+
272325 return t .upstream .Apply (gvr , applyConfiguration , ns , opts ... )
273326}
274327
0 commit comments