Skip to content

Commit 03e4763

Browse files
authored
Merge pull request #926 from detiber/update
🐛 Fakeclient: Honor AllowUnconditionalUpdate and AllowCreateOnUpdate for resources that support it
2 parents 5ef5ed9 + 59a08e6 commit 03e4763

File tree

2 files changed

+227
-0
lines changed

2 files changed

+227
-0
lines changed

pkg/client/fake/client.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,20 +108,34 @@ func (t versionedTracker) Update(gvr schema.GroupVersionResource, obj runtime.Ob
108108
if err != nil {
109109
return fmt.Errorf("failed to get accessor for object: %v", err)
110110
}
111+
111112
if accessor.GetName() == "" {
112113
return apierrors.NewInvalid(
113114
obj.GetObjectKind().GroupVersionKind().GroupKind(),
114115
accessor.GetName(),
115116
field.ErrorList{field.Required(field.NewPath("metadata.name"), "name is required")})
116117
}
118+
117119
oldObject, err := t.ObjectTracker.Get(gvr, ns, accessor.GetName())
118120
if err != nil {
121+
// If the resource is not found and the resource allows create on update, issue a
122+
// create instead.
123+
if apierrors.IsNotFound(err) && allowsCreateOnUpdate(obj) {
124+
return t.Create(gvr, obj, ns)
125+
}
119126
return err
120127
}
128+
121129
oldAccessor, err := meta.Accessor(oldObject)
122130
if err != nil {
123131
return err
124132
}
133+
134+
// If the new object does not have the resource version set and it allows unconditional update,
135+
// default it to the resource version of the existing resource
136+
if accessor.GetResourceVersion() == "" && allowsUnconditionalUpdate(obj) {
137+
accessor.SetResourceVersion(oldAccessor.GetResourceVersion())
138+
}
125139
if accessor.GetResourceVersion() != oldAccessor.GetResourceVersion() {
126140
return apierrors.NewConflict(gvr.GroupResource(), accessor.GetName(), errors.New("object was modified"))
127141
}
@@ -416,3 +430,102 @@ func (sw *fakeStatusWriter) Patch(ctx context.Context, obj runtime.Object, patch
416430
// a way to update status field only.
417431
return sw.client.Patch(ctx, obj, patch, opts...)
418432
}
433+
434+
func allowsUnconditionalUpdate(obj runtime.Object) bool {
435+
gvk := obj.GetObjectKind().GroupVersionKind()
436+
switch gvk.Group {
437+
case "apps":
438+
switch gvk.Kind {
439+
case "ControllerRevision", "DaemonSet", "Deployment", "ReplicaSet", "StatefulSet":
440+
return true
441+
}
442+
case "autoscaling":
443+
switch gvk.Kind {
444+
case "HorizontalPodAutoscaler":
445+
return true
446+
}
447+
case "batch":
448+
switch gvk.Kind {
449+
case "CronJob", "Job":
450+
return true
451+
}
452+
case "certificates":
453+
switch gvk.Kind {
454+
case "Certificates":
455+
return true
456+
}
457+
case "flowcontrol":
458+
switch gvk.Kind {
459+
case "FlowSchema", "PriorityLevelConfiguration":
460+
return true
461+
}
462+
case "networking":
463+
switch gvk.Kind {
464+
case "Ingress", "IngressClass", "NetworkPolicy":
465+
return true
466+
}
467+
case "policy":
468+
switch gvk.Kind {
469+
case "PodSecurityPolicy":
470+
return true
471+
}
472+
case "rbac":
473+
switch gvk.Kind {
474+
case "ClusterRole", "ClusterRoleBinding", "Role", "RoleBinding":
475+
return true
476+
}
477+
case "scheduling":
478+
switch gvk.Kind {
479+
case "PriorityClass":
480+
return true
481+
}
482+
case "settings":
483+
switch gvk.Kind {
484+
case "PodPreset":
485+
return true
486+
}
487+
case "storage":
488+
switch gvk.Kind {
489+
case "StorageClass":
490+
return true
491+
}
492+
case "":
493+
switch gvk.Kind {
494+
case "ConfigMap", "Endpoint", "Event", "LimitRange", "Namespace", "Node",
495+
"PersistentVolume", "PersistentVolumeClaim", "Pod", "PodTemplate",
496+
"ReplicationController", "ResourceQuota", "Secret", "Service",
497+
"ServiceAccount", "EndpointSlice":
498+
return true
499+
}
500+
}
501+
502+
return false
503+
}
504+
505+
func allowsCreateOnUpdate(obj runtime.Object) bool {
506+
gvk := obj.GetObjectKind().GroupVersionKind()
507+
switch gvk.Group {
508+
case "coordination":
509+
switch gvk.Kind {
510+
case "Lease":
511+
return true
512+
}
513+
case "node":
514+
switch gvk.Kind {
515+
case "RuntimeClass":
516+
return true
517+
}
518+
case "rbac":
519+
switch gvk.Kind {
520+
case "ClusterRole", "ClusterRoleBinding", "Role", "RoleBinding":
521+
return true
522+
}
523+
case "":
524+
switch gvk.Kind {
525+
case "Endpoint", "Event", "LimitRange", "Service":
526+
return true
527+
}
528+
}
529+
530+
return false
531+
}

pkg/client/fake/client_test.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2626

2727
appsv1 "k8s.io/api/apps/v1"
28+
coordinationv1 "k8s.io/api/coordination/v1"
2829
corev1 "k8s.io/api/core/v1"
2930
"k8s.io/apimachinery/pkg/api/errors"
3031
apierrors "k8s.io/apimachinery/pkg/api/errors"
@@ -284,6 +285,118 @@ var _ = Describe("Fake client", func() {
284285
Expect(obj.ObjectMeta.ResourceVersion).To(Equal("1"))
285286
})
286287

288+
It("should allow updates with non-set ResourceVersion for a resource that allows unconditional updates", func() {
289+
By("Updating a new configmap")
290+
newcm := &corev1.ConfigMap{
291+
TypeMeta: metav1.TypeMeta{
292+
APIVersion: "v1",
293+
Kind: "ConfigMap",
294+
},
295+
ObjectMeta: metav1.ObjectMeta{
296+
Name: "test-cm",
297+
Namespace: "ns2",
298+
},
299+
Data: map[string]string{
300+
"test-key": "new-value",
301+
},
302+
}
303+
err := cl.Update(context.Background(), newcm)
304+
Expect(err).To(BeNil())
305+
306+
By("Getting the configmap")
307+
namespacedName := types.NamespacedName{
308+
Name: "test-cm",
309+
Namespace: "ns2",
310+
}
311+
obj := &corev1.ConfigMap{}
312+
err = cl.Get(context.Background(), namespacedName, obj)
313+
Expect(err).To(BeNil())
314+
Expect(obj).To(Equal(newcm))
315+
Expect(obj.ObjectMeta.ResourceVersion).To(Equal("1"))
316+
})
317+
318+
It("should reject updates with non-set ResourceVersion for a resource that doesn't allow unconditional updates", func() {
319+
By("Creating a new binding")
320+
binding := &corev1.Binding{
321+
TypeMeta: metav1.TypeMeta{
322+
APIVersion: "v1",
323+
Kind: "Binding",
324+
},
325+
ObjectMeta: metav1.ObjectMeta{
326+
Name: "test-binding",
327+
Namespace: "ns2",
328+
},
329+
Target: corev1.ObjectReference{
330+
Kind: "ConfigMap",
331+
APIVersion: "v1",
332+
Namespace: cm.Namespace,
333+
Name: cm.Name,
334+
},
335+
}
336+
Expect(cl.Create(context.Background(), binding)).To(Succeed())
337+
338+
By("Updating the binding with a new resource lacking resource version")
339+
newBinding := &corev1.Binding{
340+
TypeMeta: metav1.TypeMeta{
341+
APIVersion: "v1",
342+
Kind: "Binding",
343+
},
344+
ObjectMeta: metav1.ObjectMeta{
345+
Name: binding.Name,
346+
Namespace: binding.Namespace,
347+
},
348+
Target: corev1.ObjectReference{
349+
Namespace: binding.Namespace,
350+
Name: "blue",
351+
},
352+
}
353+
Expect(cl.Update(context.Background(), newBinding)).NotTo(Succeed())
354+
})
355+
356+
It("should allow create on update for a resource that allows create on update", func() {
357+
By("Creating a new lease with update")
358+
lease := &coordinationv1.Lease{
359+
TypeMeta: metav1.TypeMeta{
360+
APIVersion: "coordination.k8s.io/v1",
361+
Kind: "Lease",
362+
},
363+
ObjectMeta: metav1.ObjectMeta{
364+
Name: "test-lease",
365+
Namespace: "ns2",
366+
},
367+
Spec: coordinationv1.LeaseSpec{},
368+
}
369+
Expect(cl.Create(context.Background(), lease)).To(Succeed())
370+
371+
By("Getting the lease")
372+
namespacedName := types.NamespacedName{
373+
Name: lease.Name,
374+
Namespace: lease.Namespace,
375+
}
376+
obj := &coordinationv1.Lease{}
377+
Expect(cl.Get(context.Background(), namespacedName, obj)).To(Succeed())
378+
Expect(obj).To(Equal(lease))
379+
Expect(obj.ObjectMeta.ResourceVersion).To(Equal("1"))
380+
})
381+
382+
It("should reject create on update for a resource that does not allow create on update", func() {
383+
By("Attemping to create a new configmap with update")
384+
newcm := &corev1.ConfigMap{
385+
TypeMeta: metav1.TypeMeta{
386+
APIVersion: "v1",
387+
Kind: "ConfigMap",
388+
},
389+
ObjectMeta: metav1.ObjectMeta{
390+
Name: "different-test-cm",
391+
Namespace: "ns2",
392+
},
393+
Data: map[string]string{
394+
"test-key": "new-value",
395+
},
396+
}
397+
Expect(cl.Update(context.Background(), newcm)).NotTo(Succeed())
398+
})
399+
287400
It("should reject updates with non-matching ResourceVersion", func() {
288401
By("Updating a new configmap")
289402
newcm := &corev1.ConfigMap{
@@ -427,6 +540,7 @@ var _ = Describe("Fake client", func() {
427540
scheme := runtime.NewScheme()
428541
Expect(corev1.AddToScheme(scheme)).To(Succeed())
429542
Expect(appsv1.AddToScheme(scheme)).To(Succeed())
543+
Expect(coordinationv1.AddToScheme(scheme)).To(Succeed())
430544
cl = NewFakeClientWithScheme(scheme, dep, dep2, cm)
431545
close(done)
432546
})

0 commit comments

Comments
 (0)