Skip to content

Commit ea6cd8c

Browse files
committed
⚠️ Fakeclient: Drop status changes in Update/Patch if configured
This changes makes the Update and Patch method drop the Status potion of the object if it is an in-tree object that has a status subresouce or was configured as having one. This replicas the behavior observed when using an actual Kubernetes API. This change might break tests due to the fact that it configures the in-tree resources that have a status subresoruce as such. If that happens however that likely means the test was passing incorrectly.
1 parent 5db1738 commit ea6cd8c

File tree

2 files changed

+201
-23
lines changed

2 files changed

+201
-23
lines changed

pkg/client/fake/client.go

Lines changed: 132 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import (
3535
"k8s.io/apimachinery/pkg/runtime"
3636
"k8s.io/apimachinery/pkg/runtime/schema"
3737
utilrand "k8s.io/apimachinery/pkg/util/rand"
38+
"k8s.io/apimachinery/pkg/util/sets"
3839
"k8s.io/apimachinery/pkg/util/validation/field"
3940
"k8s.io/apimachinery/pkg/watch"
4041
"k8s.io/client-go/kubernetes/scheme"
@@ -48,13 +49,15 @@ import (
4849

4950
type versionedTracker struct {
5051
testing.ObjectTracker
51-
scheme *runtime.Scheme
52+
scheme *runtime.Scheme
53+
withStatusSubresource sets.Set[schema.GroupVersionKind]
5254
}
5355

5456
type fakeClient struct {
55-
tracker versionedTracker
56-
scheme *runtime.Scheme
57-
restMapper meta.RESTMapper
57+
tracker versionedTracker
58+
scheme *runtime.Scheme
59+
restMapper meta.RESTMapper
60+
withStatusSubresource sets.Set[schema.GroupVersionKind]
5861

5962
// indexes maps each GroupVersionKind (GVK) to the indexes registered for that GVK.
6063
// The inner map maps from index name to IndexerFunc.
@@ -95,12 +98,13 @@ func NewClientBuilder() *ClientBuilder {
9598

9699
// ClientBuilder builds a fake client.
97100
type ClientBuilder struct {
98-
scheme *runtime.Scheme
99-
restMapper meta.RESTMapper
100-
initObject []client.Object
101-
initLists []client.ObjectList
102-
initRuntimeObjects []runtime.Object
103-
objectTracker testing.ObjectTracker
101+
scheme *runtime.Scheme
102+
restMapper meta.RESTMapper
103+
initObject []client.Object
104+
initLists []client.ObjectList
105+
initRuntimeObjects []runtime.Object
106+
withStatusSubresource []client.Object
107+
objectTracker testing.ObjectTracker
104108

105109
// indexes maps each GroupVersionKind (GVK) to the indexes registered for that GVK.
106110
// The inner map maps from index name to IndexerFunc.
@@ -185,6 +189,13 @@ func (f *ClientBuilder) WithIndex(obj runtime.Object, field string, extractValue
185189
return f
186190
}
187191

192+
// WithStatusSubresource configures the passed object with a status subresource, which means
193+
// calls to Update and Patch will not alters its status.
194+
func (f *ClientBuilder) WithStatusSubresource(o ...client.Object) *ClientBuilder {
195+
f.withStatusSubresource = append(f.withStatusSubresource, o...)
196+
return f
197+
}
198+
188199
// Build builds and returns a new fake client.
189200
func (f *ClientBuilder) Build() client.WithWatch {
190201
if f.scheme == nil {
@@ -196,10 +207,19 @@ func (f *ClientBuilder) Build() client.WithWatch {
196207

197208
var tracker versionedTracker
198209

210+
withStatusSubResource := sets.New(inTreeResourcesWithStatus()...)
211+
for _, o := range f.withStatusSubresource {
212+
gvk, err := apiutil.GVKForObject(o, f.scheme)
213+
if err != nil {
214+
panic(fmt.Errorf("failed to get gvk for object %T: %w", withStatusSubResource, err))
215+
}
216+
withStatusSubResource.Insert(gvk)
217+
}
218+
199219
if f.objectTracker == nil {
200-
tracker = versionedTracker{ObjectTracker: testing.NewObjectTracker(f.scheme, scheme.Codecs.UniversalDecoder()), scheme: f.scheme}
220+
tracker = versionedTracker{ObjectTracker: testing.NewObjectTracker(f.scheme, scheme.Codecs.UniversalDecoder()), scheme: f.scheme, withStatusSubresource: withStatusSubResource}
201221
} else {
202-
tracker = versionedTracker{ObjectTracker: f.objectTracker, scheme: f.scheme}
222+
tracker = versionedTracker{ObjectTracker: f.objectTracker, scheme: f.scheme, withStatusSubresource: withStatusSubResource}
203223
}
204224

205225
for _, obj := range f.initObject {
@@ -217,11 +237,13 @@ func (f *ClientBuilder) Build() client.WithWatch {
217237
panic(fmt.Errorf("failed to add runtime object %v to fake client: %w", obj, err))
218238
}
219239
}
240+
220241
return &fakeClient{
221-
tracker: tracker,
222-
scheme: f.scheme,
223-
restMapper: f.restMapper,
224-
indexes: f.indexes,
242+
tracker: tracker,
243+
scheme: f.scheme,
244+
restMapper: f.restMapper,
245+
indexes: f.indexes,
246+
withStatusSubresource: withStatusSubResource,
225247
}
226248
}
227249

@@ -318,6 +340,10 @@ func convertFromUnstructuredIfNecessary(s *runtime.Scheme, o runtime.Object) (ru
318340
}
319341

320342
func (t versionedTracker) Update(gvr schema.GroupVersionResource, obj runtime.Object, ns string) error {
343+
return t.update(gvr, obj, ns, false)
344+
}
345+
346+
func (t versionedTracker) update(gvr schema.GroupVersionResource, obj runtime.Object, ns string, isStatus bool) error {
321347
accessor, err := meta.Accessor(obj)
322348
if err != nil {
323349
return fmt.Errorf("failed to get accessor for object: %w", err)
@@ -337,6 +363,9 @@ func (t versionedTracker) Update(gvr schema.GroupVersionResource, obj runtime.Ob
337363
return err
338364
}
339365
}
366+
if !isStatus && t.withStatusSubresource.Has(gvk) {
367+
clearObjectStatus(obj)
368+
}
340369

341370
oldObject, err := t.ObjectTracker.Get(gvr, ns, accessor.GetName())
342371
if err != nil {
@@ -689,6 +718,10 @@ func (c *fakeClient) DeleteAllOf(ctx context.Context, obj client.Object, opts ..
689718
}
690719

691720
func (c *fakeClient) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error {
721+
return c.update(ctx, obj, false, opts...)
722+
}
723+
724+
func (c *fakeClient) update(ctx context.Context, obj client.Object, isStatus bool, opts ...client.UpdateOption) error {
692725
updateOptions := &client.UpdateOptions{}
693726
updateOptions.ApplyOptions(opts)
694727

@@ -706,10 +739,14 @@ func (c *fakeClient) Update(ctx context.Context, obj client.Object, opts ...clie
706739
if err != nil {
707740
return err
708741
}
709-
return c.tracker.Update(gvr, obj, accessor.GetNamespace())
742+
return c.tracker.update(gvr, obj, accessor.GetNamespace(), isStatus)
710743
}
711744

712745
func (c *fakeClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error {
746+
return c.patch(ctx, obj, patch, false, opts...)
747+
}
748+
749+
func (c *fakeClient) patch(ctx context.Context, obj client.Object, patch client.Patch, isStatus bool, opts ...client.PatchOption) error {
713750
patchOptions := &client.PatchOptions{}
714751
patchOptions.ApplyOptions(opts)
715752

@@ -732,6 +769,18 @@ func (c *fakeClient) Patch(ctx context.Context, obj client.Object, patch client.
732769
return err
733770
}
734771

772+
gvk, err := apiutil.GVKForObject(obj, c.scheme)
773+
if err != nil {
774+
return err
775+
}
776+
777+
if !isStatus && c.withStatusSubresource.Has(gvk) {
778+
data, err = clearStatus(data)
779+
if err != nil {
780+
return fmt.Errorf("failed to clear status: %w", err)
781+
}
782+
}
783+
735784
reaction := testing.ObjectReaction(c.tracker)
736785
handled, o, err := reaction(testing.NewPatchAction(gvr, accessor.GetNamespace(), accessor.GetName(), patch.Type(), data))
737786
if err != nil {
@@ -741,10 +790,6 @@ func (c *fakeClient) Patch(ctx context.Context, obj client.Object, patch client.
741790
panic("tracker could not handle patch method")
742791
}
743792

744-
gvk, err := apiutil.GVKForObject(obj, c.scheme)
745-
if err != nil {
746-
return err
747-
}
748793
ta, err := meta.TypeAccessor(o)
749794
if err != nil {
750795
return err
@@ -762,6 +807,34 @@ func (c *fakeClient) Patch(ctx context.Context, obj client.Object, patch client.
762807
return err
763808
}
764809

810+
func clearObjectStatus(o runtime.Object) {
811+
if unstructured, isUnstructured := o.(*unstructured.Unstructured); isUnstructured {
812+
delete(unstructured.Object, "status")
813+
return
814+
}
815+
tp := reflect.TypeOf(o)
816+
value := reflect.ValueOf(o)
817+
if tp.Kind() == reflect.Pointer {
818+
tp = tp.Elem()
819+
value = value.Elem()
820+
}
821+
statusType, found := tp.FieldByName("Status")
822+
if !found {
823+
return
824+
}
825+
value.FieldByName("Status").Set(reflect.New(statusType.Type).Elem())
826+
}
827+
828+
func clearStatus(in []byte) ([]byte, error) {
829+
m := map[string]any{}
830+
if err := json.Unmarshal(in, &m); err != nil {
831+
return nil, err
832+
}
833+
delete(m, "status")
834+
835+
return json.Marshal(m)
836+
}
837+
765838
func (c *fakeClient) Status() client.SubResourceWriter {
766839
return c.SubResource("status")
767840
}
@@ -818,7 +891,7 @@ func (sw *fakeSubResourceClient) Update(ctx context.Context, obj client.Object,
818891
if updateOptions.SubResourceBody != nil {
819892
body = updateOptions.SubResourceBody
820893
}
821-
return sw.client.Update(ctx, body, &updateOptions.UpdateOptions)
894+
return sw.client.update(ctx, body, true, &updateOptions.UpdateOptions)
822895
}
823896

824897
func (sw *fakeSubResourceClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.SubResourcePatchOption) error {
@@ -833,7 +906,7 @@ func (sw *fakeSubResourceClient) Patch(ctx context.Context, obj client.Object, p
833906
body = patchOptions.SubResourceBody
834907
}
835908

836-
return sw.client.Patch(ctx, body, patch, &patchOptions.PatchOptions)
909+
return sw.client.patch(ctx, body, patch, true, &patchOptions.PatchOptions)
837910
}
838911

839912
func allowsUnconditionalUpdate(gvk schema.GroupVersionKind) bool {
@@ -933,6 +1006,42 @@ func allowsCreateOnUpdate(gvk schema.GroupVersionKind) bool {
9331006
return false
9341007
}
9351008

1009+
func inTreeResourcesWithStatus() []schema.GroupVersionKind {
1010+
return []schema.GroupVersionKind{
1011+
{Version: "v1", Kind: "Namespace"},
1012+
{Version: "v1", Kind: "Node"},
1013+
{Version: "v1", Kind: "PersistentVolumeClaim"},
1014+
{Version: "v1", Kind: "PersistentVolume"},
1015+
{Version: "v1", Kind: "Pod"},
1016+
{Version: "v1", Kind: "ReplicationController"},
1017+
{Version: "v1", Kind: "Service"},
1018+
1019+
{Group: "apps", Version: "v1", Kind: "Deployment"},
1020+
{Group: "apps", Version: "v1", Kind: "DaemonSet"},
1021+
{Group: "apps", Version: "v1", Kind: "ReplicaSet"},
1022+
{Group: "apps", Version: "v1", Kind: "StatefulSet"},
1023+
1024+
{Group: "autoscaling", Version: "v1", Kind: "HorizontalPodAutoscaler"},
1025+
1026+
{Group: "batch", Version: "v1", Kind: "CronJob"},
1027+
{Group: "batch", Version: "v1", Kind: "Job"},
1028+
1029+
{Group: "certificates.k8s.io", Version: "v1", Kind: "CertificateSigningRequest"},
1030+
1031+
{Group: "networking.k8s.io", Version: "v1", Kind: "Ingress"},
1032+
{Group: "networking.k8s.io", Version: "v1", Kind: "NetworkPolicy"},
1033+
1034+
{Group: "policy", Version: "v1", Kind: "PodDisruptionBudget"},
1035+
1036+
{Group: "storage.k8s.io", Version: "v1", Kind: "VolumeAttachment"},
1037+
1038+
{Group: "apiextensions.k8s.io", Version: "v1", Kind: "CustomResourceDefinition"},
1039+
1040+
{Group: "flowcontrol.apiserver.k8s.io", Version: "v1beta2", Kind: "FlowSchema"},
1041+
{Group: "flowcontrol.apiserver.k8s.io", Version: "v1beta2", Kind: "PriorityLevelConfiguration"},
1042+
}
1043+
}
1044+
9361045
// zero zeros the value of a pointer.
9371046
func zero(x interface{}) {
9381047
if x == nil {

pkg/client/fake/client_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -972,6 +972,38 @@ var _ = Describe("Fake client", func() {
972972
Expect(err).To(BeNil())
973973
Expect(len(newObj.Finalizers)).To(Equal(0))
974974
})
975+
976+
It("should not change the status of objects that have a status subresource on update", func() {
977+
obj := &corev1.Node{
978+
ObjectMeta: metav1.ObjectMeta{
979+
Name: "node",
980+
},
981+
}
982+
Expect(cl.Create(context.Background(), obj)).To(BeNil())
983+
984+
obj.Status.Phase = corev1.NodeRunning
985+
Expect(cl.Update(context.Background(), obj)).To(BeNil())
986+
987+
Expect(cl.Get(context.Background(), client.ObjectKeyFromObject(obj), obj)).To(BeNil())
988+
989+
Expect(obj.Status).To(BeEquivalentTo(corev1.NodeStatus{}))
990+
})
991+
It("should not change the status of objects that have a status subresource on patch", func() {
992+
obj := &corev1.Node{
993+
ObjectMeta: metav1.ObjectMeta{
994+
Name: "node",
995+
},
996+
}
997+
Expect(cl.Create(context.Background(), obj)).To(BeNil())
998+
original := obj.DeepCopy()
999+
1000+
obj.Status.Phase = corev1.NodeRunning
1001+
Expect(cl.Patch(context.Background(), obj, client.MergeFrom(original))).To(BeNil())
1002+
1003+
Expect(cl.Get(context.Background(), client.ObjectKeyFromObject(obj), obj)).To(BeNil())
1004+
1005+
Expect(obj.Status).To(BeEquivalentTo(corev1.NodeStatus{}))
1006+
})
9751007
}
9761008

9771009
Context("with default scheme.Scheme", func() {
@@ -1220,6 +1252,43 @@ var _ = Describe("Fake client", func() {
12201252
Expect(err).To(BeNil())
12211253
Expect(obj).To(Equal(dep3))
12221254
})
1255+
1256+
It("should not change the status of objects that are configured to have a status subresource on update", func() {
1257+
obj := &unstructured.Unstructured{}
1258+
obj.SetAPIVersion("foo/v1")
1259+
obj.SetKind("Foo")
1260+
obj.SetName("a-foo")
1261+
cl := NewClientBuilder().WithStatusSubresource(obj).Build()
1262+
1263+
Expect(cl.Create(context.Background(), obj)).To(BeNil())
1264+
err := unstructured.SetNestedField(obj.Object, map[string]interface{}{"count": int64(2)}, "status")
1265+
Expect(err).To(BeNil())
1266+
1267+
Expect(cl.Update(context.Background(), obj)).To(BeNil())
1268+
1269+
Expect(cl.Get(context.Background(), client.ObjectKeyFromObject(obj), obj)).To(BeNil())
1270+
1271+
Expect(obj.Object["status"]).To(BeNil())
1272+
})
1273+
1274+
It("should not change the status of objects that are configured to have a status subresource on patch", func() {
1275+
obj := &unstructured.Unstructured{}
1276+
obj.SetAPIVersion("foo/v1")
1277+
obj.SetKind("Foo")
1278+
obj.SetName("a-foo")
1279+
cl := NewClientBuilder().WithStatusSubresource(obj).Build()
1280+
1281+
Expect(cl.Create(context.Background(), obj)).To(BeNil())
1282+
original := obj.DeepCopy()
1283+
1284+
err := unstructured.SetNestedField(obj.Object, map[string]interface{}{"count": int64(2)}, "status")
1285+
Expect(err).To(BeNil())
1286+
Expect(cl.Patch(context.Background(), obj, client.MergeFrom(original))).To(BeNil())
1287+
1288+
Expect(cl.Get(context.Background(), client.ObjectKeyFromObject(obj), obj)).To(BeNil())
1289+
1290+
Expect(obj.Object["status"]).To(BeNil())
1291+
})
12231292
})
12241293

12251294
var _ = Describe("Fake client builder", func() {

0 commit comments

Comments
 (0)