@@ -137,11 +137,17 @@ func (t versionedTracker) update(gvr schema.GroupVersionResource, obj runtime.Ob
137
137
if err != nil {
138
138
return err
139
139
}
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 )
141
141
if err != nil {
142
142
return err
143
143
}
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
145
151
return nil
146
152
}
147
153
@@ -158,72 +164,94 @@ func (t versionedTracker) Patch(gvr schema.GroupVersionResource, obj runtime.Obj
158
164
return err
159
165
}
160
166
167
+ gvk , err := apiutil .GVKForObject (obj , t .scheme )
168
+ if err != nil {
169
+ return err
170
+ }
171
+
161
172
// We apply patches using a client-go reaction that ends up calling the trackers Patch. As we can't change
162
173
// that reaction, we use the callstack to figure out if this originated from the status client.
163
174
isStatus := bytes .Contains (debug .Stack (), []byte ("sigs.k8s.io/controller-runtime/pkg/client/fake.(*fakeSubResourceClient).statusPatch" ))
164
175
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 )
166
177
if err != nil {
167
178
return err
168
179
}
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
170
186
return nil
171
187
}
172
188
173
189
return t .upstream .Patch (gvr , obj , ns , patchOptions )
174
190
}
175
191
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 ) {
177
204
accessor , err := meta .Accessor (obj )
178
205
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 )
180
207
}
181
208
182
209
if accessor .GetName () == "" {
183
- gvk , _ := apiutil .GVKForObject (obj , t .scheme )
184
- return nil , apierrors .NewInvalid (
210
+ return nil , false , apierrors .NewInvalid (
185
211
gvk .GroupKind (),
186
212
accessor .GetName (),
187
213
field.ErrorList {field .Required (field .NewPath ("metadata.name" ), "name is required" )})
188
214
}
189
215
190
- gvk , err := apiutil .GVKForObject (obj , t .scheme )
191
- if err != nil {
192
- return nil , err
193
- }
194
-
195
216
oldObject , err := t .Get (gvr , ns , accessor .GetName ())
196
217
if err != nil {
197
218
// If the resource is not found and the resource allows create on update, issue a
198
219
// 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
201
229
}
202
- return nil , err
230
+ return obj , false , err
203
231
}
204
232
205
233
if t .withStatusSubresource .Has (gvk ) {
206
234
if isStatus { // copy everything but status and metadata.ResourceVersion from original object
207
235
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 )
209
237
}
210
238
passedRV := accessor .GetResourceVersion ()
211
239
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 )
213
241
}
214
242
accessor .SetResourceVersion (passedRV )
215
243
} else { // copy status from original object
216
244
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 )
218
246
}
219
247
}
220
248
} else if isStatus {
221
- return nil , apierrors .NewNotFound (gvr .GroupResource (), accessor .GetName ())
249
+ return nil , false , apierrors .NewNotFound (gvr .GroupResource (), accessor .GetName ())
222
250
}
223
251
224
252
oldAccessor , err := meta .Accessor (oldObject )
225
253
if err != nil {
226
- return nil , err
254
+ return nil , false , err
227
255
}
228
256
229
257
// If the new object does not have the resource version set and it allows unconditional update,
@@ -246,29 +274,48 @@ func (t versionedTracker) updateObject(gvr schema.GroupVersionResource, obj runt
246
274
}
247
275
248
276
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" ))
250
278
}
251
279
if oldAccessor .GetResourceVersion () == "" {
252
280
oldAccessor .SetResourceVersion ("0" )
253
281
}
254
282
intResourceVersion , err := strconv .ParseUint (oldAccessor .GetResourceVersion (), 10 , 64 )
255
283
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 )
257
285
}
258
286
intResourceVersion ++
259
287
accessor .SetResourceVersion (strconv .FormatUint (intResourceVersion , 10 ))
260
288
261
289
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 ())
263
291
}
264
292
265
293
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 })
267
295
}
268
- return convertFromUnstructuredIfNecessary (t .scheme , obj )
296
+
297
+ obj , err = convertFromUnstructuredIfNecessary (t .scheme , obj )
298
+ return obj , false , err
269
299
}
270
300
271
301
func (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 , _ , err = t .updateObject (gvr , gvk , applyConfiguration , ns , false , false , true , patchOptions .DryRun )
311
+ if err != nil {
312
+ return err
313
+ }
314
+
315
+ if applyConfiguration == nil { // Object was deleted in updateObject
316
+ return nil
317
+ }
318
+
272
319
return t .upstream .Apply (gvr , applyConfiguration , ns , opts ... )
273
320
}
274
321
0 commit comments