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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

⚠️ Add Get functionality to SubResourceClient #2094

Merged
merged 1 commit into from Dec 14, 2022
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
85 changes: 64 additions & 21 deletions pkg/client/client.go
Expand Up @@ -18,6 +18,7 @@ package client

import (
"context"
"errors"
"fmt"
"strings"

Expand Down Expand Up @@ -292,18 +293,48 @@ func (c *client) Status() SubResourceWriter {
return c.SubResource("status")
}

func (c *client) SubResource(subResource string) SubResourceWriter {
return &subResourceWriter{client: c, subResource: subResource}
func (c *client) SubResource(subResource string) SubResourceClient {
return &subResourceClient{client: c, subResource: subResource}
}

// subResourceWriter is client.SubResourceWriter that writes to subresources.
type subResourceWriter struct {
// subResourceClient is client.SubResourceWriter that writes to subresources.
type subResourceClient struct {
client *client
subResource string
}

// ensure subResourceWriter implements client.SubResourceWriter.
var _ SubResourceWriter = &subResourceWriter{}
// ensure subResourceClient implements client.SubResourceClient.
var _ SubResourceClient = &subResourceClient{}

// SubResourceGetOptions holds all the possible configuration
// for a subresource Get request.
type SubResourceGetOptions struct {
Raw *metav1.GetOptions
}

// ApplyToSubResourceGet updates the configuaration to the given get options.
func (getOpt *SubResourceGetOptions) ApplyToSubResourceGet(o *SubResourceGetOptions) {
if getOpt.Raw != nil {
o.Raw = getOpt.Raw
}
}

// ApplyOptions applues the given options.
func (getOpt *SubResourceGetOptions) ApplyOptions(opts []SubResourceGetOption) *SubResourceGetOptions {
for _, o := range opts {
o.ApplyToSubResourceGet(getOpt)
}

return getOpt
}

// AsGetOptions returns the configured options as *metav1.GetOptions.
func (getOpt *SubResourceGetOptions) AsGetOptions() *metav1.GetOptions {
if getOpt.Raw == nil {
return &metav1.GetOptions{}
}
return getOpt.Raw
}

// SubResourceUpdateOptions holds all the possible configuration
// for a subresource update request.
Expand Down Expand Up @@ -398,42 +429,54 @@ func (po *SubResourcePatchOptions) ApplyToSubResourcePatch(o *SubResourcePatchOp
}
}

func (sw *subResourceWriter) Create(ctx context.Context, obj Object, subResource Object, opts ...SubResourceCreateOption) error {
defer sw.client.resetGroupVersionKind(obj, obj.GetObjectKind().GroupVersionKind())
defer sw.client.resetGroupVersionKind(subResource, subResource.GetObjectKind().GroupVersionKind())
func (sc *subResourceClient) Get(ctx context.Context, obj Object, subResource Object, opts ...SubResourceGetOption) error {
switch obj.(type) {
case *unstructured.Unstructured:
return sc.client.unstructuredClient.GetSubResource(ctx, obj, subResource, sc.subResource, opts...)
case *metav1.PartialObjectMetadata:
return errors.New("can not get subresource using only metadata")
default:
return sc.client.typedClient.GetSubResource(ctx, obj, subResource, sc.subResource, opts...)
}
}

// Create implements client.SubResourceClient
func (sc *subResourceClient) Create(ctx context.Context, obj Object, subResource Object, opts ...SubResourceCreateOption) error {
defer sc.client.resetGroupVersionKind(obj, obj.GetObjectKind().GroupVersionKind())
defer sc.client.resetGroupVersionKind(subResource, subResource.GetObjectKind().GroupVersionKind())

switch obj.(type) {
case *unstructured.Unstructured:
return sw.client.unstructuredClient.CreateSubResource(ctx, obj, subResource, sw.subResource, opts...)
return sc.client.unstructuredClient.CreateSubResource(ctx, obj, subResource, sc.subResource, opts...)
case *metav1.PartialObjectMetadata:
return fmt.Errorf("cannot update status using only metadata -- did you mean to patch?")
default:
return sw.client.typedClient.CreateSubResource(ctx, obj, subResource, sw.subResource, opts...)
return sc.client.typedClient.CreateSubResource(ctx, obj, subResource, sc.subResource, opts...)
}
}

// Update implements client.SubResourceWriter.
func (sw *subResourceWriter) Update(ctx context.Context, obj Object, opts ...SubResourceUpdateOption) error {
defer sw.client.resetGroupVersionKind(obj, obj.GetObjectKind().GroupVersionKind())
// Update implements client.SubResourceClient
func (sc *subResourceClient) Update(ctx context.Context, obj Object, opts ...SubResourceUpdateOption) error {
defer sc.client.resetGroupVersionKind(obj, obj.GetObjectKind().GroupVersionKind())
switch obj.(type) {
case *unstructured.Unstructured:
return sw.client.unstructuredClient.UpdateSubResource(ctx, obj, sw.subResource, opts...)
return sc.client.unstructuredClient.UpdateSubResource(ctx, obj, sc.subResource, opts...)
case *metav1.PartialObjectMetadata:
return fmt.Errorf("cannot update status using only metadata -- did you mean to patch?")
default:
return sw.client.typedClient.UpdateSubResource(ctx, obj, sw.subResource, opts...)
return sc.client.typedClient.UpdateSubResource(ctx, obj, sc.subResource, opts...)
}
}

// Patch implements client.SubResourceWriter.
func (sw *subResourceWriter) Patch(ctx context.Context, obj Object, patch Patch, opts ...SubResourcePatchOption) error {
defer sw.client.resetGroupVersionKind(obj, obj.GetObjectKind().GroupVersionKind())
func (sc *subResourceClient) Patch(ctx context.Context, obj Object, patch Patch, opts ...SubResourcePatchOption) error {
defer sc.client.resetGroupVersionKind(obj, obj.GetObjectKind().GroupVersionKind())
switch obj.(type) {
case *unstructured.Unstructured:
return sw.client.unstructuredClient.PatchSubResource(ctx, obj, sw.subResource, patch, opts...)
return sc.client.unstructuredClient.PatchSubResource(ctx, obj, sc.subResource, patch, opts...)
case *metav1.PartialObjectMetadata:
return sw.client.metadataClient.PatchSubResource(ctx, obj, sw.subResource, patch, opts...)
return sc.client.metadataClient.PatchSubResource(ctx, obj, sc.subResource, patch, opts...)
default:
return sw.client.typedClient.PatchSubResource(ctx, obj, sw.subResource, patch, opts...)
return sc.client.typedClient.PatchSubResource(ctx, obj, sc.subResource, patch, opts...)
}
}
43 changes: 42 additions & 1 deletion pkg/client/client_test.go
Expand Up @@ -20,6 +20,7 @@ import (
"context"
"encoding/json"
"fmt"
"reflect"
"sync/atomic"
"time"

Expand Down Expand Up @@ -740,8 +741,23 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC
})
})

Describe("SubResourceWriter", func() {
Describe("SubResourceClient", func() {
Context("with structured objects", func() {
It("should be able to read the Scale subresource", func() {
cl, err := client.New(cfg, client.Options{})
Expect(err).NotTo(HaveOccurred())
Expect(cl).NotTo(BeNil())

By("Creating a deployment")
dep, err := clientset.AppsV1().Deployments(dep.Namespace).Create(ctx, dep, metav1.CreateOptions{})
Expect(err).NotTo(HaveOccurred())

By("reading the scale subresource")
scale := &autoscalingv1.Scale{}
err = cl.SubResource("scale").Get(ctx, dep, scale)
Expect(err).NotTo(HaveOccurred())
Expect(scale.Spec.Replicas).To(Equal(*dep.Spec.Replicas))
})
It("should be able to create ServiceAccount tokens", func() {
cl, err := client.New(cfg, client.Options{})
Expect(err).NotTo(HaveOccurred())
Expand Down Expand Up @@ -901,6 +917,31 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC
})

Context("with unstructured objects", func() {
It("should be able to read the Scale subresource", func() {
cl, err := client.New(cfg, client.Options{})
Expect(err).NotTo(HaveOccurred())
Expect(cl).NotTo(BeNil())

By("Creating a deployment")
dep, err := clientset.AppsV1().Deployments(dep.Namespace).Create(ctx, dep, metav1.CreateOptions{})
Expect(err).NotTo(HaveOccurred())
dep.APIVersion = appsv1.SchemeGroupVersion.String()
dep.Kind = reflect.TypeOf(dep).Elem().Name()
depUnstructured, err := toUnstructured(dep)
Expect(err).NotTo(HaveOccurred())

By("reading the scale subresource")
scale := &unstructured.Unstructured{}
scale.SetAPIVersion("autoscaling/v1")
scale.SetKind("Scale")
err = cl.SubResource("scale").Get(ctx, depUnstructured, scale)
Expect(err).NotTo(HaveOccurred())

val, found, err := unstructured.NestedInt64(scale.UnstructuredContent(), "spec", "replicas")
Expect(err).NotTo(HaveOccurred())
Expect(found).To(BeTrue())
Expect(int32(val)).To(Equal(*dep.Spec.Replicas))
})
It("should be able to create ServiceAccount tokens", func() {
cl, err := client.New(cfg, client.Options{})
Expect(err).NotTo(HaveOccurred())
Expand Down
22 changes: 13 additions & 9 deletions pkg/client/dryrun.go
Expand Up @@ -87,29 +87,33 @@ func (c *dryRunClient) Status() SubResourceWriter {
}

// SubResource implements client.SubResourceClient.
func (c *dryRunClient) SubResource(subResource string) SubResourceWriter {
return &dryRunSubResourceWriter{client: c.client.SubResource(subResource)}
func (c *dryRunClient) SubResource(subResource string) SubResourceClient {
return &dryRunSubResourceClient{client: c.client.SubResource(subResource)}
}

// ensure dryRunSubResourceWriter implements client.SubResourceWriter.
var _ SubResourceWriter = &dryRunSubResourceWriter{}
var _ SubResourceWriter = &dryRunSubResourceClient{}

// dryRunSubResourceWriter is client.SubResourceWriter that writes status subresource with dryRun mode
// dryRunSubResourceClient is client.SubResourceWriter that writes status subresource with dryRun mode
// enforced.
type dryRunSubResourceWriter struct {
client SubResourceWriter
type dryRunSubResourceClient struct {
client SubResourceClient
}

func (sw *dryRunSubResourceWriter) Create(ctx context.Context, obj, subResource Object, opts ...SubResourceCreateOption) error {
func (sw *dryRunSubResourceClient) Get(ctx context.Context, obj, subResource Object, opts ...SubResourceGetOption) error {
return sw.client.Get(ctx, obj, subResource, opts...)
}

func (sw *dryRunSubResourceClient) Create(ctx context.Context, obj, subResource Object, opts ...SubResourceCreateOption) error {
return sw.client.Create(ctx, obj, subResource, append(opts, DryRunAll)...)
}

// Update implements client.SubResourceWriter.
func (sw *dryRunSubResourceWriter) Update(ctx context.Context, obj Object, opts ...SubResourceUpdateOption) error {
func (sw *dryRunSubResourceClient) Update(ctx context.Context, obj Object, opts ...SubResourceUpdateOption) error {
return sw.client.Update(ctx, obj, append(opts, DryRunAll)...)
}

// Patch implements client.SubResourceWriter.
func (sw *dryRunSubResourceWriter) Patch(ctx context.Context, obj Object, patch Patch, opts ...SubResourcePatchOption) error {
func (sw *dryRunSubResourceClient) Patch(ctx context.Context, obj Object, patch Patch, opts ...SubResourcePatchOption) error {
return sw.client.Patch(ctx, obj, patch, append(opts, DryRunAll)...)
}
16 changes: 10 additions & 6 deletions pkg/client/fake/client.go
Expand Up @@ -756,8 +756,8 @@ func (c *fakeClient) Status() client.SubResourceWriter {
return c.SubResource("status")
}

func (c *fakeClient) SubResource(subResource string) client.SubResourceWriter {
return &fakeSubResourceWriter{client: c}
func (c *fakeClient) SubResource(subResource string) client.SubResourceClient {
return &fakeSubResourceClient{client: c}
}

func (c *fakeClient) deleteObject(gvr schema.GroupVersionResource, accessor metav1.Object) error {
Expand Down Expand Up @@ -786,15 +786,19 @@ func getGVRFromObject(obj runtime.Object, scheme *runtime.Scheme) (schema.GroupV
return gvr, nil
}

type fakeSubResourceWriter struct {
type fakeSubResourceClient struct {
client *fakeClient
}

func (sw *fakeSubResourceWriter) Create(ctx context.Context, obj client.Object, subResource client.Object, opts ...client.SubResourceCreateOption) error {
func (sw *fakeSubResourceClient) Get(ctx context.Context, obj, subResource client.Object, opts ...client.SubResourceGetOption) error {
panic("fakeSubResourceClient does not support get")
}

func (sw *fakeSubResourceClient) Create(ctx context.Context, obj client.Object, subResource client.Object, opts ...client.SubResourceCreateOption) error {
panic("fakeSubResourceWriter does not support create")
}

func (sw *fakeSubResourceWriter) Update(ctx context.Context, obj client.Object, opts ...client.SubResourceUpdateOption) error {
func (sw *fakeSubResourceClient) Update(ctx context.Context, obj client.Object, opts ...client.SubResourceUpdateOption) error {
// TODO(droot): This results in full update of the obj (spec + subresources). Need
// a way to update subresource only.
updateOptions := client.SubResourceUpdateOptions{}
Expand All @@ -807,7 +811,7 @@ func (sw *fakeSubResourceWriter) Update(ctx context.Context, obj client.Object,
return sw.client.Update(ctx, body, &updateOptions.UpdateOptions)
}

func (sw *fakeSubResourceWriter) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.SubResourcePatchOption) error {
func (sw *fakeSubResourceClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.SubResourcePatchOption) error {
// TODO(droot): This results in full update of the obj (spec + subresources). Need
// a way to update subresource only.

Expand Down
34 changes: 25 additions & 9 deletions pkg/client/interfaces.go
Expand Up @@ -85,21 +85,21 @@ type StatusClient interface {
Status() SubResourceWriter
}

// SubResourceClient knows how to create a client which can update subresource
// SubResourceClientConstructor knows how to create a client which can update subresource
// for kubernetes objects.
type SubResourceClient interface {
// SubResource returns a subresource client for the named subResource. Known
// upstream subResources are:
// - ServiceAccount tokens:
type SubResourceClientConstructor interface {
// SubResourceClientConstructor returns a subresource client for the named subResource. Known
// upstream subResources usages are:
// - ServiceAccount token creation:
// sa := &corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "bar"}}
// token := &authenticationv1.TokenRequest{}
// c.SubResourceClient("token").Create(ctx, sa, token)
//
// - Pod evictions:
// - Pod eviction creation:
// pod := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "bar"}}
// c.SubResourceClient("eviction").Create(ctx, pod, &policyv1.Eviction{})
//
// - Pod bindings:
// - Pod binding creation:
// pod := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "bar"}}
// binding := &corev1.Binding{Target: corev1.ObjectReference{Name: "my-node"}}
// c.SubResourceClient("binding").Create(ctx, pod, binding)
Expand All @@ -116,16 +116,26 @@ type SubResourceClient interface {
// }
// c.SubResourceClient("approval").Update(ctx, csr)
//
// - Scale retrieval:
// dep := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "bar"}}
// scale := &autoscalingv1.Scale{}
// c.SubResourceClient("scale").Get(ctx, dep, scale)
//
// - Scale update:
// dep := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "bar"}}
// scale := &autoscalingv1.Scale{Spec: autoscalingv1.ScaleSpec{Replicas: 2}}
// c.SubResourceClient("scale").Update(ctx, dep, client.WithSubResourceBody(scale))
SubResource(subResource string) SubResourceWriter
SubResource(subResource string) SubResourceClient
}

// StatusWriter is kept for backward compatibility.
type StatusWriter = SubResourceWriter

// SubResourceReader knows how to read SubResources
type SubResourceReader interface {
Get(ctx context.Context, obj Object, subResource Object, opts ...SubResourceGetOption) error
}

// SubResourceWriter knows how to update subresource of a Kubernetes object.
type SubResourceWriter interface {
// Create saves the subResource object in the Kubernetes cluster. obj must be a
Expand All @@ -142,12 +152,18 @@ type SubResourceWriter interface {
Patch(ctx context.Context, obj Object, patch Patch, opts ...SubResourcePatchOption) error
}

// SubResourceClient knows how to perform CRU operations on Kubernetes objects.
type SubResourceClient interface {
SubResourceReader
SubResourceWriter
}

// Client knows how to perform CRUD operations on Kubernetes objects.
type Client interface {
Reader
Writer
StatusClient
SubResourceClient
SubResourceClientConstructor

// Scheme returns the scheme this client is using.
Scheme() *runtime.Scheme
Expand Down