diff --git a/pkg/client/client.go b/pkg/client/client.go index 11bc7a4dfb..c1c4d5d691 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -19,14 +19,12 @@ package client import ( "context" "fmt" - "reflect" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer" - "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client/apiutil" @@ -70,25 +68,22 @@ func New(config *rest.Config, options Options) (Client, error) { } } - dynamicClient, err := dynamic.NewForConfig(config) - if err != nil { - return nil, err + clientcache := &clientCache{ + config: config, + scheme: options.Scheme, + mapper: options.Mapper, + codecs: serializer.NewCodecFactory(options.Scheme), + resourceByType: make(map[schema.GroupVersionKind]*resourceMeta), } c := &client{ typedClient: typedClient{ - cache: clientCache{ - config: config, - scheme: options.Scheme, - mapper: options.Mapper, - codecs: serializer.NewCodecFactory(options.Scheme), - resourceByType: make(map[reflect.Type]*resourceMeta), - }, + cache: clientcache, paramCodec: runtime.NewParameterCodec(options.Scheme), }, unstructuredClient: unstructuredClient{ - client: dynamicClient, - restMapper: options.Mapper, + cache: clientcache, + paramCodec: noConversionParamCodec{}, }, } diff --git a/pkg/client/client_cache.go b/pkg/client/client_cache.go index f6a7e82e86..7741ac3c7e 100644 --- a/pkg/client/client_cache.go +++ b/pkg/client/client_cache.go @@ -17,7 +17,6 @@ limitations under the License. package client import ( - "reflect" "strings" "sync" @@ -45,19 +44,14 @@ type clientCache struct { codecs serializer.CodecFactory // resourceByType caches type metadata - resourceByType map[reflect.Type]*resourceMeta + resourceByType map[schema.GroupVersionKind]*resourceMeta mu sync.RWMutex } // newResource maps obj to a Kubernetes Resource and constructs a client for that Resource. // If the object is a list, the resource represents the item's type instead. -func (c *clientCache) newResource(obj runtime.Object) (*resourceMeta, error) { - gvk, err := apiutil.GVKForObject(obj, c.scheme) - if err != nil { - return nil, err - } - - if strings.HasSuffix(gvk.Kind, "List") && meta.IsListType(obj) { +func (c *clientCache) newResource(gvk schema.GroupVersionKind, isList bool) (*resourceMeta, error) { + if strings.HasSuffix(gvk.Kind, "List") && isList { // if this was a list, treat it as a request for the item's resource gvk.Kind = gvk.Kind[:len(gvk.Kind)-4] } @@ -76,12 +70,15 @@ func (c *clientCache) newResource(obj runtime.Object) (*resourceMeta, error) { // getResource returns the resource meta information for the given type of object. // If the object is a list, the resource represents the item's type instead. func (c *clientCache) getResource(obj runtime.Object) (*resourceMeta, error) { - typ := reflect.TypeOf(obj) + gvk, err := apiutil.GVKForObject(obj, c.scheme) + if err != nil { + return nil, err + } // It's better to do creation work twice than to not let multiple // people make requests at once c.mu.RLock() - r, known := c.resourceByType[typ] + r, known := c.resourceByType[gvk] c.mu.RUnlock() if known { @@ -91,11 +88,11 @@ func (c *clientCache) getResource(obj runtime.Object) (*resourceMeta, error) { // Initialize a new Client c.mu.Lock() defer c.mu.Unlock() - r, err := c.newResource(obj) + r, err = c.newResource(gvk, meta.IsListType(obj)) if err != nil { return nil, err } - c.resourceByType[typ] = r + c.resourceByType[gvk] = r return r, err } diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index c8d7cfc6a4..3148c23d9a 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -827,6 +827,57 @@ var _ = Describe("Client", func() { close(done) }) + It("should update status and preserve type information", func(done Done) { + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) + + By("initially creating a Deployment") + dep, err := clientset.AppsV1().Deployments(ns).Create(dep) + Expect(err).NotTo(HaveOccurred()) + + By("updating the status of Deployment") + u := &unstructured.Unstructured{} + dep.Status.Replicas = 1 + Expect(scheme.Convert(dep, u, nil)).To(Succeed()) + err = cl.Status().Update(context.TODO(), u) + Expect(err).NotTo(HaveOccurred()) + + By("validating updated Deployment has type information") + Expect(u.GroupVersionKind()).To(Equal(depGvk)) + + close(done) + }) + + It("should patch status and preserve type information", func(done Done) { + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) + + By("initially creating a Deployment") + dep, err := clientset.AppsV1().Deployments(ns).Create(dep) + Expect(err).NotTo(HaveOccurred()) + + By("patching the status of Deployment") + u := &unstructured.Unstructured{} + depPatch := client.MergeFrom(dep.DeepCopy()) + dep.Status.Replicas = 1 + Expect(scheme.Convert(dep, u, nil)).To(Succeed()) + err = cl.Status().Patch(context.TODO(), u, depPatch) + Expect(err).NotTo(HaveOccurred()) + + By("validating updated Deployment has type information") + Expect(u.GroupVersionKind()).To(Equal(depGvk)) + + By("validating patched Deployment has new status") + actual, err := clientset.AppsV1().Deployments(ns).Get(dep.Name, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(actual).NotTo(BeNil()) + Expect(actual.Status.Replicas).To(BeEquivalentTo(1)) + + close(done) + }) + It("should not update spec of an existing object", func(done Done) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) diff --git a/pkg/client/codec.go b/pkg/client/codec.go new file mode 100644 index 0000000000..48a8af42a6 --- /dev/null +++ b/pkg/client/codec.go @@ -0,0 +1,24 @@ +package client + +import ( + "errors" + "net/url" + + "k8s.io/apimachinery/pkg/conversion/queryparams" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +var _ runtime.ParameterCodec = noConversionParamCodec{} + +// noConversionParamCodec is a no-conversion codec for serializing parameters into URL query strings. +// it's useful in scenarios with the unstructured client and arbitrary resouces. +type noConversionParamCodec struct{} + +func (noConversionParamCodec) EncodeParameters(obj runtime.Object, to schema.GroupVersion) (url.Values, error) { + return queryparams.Convert(obj) +} + +func (noConversionParamCodec) DecodeParameters(parameters url.Values, from schema.GroupVersion, into runtime.Object) error { + return errors.New("DecodeParameters not implemented on noConversionParamCodec") +} diff --git a/pkg/client/typed_client.go b/pkg/client/typed_client.go index 6ddc1171d3..c95da09daf 100644 --- a/pkg/client/typed_client.go +++ b/pkg/client/typed_client.go @@ -25,7 +25,7 @@ import ( // client is a client.Client that reads and writes directly from/to an API server. It lazily initializes // new clients at the time they are used, and caches the client. type typedClient struct { - cache clientCache + cache *clientCache paramCodec runtime.ParameterCodec } diff --git a/pkg/client/unstructured_client.go b/pkg/client/unstructured_client.go index 440ad2f97c..06e410cb5a 100644 --- a/pkg/client/unstructured_client.go +++ b/pkg/client/unstructured_client.go @@ -21,101 +21,132 @@ import ( "fmt" "strings" - "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/dynamic" ) // client is a client.Client that reads and writes directly from/to an API server. It lazily initializes // new clients at the time they are used, and caches the client. type unstructuredClient struct { - client dynamic.Interface - restMapper meta.RESTMapper + cache *clientCache + paramCodec runtime.ParameterCodec } // Create implements client.Client -func (uc *unstructuredClient) Create(_ context.Context, obj runtime.Object, opts ...CreateOption) error { +func (uc *unstructuredClient) Create(ctx context.Context, obj runtime.Object, opts ...CreateOption) error { u, ok := obj.(*unstructured.Unstructured) if !ok { return fmt.Errorf("unstructured client did not understand object: %T", obj) } - createOpts := CreateOptions{} - createOpts.ApplyOptions(opts) - r, err := uc.getResourceInterface(u.GroupVersionKind(), u.GetNamespace()) - if err != nil { - return err - } - i, err := r.Create(u, *createOpts.AsCreateOptions()) + + gvk := u.GroupVersionKind() + + o, err := uc.cache.getObjMeta(obj) if err != nil { return err } - u.Object = i.Object - return nil + + createOpts := &CreateOptions{} + createOpts.ApplyOptions(opts) + result := o.Post(). + NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()). + Resource(o.resource()). + Body(obj). + VersionedParams(createOpts.AsCreateOptions(), uc.paramCodec). + Context(ctx). + Do(). + Into(obj) + + u.SetGroupVersionKind(gvk) + return result } // Update implements client.Client -func (uc *unstructuredClient) Update(_ context.Context, obj runtime.Object, opts ...UpdateOption) error { +func (uc *unstructuredClient) Update(ctx context.Context, obj runtime.Object, opts ...UpdateOption) error { u, ok := obj.(*unstructured.Unstructured) if !ok { return fmt.Errorf("unstructured client did not understand object: %T", obj) } - updateOpts := UpdateOptions{} - updateOpts.ApplyOptions(opts) - r, err := uc.getResourceInterface(u.GroupVersionKind(), u.GetNamespace()) - if err != nil { - return err - } - i, err := r.Update(u, *updateOpts.AsUpdateOptions()) + + gvk := u.GroupVersionKind() + + o, err := uc.cache.getObjMeta(obj) if err != nil { return err } - u.Object = i.Object - return nil + + updateOpts := UpdateOptions{} + updateOpts.ApplyOptions(opts) + result := o.Put(). + NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()). + Resource(o.resource()). + Name(o.GetName()). + Body(obj). + VersionedParams(updateOpts.AsUpdateOptions(), uc.paramCodec). + Context(ctx). + Do(). + Into(obj) + + u.SetGroupVersionKind(gvk) + return result } // Delete implements client.Client -func (uc *unstructuredClient) Delete(_ context.Context, obj runtime.Object, opts ...DeleteOption) error { - u, ok := obj.(*unstructured.Unstructured) +func (uc *unstructuredClient) Delete(ctx context.Context, obj runtime.Object, opts ...DeleteOption) error { + _, ok := obj.(*unstructured.Unstructured) if !ok { return fmt.Errorf("unstructured client did not understand object: %T", obj) } - r, err := uc.getResourceInterface(u.GroupVersionKind(), u.GetNamespace()) + + o, err := uc.cache.getObjMeta(obj) if err != nil { return err } + deleteOpts := DeleteOptions{} deleteOpts.ApplyOptions(opts) - err = r.Delete(u.GetName(), deleteOpts.AsDeleteOptions()) - return err + return o.Delete(). + NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()). + Resource(o.resource()). + Name(o.GetName()). + Body(deleteOpts.AsDeleteOptions()). + Context(ctx). + Do(). + Error() } // DeleteAllOf implements client.Client -func (uc *unstructuredClient) DeleteAllOf(_ context.Context, obj runtime.Object, opts ...DeleteAllOfOption) error { - u, ok := obj.(*unstructured.Unstructured) +func (uc *unstructuredClient) DeleteAllOf(ctx context.Context, obj runtime.Object, opts ...DeleteAllOfOption) error { + _, ok := obj.(*unstructured.Unstructured) if !ok { return fmt.Errorf("unstructured client did not understand object: %T", obj) } - r, err := uc.getResourceInterface(u.GroupVersionKind(), u.GetNamespace()) + + o, err := uc.cache.getObjMeta(obj) if err != nil { return err } deleteAllOfOpts := DeleteAllOfOptions{} deleteAllOfOpts.ApplyOptions(opts) - err = r.DeleteCollection(deleteAllOfOpts.AsDeleteOptions(), *deleteAllOfOpts.AsListOptions()) - return err + return o.Delete(). + NamespaceIfScoped(deleteAllOfOpts.ListOptions.Namespace, o.isNamespaced()). + Resource(o.resource()). + VersionedParams(deleteAllOfOpts.AsListOptions(), uc.paramCodec). + Body(deleteAllOfOpts.AsDeleteOptions()). + Context(ctx). + Do(). + Error() } // Patch implements client.Client -func (uc *unstructuredClient) Patch(_ context.Context, obj runtime.Object, patch Patch, opts ...PatchOption) error { - u, ok := obj.(*unstructured.Unstructured) +func (uc *unstructuredClient) Patch(ctx context.Context, obj runtime.Object, patch Patch, opts ...PatchOption) error { + _, ok := obj.(*unstructured.Unstructured) if !ok { return fmt.Errorf("unstructured client did not understand object: %T", obj) } - r, err := uc.getResourceInterface(u.GroupVersionKind(), u.GetNamespace()) + + o, err := uc.cache.getObjMeta(obj) if err != nil { return err } @@ -126,81 +157,105 @@ func (uc *unstructuredClient) Patch(_ context.Context, obj runtime.Object, patch } patchOpts := &PatchOptions{} - i, err := r.Patch(u.GetName(), patch.Type(), data, *patchOpts.ApplyOptions(opts).AsPatchOptions()) - if err != nil { - return err - } - u.Object = i.Object - return nil + return o.Patch(patch.Type()). + NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()). + Resource(o.resource()). + Name(o.GetName()). + VersionedParams(patchOpts.ApplyOptions(opts).AsPatchOptions(), uc.paramCodec). + Body(data). + Context(ctx). + Do(). + Into(obj) } // Get implements client.Client -func (uc *unstructuredClient) Get(_ context.Context, key ObjectKey, obj runtime.Object) error { +func (uc *unstructuredClient) Get(ctx context.Context, key ObjectKey, obj runtime.Object) error { u, ok := obj.(*unstructured.Unstructured) if !ok { return fmt.Errorf("unstructured client did not understand object: %T", obj) } - r, err := uc.getResourceInterface(u.GroupVersionKind(), key.Namespace) - if err != nil { - return err - } - i, err := r.Get(key.Name, metav1.GetOptions{}) + + gvk := u.GroupVersionKind() + + r, err := uc.cache.getResource(obj) if err != nil { return err } - u.Object = i.Object - return nil + + result := r.Get(). + NamespaceIfScoped(key.Namespace, r.isNamespaced()). + Resource(r.resource()). + Context(ctx). + Name(key.Name). + Do(). + Into(obj) + + u.SetGroupVersionKind(gvk) + + return result } // List implements client.Client -func (uc *unstructuredClient) List(_ context.Context, obj runtime.Object, opts ...ListOption) error { +func (uc *unstructuredClient) List(ctx context.Context, obj runtime.Object, opts ...ListOption) error { u, ok := obj.(*unstructured.UnstructuredList) if !ok { return fmt.Errorf("unstructured client did not understand object: %T", obj) } + gvk := u.GroupVersionKind() if strings.HasSuffix(gvk.Kind, "List") { gvk.Kind = gvk.Kind[:len(gvk.Kind)-4] } + listOpts := ListOptions{} listOpts.ApplyOptions(opts) - r, err := uc.getResourceInterface(gvk, listOpts.Namespace) - if err != nil { - return err - } - i, err := r.List(*listOpts.AsListOptions()) + r, err := uc.cache.getResource(obj) if err != nil { return err } - u.Items = i.Items - u.Object = i.Object - return nil + + return r.Get(). + NamespaceIfScoped(listOpts.Namespace, r.isNamespaced()). + Resource(r.resource()). + VersionedParams(listOpts.AsListOptions(), uc.paramCodec). + Context(ctx). + Do(). + Into(obj) } -func (uc *unstructuredClient) UpdateStatus(_ context.Context, obj runtime.Object, opts ...UpdateOption) error { - u, ok := obj.(*unstructured.Unstructured) +func (uc *unstructuredClient) UpdateStatus(ctx context.Context, obj runtime.Object, opts ...UpdateOption) error { + _, ok := obj.(*unstructured.Unstructured) if !ok { return fmt.Errorf("unstructured client did not understand object: %T", obj) } - r, err := uc.getResourceInterface(u.GroupVersionKind(), u.GetNamespace()) - if err != nil { - return err - } - i, err := r.UpdateStatus(u, *(&UpdateOptions{}).ApplyOptions(opts).AsUpdateOptions()) + + o, err := uc.cache.getObjMeta(obj) if err != nil { return err } - u.Object = i.Object - return nil + + return o.Put(). + NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()). + Resource(o.resource()). + Name(o.GetName()). + SubResource("status"). + Body(obj). + VersionedParams((&UpdateOptions{}).ApplyOptions(opts).AsUpdateOptions(), uc.paramCodec). + Context(ctx). + Do(). + Into(obj) } -func (uc *unstructuredClient) PatchStatus(_ context.Context, obj runtime.Object, patch Patch, opts ...PatchOption) error { +func (uc *unstructuredClient) PatchStatus(ctx context.Context, obj runtime.Object, patch Patch, opts ...PatchOption) error { u, ok := obj.(*unstructured.Unstructured) if !ok { return fmt.Errorf("unstructured client did not understand object: %T", obj) } - r, err := uc.getResourceInterface(u.GroupVersionKind(), u.GetNamespace()) + + gvk := u.GroupVersionKind() + + o, err := uc.cache.getObjMeta(obj) if err != nil { return err } @@ -210,21 +265,18 @@ func (uc *unstructuredClient) PatchStatus(_ context.Context, obj runtime.Object, return err } - i, err := r.Patch(u.GetName(), patch.Type(), data, *(&PatchOptions{}).ApplyOptions(opts).AsPatchOptions(), "status") - if err != nil { - return err - } - u.Object = i.Object - return nil -} + patchOpts := &PatchOptions{} + result := o.Patch(patch.Type()). + NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()). + Resource(o.resource()). + Name(o.GetName()). + SubResource("status"). + Body(data). + VersionedParams(patchOpts.ApplyOptions(opts).AsPatchOptions(), uc.paramCodec). + Context(ctx). + Do(). + Into(u) -func (uc *unstructuredClient) getResourceInterface(gvk schema.GroupVersionKind, ns string) (dynamic.ResourceInterface, error) { - mapping, err := uc.restMapper.RESTMapping(gvk.GroupKind(), gvk.Version) - if err != nil { - return nil, err - } - if mapping.Scope.Name() == meta.RESTScopeNameRoot { - return uc.client.Resource(mapping.Resource), nil - } - return uc.client.Resource(mapping.Resource).Namespace(ns), nil + u.SetGroupVersionKind(gvk) + return result }