Skip to content

Commit

Permalink
Handle unstructured status update with fake client
Browse files Browse the repository at this point in the history
In order to prevent unintentional mutations outside of the status during
a status update, the non-status fields are copied back onto the passed
object.

This operation now gracefully handles both unstructured and typed
objects. Previously, it would panic if an unstructured object was passed
for a GVK known to the scheme, as internally the object within the
tracker is converted to the typed equivalent. The two types cannot
be directly assigned to each other and instead must be copied.

Signed-off-by: Scott Andrews <[email protected]>
  • Loading branch information
scothis committed Sep 14, 2023
1 parent 8117577 commit 6860cfb
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 1 deletion.
17 changes: 16 additions & 1 deletion pkg/client/fake/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,9 @@ func (t versionedTracker) update(gvr schema.GroupVersionResource, obj runtime.Ob
return fmt.Errorf("failed to copy non-status field for object with status subresouce: %w", err)
}
passedRV := accessor.GetResourceVersion()
reflect.ValueOf(obj).Elem().Set(reflect.ValueOf(oldObject.DeepCopyObject()).Elem())
if err := copyFrom(oldObject, obj); err != nil {
return fmt.Errorf("failed to restore non-status fields: %w", err)
}
accessor.SetResourceVersion(passedRV)
} else { // copy status from original object
if err := copyStatusFrom(oldObject, obj); err != nil {
Expand Down Expand Up @@ -972,6 +974,19 @@ func copyStatusFrom(old, new runtime.Object) error {
return nil
}

// copyFrom copies from old into new
func copyFrom(old, new runtime.Object) error {
oldMapStringAny, err := toMapStringAny(old)
if err != nil {
return fmt.Errorf("failed to convert old to *unstructured.Unstructured: %w", err)
}
if err := fromMapStringAny(oldMapStringAny, new); err != nil {
return fmt.Errorf("failed to convert back from map[string]any: %w", err)
}

return nil
}

func toMapStringAny(obj runtime.Object) (map[string]any, error) {
if unstructured, isUnstructured := obj.(*unstructured.Unstructured); isUnstructured {
return unstructured.Object, nil
Expand Down
46 changes: 46 additions & 0 deletions pkg/client/fake/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1677,6 +1677,52 @@ var _ = Describe("Fake client", func() {
Expect(obj.Object["spec"]).To(BeEquivalentTo("original"))
})

It("should not change the status of known unstructured objects that have a status subresource on update", func() {
obj := &unstructured.Unstructured{}
obj.SetAPIVersion("v1")
obj.SetKind("Pod")
obj.SetName("pod")
err := unstructured.SetNestedField(obj.Object, "Always", "spec", "restartPolicy")
Expect(err).NotTo(HaveOccurred())
err = unstructured.SetNestedField(obj.Object, "Pending", "status", "phase")
Expect(err).NotTo(HaveOccurred())
cl := NewClientBuilder().WithStatusSubresource(obj).WithObjects(obj).Build()

err = unstructured.SetNestedField(obj.Object, "Never", "spec", "restartPolicy")
Expect(err).NotTo(HaveOccurred())
err = unstructured.SetNestedField(obj.Object, "Running", "status", "phase")
Expect(err).NotTo(HaveOccurred())

Expect(cl.Update(context.Background(), obj)).To(Succeed())
Expect(cl.Get(context.Background(), client.ObjectKeyFromObject(obj), obj)).To(Succeed())

Expect(obj.Object["spec"].(map[string]any)["restartPolicy"]).To(Equal("Never"))
Expect(obj.Object["status"].(map[string]any)["phase"]).To(Equal("Pending"))
})

It("should not change non-status field of known unstructured objects that have a status subresource on status update", func() {
obj := &unstructured.Unstructured{}
obj.SetAPIVersion("v1")
obj.SetKind("Pod")
obj.SetName("pod")
err := unstructured.SetNestedField(obj.Object, "Always", "spec", "restartPolicy")
Expect(err).NotTo(HaveOccurred())
err = unstructured.SetNestedField(obj.Object, "Pending", "status", "phase")
Expect(err).NotTo(HaveOccurred())
cl := NewClientBuilder().WithStatusSubresource(obj).WithObjects(obj).Build()

err = unstructured.SetNestedField(obj.Object, "Never", "spec", "restartPolicy")
Expect(err).NotTo(HaveOccurred())
err = unstructured.SetNestedField(obj.Object, "Running", "status", "phase")
Expect(err).NotTo(HaveOccurred())

Expect(cl.Status().Update(context.Background(), obj)).To(Succeed())
Expect(cl.Get(context.Background(), client.ObjectKeyFromObject(obj), obj)).To(Succeed())

Expect(obj.Object["spec"].(map[string]any)["restartPolicy"]).To(Equal("Always"))
Expect(obj.Object["status"].(map[string]any)["phase"]).To(Equal("Running"))
})

It("should not change the status of unstructured objects that are configured to have a status subresource on patch", func() {
obj := &unstructured.Unstructured{}
obj.SetAPIVersion("foo/v1")
Expand Down

0 comments on commit 6860cfb

Please sign in to comment.