Skip to content

Commit dcd5e10

Browse files
committed
🐛 Fakeclient: Allow manipulating registered types through unstructured
Currently, manipulating an object that is registered through unstructured will break all subsequent List requests that include said object. This happens because the tracker unconditionally saves objects but it's List() assumes that what it finds can be assigned to the .Items slice of an object produced through scheme.New(ListGVK).
1 parent 0a9a777 commit dcd5e10

File tree

2 files changed

+87
-1
lines changed

2 files changed

+87
-1
lines changed

pkg/client/fake/client.go

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,11 @@ func (t versionedTracker) Add(obj runtime.Object) error {
169169
// be recognized
170170
accessor.SetResourceVersion(trackerAddResourceVersion)
171171
}
172+
173+
obj, err = convertFromUnstructuredIfNecessary(t.scheme, obj)
174+
if err != nil {
175+
return err
176+
}
172177
if err := t.ObjectTracker.Add(obj); err != nil {
173178
return err
174179
}
@@ -192,13 +197,45 @@ func (t versionedTracker) Create(gvr schema.GroupVersionResource, obj runtime.Ob
192197
return apierrors.NewBadRequest("resourceVersion can not be set for Create requests")
193198
}
194199
accessor.SetResourceVersion("1")
200+
obj, err = convertFromUnstructuredIfNecessary(t.scheme, obj)
201+
if err != nil {
202+
return err
203+
}
195204
if err := t.ObjectTracker.Create(gvr, obj, ns); err != nil {
196205
accessor.SetResourceVersion("")
197206
return err
198207
}
208+
199209
return nil
200210
}
201211

212+
// convertFromUnstructuredIfNecessary will convert *unstructured.Unstructured for a GVK that is recocnized
213+
// by the schema into the whatever the schema produces with New() for said GVK.
214+
// This is required because the tracker unconditionally saves on manipulations, but it's List() implementation
215+
// tries to assign whatever it finds into a ListType it gets from schema.New() - Thus we have to ensure
216+
// we save as the very same type, otherwise subsequent List requests will fail.
217+
func convertFromUnstructuredIfNecessary(s *runtime.Scheme, o runtime.Object) (runtime.Object, error) {
218+
u, isUnstructured := o.(*unstructured.Unstructured)
219+
if !isUnstructured || !s.Recognizes(u.GroupVersionKind()) {
220+
return o, nil
221+
}
222+
223+
typed, err := s.New(u.GroupVersionKind())
224+
if err != nil {
225+
return nil, fmt.Errorf("scheme recognizes %s but failed to produce an object for it: %w", u.GroupVersionKind().String(), err)
226+
}
227+
228+
unstructuredSerialized, err := json.Marshal(u)
229+
if err != nil {
230+
return nil, fmt.Errorf("failed to serialize %T: %w", unstructuredSerialized, err)
231+
}
232+
if err := json.Unmarshal(unstructuredSerialized, typed); err != nil {
233+
return nil, fmt.Errorf("failed to unmarshal the content of %T into %T: %w", u, typed, err)
234+
}
235+
236+
return typed, nil
237+
}
238+
202239
func (t versionedTracker) Update(gvr schema.GroupVersionResource, obj runtime.Object, ns string) error {
203240
accessor, err := meta.Accessor(obj)
204241
if err != nil {
@@ -255,6 +292,10 @@ func (t versionedTracker) Update(gvr schema.GroupVersionResource, obj runtime.Ob
255292
if !accessor.GetDeletionTimestamp().IsZero() && len(accessor.GetFinalizers()) == 0 {
256293
return t.ObjectTracker.Delete(gvr, accessor.GetNamespace(), accessor.GetName())
257294
}
295+
obj, err = convertFromUnstructuredIfNecessary(t.scheme, obj)
296+
if err != nil {
297+
return err
298+
}
258299
return t.ObjectTracker.Update(gvr, obj, ns)
259300
}
260301

@@ -318,7 +359,7 @@ func (c *fakeClient) List(ctx context.Context, obj client.ObjectList, opts ...cl
318359
}
319360

320361
if _, isUnstructuredList := obj.(*unstructured.UnstructuredList); isUnstructuredList && !c.scheme.Recognizes(gvk) {
321-
// We need tor register the ListKind with UnstructuredList:
362+
// We need to register the ListKind with UnstructuredList:
322363
// https://github.com/kubernetes/kubernetes/blob/7b2776b89fb1be28d4e9203bdeec079be903c103/staging/src/k8s.io/client-go/dynamic/fake/simple.go#L44-L51
323364
c.schemeWriteLock.Lock()
324365
c.scheme.AddKnownTypeWithName(gvk.GroupVersion().WithKind(gvk.Kind+"List"), &unstructured.UnstructuredList{})

pkg/client/fake/client_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,51 @@ var _ = Describe("Fake client", func() {
140140
Expect(list.Items).To(HaveLen(2))
141141
})
142142

143+
It("should be able to retrieve registered objects that got manipulated as unstructured", func() {
144+
list := func() {
145+
By("Listing all endpoints in a namespace")
146+
list := &unstructured.UnstructuredList{}
147+
list.SetAPIVersion("v1")
148+
list.SetKind("EndpointsList")
149+
err := cl.List(context.Background(), list, client.InNamespace("ns1"))
150+
Expect(err).To(BeNil())
151+
Expect(list.Items).To(HaveLen(1))
152+
}
153+
154+
unstructuredEndpoint := func() *unstructured.Unstructured {
155+
item := &unstructured.Unstructured{}
156+
item.SetAPIVersion("v1")
157+
item.SetKind("Endpoints")
158+
item.SetName("test-endpoint")
159+
item.SetNamespace("ns1")
160+
return item
161+
}
162+
163+
By("Adding the object during client initialization")
164+
cl = NewFakeClient(unstructuredEndpoint())
165+
list()
166+
Expect(cl.Delete(context.Background(), unstructuredEndpoint())).To(BeNil())
167+
168+
By("Creating an object")
169+
item := unstructuredEndpoint()
170+
err := cl.Create(context.Background(), item)
171+
Expect(err).To(BeNil())
172+
list()
173+
174+
By("Updating the object")
175+
item.SetAnnotations(map[string]string{"foo": "bar"})
176+
err = cl.Update(context.Background(), item)
177+
Expect(err).To(BeNil())
178+
list()
179+
180+
By("Patching the object")
181+
old := item.DeepCopy()
182+
item.SetAnnotations(map[string]string{"bar": "baz"})
183+
err = cl.Patch(context.Background(), item, client.MergeFrom(old))
184+
Expect(err).To(BeNil())
185+
list()
186+
})
187+
143188
It("should be able to Create an unregistered type using unstructured", func() {
144189
item := &unstructured.Unstructured{}
145190
item.SetAPIVersion("custom/v1")

0 commit comments

Comments
 (0)