@@ -121,11 +121,17 @@ func (t versionedTracker) update(gvr schema.GroupVersionResource, obj runtime.Ob
121121 if err != nil {
122122 return err
123123 }
124- obj , err = t .updateObject (gvr , obj , ns , isStatus , deleting , opts .DryRun )
124+ obj , needsCreate , err : = t .updateObject (gvr , gvk , obj , ns , isStatus , deleting , allowsCreateOnUpdate ( gvk ) , opts .DryRun )
125125 if err != nil {
126126 return err
127127 }
128- if obj == nil {
128+
129+ if needsCreate {
130+ opts := metav1.CreateOptions {DryRun : opts .DryRun , FieldManager : opts .FieldManager }
131+ return t .Create (gvr , obj , ns , opts )
132+ }
133+
134+ if obj == nil { // Object was deleted in updateObject
129135 return nil
130136 }
131137
@@ -142,72 +148,94 @@ func (t versionedTracker) Patch(gvr schema.GroupVersionResource, obj runtime.Obj
142148 return err
143149 }
144150
151+ gvk , err := apiutil .GVKForObject (obj , t .scheme )
152+ if err != nil {
153+ return err
154+ }
155+
145156 // We apply patches using a client-go reaction that ends up calling the trackers Patch. As we can't change
146157 // that reaction, we use the callstack to figure out if this originated from the status client.
147158 isStatus := bytes .Contains (debug .Stack (), []byte ("sigs.k8s.io/controller-runtime/pkg/client/fake.(*fakeSubResourceClient).statusPatch" ))
148159
149- obj , err = t .updateObject (gvr , obj , ns , isStatus , false , patchOptions .DryRun )
160+ obj , needsCreate , err : = t .updateObject (gvr , gvk , obj , ns , isStatus , false , allowsCreateOnUpdate ( gvk ) , patchOptions .DryRun )
150161 if err != nil {
151162 return err
152163 }
153- if obj == nil {
164+ if needsCreate {
165+ opts := metav1.CreateOptions {DryRun : patchOptions .DryRun , FieldManager : patchOptions .FieldManager }
166+ return t .Create (gvr , obj , ns , opts )
167+ }
168+
169+ if obj == nil { // Object was deleted in updateObject
154170 return nil
155171 }
156172
157173 return t .upstream .Patch (gvr , obj , ns , patchOptions )
158174}
159175
160- func (t versionedTracker ) updateObject (gvr schema.GroupVersionResource , obj runtime.Object , ns string , isStatus , deleting bool , dryRun []string ) (runtime.Object , error ) {
176+ // updateObject performs a number of validations and changes before the
177+ // related to object updates, such as checking and updating the resourceVersion.
178+ func (t versionedTracker ) updateObject (
179+ gvr schema.GroupVersionResource ,
180+ gvk schema.GroupVersionKind ,
181+ obj runtime.Object ,
182+ ns string ,
183+ isStatus bool ,
184+ deleting bool ,
185+ allowCreateOnUpdate bool ,
186+ dryRun []string ,
187+ ) (result runtime.Object , needsCreate bool , _ error ) {
161188 accessor , err := meta .Accessor (obj )
162189 if err != nil {
163- return nil , fmt .Errorf ("failed to get accessor for object: %w" , err )
190+ return nil , false , fmt .Errorf ("failed to get accessor for object: %w" , err )
164191 }
165192
166193 if accessor .GetName () == "" {
167- gvk , _ := apiutil .GVKForObject (obj , t .scheme )
168- return nil , apierrors .NewInvalid (
194+ return nil , false , apierrors .NewInvalid (
169195 gvk .GroupKind (),
170196 accessor .GetName (),
171197 field.ErrorList {field .Required (field .NewPath ("metadata.name" ), "name is required" )})
172198 }
173199
174- gvk , err := apiutil .GVKForObject (obj , t .scheme )
175- if err != nil {
176- return nil , err
177- }
178-
179200 oldObject , err := t .Get (gvr , ns , accessor .GetName ())
180201 if err != nil {
181202 // If the resource is not found and the resource allows create on update, issue a
182203 // create instead.
183- if apierrors .IsNotFound (err ) && allowsCreateOnUpdate (gvk ) {
184- return nil , t .Create (gvr , obj , ns )
204+ if apierrors .IsNotFound (err ) && allowCreateOnUpdate {
205+ // Pass this info to the caller rather than create, because in the SSA case it
206+ // must be created by calling Apply in the upstream tracker, not Create.
207+ // This is because SSA considers Apply and Non-Apply operations to be different
208+ // even then they use the same fieldManager. This behavior is also observable
209+ // with a real Kubernetes apiserver.
210+ //
211+ // Ref https://kubernetes.slack.com/archives/C0EG7JC6T/p1757868204458989?thread_ts=1757808656.002569&cid=C0EG7JC6T
212+ return obj , true , nil
185213 }
186- return nil , err
214+ return obj , false , err
187215 }
188216
189217 if t .withStatusSubresource .Has (gvk ) {
190218 if isStatus { // copy everything but status and metadata.ResourceVersion from original object
191219 if err := copyStatusFrom (obj , oldObject ); err != nil {
192- return nil , fmt .Errorf ("failed to copy non-status field for object with status subresouce: %w" , err )
220+ return nil , false , fmt .Errorf ("failed to copy non-status field for object with status subresouce: %w" , err )
193221 }
194222 passedRV := accessor .GetResourceVersion ()
195223 if err := copyFrom (oldObject , obj ); err != nil {
196- return nil , fmt .Errorf ("failed to restore non-status fields: %w" , err )
224+ return nil , false , fmt .Errorf ("failed to restore non-status fields: %w" , err )
197225 }
198226 accessor .SetResourceVersion (passedRV )
199227 } else { // copy status from original object
200228 if err := copyStatusFrom (oldObject , obj ); err != nil {
201- return nil , fmt .Errorf ("failed to copy the status for object with status subresource: %w" , err )
229+ return nil , false , fmt .Errorf ("failed to copy the status for object with status subresource: %w" , err )
202230 }
203231 }
204232 } else if isStatus {
205- return nil , apierrors .NewNotFound (gvr .GroupResource (), accessor .GetName ())
233+ return nil , false , apierrors .NewNotFound (gvr .GroupResource (), accessor .GetName ())
206234 }
207235
208236 oldAccessor , err := meta .Accessor (oldObject )
209237 if err != nil {
210- return nil , err
238+ return nil , false , err
211239 }
212240
213241 // If the new object does not have the resource version set and it allows unconditional update,
@@ -230,28 +258,47 @@ func (t versionedTracker) updateObject(gvr schema.GroupVersionResource, obj runt
230258 }
231259
232260 if accessor .GetResourceVersion () != oldAccessor .GetResourceVersion () {
233- return nil , apierrors .NewConflict (gvr .GroupResource (), accessor .GetName (), errors .New ("object was modified" ))
261+ return nil , false , apierrors .NewConflict (gvr .GroupResource (), accessor .GetName (), errors .New ("object was modified" ))
234262 }
235263 if oldAccessor .GetResourceVersion () == "" {
236264 oldAccessor .SetResourceVersion ("0" )
237265 }
238266 intResourceVersion , err := strconv .ParseUint (oldAccessor .GetResourceVersion (), 10 , 64 )
239267 if err != nil {
240- return nil , fmt .Errorf ("can not convert resourceVersion %q to int: %w" , oldAccessor .GetResourceVersion (), err )
268+ return nil , false , fmt .Errorf ("can not convert resourceVersion %q to int: %w" , oldAccessor .GetResourceVersion (), err )
241269 }
242270 intResourceVersion ++
243271 accessor .SetResourceVersion (strconv .FormatUint (intResourceVersion , 10 ))
244272
245273 if ! deleting && ! deletionTimestampEqual (accessor , oldAccessor ) {
246- return nil , fmt .Errorf ("error: Unable to edit %s: metadata.deletionTimestamp field is immutable" , accessor .GetName ())
274+ return nil , false , fmt .Errorf ("error: Unable to edit %s: metadata.deletionTimestamp field is immutable" , accessor .GetName ())
247275 }
248276
249277 if ! accessor .GetDeletionTimestamp ().IsZero () && len (accessor .GetFinalizers ()) == 0 {
250- return nil , t .Delete (gvr , accessor .GetNamespace (), accessor .GetName (), metav1.DeleteOptions {DryRun : dryRun })
278+ return nil , false , t .Delete (gvr , accessor .GetNamespace (), accessor .GetName (), metav1.DeleteOptions {DryRun : dryRun })
251279 }
252- return convertFromUnstructuredIfNecessary (t .scheme , obj )
280+
281+ obj , err = convertFromUnstructuredIfNecessary (t .scheme , obj )
282+ return obj , false , err
253283}
254284func (t versionedTracker ) Apply (gvr schema.GroupVersionResource , applyConfiguration runtime.Object , ns string , opts ... metav1.PatchOptions ) error {
285+ patchOptions , err := getSingleOrZeroOptions (opts )
286+ if err != nil {
287+ return err
288+ }
289+ gvk , err := apiutil .GVKForObject (applyConfiguration , t .scheme )
290+ if err != nil {
291+ return err
292+ }
293+ applyConfiguration , _ , err = t .updateObject (gvr , gvk , applyConfiguration , ns , false , false , true , patchOptions .DryRun )
294+ if err != nil {
295+ return err
296+ }
297+
298+ if applyConfiguration == nil { // Object was deleted in updateObject
299+ return nil
300+ }
301+
255302 return t .upstream .Apply (gvr , applyConfiguration , ns , opts ... )
256303}
257304func (t versionedTracker ) Delete (gvr schema.GroupVersionResource , ns , name string , opts ... metav1.DeleteOptions ) error {
0 commit comments