Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

馃悰 Handle unstructured status update with fake client #2495

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 16 additions & 1 deletion pkg/client/fake/client.go
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original reason for not doing this anymore is that it might leave stale data in the target. I am confused as to why this isn't a problem anymore. Maybe we just didn't add sufficient test coverage?

Copy link
Member Author

@scothis scothis Sep 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't changing existing behavior. No tests were modified or removed. What it does is handle a mix of typed and unstructured objects gracefully, instead of panicing.

What stale data is left behind?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What @alvaroaleman is referring to I believe is that before this function was added, we removed a function similar to this where we were doing a copyFrom because of the issues with that function leaving stale data around.
#2479

#2479 (comment)

If this being added doesn't impact what is already there (covers previous tests and your new test case) and handles the new case to fix the regression, the new test coverage should be sufficient to have this fix be in and resolve the issue with unstructured stuff.

I believe we have unstructured.Unstructured tests cases and if not being caught in testing, it begs the question how did those tests pass, if this problem still presented itself.

IANAM, (I am not a maintainer)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the previous tests and code handled both typed->typed and unstructured->unstructured, but triggered a panic with typed->unstructured. The mix can happen when the GVK is registered in the scheme, so the resource in the tracker is always converted to the typed form, and then the user performs a status update using an unstructured object.

It's a bit esoteric as most users will favor a typed resource when available, but we hit it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, we also added code to zero the target, that is why it works. Thanks for the patch and sorry for the review delay

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
78 changes: 78 additions & 0 deletions pkg/client/fake/client_test.go
Expand Up @@ -1677,6 +1677,84 @@ 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 := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "pod",
},
Spec: corev1.PodSpec{
RestartPolicy: corev1.RestartPolicyAlways,
},
Status: corev1.PodStatus{
Phase: corev1.PodPending,
},
}
cl := NewClientBuilder().WithStatusSubresource(obj).WithObjects(obj).Build()

// update using unstructured
u := &unstructured.Unstructured{}
u.SetAPIVersion("v1")
u.SetKind("Pod")
u.SetName(obj.Name)
err := cl.Get(context.Background(), client.ObjectKeyFromObject(u), u)
Expect(err).NotTo(HaveOccurred())

err = unstructured.SetNestedField(u.Object, string(corev1.RestartPolicyNever), "spec", "restartPolicy")
Expect(err).NotTo(HaveOccurred())
err = unstructured.SetNestedField(u.Object, string(corev1.PodRunning), "status", "phase")
Expect(err).NotTo(HaveOccurred())

Expect(cl.Update(context.Background(), u)).To(Succeed())

actual := &corev1.Pod{}
Expect(cl.Get(context.Background(), client.ObjectKeyFromObject(obj), actual)).To(Succeed())
obj.APIVersion = u.GetAPIVersion()
obj.Kind = u.GetKind()
obj.ResourceVersion = actual.ResourceVersion
// only the spec mutation should persist
obj.Spec.RestartPolicy = corev1.RestartPolicyNever
Expect(cmp.Diff(obj, actual)).To(BeEmpty())
})

It("should not change non-status field of known unstructured objects that have a status subresource on status update", func() {
obj := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "pod",
},
Spec: corev1.PodSpec{
RestartPolicy: corev1.RestartPolicyAlways,
},
Status: corev1.PodStatus{
Phase: corev1.PodPending,
},
}
cl := NewClientBuilder().WithStatusSubresource(obj).WithObjects(obj).Build()

// status update using unstructured
u := &unstructured.Unstructured{}
u.SetAPIVersion("v1")
u.SetKind("Pod")
u.SetName(obj.Name)
err := cl.Get(context.Background(), client.ObjectKeyFromObject(u), u)
Expect(err).NotTo(HaveOccurred())

err = unstructured.SetNestedField(u.Object, string(corev1.RestartPolicyNever), "spec", "restartPolicy")
Expect(err).NotTo(HaveOccurred())
err = unstructured.SetNestedField(u.Object, string(corev1.PodRunning), "status", "phase")
Expect(err).NotTo(HaveOccurred())

Expect(cl.Status().Update(context.Background(), u)).To(Succeed())

actual := &corev1.Pod{}
Expect(cl.Get(context.Background(), client.ObjectKeyFromObject(obj), actual)).To(Succeed())
obj.APIVersion = "v1"
obj.Kind = "Pod"
obj.ResourceVersion = actual.ResourceVersion
// only the status mutation should persist
obj.Status.Phase = corev1.PodRunning
Expect(cmp.Diff(obj, actual)).To(BeEmpty())
})

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