@@ -121,11 +121,17 @@ func (t versionedTracker) update(gvr schema.GroupVersionResource, obj runtime.Ob
121
121
if err != nil {
122
122
return err
123
123
}
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 )
125
125
if err != nil {
126
126
return err
127
127
}
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
129
135
return nil
130
136
}
131
137
@@ -142,72 +148,94 @@ func (t versionedTracker) Patch(gvr schema.GroupVersionResource, obj runtime.Obj
142
148
return err
143
149
}
144
150
151
+ gvk , err := apiutil .GVKForObject (obj , t .scheme )
152
+ if err != nil {
153
+ return err
154
+ }
155
+
145
156
// We apply patches using a client-go reaction that ends up calling the trackers Patch. As we can't change
146
157
// that reaction, we use the callstack to figure out if this originated from the status client.
147
158
isStatus := bytes .Contains (debug .Stack (), []byte ("sigs.k8s.io/controller-runtime/pkg/client/fake.(*fakeSubResourceClient).statusPatch" ))
148
159
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 )
150
161
if err != nil {
151
162
return err
152
163
}
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
154
170
return nil
155
171
}
156
172
157
173
return t .upstream .Patch (gvr , obj , ns , patchOptions )
158
174
}
159
175
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 ) {
161
188
accessor , err := meta .Accessor (obj )
162
189
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 )
164
191
}
165
192
166
193
if accessor .GetName () == "" {
167
- gvk , _ := apiutil .GVKForObject (obj , t .scheme )
168
- return nil , apierrors .NewInvalid (
194
+ return nil , false , apierrors .NewInvalid (
169
195
gvk .GroupKind (),
170
196
accessor .GetName (),
171
197
field.ErrorList {field .Required (field .NewPath ("metadata.name" ), "name is required" )})
172
198
}
173
199
174
- gvk , err := apiutil .GVKForObject (obj , t .scheme )
175
- if err != nil {
176
- return nil , err
177
- }
178
-
179
200
oldObject , err := t .Get (gvr , ns , accessor .GetName ())
180
201
if err != nil {
181
202
// If the resource is not found and the resource allows create on update, issue a
182
203
// 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
185
213
}
186
- return nil , err
214
+ return obj , false , err
187
215
}
188
216
189
217
if t .withStatusSubresource .Has (gvk ) {
190
218
if isStatus { // copy everything but status and metadata.ResourceVersion from original object
191
219
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 )
193
221
}
194
222
passedRV := accessor .GetResourceVersion ()
195
223
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 )
197
225
}
198
226
accessor .SetResourceVersion (passedRV )
199
227
} else { // copy status from original object
200
228
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 )
202
230
}
203
231
}
204
232
} else if isStatus {
205
- return nil , apierrors .NewNotFound (gvr .GroupResource (), accessor .GetName ())
233
+ return nil , false , apierrors .NewNotFound (gvr .GroupResource (), accessor .GetName ())
206
234
}
207
235
208
236
oldAccessor , err := meta .Accessor (oldObject )
209
237
if err != nil {
210
- return nil , err
238
+ return nil , false , err
211
239
}
212
240
213
241
// 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
230
258
}
231
259
232
260
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" ))
234
262
}
235
263
if oldAccessor .GetResourceVersion () == "" {
236
264
oldAccessor .SetResourceVersion ("0" )
237
265
}
238
266
intResourceVersion , err := strconv .ParseUint (oldAccessor .GetResourceVersion (), 10 , 64 )
239
267
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 )
241
269
}
242
270
intResourceVersion ++
243
271
accessor .SetResourceVersion (strconv .FormatUint (intResourceVersion , 10 ))
244
272
245
273
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 ())
247
275
}
248
276
249
277
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 })
251
279
}
252
- return convertFromUnstructuredIfNecessary (t .scheme , obj )
280
+
281
+ obj , err = convertFromUnstructuredIfNecessary (t .scheme , obj )
282
+ return obj , false , err
253
283
}
254
284
func (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
+
255
302
return t .upstream .Apply (gvr , applyConfiguration , ns , opts ... )
256
303
}
257
304
func (t versionedTracker ) Delete (gvr schema.GroupVersionResource , ns , name string , opts ... metav1.DeleteOptions ) error {
0 commit comments