From f06aa3ec8209a9826c18217169d01acaafe875e1 Mon Sep 17 00:00:00 2001 From: Shawn Hurley Date: Tue, 31 Jul 2018 15:30:01 -0400 Subject: [PATCH 1/8] Adding ability for clients, cache and watcher to work with unstructured * remove cache reader marshal/unmarshal * adding seperation between types and unstructured types in the cache --- pkg/cache/internal/informers_map.go | 110 +++++++++++++++++++--------- pkg/client/apiutil/apimachinery.go | 29 +++++++- pkg/client/client.go | 12 +-- pkg/client/client_cache.go | 50 +++++++++++-- 4 files changed, 152 insertions(+), 49 deletions(-) diff --git a/pkg/cache/internal/informers_map.go b/pkg/cache/internal/informers_map.go index 4564fc95e1..7b8aaec526 100644 --- a/pkg/cache/internal/informers_map.go +++ b/pkg/cache/internal/informers_map.go @@ -23,6 +23,7 @@ import ( "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/apimachinery/pkg/runtime/serializer" @@ -38,13 +39,14 @@ func NewInformersMap(config *rest.Config, mapper meta.RESTMapper, resync time.Duration) *InformersMap { ip := &InformersMap{ - config: config, - Scheme: scheme, - mapper: mapper, - informersByGVK: make(map[schema.GroupVersionKind]*MapEntry), - codecs: serializer.NewCodecFactory(scheme), - paramCodec: runtime.NewParameterCodec(scheme), - resync: resync, + config: config, + Scheme: scheme, + mapper: mapper, + informersByGVK: make(map[schema.GroupVersionKind]*MapEntry), + unstructuredInformerByGVK: make(map[schema.GroupVersionKind]*MapEntry), + codecs: serializer.NewCodecFactory(scheme), + paramCodec: runtime.NewParameterCodec(scheme), + resync: resync, } return ip } @@ -73,6 +75,10 @@ type InformersMap struct { // informersByGVK is the cache of informers keyed by groupVersionKind informersByGVK map[schema.GroupVersionKind]*MapEntry + // unstructuredInformerByGVK is a cache of informers for unstructured types + // keyed by groupVersionKind + unstructuredInformerByGVK map[schema.GroupVersionKind]*MapEntry + // codecs is used to create a new REST client codecs serializer.CodecFactory @@ -87,6 +93,8 @@ type InformersMap struct { // mu guards access to the map mu sync.RWMutex + // mu guards access to the unstructured map + unstructuredMu sync.RWMutex // start is true if the informers have been started started bool @@ -96,7 +104,9 @@ type InformersMap struct { func (ip *InformersMap) Start(stop <-chan struct{}) error { func() { ip.mu.Lock() + ip.unstructuredMu.Lock() defer ip.mu.Unlock() + defer ip.unstructuredMu.Unlock() // Set the stop channel so it can be passed to informers that are added later ip.stop = stop @@ -106,6 +116,11 @@ func (ip *InformersMap) Start(stop <-chan struct{}) error { go informer.Informer.Run(stop) } + // Start each unstructured informer + for _, informer := range ip.unstructuredInformerByGVK { + go informer.Informer.Run(stop) + } + // Set started to true so we immediately start any informers added later. ip.started = true }() @@ -115,23 +130,36 @@ func (ip *InformersMap) Start(stop <-chan struct{}) error { // WaitForCacheSync waits until all the caches have been synced func (ip *InformersMap) WaitForCacheSync(stop <-chan struct{}) bool { - syncedFuncs := make([]cache.InformerSynced, 0, len(ip.informersByGVK)) + syncedFuncs := make([]cache.InformerSynced, 0, len(ip.informersByGVK)+len(ip.unstructuredInformerByGVK)) for _, informer := range ip.informersByGVK { syncedFuncs = append(syncedFuncs, informer.Informer.HasSynced) } + for _, informer := range ip.unstructuredInformerByGVK { + syncedFuncs = append(syncedFuncs, informer.Informer.HasSynced) + } return cache.WaitForCacheSync(stop, syncedFuncs...) } +func (ip *InformersMap) getMapEntry(gvk schema.GroupVersionKind, isUnstructured bool) (*MapEntry, bool) { + if isUnstructured { + ip.unstructuredMu.RLock() + defer ip.unstructuredMu.RUnlock() + i, ok := ip.unstructuredInformerByGVK[gvk] + return i, ok + } + ip.mu.RLock() + defer ip.mu.RUnlock() + i, ok := ip.informersByGVK[gvk] + return i, ok + +} + // Get will create a new Informer and add it to the map of InformersMap if none exists. Returns // the Informer from the map. func (ip *InformersMap) Get(gvk schema.GroupVersionKind, obj runtime.Object) (*MapEntry, error) { + _, isUnstructured := obj.(*unstructured.Unstructured) // Return the informer if it is found - i, ok := func() (*MapEntry, bool) { - ip.mu.RLock() - defer ip.mu.RUnlock() - i, ok := ip.informersByGVK[gvk] - return i, ok - }() + i, ok := ip.getMapEntry(gvk, isUnstructured) if ok { return i, nil } @@ -140,21 +168,27 @@ func (ip *InformersMap) Get(gvk schema.GroupVersionKind, obj runtime.Object) (*M // need to be locked var sync bool i, err := func() (*MapEntry, error) { - ip.mu.Lock() - defer ip.mu.Unlock() - - // Check the cache to see if we already have an Informer. If we do, return the Informer. + var ok bool + var i *MapEntry + // Check the caches to see if we already have an Informer. If we do, return the Informer. // This is for the case where 2 routines tried to get the informer when it wasn't in the map // so neither returned early, but the first one created it. - var ok bool - i, ok := ip.informersByGVK[gvk] + if isUnstructured { + ip.unstructuredMu.Lock() + defer ip.unstructuredMu.Unlock() + i, ok = ip.unstructuredInformerByGVK[gvk] + } else { + ip.mu.Lock() + defer ip.mu.Unlock() + i, ok = ip.informersByGVK[gvk] + } if ok { return i, nil } // Create a NewSharedIndexInformer and add it to the map. var lw *cache.ListWatch - lw, err := ip.newListWatch(gvk) + lw, err := ip.newListWatch(gvk, isUnstructured) if err != nil { return nil, err } @@ -191,14 +225,7 @@ func (ip *InformersMap) Get(gvk schema.GroupVersionKind, obj runtime.Object) (*M } // newListWatch returns a new ListWatch object that can be used to create a SharedIndexInformer. -func (ip *InformersMap) newListWatch(gvk schema.GroupVersionKind) (*cache.ListWatch, error) { - // Construct a RESTClient for the groupVersionKind that we will use to - // talk to the apiserver. - client, err := apiutil.RESTClientForGVK(gvk, ip.config, ip.codecs) - if err != nil { - return nil, err - } - +func (ip *InformersMap) newListWatch(gvk schema.GroupVersionKind, isUnstructured bool) (*cache.ListWatch, error) { // Kubernetes APIs work against Resources, not GroupVersionKinds. Map the // groupVersionKind to the Resource API we will use. mapping, err := ip.mapper.RESTMapping(gvk.GroupKind(), gvk.Version) @@ -206,11 +233,26 @@ func (ip *InformersMap) newListWatch(gvk schema.GroupVersionKind) (*cache.ListWa return nil, err } - // Get a listObject for listing that the ListWatch can DeepCopy - listGVK := gvk.GroupVersion().WithKind(gvk.Kind + "List") - listObj, err := ip.Scheme.New(listGVK) - if err != nil { - return nil, err + // Construct a RESTClient for the groupVersionKind that we will use to + // talk to the apiserver. + var client rest.Interface + var listObj runtime.Object + if isUnstructured { + listObj = &unstructured.UnstructuredList{} + client, err = apiutil.RESTUnstructuredClientForGVK(gvk, ip.config) + if err != nil { + return nil, err + } + } else { + client, err = apiutil.RESTClientForGVK(gvk, ip.config, ip.codecs) + if err != nil { + return nil, err + } + listGVK := gvk.GroupVersion().WithKind(gvk.Kind + "List") + listObj, err = ip.Scheme.New(listGVK) + if err != nil { + return nil, err + } } // Create a new ListWatch for the obj diff --git a/pkg/client/apiutil/apimachinery.go b/pkg/client/apiutil/apimachinery.go index 9365eb2af0..425e7091c1 100644 --- a/pkg/client/apiutil/apimachinery.go +++ b/pkg/client/apiutil/apimachinery.go @@ -20,10 +20,12 @@ import ( "fmt" "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/discovery" + "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "k8s.io/client-go/restmapper" ) @@ -65,6 +67,29 @@ func GVKForObject(obj runtime.Object, scheme *runtime.Scheme) (schema.GroupVersi // RESTClientForGVK constructs a new rest.Interface capable of accessing the resource associated // with the given GroupVersionKind. func RESTClientForGVK(gvk schema.GroupVersionKind, baseConfig *rest.Config, codecs serializer.CodecFactory) (rest.Interface, error) { + cfg := createRestConfig(gvk, baseConfig) + cfg.NegotiatedSerializer = serializer.DirectCodecFactory{CodecFactory: codecs} + return rest.RESTClientFor(cfg) +} + +// RESTUnstructuredClientForGVK constructs a new rest.Interface for accessing unstructured resources. +func RESTUnstructuredClientForGVK(gvk schema.GroupVersionKind, baseConfig *rest.Config) (rest.Interface, error) { + cfg := createRestConfig(gvk, baseConfig) + var jsonInfo runtime.SerializerInfo + for _, info := range scheme.Codecs.SupportedMediaTypes() { + if info.MediaType == runtime.ContentTypeJSON { + jsonInfo = info + break + } + } + jsonInfo.Serializer = unstructured.UnstructuredJSONScheme + cfg.NegotiatedSerializer = serializer.NegotiatedSerializerWrapper(jsonInfo) + + return rest.RESTClientFor(cfg) +} + +//createRestConfig copies the base config and updates needed fields for a new rest config +func createRestConfig(gvk schema.GroupVersionKind, baseConfig *rest.Config) *rest.Config { gv := gvk.GroupVersion() cfg := rest.CopyConfig(baseConfig) @@ -74,9 +99,9 @@ func RESTClientForGVK(gvk schema.GroupVersionKind, baseConfig *rest.Config, code } else { cfg.APIPath = "/apis" } - cfg.NegotiatedSerializer = serializer.DirectCodecFactory{CodecFactory: codecs} if cfg.UserAgent == "" { cfg.UserAgent = rest.DefaultKubernetesUserAgent() } - return rest.RESTClientFor(cfg) + return cfg + } diff --git a/pkg/client/client.go b/pkg/client/client.go index 195d180dda..780c4386bf 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -23,6 +23,7 @@ import ( "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" @@ -60,11 +61,12 @@ func New(config *rest.Config, options Options) (Client, error) { c := &client{ cache: clientCache{ - config: config, - scheme: options.Scheme, - mapper: options.Mapper, - codecs: serializer.NewCodecFactory(options.Scheme), - resourceByType: make(map[reflect.Type]*resourceMeta), + config: config, + scheme: options.Scheme, + mapper: options.Mapper, + codecs: serializer.NewCodecFactory(options.Scheme), + resourceByType: make(map[reflect.Type]*resourceMeta), + unstructuredResourceByGVK: make(map[schema.GroupVersionKind]*resourceMeta), }, paramCodec: runtime.NewParameterCodec(options.Scheme), } diff --git a/pkg/client/client_cache.go b/pkg/client/client_cache.go index d6452ab62e..2b522b0c5d 100644 --- a/pkg/client/client_cache.go +++ b/pkg/client/client_cache.go @@ -23,6 +23,7 @@ import ( "k8s.io/apimachinery/pkg/api/meta" "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/apimachinery/pkg/runtime/serializer" @@ -46,7 +47,10 @@ type clientCache struct { // resourceByType caches type metadata resourceByType map[reflect.Type]*resourceMeta - mu sync.RWMutex + // resourceByGVK caches type metadata for unstructured + unstructuredResourceByGVK map[schema.GroupVersionKind]*resourceMeta + muByType sync.RWMutex + muByGVK sync.RWMutex } // newResource maps obj to a Kubernetes Resource and constructs a client for that Resource. @@ -73,24 +77,44 @@ func (c *clientCache) newResource(obj runtime.Object) (*resourceMeta, error) { return &resourceMeta{Interface: client, mapping: mapping, gvk: gvk}, nil } -// 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) { +func (c *clientCache) getUnstructuredResourceByGVK(obj runtime.Object) (*resourceMeta, error) { + // It's better to do creation work twice than to not let multiple + // people make requests at once + c.muByGVK.RLock() + r, known := c.unstructuredResourceByGVK[obj.GetObjectKind().GroupVersionKind()] + c.muByGVK.RUnlock() + + if known { + return r, nil + } + + // Initialize a new Client + c.muByGVK.Lock() + defer c.muByGVK.Unlock() + r, err := c.newResource(obj) + if err != nil { + return nil, err + } + c.unstructuredResourceByGVK[obj.GetObjectKind().GroupVersionKind()] = r + return r, err +} + +func (c *clientCache) getResourceByType(obj runtime.Object) (*resourceMeta, error) { typ := reflect.TypeOf(obj) // It's better to do creation work twice than to not let multiple // people make requests at once - c.mu.RLock() + c.muByType.RLock() r, known := c.resourceByType[typ] - c.mu.RUnlock() + c.muByType.RUnlock() if known { return r, nil } // Initialize a new Client - c.mu.Lock() - defer c.mu.Unlock() + c.muByType.Lock() + defer c.muByType.Unlock() r, err := c.newResource(obj) if err != nil { return nil, err @@ -99,6 +123,16 @@ func (c *clientCache) getResource(obj runtime.Object) (*resourceMeta, error) { return r, err } +// 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) { + _, isUnstructured := obj.(*unstructured.Unstructured) + if isUnstructured { + return c.getUnstructuredResourceByGVK(obj) + } + return c.getResourceByType(obj) +} + // getObjMeta returns objMeta containing both type and object metadata and state func (c *clientCache) getObjMeta(obj runtime.Object) (*objMeta, error) { r, err := c.getResource(obj) From dbc79c9dae624eab9db240a65884e3e9f46f2be1 Mon Sep 17 00:00:00 2001 From: Shawn Hurley Date: Tue, 7 Aug 2018 12:08:22 -0400 Subject: [PATCH 2/8] Adding delegating reader for get and list requests for unstructured objs * By default the client should not create informers for get and list requsts of unstructured types. --- pkg/client/client_cache.go | 8 +++++++- pkg/client/split.go | 33 +++++++++++++++++++++++++++++++++ pkg/manager/manager.go | 9 ++++++++- 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/pkg/client/client_cache.go b/pkg/client/client_cache.go index 2b522b0c5d..b7ba18d0c1 100644 --- a/pkg/client/client_cache.go +++ b/pkg/client/client_cache.go @@ -66,7 +66,13 @@ func (c *clientCache) newResource(obj runtime.Object) (*resourceMeta, error) { gvk.Kind = gvk.Kind[:len(gvk.Kind)-4] } - client, err := apiutil.RESTClientForGVK(gvk, c.config, c.codecs) + _, isUnstructured := obj.(*unstructured.Unstructured) + var client rest.Interface + if isUnstructured { + client, err = apiutil.RESTUnstructuredClientForGVK(gvk, c.config) + } else { + client, err = apiutil.RESTClientForGVK(gvk, c.config, c.codecs) + } if err != nil { return nil, err } diff --git a/pkg/client/split.go b/pkg/client/split.go index 24e49071bf..efcf6d4c31 100644 --- a/pkg/client/split.go +++ b/pkg/client/split.go @@ -16,6 +16,13 @@ limitations under the License. package client +import ( + "context" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" +) + // DelegatingClient forms an interface Client by composing separate // reader, writer and statusclient interfaces. This way, you can have an Client that // reads from a cache and writes to the API server. @@ -24,3 +31,29 @@ type DelegatingClient struct { Writer StatusClient } + +// DelegatingReader forms a interface Reader that will cause Get and List +// requests for unstructured types to use the ClientReader while +// requests for any other type of object with use the CacheReader. +type DelegatingReader struct { + CacheReader Reader + ClientReader Reader +} + +// Get retrieves an obj for a given object key from the Kubernetes Cluster. +func (d *DelegatingReader) Get(ctx context.Context, key ObjectKey, obj runtime.Object) error { + _, isUnstructured := obj.(*unstructured.Unstructured) + if isUnstructured { + return d.ClientReader.Get(ctx, key, obj) + } + return d.CacheReader.Get(ctx, key, obj) +} + +// List retrieves list of objects for a given namespace and list options. +func (d *DelegatingReader) List(ctx context.Context, opts *ListOptions, list runtime.Object) error { + _, isUnstructured := list.(*unstructured.UnstructuredList) + if isUnstructured { + return d.ClientReader.List(ctx, opts, list) + } + return d.CacheReader.List(ctx, opts, list) +} diff --git a/pkg/manager/manager.go b/pkg/manager/manager.go index cd093a2d96..e4cdbb22c4 100644 --- a/pkg/manager/manager.go +++ b/pkg/manager/manager.go @@ -187,7 +187,14 @@ func New(config *rest.Config, options Options) (Manager, error) { errChan: make(chan error), cache: cache, fieldIndexes: cache, - client: client.DelegatingClient{Reader: cache, Writer: writeObj, StatusClient: writeObj}, + client: client.DelegatingClient{ + Reader: &client.DelegatingReader{ + CacheReader: cache, + ClientReader: writeObj, + }, + Writer: writeObj, + StatusClient: writeObj, + }, recorderProvider: recorderProvider, resourceLock: resourceLock, mapper: mapper, From 07715f66450d504dd63c90b72065e3f7bdf95ee3 Mon Sep 17 00:00:00 2001 From: Shawn Hurley Date: Wed, 8 Aug 2018 11:12:14 -0400 Subject: [PATCH 3/8] Adding test cases for getting and listing unstructured types. --- pkg/client/client.go | 1 + pkg/client/client_cache.go | 3 +- pkg/client/client_test.go | 59 ++++++++++++++++++++++++++++++++------ 3 files changed, 54 insertions(+), 9 deletions(-) diff --git a/pkg/client/client.go b/pkg/client/client.go index 780c4386bf..1ac8bd65e2 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -158,6 +158,7 @@ func (c *client) List(ctx context.Context, opts *ListOptions, obj runtime.Object VersionedParams(opts.AsListOptions(), c.paramCodec). Do(). Into(obj) + return err } // Status implements client.StatusClient diff --git a/pkg/client/client_cache.go b/pkg/client/client_cache.go index b7ba18d0c1..30b7240d1b 100644 --- a/pkg/client/client_cache.go +++ b/pkg/client/client_cache.go @@ -133,7 +133,8 @@ func (c *clientCache) getResourceByType(obj runtime.Object) (*resourceMeta, erro // If the object is a list, the resource represents the item's type instead. func (c *clientCache) getResource(obj runtime.Object) (*resourceMeta, error) { _, isUnstructured := obj.(*unstructured.Unstructured) - if isUnstructured { + _, isUnstructuredList := obj.(*unstructured.UnstructuredList) + if isUnstructured || isUnstructuredList { return c.getUnstructuredResourceByGVK(obj) } return c.getResourceByType(obj) diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index 072322c8ed..ecf935221c 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -28,6 +28,7 @@ import ( 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" "sigs.k8s.io/controller-runtime/pkg/client" kscheme "k8s.io/client-go/kubernetes/scheme" @@ -642,23 +643,28 @@ var _ = Describe("Client", func() { dep, err := clientset.AppsV1().Deployments(ns).Create(dep) Expect(err).NotTo(HaveOccurred()) - By("encoding the Deployment as unstructured") - var u runtime.Unstructured = &unstructured.Unstructured{} - scheme.Convert(dep, u, nil) - cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) + By("encoding the Deployment as unstructured") + var u runtime.Unstructured = &unstructured.Unstructured{} + scheme.Convert(dep, u, nil) + By("fetching the created Deployment") - var actual appsv1.Deployment + var actual unstructured.Unstructured + actual.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "apps", + Kind: "Deployment", + Version: "v1", + }) key := client.ObjectKey{Namespace: ns, Name: dep.Name} err = cl.Get(context.TODO(), key, &actual) Expect(err).NotTo(HaveOccurred()) Expect(actual).NotTo(BeNil()) By("validating the fetched Deployment equals the created one") - Expect(dep).To(Equal(&actual)) + Expect(u).To(Equal(&actual)) close(done) }) @@ -698,14 +704,19 @@ var _ = Describe("Client", func() { Expect(cl).NotTo(BeNil()) By("fetching the created Node") - var actual corev1.Node + var actual unstructured.Unstructured + actual.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "", + Kind: "Node", + Version: "v1", + }) key := client.ObjectKey{Namespace: ns, Name: node.Name} err = cl.Get(context.TODO(), key, &actual) Expect(err).NotTo(HaveOccurred()) Expect(actual).NotTo(BeNil()) By("validating the fetched Node equals the created one") - Expect(node).To(Equal(&actual)) + Expect(u).To(Equal(&actual)) close(done) }) @@ -778,6 +789,38 @@ var _ = Describe("Client", func() { close(done) }, serverSideTimeoutSeconds) + It("should fetch unstructured collection of objects", func(done Done) { + By("create an initial object") + _, err := clientset.AppsV1().Deployments(ns).Create(dep) + Expect(err).NotTo(HaveOccurred()) + + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + + By("listing all objects of that type in the cluster") + deps := &unstructured.UnstructuredList{} + deps.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "apps", + Kind: "DeploymentList", + Version: "v1", + }) + err = cl.List(context.Background(), nil, deps) + Expect(err).NotTo(HaveOccurred()) + + Expect(deps.Items).NotTo(BeEmpty()) + hasDep := false + for _, item := range deps.Items { + if item.GetName() == dep.Name && item.GetNamespace() == dep.Namespace { + fmt.Printf("HERE!!!!!!!! ITEM: %v\n\n", item) + hasDep = true + fmt.Printf("HERE hasDep: %v\n\n", hasDep) + break + } + } + Expect(hasDep).To(BeTrue()) + close(done) + }, serverSideTimeoutSeconds) + It("should return an empty list if there are no matching objects", func(done Done) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) From 247e85ae61c1686b1db53823db55344991aa109c Mon Sep 17 00:00:00 2001 From: Shawn Hurley Date: Thu, 9 Aug 2018 13:14:24 -0400 Subject: [PATCH 4/8] Adding unstructured clients testing. --- pkg/client/apiutil/apimachinery.go | 34 +- pkg/client/client_test.go | 1825 ++++++++++++++++++---------- 2 files changed, 1231 insertions(+), 628 deletions(-) diff --git a/pkg/client/apiutil/apimachinery.go b/pkg/client/apiutil/apimachinery.go index 425e7091c1..c9419842fb 100644 --- a/pkg/client/apiutil/apimachinery.go +++ b/pkg/client/apiutil/apimachinery.go @@ -17,7 +17,12 @@ limitations under the License. package apiutil import ( + "encoding/json" "fmt" + "io" + "strings" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -82,7 +87,7 @@ func RESTUnstructuredClientForGVK(gvk schema.GroupVersionKind, baseConfig *rest. break } } - jsonInfo.Serializer = unstructured.UnstructuredJSONScheme + jsonInfo.Serializer = dynamicCodec{} cfg.NegotiatedSerializer = serializer.NegotiatedSerializerWrapper(jsonInfo) return rest.RESTClientFor(cfg) @@ -105,3 +110,30 @@ func createRestConfig(gvk schema.GroupVersionKind, baseConfig *rest.Config) *res return cfg } + +//Copied from deprecated-dynamic/bad_debt.go +// dynamicCodec is a codec that wraps the standard unstructured codec +// with special handling for Status objects. +// Deprecated only used by test code and its wrong +type dynamicCodec struct{} + +func (dynamicCodec) Decode(data []byte, gvk *schema.GroupVersionKind, obj runtime.Object) (runtime.Object, *schema.GroupVersionKind, error) { + obj, gvk, err := unstructured.UnstructuredJSONScheme.Decode(data, gvk, obj) + if err != nil { + return nil, nil, err + } + + if _, ok := obj.(*metav1.Status); !ok && strings.ToLower(gvk.Kind) == "status" { + obj = &metav1.Status{} + err := json.Unmarshal(data, obj) + if err != nil { + return nil, nil, err + } + } + + return obj, gvk, nil +} + +func (dynamicCodec) Encode(obj runtime.Object, w io.Writer) error { + return unstructured.UnstructuredJSONScheme.Encode(obj, w) +} diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index ecf935221c..4d1f9beacb 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -25,6 +25,7 @@ import ( . "github.com/onsi/gomega" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -161,236 +162,381 @@ var _ = Describe("Client", func() { }) Describe("Create", func() { - It("should create a new object from a go struct", func(done Done) { - cl, err := client.New(cfg, client.Options{}) - Expect(err).NotTo(HaveOccurred()) - Expect(cl).NotTo(BeNil()) + Context("with structured objects", func() { + It("should create a new object from a go struct", func(done Done) { + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) - By("creating the object") - err = cl.Create(context.TODO(), dep) - Expect(err).NotTo(HaveOccurred()) + By("creating the object") + err = cl.Create(context.TODO(), dep) + Expect(err).NotTo(HaveOccurred()) - actual, err := clientset.AppsV1().Deployments(ns).Get(dep.Name, metav1.GetOptions{}) - Expect(err).NotTo(HaveOccurred()) - Expect(actual).NotTo(BeNil()) + actual, err := clientset.AppsV1().Deployments(ns).Get(dep.Name, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(actual).NotTo(BeNil()) - By("writing the result back to the go struct") - Expect(dep).To(Equal(actual)) + By("writing the result back to the go struct") + Expect(dep).To(Equal(actual)) - close(done) - }) - - It("should create a new object from an unstructured object", func(done Done) { - cl, err := client.New(cfg, client.Options{}) - Expect(err).NotTo(HaveOccurred()) - Expect(cl).NotTo(BeNil()) - - By("encoding the Deployment as unstructured") - var u runtime.Unstructured = &unstructured.Unstructured{} - scheme.Convert(dep, u, nil) - - By("creating the unstructured Deployment") - err = cl.Create(context.TODO(), u) - Expect(err).NotTo(HaveOccurred()) + close(done) + }) - By("fetching newly created unstructured Deployment") - actual, err := clientset.AppsV1().Deployments(ns).Get(dep.Name, metav1.GetOptions{}) - Expect(err).NotTo(HaveOccurred()) - Expect(actual).NotTo(BeNil()) + It("should create a new object non-namespace object from a go struct", func(done Done) { + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) - close(done) - }) + By("creating the object") + err = cl.Create(context.TODO(), node) + Expect(err).NotTo(HaveOccurred()) - It("should create a new object non-namespace object from a go struct", func(done Done) { - cl, err := client.New(cfg, client.Options{}) - Expect(err).NotTo(HaveOccurred()) - Expect(cl).NotTo(BeNil()) + actual, err := clientset.CoreV1().Nodes().Get(node.Name, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(actual).NotTo(BeNil()) - By("creating the object") - err = cl.Create(context.TODO(), node) - Expect(err).NotTo(HaveOccurred()) + By("writing the result back to the go struct") + Expect(node).To(Equal(actual)) - actual, err := clientset.CoreV1().Nodes().Get(node.Name, metav1.GetOptions{}) - Expect(err).NotTo(HaveOccurred()) - Expect(actual).NotTo(BeNil()) + close(done) + }) - By("writing the result back to the go struct") - Expect(node).To(Equal(actual)) + It("should fail if the object already exists", func(done Done) { + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) - close(done) - }) + old := dep.DeepCopy() - It("should fail if the object already exists", func(done Done) { - cl, err := client.New(cfg, client.Options{}) - Expect(err).NotTo(HaveOccurred()) - Expect(cl).NotTo(BeNil()) + By("creating the object") + err = cl.Create(context.TODO(), dep) + Expect(err).NotTo(HaveOccurred()) - old := dep.DeepCopy() + actual, err := clientset.AppsV1().Deployments(ns).Get(dep.Name, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(actual).NotTo(BeNil()) - By("creating the object") - err = cl.Create(context.TODO(), dep) - Expect(err).NotTo(HaveOccurred()) + By("creating the object a second time") + err = cl.Create(context.TODO(), old) + Expect(err).To(HaveOccurred()) + Expect(errors.IsAlreadyExists(err)).To(BeTrue()) - actual, err := clientset.AppsV1().Deployments(ns).Get(dep.Name, metav1.GetOptions{}) - Expect(err).NotTo(HaveOccurred()) - Expect(actual).NotTo(BeNil()) + close(done) + }) - By("creating the object a second time") - err = cl.Create(context.TODO(), old) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("already exists")) + It("should fail if the object does not pass server-side validation", func(done Done) { + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) + + By("creating the pod, since required field Containers is empty") + err = cl.Create(context.TODO(), pod) + Expect(err).To(HaveOccurred()) + // TODO(seans): Add test to validate the returned error. Problems currently with + // different returned error locally versus travis. + + close(done) + }, serverSideTimeoutSeconds) + + It("should fail if the object cannot be mapped to a GVK", func() { + By("creating client with empty Scheme") + emptyScheme := runtime.NewScheme() + cl, err := client.New(cfg, client.Options{Scheme: emptyScheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) + + By("creating the object fails") + err = cl.Create(context.TODO(), dep) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("no kind is registered for the type")) + }) - close(done) + PIt("should fail if the GVK cannot be mapped to a Resource", func() { + // TODO(seans3): implement these + // Example: ListOptions + }) }) - It("should fail if the object does not pass server-side validation", func(done Done) { - cl, err := client.New(cfg, client.Options{}) - Expect(err).NotTo(HaveOccurred()) - Expect(cl).NotTo(BeNil()) + Context("with unstructured objects", func() { + It("should create a new object from a go struct", func(done Done) { + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) + + By("encoding the deployment as unstructured") + u := &unstructured.Unstructured{} + scheme.Convert(dep, u, nil) + u.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "apps", + Kind: "Deployment", + Version: "v1", + }) + + By("creating the object") + err = cl.Create(context.TODO(), u) + Expect(err).NotTo(HaveOccurred()) + + actual, err := clientset.AppsV1().Deployments(ns).Get(dep.Name, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(actual).NotTo(BeNil()) + close(done) + }) - By("creating the pod, since required field Containers is empty") - err = cl.Create(context.TODO(), pod) - Expect(err).To(HaveOccurred()) - // TODO(seans): Add test to validate the returned error. Problems currently with - // different returned error locally versus travis. + It("should create a new non-namespace object ", func(done Done) { + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) + + By("encoding the deployment as unstructured") + u := &unstructured.Unstructured{} + scheme.Convert(node, u, nil) + u.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "", + Kind: "Node", + Version: "v1", + }) + + By("creating the object") + err = cl.Create(context.TODO(), node) + Expect(err).NotTo(HaveOccurred()) + + actual, err := clientset.CoreV1().Nodes().Get(node.Name, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(actual).NotTo(BeNil()) + au := &unstructured.Unstructured{} + scheme.Convert(actual, au, nil) + scheme.Convert(node, u, nil) + By("writing the result back to the go struct") + + Expect(u).To(Equal(au)) + + close(done) + }) - close(done) - }, serverSideTimeoutSeconds) + It("should fail if the object already exists", func(done Done) { + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) + + old := dep.DeepCopy() + + By("creating the object") + err = cl.Create(context.TODO(), dep) + Expect(err).NotTo(HaveOccurred()) + actual, err := clientset.AppsV1().Deployments(ns).Get(dep.Name, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(actual).NotTo(BeNil()) + + By("encoding the deployment as unstructured") + u := &unstructured.Unstructured{} + scheme.Convert(old, u, nil) + u.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "apps", + Kind: "Deployment", + Version: "v1", + }) + + By("creating the object a second time") + err = cl.Create(context.TODO(), u) + Expect(err).To(HaveOccurred()) + Expect(errors.IsAlreadyExists(err)).To(BeTrue()) + + close(done) + }) - It("should fail if the object cannot be mapped to a GVK", func() { - By("creating client with empty Scheme") - emptyScheme := runtime.NewScheme() - cl, err := client.New(cfg, client.Options{Scheme: emptyScheme}) - Expect(err).NotTo(HaveOccurred()) - Expect(cl).NotTo(BeNil()) + It("should fail if the object does not pass server-side validation", func(done Done) { + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) + + By("creating the pod, since required field Containers is empty") + u := &unstructured.Unstructured{} + scheme.Convert(pod, u, nil) + u.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "Pod", + }) + err = cl.Create(context.TODO(), u) + Expect(err).To(HaveOccurred()) + // TODO(seans): Add test to validate the returned error. Problems currently with + // different returned error locally versus travis. + + close(done) + }, serverSideTimeoutSeconds) - By("creating the object fails") - err = cl.Create(context.TODO(), dep) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("no kind is registered for the type")) }) - PIt("should fail if the GVK cannot be mapped to a Resource", func() { - // TODO(seans3): implement these - // Example: ListOptions - }) }) Describe("Update", func() { - It("should update an existing object from a go struct", 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 Deployment") - dep.Annotations = map[string]string{"foo": "bar"} - err = cl.Update(context.TODO(), dep) - Expect(err).NotTo(HaveOccurred()) - - By("validating updated Deployment has new annotation") - actual, err := clientset.AppsV1().Deployments(ns).Get(dep.Name, metav1.GetOptions{}) - Expect(err).NotTo(HaveOccurred()) - Expect(actual).NotTo(BeNil()) - Expect(actual.Annotations["foo"]).To(Equal("bar")) - - close(done) - }) - - It("should update an existing new object from an unstructured object", 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 and encoding the Deployment as unstructured") - var u runtime.Unstructured = &unstructured.Unstructured{} - dep.Annotations = map[string]string{"foo": "bar"} - scheme.Convert(dep, u, nil) - - By("updating the Deployment") - err = cl.Update(context.TODO(), u) - Expect(err).NotTo(HaveOccurred()) + Context("with structured objects", func() { + It("should update an existing object from a go struct", 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 Deployment") + dep.Annotations = map[string]string{"foo": "bar"} + err = cl.Update(context.TODO(), dep) + Expect(err).NotTo(HaveOccurred()) + + By("validating updated Deployment has new annotation") + actual, err := clientset.AppsV1().Deployments(ns).Get(dep.Name, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(actual).NotTo(BeNil()) + Expect(actual.Annotations["foo"]).To(Equal("bar")) + + close(done) + }) - By("fetching newly created unstructured Deployment has new annotation") - actual, err := clientset.AppsV1().Deployments(ns).Get(dep.Name, metav1.GetOptions{}) - Expect(err).NotTo(HaveOccurred()) - Expect(actual).NotTo(BeNil()) - Expect(actual.Annotations["foo"]).To(Equal("bar")) + It("should update an existing object non-namespace object from a go struct", func(done Done) { + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) - close(done) - }) + node, err := clientset.CoreV1().Nodes().Create(node) + Expect(err).NotTo(HaveOccurred()) - It("should update an existing object non-namespace object from a go struct", func(done Done) { - cl, err := client.New(cfg, client.Options{}) - Expect(err).NotTo(HaveOccurred()) - Expect(cl).NotTo(BeNil()) + By("updating the object") + node.Annotations = map[string]string{"foo": "bar"} + err = cl.Update(context.TODO(), node) + Expect(err).NotTo(HaveOccurred()) - node, err := clientset.CoreV1().Nodes().Create(node) - Expect(err).NotTo(HaveOccurred()) + By("validate updated Node had new annotation") + actual, err := clientset.CoreV1().Nodes().Get(node.Name, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(actual).NotTo(BeNil()) + Expect(actual.Annotations["foo"]).To(Equal("bar")) - By("updating the object") - node.Annotations = map[string]string{"foo": "bar"} - err = cl.Update(context.TODO(), node) - Expect(err).NotTo(HaveOccurred()) + close(done) + }) - By("validate updated Node had new annotation") - actual, err := clientset.CoreV1().Nodes().Get(node.Name, metav1.GetOptions{}) - Expect(err).NotTo(HaveOccurred()) - Expect(actual).NotTo(BeNil()) - Expect(actual.Annotations["foo"]).To(Equal("bar")) + It("should fail if the object does not exists", func(done Done) { + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) - close(done) - }) + By("updating non-existent object") + err = cl.Update(context.TODO(), dep) + Expect(err).To(HaveOccurred()) - It("should fail if the object does not exists", func(done Done) { - cl, err := client.New(cfg, client.Options{}) - Expect(err).NotTo(HaveOccurred()) - Expect(cl).NotTo(BeNil()) + close(done) + }) - By("updating non-existent object") - err = cl.Update(context.TODO(), dep) - Expect(err).To(HaveOccurred()) + PIt("should fail if the object does not pass server-side validation", func() { - close(done) - }) + }) - PIt("should fail if the object does not pass server-side validation", func() { + PIt("should fail if the object doesn't have meta", func() { - }) + }) - PIt("should fail if the object doesn't have meta", func() { + It("should fail if the object cannot be mapped to a GVK", func(done Done) { + By("creating client with empty Scheme") + emptyScheme := runtime.NewScheme() + cl, err := client.New(cfg, client.Options{Scheme: emptyScheme}) + 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()) - It("should fail if the object cannot be mapped to a GVK", func(done Done) { - By("creating client with empty Scheme") - emptyScheme := runtime.NewScheme() - cl, err := client.New(cfg, client.Options{Scheme: emptyScheme}) - Expect(err).NotTo(HaveOccurred()) - Expect(cl).NotTo(BeNil()) + By("updating the Deployment") + dep.Annotations = map[string]string{"foo": "bar"} + err = cl.Update(context.TODO(), dep) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("no kind is registered for the type")) - By("initially creating a Deployment") - dep, err := clientset.AppsV1().Deployments(ns).Create(dep) - Expect(err).NotTo(HaveOccurred()) + close(done) + }) - By("updating the Deployment") - dep.Annotations = map[string]string{"foo": "bar"} - err = cl.Update(context.TODO(), dep) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("no kind is registered for the type")) + PIt("should fail if the GVK cannot be mapped to a Resource", func() { - close(done) + }) }) + Context("with unstructured objects", func() { + It("should update an existing object from a go struct", 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 Deployment") + u := &unstructured.Unstructured{} + scheme.Convert(dep, u, nil) + u.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "apps", + Kind: "Deployment", + Version: "v1", + }) + u.SetAnnotations(map[string]string{"foo": "bar"}) + err = cl.Update(context.TODO(), u) + Expect(err).NotTo(HaveOccurred()) + + By("validating updated Deployment has new annotation") + actual, err := clientset.AppsV1().Deployments(ns).Get(dep.Name, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(actual).NotTo(BeNil()) + Expect(actual.Annotations["foo"]).To(Equal("bar")) + + close(done) + }) - PIt("should fail if the GVK cannot be mapped to a Resource", func() { - + It("should update an existing object non-namespace object from a go struct", func(done Done) { + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) + + node, err := clientset.CoreV1().Nodes().Create(node) + Expect(err).NotTo(HaveOccurred()) + + By("updating the object") + u := &unstructured.Unstructured{} + scheme.Convert(node, u, nil) + u.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "", + Kind: "Node", + Version: "v1", + }) + u.SetAnnotations(map[string]string{"foo": "bar"}) + err = cl.Update(context.TODO(), u) + Expect(err).NotTo(HaveOccurred()) + + By("validate updated Node had new annotation") + actual, err := clientset.CoreV1().Nodes().Get(node.Name, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(actual).NotTo(BeNil()) + Expect(actual.Annotations["foo"]).To(Equal("bar")) + + close(done) + }) + It("should fail if the object does not exists", func(done Done) { + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) + + By("updating non-existent object") + u := &unstructured.Unstructured{} + scheme.Convert(dep, u, nil) + u.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "apps", + Kind: "Deployment", + Version: "v1", + }) + err = cl.Update(context.TODO(), dep) + Expect(err).To(HaveOccurred()) + + close(done) + }) }) }) @@ -508,516 +654,867 @@ var _ = Describe("Client", func() { }) Describe("Delete", func() { - It("should delete an existing object from a go struct", 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("deleting the Deployment") - depName := dep.Name - err = cl.Delete(context.TODO(), dep) - Expect(err).NotTo(HaveOccurred()) - - By("validating the Deployment no longer exists") - _, err = clientset.AppsV1().Deployments(ns).Get(depName, metav1.GetOptions{}) - Expect(err).To(HaveOccurred()) - - close(done) - }) - - It("should delete an existing from an unstructured object", func(done Done) { - cl, err := client.New(cfg, client.Options{}) - Expect(err).NotTo(HaveOccurred()) - Expect(cl).NotTo(BeNil()) + Context("with structured objects", func() { + It("should delete an existing object from a go struct", 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("deleting the Deployment") + depName := dep.Name + err = cl.Delete(context.TODO(), dep) + Expect(err).NotTo(HaveOccurred()) + + By("validating the Deployment no longer exists") + _, err = clientset.AppsV1().Deployments(ns).Get(depName, metav1.GetOptions{}) + Expect(err).To(HaveOccurred()) + + close(done) + }) - By("initially creating a Deployment") - dep, err := clientset.AppsV1().Deployments(ns).Create(dep) - Expect(err).NotTo(HaveOccurred()) + It("should delete an existing object non-namespace object from a go struct", func(done Done) { + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) - By("encoding the Deployment as unstructured") - var u runtime.Unstructured = &unstructured.Unstructured{} - scheme.Convert(dep, u, nil) + By("initially creating a Node") + node, err := clientset.CoreV1().Nodes().Create(node) + Expect(err).NotTo(HaveOccurred()) - By("deleting the unstructured Deployment") - depName := dep.Name - err = cl.Delete(context.TODO(), u) - Expect(err).NotTo(HaveOccurred()) + By("deleting the Node") + nodeName := node.Name + err = cl.Delete(context.TODO(), node) + Expect(err).NotTo(HaveOccurred()) - By("fetching newly created unstructured Deployment") - _, err = clientset.AppsV1().Deployments(ns).Get(depName, metav1.GetOptions{}) - Expect(err).To(HaveOccurred()) + By("validating the Node no longer exists") + _, err = clientset.CoreV1().Nodes().Get(nodeName, metav1.GetOptions{}) + Expect(err).To(HaveOccurred()) - close(done) - }) - - It("should delete an existing object non-namespace object from a go struct", func(done Done) { - cl, err := client.New(cfg, client.Options{}) - Expect(err).NotTo(HaveOccurred()) - Expect(cl).NotTo(BeNil()) - - By("initially creating a Node") - node, err := clientset.CoreV1().Nodes().Create(node) - Expect(err).NotTo(HaveOccurred()) + close(done) + }) - By("deleting the Node") - nodeName := node.Name - err = cl.Delete(context.TODO(), node) - Expect(err).NotTo(HaveOccurred()) + It("should fail if the object does not exists", func(done Done) { + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) - By("validating the Node no longer exists") - _, err = clientset.CoreV1().Nodes().Get(nodeName, metav1.GetOptions{}) - Expect(err).To(HaveOccurred()) + By("Deleting node before it is ever created") + err = cl.Delete(context.TODO(), node) + Expect(err).To(HaveOccurred()) - close(done) - }) - - It("should fail if the object does not exists", func(done Done) { - cl, err := client.New(cfg, client.Options{}) - Expect(err).NotTo(HaveOccurred()) - Expect(cl).NotTo(BeNil()) + close(done) + }) - By("Deleting node before it is ever created") - err = cl.Delete(context.TODO(), node) - Expect(err).To(HaveOccurred()) + PIt("should fail if the object doesn't have meta", func() { - close(done) - }) + }) - PIt("should fail if the object doesn't have meta", func() { + It("should fail if the object cannot be mapped to a GVK", func(done Done) { + By("creating client with empty Scheme") + emptyScheme := runtime.NewScheme() + cl, err := client.New(cfg, client.Options{Scheme: emptyScheme}) + 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()) - It("should fail if the object cannot be mapped to a GVK", func(done Done) { - By("creating client with empty Scheme") - emptyScheme := runtime.NewScheme() - cl, err := client.New(cfg, client.Options{Scheme: emptyScheme}) - Expect(err).NotTo(HaveOccurred()) - Expect(cl).NotTo(BeNil()) + By("deleting the Deployment fails") + err = cl.Delete(context.TODO(), dep) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("no kind is registered for the type")) - By("initially creating a Deployment") - dep, err := clientset.AppsV1().Deployments(ns).Create(dep) - Expect(err).NotTo(HaveOccurred()) + close(done) + }) - By("deleting the Deployment fails") - err = cl.Delete(context.TODO(), dep) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("no kind is registered for the type")) + PIt("should fail if the GVK cannot be mapped to a Resource", func() { - close(done) + }) }) + Context("with unstructured objects", func() { + It("should delete an existing object from a go struct", 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("deleting the Deployment") + depName := dep.Name + u := &unstructured.Unstructured{} + scheme.Convert(dep, u, nil) + u.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "apps", + Kind: "Deployment", + Version: "v1", + }) + err = cl.Delete(context.TODO(), u) + Expect(err).NotTo(HaveOccurred()) + + By("validating the Deployment no longer exists") + _, err = clientset.AppsV1().Deployments(ns).Get(depName, metav1.GetOptions{}) + Expect(err).To(HaveOccurred()) + + close(done) + }) - PIt("should fail if the GVK cannot be mapped to a Resource", func() { + It("should delete an existing object non-namespace object from a go struct", func(done Done) { + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) + + By("initially creating a Node") + node, err := clientset.CoreV1().Nodes().Create(node) + Expect(err).NotTo(HaveOccurred()) + + By("deleting the Node") + nodeName := node.Name + u := &unstructured.Unstructured{} + scheme.Convert(node, u, nil) + u.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "", + Kind: "Node", + Version: "v1", + }) + err = cl.Delete(context.TODO(), u) + Expect(err).NotTo(HaveOccurred()) + + By("validating the Node no longer exists") + _, err = clientset.CoreV1().Nodes().Get(nodeName, metav1.GetOptions{}) + Expect(err).To(HaveOccurred()) + + close(done) + }) + It("should fail if the object does not exists", func(done Done) { + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) + + By("Deleting node before it is ever created") + u := &unstructured.Unstructured{} + scheme.Convert(node, u, nil) + u.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "", + Kind: "Node", + Version: "v1", + }) + err = cl.Delete(context.TODO(), node) + Expect(err).To(HaveOccurred()) + + close(done) + }) }) }) Describe("Get", func() { - It("should fetch an existing object for a go struct", func(done Done) { - By("first creating the Deployment") - dep, err := clientset.AppsV1().Deployments(ns).Create(dep) - Expect(err).NotTo(HaveOccurred()) - - cl, err := client.New(cfg, client.Options{}) - Expect(err).NotTo(HaveOccurred()) - Expect(cl).NotTo(BeNil()) - - By("fetching the created Deployment") - var actual appsv1.Deployment - key := client.ObjectKey{Namespace: ns, Name: dep.Name} - err = cl.Get(context.TODO(), key, &actual) - Expect(err).NotTo(HaveOccurred()) - Expect(actual).NotTo(BeNil()) - - By("validating the fetched deployment equals the created one") - Expect(dep).To(Equal(&actual)) - - close(done) - }) - - It("should fetch an existing object for an unstructured", func(done Done) { - By("first creating the Deployment") - dep, err := clientset.AppsV1().Deployments(ns).Create(dep) - Expect(err).NotTo(HaveOccurred()) - - cl, err := client.New(cfg, client.Options{}) - Expect(err).NotTo(HaveOccurred()) - Expect(cl).NotTo(BeNil()) - - By("encoding the Deployment as unstructured") - var u runtime.Unstructured = &unstructured.Unstructured{} - scheme.Convert(dep, u, nil) - - By("fetching the created Deployment") - var actual unstructured.Unstructured - actual.SetGroupVersionKind(schema.GroupVersionKind{ - Group: "apps", - Kind: "Deployment", - Version: "v1", + Context("with structured objects", func() { + It("should fetch an existing object for a go struct", func(done Done) { + By("first creating the Deployment") + dep, err := clientset.AppsV1().Deployments(ns).Create(dep) + Expect(err).NotTo(HaveOccurred()) + + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) + + By("fetching the created Deployment") + var actual appsv1.Deployment + key := client.ObjectKey{Namespace: ns, Name: dep.Name} + err = cl.Get(context.TODO(), key, &actual) + Expect(err).NotTo(HaveOccurred()) + Expect(actual).NotTo(BeNil()) + + By("validating the fetched deployment equals the created one") + Expect(dep).To(Equal(&actual)) + + close(done) }) - key := client.ObjectKey{Namespace: ns, Name: dep.Name} - err = cl.Get(context.TODO(), key, &actual) - Expect(err).NotTo(HaveOccurred()) - Expect(actual).NotTo(BeNil()) - By("validating the fetched Deployment equals the created one") - Expect(u).To(Equal(&actual)) + It("should fetch an existing non-namespace object for a go struct", func(done Done) { + By("first creating the object") + node, err := clientset.CoreV1().Nodes().Create(node) + Expect(err).NotTo(HaveOccurred()) - close(done) - }) + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) - It("should fetch an existing non-namespace object for a go struct", func(done Done) { - By("first creating the object") - node, err := clientset.CoreV1().Nodes().Create(node) - Expect(err).NotTo(HaveOccurred()) + By("retrieving node through client") + var actual corev1.Node + key := client.ObjectKey{Namespace: ns, Name: node.Name} + err = cl.Get(context.TODO(), key, &actual) + Expect(err).NotTo(HaveOccurred()) + Expect(actual).NotTo(BeNil()) - cl, err := client.New(cfg, client.Options{}) - Expect(err).NotTo(HaveOccurred()) - Expect(cl).NotTo(BeNil()) - - By("retrieving node through client") - var actual corev1.Node - key := client.ObjectKey{Namespace: ns, Name: node.Name} - err = cl.Get(context.TODO(), key, &actual) - Expect(err).NotTo(HaveOccurred()) - Expect(actual).NotTo(BeNil()) - - Expect(node).To(Equal(&actual)) + Expect(node).To(Equal(&actual)) - close(done) - }) + close(done) + }) - It("should fetch an existing non-namespace object for an unstructured", func(done Done) { - By("first creating the Node") - node, err := clientset.CoreV1().Nodes().Create(node) - Expect(err).NotTo(HaveOccurred()) + It("should fail if the object does not exists", func(done Done) { + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) - By("encoding the Node as unstructured") - var u runtime.Unstructured = &unstructured.Unstructured{} - scheme.Convert(node, u, nil) + By("fetching object that has not been created yet") + key := client.ObjectKey{Namespace: ns, Name: dep.Name} + var actual appsv1.Deployment + err = cl.Get(context.TODO(), key, &actual) + Expect(err).To(HaveOccurred()) - cl, err := client.New(cfg, client.Options{}) - Expect(err).NotTo(HaveOccurred()) - Expect(cl).NotTo(BeNil()) - - By("fetching the created Node") - var actual unstructured.Unstructured - actual.SetGroupVersionKind(schema.GroupVersionKind{ - Group: "", - Kind: "Node", - Version: "v1", + close(done) }) - key := client.ObjectKey{Namespace: ns, Name: node.Name} - err = cl.Get(context.TODO(), key, &actual) - Expect(err).NotTo(HaveOccurred()) - Expect(actual).NotTo(BeNil()) - By("validating the fetched Node equals the created one") - Expect(u).To(Equal(&actual)) + PIt("should fail if the object doesn't have meta", func() { - close(done) - }) - - It("should fail if the object does not exists", func(done Done) { - cl, err := client.New(cfg, client.Options{}) - Expect(err).NotTo(HaveOccurred()) - Expect(cl).NotTo(BeNil()) - - By("fetching object that has not been created yet") - key := client.ObjectKey{Namespace: ns, Name: dep.Name} - var actual appsv1.Deployment - err = cl.Get(context.TODO(), key, &actual) - Expect(err).To(HaveOccurred()) + }) - close(done) - }) + It("should fail if the object cannot be mapped to a GVK", func() { + By("first creating the Deployment") + dep, err := clientset.AppsV1().Deployments(ns).Create(dep) + Expect(err).NotTo(HaveOccurred()) + + By("creating a client with an empty Scheme") + emptyScheme := runtime.NewScheme() + cl, err := client.New(cfg, client.Options{Scheme: emptyScheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) + + By("fetching the created Deployment fails") + var actual appsv1.Deployment + key := client.ObjectKey{Namespace: ns, Name: dep.Name} + err = cl.Get(context.TODO(), key, &actual) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("no kind is registered for the type")) + }) - PIt("should fail if the object doesn't have meta", func() { + PIt("should fail if the GVK cannot be mapped to a Resource", func() { + }) }) - It("should fail if the object cannot be mapped to a GVK", func() { - By("first creating the Deployment") - dep, err := clientset.AppsV1().Deployments(ns).Create(dep) - Expect(err).NotTo(HaveOccurred()) + Context("with unstructured objects", func() { + It("should fetch an existing object", func(done Done) { + By("first creating the Deployment") + dep, err := clientset.AppsV1().Deployments(ns).Create(dep) + Expect(err).NotTo(HaveOccurred()) + + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) + + By("encoding the Deployment as unstructured") + var u runtime.Unstructured = &unstructured.Unstructured{} + scheme.Convert(dep, u, nil) + + By("fetching the created Deployment") + var actual unstructured.Unstructured + actual.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "apps", + Kind: "Deployment", + Version: "v1", + }) + key := client.ObjectKey{Namespace: ns, Name: dep.Name} + err = cl.Get(context.TODO(), key, &actual) + Expect(err).NotTo(HaveOccurred()) + Expect(actual).NotTo(BeNil()) + + By("validating the fetched Deployment equals the created one") + Expect(u).To(Equal(&actual)) + + close(done) + }) - By("creating a client with an empty Scheme") - emptyScheme := runtime.NewScheme() - cl, err := client.New(cfg, client.Options{Scheme: emptyScheme}) - Expect(err).NotTo(HaveOccurred()) - Expect(cl).NotTo(BeNil()) + It("should fetch an existing non-namespace object", func(done Done) { + By("first creating the Node") + node, err := clientset.CoreV1().Nodes().Create(node) + Expect(err).NotTo(HaveOccurred()) + + By("encoding the Node as unstructured") + var u runtime.Unstructured = &unstructured.Unstructured{} + scheme.Convert(node, u, nil) + + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) + + By("fetching the created Node") + var actual unstructured.Unstructured + actual.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "", + Kind: "Node", + Version: "v1", + }) + key := client.ObjectKey{Namespace: ns, Name: node.Name} + err = cl.Get(context.TODO(), key, &actual) + Expect(err).NotTo(HaveOccurred()) + Expect(actual).NotTo(BeNil()) + + By("validating the fetched Node equals the created one") + Expect(u).To(Equal(&actual)) + + close(done) + }) - By("fetching the created Deployment fails") - var actual appsv1.Deployment - key := client.ObjectKey{Namespace: ns, Name: dep.Name} - err = cl.Get(context.TODO(), key, &actual) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("no kind is registered for the type")) - }) + It("should fail if the object does not exists", func(done Done) { + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) - PIt("should fail if the GVK cannot be mapped to a Resource", func() { + By("fetching object that has not been created yet") + key := client.ObjectKey{Namespace: ns, Name: dep.Name} + u := &unstructured.Unstructured{} + err = cl.Get(context.TODO(), key, u) + Expect(err).To(HaveOccurred()) + close(done) + }) }) }) Describe("List", func() { - It("should fetch collection of objects", func(done Done) { - By("creating an initial object") - dep, err := clientset.AppsV1().Deployments(ns).Create(dep) - Expect(err).NotTo(HaveOccurred()) - - cl, err := client.New(cfg, client.Options{}) - Expect(err).NotTo(HaveOccurred()) - - By("listing all objects of that type in the cluster") - deps := &appsv1.DeploymentList{} - Expect(cl.List(context.Background(), nil, deps)).NotTo(HaveOccurred()) - - Expect(deps.Items).NotTo(BeEmpty()) - hasDep := false - for _, item := range deps.Items { - if item.Name == dep.Name && item.Namespace == dep.Namespace { - hasDep = true - break + Context("with structured objects", func() { + It("should fetch collection of objects", func(done Done) { + By("creating an initial object") + dep, err := clientset.AppsV1().Deployments(ns).Create(dep) + Expect(err).NotTo(HaveOccurred()) + + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + + By("listing all objects of that type in the cluster") + deps := &appsv1.DeploymentList{} + Expect(cl.List(context.Background(), nil, deps)).NotTo(HaveOccurred()) + + Expect(deps.Items).NotTo(BeEmpty()) + hasDep := false + for _, item := range deps.Items { + if item.Name == dep.Name && item.Namespace == dep.Namespace { + hasDep = true + break + } } - } - Expect(hasDep).To(BeTrue()) - - close(done) - }, serverSideTimeoutSeconds) - - It("should fetch unstructured collection of objects", func(done Done) { - By("create an initial object") - _, err := clientset.AppsV1().Deployments(ns).Create(dep) - Expect(err).NotTo(HaveOccurred()) - - cl, err := client.New(cfg, client.Options{}) - Expect(err).NotTo(HaveOccurred()) - - By("listing all objects of that type in the cluster") - deps := &unstructured.UnstructuredList{} - deps.SetGroupVersionKind(schema.GroupVersionKind{ - Group: "apps", - Kind: "DeploymentList", - Version: "v1", - }) - err = cl.List(context.Background(), nil, deps) - Expect(err).NotTo(HaveOccurred()) - - Expect(deps.Items).NotTo(BeEmpty()) - hasDep := false - for _, item := range deps.Items { - if item.GetName() == dep.Name && item.GetNamespace() == dep.Namespace { - fmt.Printf("HERE!!!!!!!! ITEM: %v\n\n", item) - hasDep = true - fmt.Printf("HERE hasDep: %v\n\n", hasDep) - break + Expect(hasDep).To(BeTrue()) + + close(done) + }, serverSideTimeoutSeconds) + + It("should fetch unstructured collection of objects", func(done Done) { + By("create an initial object") + _, err := clientset.AppsV1().Deployments(ns).Create(dep) + Expect(err).NotTo(HaveOccurred()) + + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + + By("listing all objects of that type in the cluster") + deps := &unstructured.UnstructuredList{} + deps.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "apps", + Kind: "DeploymentList", + Version: "v1", + }) + err = cl.List(context.Background(), nil, deps) + Expect(err).NotTo(HaveOccurred()) + + Expect(deps.Items).NotTo(BeEmpty()) + hasDep := false + for _, item := range deps.Items { + if item.GetName() == dep.Name && item.GetNamespace() == dep.Namespace { + fmt.Printf("HERE!!!!!!!! ITEM: %v\n\n", item) + hasDep = true + fmt.Printf("HERE hasDep: %v\n\n", hasDep) + break + } } - } - Expect(hasDep).To(BeTrue()) - close(done) - }, serverSideTimeoutSeconds) - - It("should return an empty list if there are no matching objects", func(done Done) { - cl, err := client.New(cfg, client.Options{}) - Expect(err).NotTo(HaveOccurred()) - - By("listing all Deployments in the cluster") - deps := &appsv1.DeploymentList{} - Expect(cl.List(context.Background(), nil, deps)).NotTo(HaveOccurred()) - - By("validating no Deployments are returned") - Expect(deps.Items).To(BeEmpty()) - - close(done) - }, serverSideTimeoutSeconds) - - // TODO(seans): get label selector test working - // It("should filter results by label selector", func(done Done) { - // By("creating a Deployment with the app=frontend label") - // depFrontend := &appsv1.Deployment{ - // ObjectMeta: metav1.ObjectMeta{Name: "deployment-frontend", Namespace: ns}, - // Spec: appsv1.DeploymentSpec{ - // Selector: &metav1.LabelSelector{ - // MatchLabels: map[string]string{"app": "frontend"}, - // }, - // Template: corev1.PodTemplateSpec{ - // ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "frontend"}}, - // Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}}, - // }, - // }, - // } - // depFrontend, err := clientset.AppsV1().Deployments(ns).Create(depFrontend) - // Expect(err).NotTo(HaveOccurred()) - - // By("creating a Deployment with the app=backend label") - // depBackend := &appsv1.Deployment{ - // ObjectMeta: metav1.ObjectMeta{Name: "deployment-backend", Namespace: ns}, - // Spec: appsv1.DeploymentSpec{ - // Selector: &metav1.LabelSelector{ - // MatchLabels: map[string]string{"app": "backend"}, - // }, - // Template: corev1.PodTemplateSpec{ - // ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "backend"}}, - // Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}}, - // }, - // }, - // } - // depBackend, err = clientset.AppsV1().Deployments(ns).Create(depBackend) - // Expect(err).NotTo(HaveOccurred()) - - // cl, err := client.New(cfg, client.Options{}) - // Expect(err).NotTo(HaveOccurred()) - - // By("listing all Deployments with label app=backend") - // deps := &appsv1.DeploymentList{} - // labels := map[string]string{"app": "backend"} - // lo := client.InNamespace(ns).MatchingLabels(labels) - // Expect(cl.List(context.Background(), lo, deps)).NotTo(HaveOccurred()) - - // By("only the Deployment with the backend label is returned") - // Expect(deps.Items).NotTo(BeEmpty()) - // Expect(1).To(Equal(len(deps.Items))) - // actual := deps.Items[0] - // Expect(actual.Name).To(Equal("deployment-backend")) - - // deleteDeployment(depFrontend, ns) - // deleteDeployment(depBackend, ns) - - // close(done) - // }, serverSideTimeoutSeconds) - - It("should filter results by namespace selector", func(done Done) { - By("creating a Deployment in test-namespace-1") - tns1 := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test-namespace-1"}} - _, err := clientset.CoreV1().Namespaces().Create(tns1) - Expect(err).NotTo(HaveOccurred()) - depFrontend := &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{Name: "deployment-frontend", Namespace: "test-namespace-1"}, - Spec: appsv1.DeploymentSpec{ - Selector: &metav1.LabelSelector{ - MatchLabels: map[string]string{"app": "frontend"}, + Expect(hasDep).To(BeTrue()) + close(done) + }, serverSideTimeoutSeconds) + + It("should return an empty list if there are no matching objects", func(done Done) { + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + + By("listing all Deployments in the cluster") + deps := &appsv1.DeploymentList{} + Expect(cl.List(context.Background(), nil, deps)).NotTo(HaveOccurred()) + + By("validating no Deployments are returned") + Expect(deps.Items).To(BeEmpty()) + + close(done) + }, serverSideTimeoutSeconds) + + // TODO(seans): get label selector test working + It("should filter results by label selector", func(done Done) { + By("creating a Deployment with the app=frontend label") + depFrontend := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "deployment-frontend", + Namespace: ns, + Labels: map[string]string{"app": "frontend"}, }, - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "frontend"}}, - Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}}, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "frontend"}, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "frontend"}}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}}, + }, }, - }, - } - depFrontend, err = clientset.AppsV1().Deployments("test-namespace-1").Create(depFrontend) - Expect(err).NotTo(HaveOccurred()) - - By("creating a Deployment in test-namespace-2") - tns2 := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test-namespace-2"}} - _, err = clientset.CoreV1().Namespaces().Create(tns2) - Expect(err).NotTo(HaveOccurred()) - depBackend := &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{Name: "deployment-backend", Namespace: "test-namespace-2"}, - Spec: appsv1.DeploymentSpec{ - Selector: &metav1.LabelSelector{ - MatchLabels: map[string]string{"app": "backend"}, + } + depFrontend, err := clientset.AppsV1().Deployments(ns).Create(depFrontend) + Expect(err).NotTo(HaveOccurred()) + + By("creating a Deployment with the app=backend label") + depBackend := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "deployment-backend", + Namespace: ns, + Labels: map[string]string{"app": "backend"}, }, - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "backend"}}, - Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}}, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "backend"}, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "backend"}}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}}, + }, }, - }, - } - depBackend, err = clientset.AppsV1().Deployments("test-namespace-2").Create(depBackend) - Expect(err).NotTo(HaveOccurred()) - - cl, err := client.New(cfg, client.Options{}) - Expect(err).NotTo(HaveOccurred()) - - By("listing all Deployments in test-namespace-1") - deps := &appsv1.DeploymentList{} - lo := client.InNamespace("test-namespace-1") - Expect(cl.List(context.Background(), lo, deps)).NotTo(HaveOccurred()) - - By("only the Deployment in test-namespace-1 is returned") - Expect(deps.Items).NotTo(BeEmpty()) - Expect(1).To(Equal(len(deps.Items))) - actual := deps.Items[0] - Expect(actual.Name).To(Equal("deployment-frontend")) - - deleteDeployment(depFrontend, "test-namespace-1") - deleteDeployment(depBackend, "test-namespace-2") - deleteNamespace(tns1) - deleteNamespace(tns2) - - close(done) - }, serverSideTimeoutSeconds) - - It("should filter results by field selector", func(done Done) { - By("creating a Deployment with name deployment-frontend") - depFrontend := &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{Name: "deployment-frontend", Namespace: ns}, - Spec: appsv1.DeploymentSpec{ - Selector: &metav1.LabelSelector{ - MatchLabels: map[string]string{"app": "frontend"}, + } + depBackend, err = clientset.AppsV1().Deployments(ns).Create(depBackend) + Expect(err).NotTo(HaveOccurred()) + + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + + By("listing all Deployments with label app=backend") + deps := &appsv1.DeploymentList{} + labels := map[string]string{"app": "backend"} + err = cl.List(context.Background(), deps, + client.MatchingLabels(labels), + ) + Expect(err).NotTo(HaveOccurred()) + + By("only the Deployment with the backend label is returned") + Expect(deps.Items).NotTo(BeEmpty()) + Expect(1).To(Equal(len(deps.Items))) + actual := deps.Items[0] + Expect(actual.Name).To(Equal("deployment-backend")) + + deleteDeployment(depFrontend, ns) + deleteDeployment(depBackend, ns) + + close(done) + }, serverSideTimeoutSeconds) + + It("should filter results by namespace selector", func(done Done) { + By("creating a Deployment in test-namespace-1") + tns1 := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test-namespace-1"}} + _, err := clientset.CoreV1().Namespaces().Create(tns1) + Expect(err).NotTo(HaveOccurred()) + depFrontend := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: "deployment-frontend", Namespace: "test-namespace-1"}, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "frontend"}, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "frontend"}}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}}, + }, }, - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "frontend"}}, - Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}}, + } + depFrontend, err = clientset.AppsV1().Deployments("test-namespace-1").Create(depFrontend) + Expect(err).NotTo(HaveOccurred()) + + By("creating a Deployment in test-namespace-2") + tns2 := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test-namespace-2"}} + _, err = clientset.CoreV1().Namespaces().Create(tns2) + Expect(err).NotTo(HaveOccurred()) + depBackend := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: "deployment-backend", Namespace: "test-namespace-2"}, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "backend"}, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "backend"}}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}}, + }, }, - }, - } - depFrontend, err := clientset.AppsV1().Deployments(ns).Create(depFrontend) - Expect(err).NotTo(HaveOccurred()) - - By("creating a Deployment with name deployment-backend") - depBackend := &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{Name: "deployment-backend", Namespace: ns}, - Spec: appsv1.DeploymentSpec{ - Selector: &metav1.LabelSelector{ - MatchLabels: map[string]string{"app": "backend"}, + } + depBackend, err = clientset.AppsV1().Deployments("test-namespace-2").Create(depBackend) + Expect(err).NotTo(HaveOccurred()) + + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + + By("listing all Deployments in test-namespace-1") + deps := &appsv1.DeploymentList{} + err = cl.List(context.Background(), deps, client.InNamespace("test-namespace-1")) + Expect(err).NotTo(HaveOccurred()) + + By("only the Deployment in test-namespace-1 is returned") + Expect(deps.Items).NotTo(BeEmpty()) + Expect(1).To(Equal(len(deps.Items))) + actual := deps.Items[0] + Expect(actual.Name).To(Equal("deployment-frontend")) + + deleteDeployment(depFrontend, "test-namespace-1") + deleteDeployment(depBackend, "test-namespace-2") + deleteNamespace(tns1) + deleteNamespace(tns2) + + close(done) + }, serverSideTimeoutSeconds) + + It("should filter results by field selector", func(done Done) { + By("creating a Deployment with name deployment-frontend") + depFrontend := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: "deployment-frontend", Namespace: ns}, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "frontend"}, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "frontend"}}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}}, + }, }, - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "backend"}}, - Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}}, + } + depFrontend, err := clientset.AppsV1().Deployments(ns).Create(depFrontend) + Expect(err).NotTo(HaveOccurred()) + + By("creating a Deployment with name deployment-backend") + depBackend := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: "deployment-backend", Namespace: ns}, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "backend"}, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "backend"}}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}}, + }, }, - }, - } - depBackend, err = clientset.AppsV1().Deployments(ns).Create(depBackend) - Expect(err).NotTo(HaveOccurred()) - - cl, err := client.New(cfg, client.Options{}) - Expect(err).NotTo(HaveOccurred()) - - By("listing all Deployments with field metadata.name=deployment-backend") - deps := &appsv1.DeploymentList{} - lo := client.MatchingField("metadata.name", "deployment-backend") - Expect(cl.List(context.Background(), lo, deps)).NotTo(HaveOccurred()) - - By("only the Deployment with the backend field is returned") - Expect(deps.Items).NotTo(BeEmpty()) - Expect(1).To(Equal(len(deps.Items))) - actual := deps.Items[0] - Expect(actual.Name).To(Equal("deployment-backend")) - - deleteDeployment(depFrontend, ns) - deleteDeployment(depBackend, ns) - - close(done) - }, serverSideTimeoutSeconds) - - PIt("should fail if it cannot get a client", func() { + } + depBackend, err = clientset.AppsV1().Deployments(ns).Create(depBackend) + Expect(err).NotTo(HaveOccurred()) + + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + + By("listing all Deployments with field metadata.name=deployment-backend") + deps := &appsv1.DeploymentList{} + err = cl.List(context.Background(), deps, client.MatchingField("metadata.name", "deployment-backend")) + Expect(err).NotTo(HaveOccurred()) + + By("only the Deployment with the backend field is returned") + Expect(deps.Items).NotTo(BeEmpty()) + Expect(1).To(Equal(len(deps.Items))) + actual := deps.Items[0] + Expect(actual.Name).To(Equal("deployment-backend")) + + deleteDeployment(depFrontend, ns) + deleteDeployment(depBackend, ns) + + close(done) + }, serverSideTimeoutSeconds) + + It("should filter results by namespace selector and label selector", func(done Done) { + By("creating a Deployment in test-namespace-3 with the app=frontend label") + tns3 := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test-namespace-3"}} + _, err := clientset.CoreV1().Namespaces().Create(tns3) + Expect(err).NotTo(HaveOccurred()) + depFrontend3 := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "deployment-frontend", + Namespace: "test-namespace-3", + Labels: map[string]string{"app": "frontend"}, + }, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "frontend"}, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "frontend"}}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}}, + }, + }, + } + depFrontend3, err = clientset.AppsV1().Deployments("test-namespace-3").Create(depFrontend3) + Expect(err).NotTo(HaveOccurred()) + + By("creating a Deployment in test-namespace-3 with the app=backend label") + depBackend3 := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "deployment-backend", + Namespace: "test-namespace-3", + Labels: map[string]string{"app": "backend"}, + }, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "backend"}, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "backend"}}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}}, + }, + }, + } + depBackend3, err = clientset.AppsV1().Deployments("test-namespace-3").Create(depBackend3) + Expect(err).NotTo(HaveOccurred()) + + By("creating a Deployment in test-namespace-4 with the app=frontend label") + tns4 := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test-namespace-4"}} + _, err = clientset.CoreV1().Namespaces().Create(tns4) + Expect(err).NotTo(HaveOccurred()) + depFrontend4 := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "deployment-frontend", + Namespace: "test-namespace-4", + Labels: map[string]string{"app": "frontend"}, + }, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "frontend"}, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "frontend"}}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}}, + }, + }, + } + depFrontend4, err = clientset.AppsV1().Deployments("test-namespace-4").Create(depFrontend4) + Expect(err).NotTo(HaveOccurred()) + + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + + By("listing all Deployments in test-namespace-3 with label app=frontend") + deps := &appsv1.DeploymentList{} + labels := map[string]string{"app": "frontend"} + err = cl.List(context.Background(), deps, + client.InNamespace("test-namespace-3"), + client.MatchingLabels(labels), + ) + Expect(err).NotTo(HaveOccurred()) + + By("only the Deployment in test-namespace-3 with label app=frontend is returned") + Expect(deps.Items).NotTo(BeEmpty()) + Expect(1).To(Equal(len(deps.Items))) + actual := deps.Items[0] + Expect(actual.Name).To(Equal("deployment-frontend")) + Expect(actual.Namespace).To(Equal("test-namespace-3")) + + deleteDeployment(depFrontend3, "test-namespace-3") + deleteDeployment(depBackend3, "test-namespace-3") + deleteDeployment(depFrontend4, "test-namespace-4") + deleteNamespace(tns3) + deleteNamespace(tns4) + + close(done) + }, serverSideTimeoutSeconds) + + PIt("should fail if the object doesn't have meta", func() { - }) + }) - PIt("should fail if the object doesn't have meta", func() { + PIt("should fail if the object cannot be mapped to a GVK", func() { - }) + }) - PIt("should fail if the object cannot be mapped to a GVK", func() { + PIt("should fail if the GVK cannot be mapped to a Resource", func() { + }) }) - PIt("should fail if the GVK cannot be mapped to a Resource", func() { - + Context("with unstructured objects", func() { + It("should fetch collection of objects", func(done Done) { + By("create an initial object") + _, err := clientset.AppsV1().Deployments(ns).Create(dep) + Expect(err).NotTo(HaveOccurred()) + + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + + By("listing all objects of that type in the cluster") + deps := &unstructured.UnstructuredList{} + deps.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "apps", + Kind: "DeploymentList", + Version: "v1", + }) + err = cl.List(context.Background(), deps) + Expect(err).NotTo(HaveOccurred()) + + Expect(deps.Items).NotTo(BeEmpty()) + hasDep := false + for _, item := range deps.Items { + if item.GetName() == dep.Name && item.GetNamespace() == dep.Namespace { + hasDep = true + break + } + } + Expect(hasDep).To(BeTrue()) + close(done) + }, serverSideTimeoutSeconds) + + It("should return an empty list if there are no matching objects", func(done Done) { + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + + By("listing all Deployments in the cluster") + deps := &unstructured.UnstructuredList{} + deps.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "apps", + Kind: "DeploymentList", + Version: "v1", + }) + Expect(cl.List(context.Background(), deps)).NotTo(HaveOccurred()) + + By("validating no Deployments are returned") + Expect(deps.Items).To(BeEmpty()) + + close(done) + }, serverSideTimeoutSeconds) + + It("should filter results by namespace selector", func(done Done) { + By("creating a Deployment in test-namespace-3") + tns1 := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test-namespace-3"}} + _, err := clientset.CoreV1().Namespaces().Create(tns1) + Expect(err).NotTo(HaveOccurred()) + depFrontend := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: "deployment-frontend", Namespace: "test-namespace-3"}, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "frontend"}, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "frontend"}}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}}, + }, + }, + } + depFrontend, err = clientset.AppsV1().Deployments("test-namespace-3").Create(depFrontend) + Expect(err).NotTo(HaveOccurred()) + + By("creating a Deployment in test-namespace-4") + tns2 := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test-namespace-4"}} + _, err = clientset.CoreV1().Namespaces().Create(tns2) + Expect(err).NotTo(HaveOccurred()) + depBackend := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: "deployment-backend", Namespace: "test-namespace-4"}, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "backend"}, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "backend"}}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}}, + }, + }, + } + depBackend, err = clientset.AppsV1().Deployments("test-namespace-4").Create(depBackend) + Expect(err).NotTo(HaveOccurred()) + + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + + By("listing all Deployments in test-namespace-3") + deps := &unstructured.UnstructuredList{} + deps.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "apps", + Kind: "DeploymentList", + Version: "v1", + }) + lo := client.InNamespace("test-namespace-3") + err = cl.List(context.Background(), deps, client.InNamespace("test-namespace-3")).NotTo(HaveOccurred()) + Expect(err).NotTo(HaveOccurred()) + + By("only the Deployment in test-namespace-3 is returned") + Expect(deps.Items).NotTo(BeEmpty()) + Expect(1).To(Equal(len(deps.Items))) + actual := deps.Items[0] + Expect(actual.GetName()).To(Equal("deployment-frontend")) + + deleteDeployment(depFrontend, "test-namespace-3") + deleteDeployment(depBackend, "test-namespace-4") + deleteNamespace(tns1) + deleteNamespace(tns2) + + close(done) + }, serverSideTimeoutSeconds) + + /* + It("should filter results by field selector", func(done Done) { + By("creating a Deployment with name deployment-frontend") + depFrontend := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: "deployment-frontend", Namespace: ns}, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "frontend"}, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "frontend"}}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}}, + }, + }, + } + depFrontend, err := clientset.AppsV1().Deployments(ns).Create(depFrontend) + Expect(err).NotTo(HaveOccurred()) + + By("creating a Deployment with name deployment-backend") + depBackend := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: "deployment-backend", Namespace: ns}, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "backend"}, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "backend"}}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}}, + }, + }, + } + depBackend, err = clientset.AppsV1().Deployments(ns).Create(depBackend) + Expect(err).NotTo(HaveOccurred()) + + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + + By("listing all Deployments with field metadata.name=deployment-backend") + deps := &appsv1.DeploymentList{} + lo := client.MatchingField("metadata.name", "deployment-backend") + Expect(cl.List(context.Background(), lo, deps)).NotTo(HaveOccurred()) + + By("only the Deployment with the backend field is returned") + Expect(deps.Items).NotTo(BeEmpty()) + Expect(1).To(Equal(len(deps.Items))) + actual := deps.Items[0] + Expect(actual.Name).To(Equal("deployment-backend")) + + deleteDeployment(depFrontend, ns) + deleteDeployment(depBackend, ns) + + close(done) + }, serverSideTimeoutSeconds) + + It("should fail if the object doesn't have meta", func() { + + }) + */ }) }) @@ -1135,3 +1632,77 @@ var _ = Describe("Client", func() { }) }) }) + +var _ = Describe("DelegatingReader", func() { + Describe("Get", func() { + It("should call cache reader when structured object", func() { + cachedReader := &fakeReader{} + clientReader := &fakeReader{} + dReader := client.DelegatingReader{ + CacheReader: cachedReader, + ClientReader: clientReader, + } + var actual appsv1.Deployment + key := client.ObjectKey{Namespace: "ns", Name: "name"} + dReader.Get(context.TODO(), key, &actual) + Expect(1).To(Equal(cachedReader.Called)) + Expect(0).To(Equal(clientReader.Called)) + }) + It("should call client reader when structured object", func() { + cachedReader := &fakeReader{} + clientReader := &fakeReader{} + dReader := client.DelegatingReader{ + CacheReader: cachedReader, + ClientReader: clientReader, + } + var actual unstructured.Unstructured + key := client.ObjectKey{Namespace: "ns", Name: "name"} + dReader.Get(context.TODO(), key, &actual) + Expect(0).To(Equal(cachedReader.Called)) + Expect(1).To(Equal(clientReader.Called)) + }) + }) + Describe("List", func() { + It("should call cache reader when structured object", func() { + cachedReader := &fakeReader{} + clientReader := &fakeReader{} + dReader := client.DelegatingReader{ + CacheReader: cachedReader, + ClientReader: clientReader, + } + var actual appsv1.DeploymentList + dReader.List(context.Background(), nil, &actual) + Expect(1).To(Equal(cachedReader.Called)) + Expect(0).To(Equal(clientReader.Called)) + + }) + It("should call client reader when structured object", func() { + cachedReader := &fakeReader{} + clientReader := &fakeReader{} + dReader := client.DelegatingReader{ + CacheReader: cachedReader, + ClientReader: clientReader, + } + + var actual unstructured.UnstructuredList + dReader.List(context.Background(), nil, &actual) + Expect(0).To(Equal(cachedReader.Called)) + Expect(1).To(Equal(clientReader.Called)) + + }) + }) +}) + +type fakeReader struct { + Called int +} + +func (f *fakeReader) Get(ctx context.Context, key client.ObjectKey, obj runtime.Object) error { + f.Called = f.Called + 1 + return nil +} + +func (f *fakeReader) List(ctx context.Context, opts *client.ListOptions, list runtime.Object) error { + f.Called = f.Called + 1 + return nil +} From 774420021b2feaabd7175aecbd3f190995e299d0 Mon Sep 17 00:00:00 2001 From: Shawn Hurley Date: Fri, 10 Aug 2018 10:41:55 -0400 Subject: [PATCH 5/8] Adding informer cache tests. --- pkg/cache/cache_test.go | 655 +++++++++++++++++++--------- pkg/cache/informer_cache.go | 27 +- pkg/cache/internal/informers_map.go | 14 +- 3 files changed, 485 insertions(+), 211 deletions(-) diff --git a/pkg/cache/cache_test.go b/pkg/cache/cache_test.go index 82441777d8..badef27794 100644 --- a/pkg/cache/cache_test.go +++ b/pkg/cache/cache_test.go @@ -18,14 +18,17 @@ package cache_test import ( "context" + "fmt" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" kcorev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" kmetav1 "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" + kscheme "k8s.io/client-go/kubernetes/scheme" kcache "k8s.io/client-go/tools/cache" "sigs.k8s.io/controller-runtime/pkg/cache" @@ -109,223 +112,469 @@ var _ = Describe("Informer Cache", func() { }) Describe("as a Reader", func() { - - It("should be able to list objects that haven't been watched previously", func() { - By("listing all services in the cluster") - listObj := &kcorev1.ServiceList{} - Expect(informerCache.List(context.Background(), nil, listObj)).To(Succeed()) - - By("verifying that the returned list contains the Kubernetes service") - // NB: kubernetes default service is automatically created in testenv. - Expect(listObj.Items).NotTo(BeEmpty()) - hasKubeService := false - for _, svc := range listObj.Items { - if svc.Namespace == "default" && svc.Name == "kubernetes" { - hasKubeService = true - break + Context("with structured objects", func() { + + It("should be able to list objects that haven't been watched previously", func() { + By("listing all services in the cluster") + listObj := &kcorev1.ServiceList{} + Expect(informerCache.List(context.Background(), listObj)).To(Succeed()) + + By("verifying that the returned list contains the Kubernetes service") + // NB: kubernetes default service is automatically created in testenv. + Expect(listObj.Items).NotTo(BeEmpty()) + hasKubeService := false + for _, svc := range listObj.Items { + if svc.Namespace == "default" && svc.Name == "kubernetes" { + hasKubeService = true + break + } } - } - Expect(hasKubeService).To(BeTrue()) - }) - - It("should be able to get objects that haven't been watched previously", func() { - By("getting the Kubernetes service") - svc := &kcorev1.Service{} - svcKey := client.ObjectKey{Namespace: "default", Name: "kubernetes"} - Expect(informerCache.Get(context.Background(), svcKey, svc)).To(Succeed()) - - By("verifying that the returned service looks reasonable") - Expect(svc.Name).To(Equal("kubernetes")) - Expect(svc.Namespace).To(Equal("default")) - }) - - It("should support filtering by labels in a single namespace", func() { - By("listing pods with a particular label") - // NB: each pod has a "test-label": - out := kcorev1.PodList{} - Expect(informerCache.List(context.Background(), client.InNamespace(testNamespaceTwo). - MatchingLabels(map[string]string{"test-label": "test-pod-2"}), &out)).To(Succeed()) - - By("verifying the returned pods have the correct label") - Expect(out.Items).NotTo(BeEmpty()) - Expect(out.Items).Should(HaveLen(1)) - actual := out.Items[0] - Expect(actual.Labels["test-label"]).To(Equal("test-pod-2")) - }) - - It("should support filtering by labels from multiple namespaces", func() { - By("creating another pod with the same label but different namespace") - anotherPod := createPod("test-pod-2", testNamespaceOne, kcorev1.RestartPolicyAlways) - - By("listing pods with a particular label") - // NB: each pod has a "test-label": - out := kcorev1.PodList{} - labels := map[string]string{"test-label": "test-pod-2"} - Expect(informerCache.List(context.Background(), - client.MatchingLabels(labels), &out)).To(Succeed()) - - By("verifying multiple pods with the same label in different namespaces are returned") - Expect(out.Items).NotTo(BeEmpty()) - Expect(out.Items).Should(HaveLen(2)) - for _, actual := range out.Items { + Expect(hasKubeService).To(BeTrue()) + }) + + It("should be able to get objects that haven't been watched previously", func() { + By("getting the Kubernetes service") + svc := &kcorev1.Service{} + svcKey := client.ObjectKey{Namespace: "default", Name: "kubernetes"} + Expect(informerCache.Get(context.Background(), svcKey, svc)).To(Succeed()) + + By("verifying that the returned service looks reasonable") + Expect(svc.Name).To(Equal("kubernetes")) + Expect(svc.Namespace).To(Equal("default")) + }) + + It("should support filtering by labels in a single namespace", func() { + By("listing pods with a particular label") + // NB: each pod has a "test-label": + out := kcorev1.PodList{} + Expect(informerCache.List(context.Background(), &out, + client.InNamespace(testNamespaceTwo), + client.MatchingLabels(map[string]string{"test-label": "test-pod-2"}), + )).To(Succeed()) + + By("verifying the returned pods have the correct label") + Expect(out.Items).NotTo(BeEmpty()) + Expect(out.Items).Should(HaveLen(1)) + actual := out.Items[0] Expect(actual.Labels["test-label"]).To(Equal("test-pod-2")) - } - - deletePod(anotherPod) - }) - - It("should be able to list objects by namespace", func() { - By("listing pods in test-namespace-1") - listObj := &kcorev1.PodList{} - Expect(informerCache.List(context.Background(), - client.InNamespace(testNamespaceOne), - listObj)).To(Succeed()) - - By("verifying that the returned pods are in test-namespace-1") - Expect(listObj.Items).NotTo(BeEmpty()) - Expect(listObj.Items).Should(HaveLen(1)) - actual := listObj.Items[0] - Expect(actual.Namespace).To(Equal(testNamespaceOne)) - }) - - It("should deep copy the object unless told otherwise", func() { - By("retrieving a specific pod from the cache") - out := &kcorev1.Pod{} - podKey := client.ObjectKey{Name: "test-pod-2", Namespace: testNamespaceTwo} - Expect(informerCache.Get(context.Background(), podKey, out)).To(Succeed()) - - By("verifying the retrieved pod is equal to a known pod") - Expect(out).To(Equal(knownPod2)) - - By("altering a field in the retrieved pod") - *out.Spec.ActiveDeadlineSeconds = 4 + }) + + It("should support filtering by labels from multiple namespaces", func() { + By("creating another pod with the same label but different namespace") + anotherPod := createPod("test-pod-2", testNamespaceOne, kcorev1.RestartPolicyAlways) + + By("listing pods with a particular label") + // NB: each pod has a "test-label": + out := kcorev1.PodList{} + labels := map[string]string{"test-label": "test-pod-2"} + Expect(informerCache.List(context.Background(), &out, + client.MatchingLabels(labels), + )).To(Succeed()) + + By("verifying multiple pods with the same label in different namespaces are returned") + Expect(out.Items).NotTo(BeEmpty()) + Expect(out.Items).Should(HaveLen(2)) + for _, actual := range out.Items { + Expect(actual.Labels["test-label"]).To(Equal("test-pod-2")) + } - By("verifying the pods are no longer equal") - Expect(out).NotTo(Equal(knownPod2)) + deletePod(anotherPod) + }) + + It("should be able to list objects by namespace", func() { + By("listing pods in test-namespace-1") + listObj := &kcorev1.PodList{} + Expect(informerCache.List(context.Background(), + client.InNamespace(testNamespaceOne), + listObj)).To(Succeed()) + + By("verifying that the returned pods are in test-namespace-1") + Expect(listObj.Items).NotTo(BeEmpty()) + Expect(listObj.Items).Should(HaveLen(1)) + actual := listObj.Items[0] + Expect(actual.Namespace).To(Equal(testNamespaceOne)) + }) + + It("should deep copy the object unless told otherwise", func() { + By("retrieving a specific pod from the cache") + out := &kcorev1.Pod{} + podKey := client.ObjectKey{Name: "test-pod-2", Namespace: testNamespaceTwo} + Expect(informerCache.Get(context.Background(), podKey, out)).To(Succeed()) + + By("verifying the retrieved pod is equal to a known pod") + Expect(out).To(Equal(knownPod2)) + + By("altering a field in the retrieved pod") + *out.Spec.ActiveDeadlineSeconds = 4 + + By("verifying the pods are no longer equal") + Expect(out).NotTo(Equal(knownPod2)) + }) + + It("should return an error if the object is not found", func() { + By("getting a service that does not exists") + svc := &kcorev1.Service{} + svcKey := client.ObjectKey{Namespace: "unknown", Name: "unknown"} + + By("verifying that an error is returned") + err := informerCache.Get(context.Background(), svcKey, svc) + Expect(err).To(HaveOccurred()) + Expect(errors.IsNotFound(err)).To(BeTrue()) + }) }) + Context("with unstructured objects", func() { + It("should be able to list objects that haven't been watched previously", func() { + By("listing all services in the cluster") + listObj := &unstructured.UnstructuredList{} + listObj.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "ServiceList", + }) + Expect(informerCache.List(context.Background(), nil, listObj)).To(Succeed()) + + By("verifying that the returned list contains the Kubernetes service") + // NB: kubernetes default service is automatically created in testenv. + Expect(listObj.Items).NotTo(BeEmpty()) + hasKubeService := false + for _, svc := range listObj.Items { + if svc.GetNamespace() == "default" && svc.GetName() == "kubernetes" { + hasKubeService = true + break + } + } + Expect(hasKubeService).To(BeTrue()) + }) + + It("should be able to get objects that haven't been watched previously", func() { + By("getting the Kubernetes service") + svc := &unstructured.Unstructured{} + svc.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "Service", + }) + svcKey := client.ObjectKey{Namespace: "default", Name: "kubernetes"} + Expect(informerCache.Get(context.Background(), svcKey, svc)).To(Succeed()) + + By("verifying that the returned service looks reasonable") + Expect(svc.GetName()).To(Equal("kubernetes")) + Expect(svc.GetNamespace()).To(Equal("default")) + }) + + It("should support filtering by labels in a single namespace", func() { + By("listing pods with a particular label") + // NB: each pod has a "test-label": + out := unstructured.UnstructuredList{} + out.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "PodList", + }) + Expect(informerCache.List(context.Background(), client.InNamespace(testNamespaceTwo). + MatchingLabels(map[string]string{"test-label": "test-pod-2"}), &out)).To(Succeed()) + + By("verifying the returned pods have the correct label") + Expect(out.Items).NotTo(BeEmpty()) + Expect(out.Items).Should(HaveLen(1)) + actual := out.Items[0] + Expect(actual.GetLabels()["test-label"]).To(Equal("test-pod-2")) + }) + + It("should support filtering by labels from multiple namespaces", func() { + By("creating another pod with the same label but different namespace") + anotherPod := createPod("test-pod-2", testNamespaceOne, kcorev1.RestartPolicyAlways) + + By("listing pods with a particular label") + // NB: each pod has a "test-label": + out := unstructured.UnstructuredList{} + out.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "PodList", + }) + labels := map[string]string{"test-label": "test-pod-2"} + Expect(informerCache.List(context.Background(), + client.MatchingLabels(labels), &out)).To(Succeed()) + + By("verifying multiple pods with the same label in different namespaces are returned") + Expect(out.Items).NotTo(BeEmpty()) + Expect(out.Items).Should(HaveLen(2)) + for _, actual := range out.Items { + Expect(actual.GetLabels()["test-label"]).To(Equal("test-pod-2")) + } - It("should return an error if the object is not found", func() { - By("getting a service that does not exists") - svc := &kcorev1.Service{} - svcKey := client.ObjectKey{Namespace: "unknown", Name: "unknown"} - - By("verifying that an error is returned") - err := informerCache.Get(context.Background(), svcKey, svc) - Expect(err).To(HaveOccurred()) - Expect(errors.IsNotFound(err)).To(BeTrue()) + deletePod(anotherPod) + }) + + It("should be able to list objects by namespace", func() { + By("listing pods in test-namespace-1") + listObj := &unstructured.UnstructuredList{} + listObj.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "PodList", + }) + Expect(informerCache.List(context.Background(), + client.InNamespace(testNamespaceOne), + listObj)).To(Succeed()) + + By("verifying that the returned pods are in test-namespace-1") + Expect(listObj.Items).NotTo(BeEmpty()) + Expect(listObj.Items).Should(HaveLen(1)) + actual := listObj.Items[0] + Expect(actual.GetNamespace()).To(Equal(testNamespaceOne)) + }) + + It("should deep copy the object unless told otherwise", func() { + By("retrieving a specific pod from the cache") + out := &unstructured.Unstructured{} + out.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "Pod", + }) + uKnownPod2 := &unstructured.Unstructured{} + kscheme.Scheme.Convert(knownPod2, uKnownPod2, nil) + + podKey := client.ObjectKey{Name: "test-pod-2", Namespace: testNamespaceTwo} + Expect(informerCache.Get(context.Background(), podKey, out)).To(Succeed()) + + By("verifying the retrieved pod is equal to a known pod") + Expect(out).To(Equal(uKnownPod2)) + + By("altering a field in the retrieved pod") + m, _ := out.Object["spec"].(map[string]interface{}) + m["activeDeadlineSeconds"] = 4 + + By("verifying the pods are no longer equal") + Expect(out).NotTo(Equal(knownPod2)) + }) + + It("should return an error if the object is not found", func() { + By("getting a service that does not exists") + svc := &unstructured.Unstructured{} + svc.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "Service", + }) + svcKey := client.ObjectKey{Namespace: "unknown", Name: "unknown"} + + By("verifying that an error is returned") + err := informerCache.Get(context.Background(), svcKey, svc) + Expect(err).To(HaveOccurred()) + Expect(errors.IsNotFound(err)).To(BeTrue()) + }) }) }) Describe("as an Informer", func() { - - It("should be able to get informer for the object", func(done Done) { - By("getting a shared index informer for a pod") - pod := &kcorev1.Pod{ - ObjectMeta: kmetav1.ObjectMeta{ - Name: "informer-obj", - Namespace: "default", - }, - Spec: kcorev1.PodSpec{ - Containers: []kcorev1.Container{ - { - Name: "nginx", - Image: "nginx", + Context("with structured objects", func() { + It("should be able to get informer for the object", func(done Done) { + By("getting a shared index informer for a pod") + pod := &kcorev1.Pod{ + ObjectMeta: kmetav1.ObjectMeta{ + Name: "informer-obj", + Namespace: "default", + }, + Spec: kcorev1.PodSpec{ + Containers: []kcorev1.Container{ + { + Name: "nginx", + Image: "nginx", + }, }, }, - }, - } - sii, err := informerCache.GetInformer(pod) - Expect(err).NotTo(HaveOccurred()) - Expect(sii).NotTo(BeNil()) - Expect(sii.HasSynced()).To(BeTrue()) - - By("adding an event handler listening for object creation which sends the object to a channel") - out := make(chan interface{}) - addFunc := func(obj interface{}) { - out <- obj - } - sii.AddEventHandler(kcache.ResourceEventHandlerFuncs{AddFunc: addFunc}) - - By("adding an object") - cl, err := client.New(cfg, client.Options{}) - Expect(err).NotTo(HaveOccurred()) - Expect(cl.Create(context.Background(), pod)).To(Succeed()) - - By("verifying the object is received on the channel") - Eventually(out).Should(Receive(Equal(pod))) - close(done) - }) - - // TODO: Add a test for when GVK is not in Scheme. Does code support informer for unstructured object? - It("should be able to get an informer by group/version/kind", func(done Done) { - By("getting an shared index informer for gvk = core/v1/pod") - gvk := schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"} - sii, err := informerCache.GetInformerForKind(gvk) - Expect(err).NotTo(HaveOccurred()) - Expect(sii).NotTo(BeNil()) - Expect(sii.HasSynced()).To(BeTrue()) - - By("adding an event handler listening for object creation which sends the object to a channel") - out := make(chan interface{}) - addFunc := func(obj interface{}) { - out <- obj - } - sii.AddEventHandler(kcache.ResourceEventHandlerFuncs{AddFunc: addFunc}) - - By("adding an object") - cl, err := client.New(cfg, client.Options{}) - Expect(err).NotTo(HaveOccurred()) - pod := &kcorev1.Pod{ - ObjectMeta: kmetav1.ObjectMeta{ - Name: "informer-gvk", - Namespace: "default", - }, - Spec: kcorev1.PodSpec{ - Containers: []kcorev1.Container{ - { - Name: "nginx", - Image: "nginx", + } + sii, err := informerCache.GetInformer(pod) + Expect(err).NotTo(HaveOccurred()) + Expect(sii).NotTo(BeNil()) + Expect(sii.HasSynced()).To(BeTrue()) + + By("adding an event handler listening for object creation which sends the object to a channel") + out := make(chan interface{}) + addFunc := func(obj interface{}) { + out <- obj + } + sii.AddEventHandler(kcache.ResourceEventHandlerFuncs{AddFunc: addFunc}) + + By("adding an object") + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl.Create(context.Background(), pod)).To(Succeed()) + + By("verifying the object is received on the channel") + Eventually(out).Should(Receive(Equal(pod))) + close(done) + }) + + // TODO: Add a test for when GVK is not in Scheme. Does code support informer for unstructured object? + It("should be able to get an informer by group/version/kind", func(done Done) { + By("getting an shared index informer for gvk = core/v1/pod") + gvk := schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"} + sii, err := informerCache.GetInformerForKind(gvk) + Expect(err).NotTo(HaveOccurred()) + Expect(sii).NotTo(BeNil()) + Expect(sii.HasSynced()).To(BeTrue()) + + By("adding an event handler listening for object creation which sends the object to a channel") + out := make(chan interface{}) + addFunc := func(obj interface{}) { + out <- obj + } + sii.AddEventHandler(kcache.ResourceEventHandlerFuncs{AddFunc: addFunc}) + + By("adding an object") + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + pod := &kcorev1.Pod{ + ObjectMeta: kmetav1.ObjectMeta{ + Name: "informer-gvk", + Namespace: "default", + }, + Spec: kcorev1.PodSpec{ + Containers: []kcorev1.Container{ + { + Name: "nginx", + Image: "nginx", + }, }, }, - }, - } - Expect(cl.Create(context.Background(), pod)).To(Succeed()) - - By("verifying the object is received on the channel") - Eventually(out).Should(Receive(Equal(pod))) - close(done) + } + Expect(cl.Create(context.Background(), pod)).To(Succeed()) + + By("verifying the object is received on the channel") + Eventually(out).Should(Receive(Equal(pod))) + close(done) + }) + + It("should be able to index an object field then retrieve objects by that field", func() { + By("creating the cache") + informer, err := cache.New(cfg, cache.Options{}) + Expect(err).NotTo(HaveOccurred()) + + By("indexing the restartPolicy field of the Pod object before starting") + pod := &kcorev1.Pod{} + indexFunc := func(obj runtime.Object) []string { + return []string{string(obj.(*kcorev1.Pod).Spec.RestartPolicy)} + } + Expect(informer.IndexField(pod, "spec.restartPolicy", indexFunc)).To(Succeed()) + + By("running the cache and waiting for it to sync") + go func() { + defer GinkgoRecover() + Expect(informer.Start(stop)).To(Succeed()) + }() + Expect(informer.WaitForCacheSync(stop)).NotTo(BeFalse()) + + By("listing Pods with restartPolicyOnFailure") + listObj := &kcorev1.PodList{} + Expect(informer.List(context.Background(), listObj, + client.MatchingField("spec.restartPolicy", "OnFailure"), + )).To(Succeed()) + + By("verifying that the returned pods have correct restart policy") + Expect(listObj.Items).NotTo(BeEmpty()) + Expect(listObj.Items).Should(HaveLen(1)) + actual := listObj.Items[0] + Expect(actual.Name).To(Equal("test-pod-3")) + }) }) - - It("should be able to index an object field then retrieve objects by that field", func() { - By("creating the cache") - informer, err := cache.New(cfg, cache.Options{}) - Expect(err).NotTo(HaveOccurred()) - - By("indexing the restartPolicy field of the Pod object before starting") - pod := &kcorev1.Pod{} - indexFunc := func(obj runtime.Object) []string { - return []string{string(obj.(*kcorev1.Pod).Spec.RestartPolicy)} - } - Expect(informer.IndexField(pod, "spec.restartPolicy", indexFunc)).To(Succeed()) - - By("running the cache and waiting for it to sync") - go func() { - defer GinkgoRecover() - Expect(informer.Start(stop)).To(Succeed()) - }() - Expect(informer.WaitForCacheSync(stop)).NotTo(BeFalse()) - - By("listing Pods with restartPolicyOnFailure") - listObj := &kcorev1.PodList{} - Expect(informer.List(context.Background(), - client.MatchingField("spec.restartPolicy", "OnFailure"), - listObj)).To(Succeed()) - - By("verifying that the returned pods have correct restart policy") - Expect(listObj.Items).NotTo(BeEmpty()) - Expect(listObj.Items).Should(HaveLen(1)) - actual := listObj.Items[0] - Expect(actual.Name).To(Equal("test-pod-3")) + Context("with unstructured objects", func() { + It("should be able to get informer for the object", func(done Done) { + By("getting a shared index informer for a pod") + + pod := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "containers": []map[string]interface{}{ + map[string]interface{}{ + "name": "nginx", + "image": "nginx", + }, + }, + }, + }, + } + pod.SetName("informer-obj2") + pod.SetNamespace("default") + pod.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "Pod", + }) + sii, err := informerCache.GetInformer(pod) + Expect(err).NotTo(HaveOccurred()) + Expect(sii).NotTo(BeNil()) + Expect(sii.HasSynced()).To(BeTrue()) + + By("adding an event handler listening for object creation which sends the object to a channel") + out := make(chan interface{}) + addFunc := func(obj interface{}) { + out <- obj + } + sii.AddEventHandler(kcache.ResourceEventHandlerFuncs{AddFunc: addFunc}) + + By("adding an object") + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl.Create(context.Background(), pod)).To(Succeed()) + + By("verifying the object is received on the channel") + Eventually(out).Should(Receive(Equal(pod))) + close(done) + }) + + It("should be able to index an object field then retrieve objects by that field", func() { + By("creating the cache") + informer, err := cache.New(cfg, cache.Options{}) + Expect(err).NotTo(HaveOccurred()) + + By("indexing the restartPolicy field of the Pod object before starting") + pod := &unstructured.Unstructured{} + pod.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "Pod", + }) + indexFunc := func(obj runtime.Object) []string { + s, ok := obj.(*unstructured.Unstructured).Object["spec"] + if !ok { + return []string{} + } + m, ok := s.(map[string]interface{}) + if !ok { + return []string{} + } + return []string{fmt.Sprintf("%v", m["restartPolicy"])} + } + Expect(informer.IndexField(pod, "spec.restartPolicy", indexFunc)).To(Succeed()) + + By("running the cache and waiting for it to sync") + go func() { + defer GinkgoRecover() + Expect(informer.Start(stop)).To(Succeed()) + }() + Expect(informer.WaitForCacheSync(stop)).NotTo(BeFalse()) + + By("listing Pods with restartPolicyOnFailure") + listObj := &unstructured.UnstructuredList{} + listObj.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "PodList", + }) + Expect(informer.List(context.Background(), + client.MatchingField("spec.restartPolicy", "OnFailure"), + listObj)).To(Succeed()) + + By("verifying that the returned pods have correct restart policy") + Expect(listObj.Items).NotTo(BeEmpty()) + Expect(listObj.Items).Should(HaveLen(1)) + actual := listObj.Items[0] + Expect(actual.GetName()).To(Equal("test-pod-3")) + }) }) }) }) diff --git a/pkg/cache/informer_cache.go b/pkg/cache/informer_cache.go index 03298baeeb..94d9e85cda 100644 --- a/pkg/cache/informer_cache.go +++ b/pkg/cache/informer_cache.go @@ -23,6 +23,7 @@ import ( "strings" apimeta "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/client-go/tools/cache" @@ -73,13 +74,25 @@ func (ip *informerCache) List(ctx context.Context, opts *client.ListOptions, out } // we need the non-list GVK, so chop off the "List" from the end of the kind gvk.Kind = gvk.Kind[:len(gvk.Kind)-4] - - // http://knowyourmeme.com/memes/this-is-fine - elemType := reflect.Indirect(reflect.ValueOf(itemsPtr)).Type().Elem() - cacheTypeValue := reflect.Zero(reflect.PtrTo(elemType)) - cacheTypeObj, ok := cacheTypeValue.Interface().(runtime.Object) - if !ok { - return fmt.Errorf("cannot get cache for %T, its element %T is not a runtime.Object", out, cacheTypeValue.Interface()) + _, isUnstructured := out.(*unstructured.UnstructuredList) + var cacheTypeObj runtime.Object + if isUnstructured { + u := &unstructured.Unstructured{} + u.SetGroupVersionKind(gvk) + cacheTypeObj = u + } else { + itemsPtr, err := apimeta.GetItemsPtr(out) + if err != nil { + return nil + } + // http://knowyourmeme.com/memes/this-is-fine + elemType := reflect.Indirect(reflect.ValueOf(itemsPtr)).Type().Elem() + cacheTypeValue := reflect.Zero(reflect.PtrTo(elemType)) + var ok bool + cacheTypeObj, ok = cacheTypeValue.Interface().(runtime.Object) + if !ok { + return fmt.Errorf("cannot get cache for %T, its element %T is not a runtime.Object", out, cacheTypeValue.Interface()) + } } cache, err := ip.InformersMap.Get(gvk, cacheTypeObj) diff --git a/pkg/cache/internal/informers_map.go b/pkg/cache/internal/informers_map.go index 7b8aaec526..e2de336930 100644 --- a/pkg/cache/internal/informers_map.go +++ b/pkg/cache/internal/informers_map.go @@ -158,6 +158,8 @@ func (ip *InformersMap) getMapEntry(gvk schema.GroupVersionKind, isUnstructured // the Informer from the map. func (ip *InformersMap) Get(gvk schema.GroupVersionKind, obj runtime.Object) (*MapEntry, error) { _, isUnstructured := obj.(*unstructured.Unstructured) + _, isUnstructuredList := obj.(*unstructured.UnstructuredList) + isUnstructured = isUnstructured || isUnstructuredList // Return the informer if it is found i, ok := ip.getMapEntry(gvk, isUnstructured) if ok { @@ -199,7 +201,7 @@ func (ip *InformersMap) Get(gvk schema.GroupVersionKind, obj runtime.Object) (*M Informer: ni, Reader: CacheReader{indexer: ni.GetIndexer(), groupVersionKind: gvk}, } - ip.informersByGVK[gvk] = i + ip.setMap(i, gvk, isUnstructured) // Start the Informer if need by // TODO(seans): write thorough tests and document what happens here - can you add indexers? @@ -224,6 +226,16 @@ func (ip *InformersMap) Get(gvk schema.GroupVersionKind, obj runtime.Object) (*M return i, err } +// setMap - helper function to decide which map to add to. +func (ip *InformersMap) setMap(i *MapEntry, gvk schema.GroupVersionKind, isUnstructured bool) { + if isUnstructured { + ip.unstructuredInformerByGVK[gvk] = i + } else { + + ip.informersByGVK[gvk] = i + } +} + // newListWatch returns a new ListWatch object that can be used to create a SharedIndexInformer. func (ip *InformersMap) newListWatch(gvk schema.GroupVersionKind, isUnstructured bool) (*cache.ListWatch, error) { // Kubernetes APIs work against Resources, not GroupVersionKinds. Map the From 10ed9c2c8911b4493dda9d812f17e2c24792649f Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Tue, 14 Aug 2018 17:03:42 -0400 Subject: [PATCH 6/8] Adding ability to use dynamic list for unstructured list watcher. --- pkg/cache/internal/deleg_map.go | 95 ++++++++++++++ pkg/cache/internal/informers_map.go | 186 +++++++++++++--------------- pkg/client/apiutil/apimachinery.go | 51 -------- pkg/client/client.go | 1 - pkg/client/client_cache.go | 12 +- pkg/client/client_test.go | 101 +++++++-------- 6 files changed, 236 insertions(+), 210 deletions(-) create mode 100644 pkg/cache/internal/deleg_map.go diff --git a/pkg/cache/internal/deleg_map.go b/pkg/cache/internal/deleg_map.go new file mode 100644 index 0000000000..feb1c6625f --- /dev/null +++ b/pkg/cache/internal/deleg_map.go @@ -0,0 +1,95 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package internal + +import ( + "time" + + "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/client-go/rest" + "k8s.io/client-go/tools/cache" +) + +// InformersMap create and caches Informers for (runtime.Object, schema.GroupVersionKind) pairs. +// It uses a standard parameter codec constructed based on the given generated Scheme. +type InformersMap struct { + // we abstract over the details of structured vs unstructured with the specificInformerMaps + + structured *specificInformersMap + unstructured *specificInformersMap + + // Scheme maps runtime.Objects to GroupVersionKinds + Scheme *runtime.Scheme +} + +// NewInformersMap creates a new InformersMap that can create informers for +// both structured and unstructured objects. +func NewInformersMap(config *rest.Config, + scheme *runtime.Scheme, + mapper meta.RESTMapper, + resync time.Duration) *InformersMap { + + return &InformersMap{ + structured: newStructuredInformersMap(config, scheme, mapper, resync), + unstructured: newUnstructuredInformersMap(config, scheme, mapper, resync), + + Scheme: scheme, + } +} + +// Start calls Run on each of the informers and sets started to true. Blocks on the stop channel. +func (m *InformersMap) Start(stop <-chan struct{}) error { + go m.structured.Start(stop) + go m.unstructured.Start(stop) + <-stop + return nil +} + +// WaitForCacheSync waits until all the caches have been synced. +func (m *InformersMap) WaitForCacheSync(stop <-chan struct{}) bool { + syncedFuncs := append([]cache.InformerSynced(nil), m.structured.HasSyncedFuncs()...) + syncedFuncs = append(syncedFuncs, m.unstructured.HasSyncedFuncs()...) + + return cache.WaitForCacheSync(stop, syncedFuncs...) +} + +// Get will create a new Informer and add it to the map of InformersMap if none exists. Returns +// the Informer from the map. +func (m *InformersMap) Get(gvk schema.GroupVersionKind, obj runtime.Object) (*MapEntry, error) { + _, isUnstructured := obj.(*unstructured.Unstructured) + _, isUnstructuredList := obj.(*unstructured.UnstructuredList) + isUnstructured = isUnstructured || isUnstructuredList + + if isUnstructured { + return m.unstructured.Get(gvk, obj) + } + + return m.structured.Get(gvk, obj) +} + +// newStructuredInformersMap creates a new InformersMap for structured objects. +func newStructuredInformersMap(config *rest.Config, scheme *runtime.Scheme, mapper meta.RESTMapper, resync time.Duration) *specificInformersMap { + return newSpecificInformersMap(config, scheme, mapper, resync, createStructuredListWatch) +} + +// newUnstructuredInformersMap creates a new InformersMap for unstructured objects. +func newUnstructuredInformersMap(config *rest.Config, scheme *runtime.Scheme, mapper meta.RESTMapper, resync time.Duration) *specificInformersMap { + return newSpecificInformersMap(config, scheme, mapper, resync, createUnstructuredListWatch) +} diff --git a/pkg/cache/internal/informers_map.go b/pkg/cache/internal/informers_map.go index e2de336930..1e3776f7ae 100644 --- a/pkg/cache/internal/informers_map.go +++ b/pkg/cache/internal/informers_map.go @@ -23,30 +23,35 @@ import ( "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/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/dynamic" "k8s.io/client-go/rest" "k8s.io/client-go/tools/cache" + "sigs.k8s.io/controller-runtime/pkg/client/apiutil" ) -// NewInformersMap returns a new InformersMap -func NewInformersMap(config *rest.Config, +// clientListWatcherFunc knows how to create a ListWatcher +type createListWatcherFunc func(gvk schema.GroupVersionKind, ip *specificInformersMap) (*cache.ListWatch, error) + +// newSpecificInformersMap returns a new specificInformersMap (like +// the generical InformersMap, except that it doesn't implement WaitForCacheSync). +func newSpecificInformersMap(config *rest.Config, scheme *runtime.Scheme, mapper meta.RESTMapper, - resync time.Duration) *InformersMap { - ip := &InformersMap{ - config: config, - Scheme: scheme, - mapper: mapper, - informersByGVK: make(map[schema.GroupVersionKind]*MapEntry), - unstructuredInformerByGVK: make(map[schema.GroupVersionKind]*MapEntry), - codecs: serializer.NewCodecFactory(scheme), - paramCodec: runtime.NewParameterCodec(scheme), - resync: resync, + resync time.Duration, createListWatcher createListWatcherFunc) *specificInformersMap { + ip := &specificInformersMap{ + config: config, + Scheme: scheme, + mapper: mapper, + informersByGVK: make(map[schema.GroupVersionKind]*MapEntry), + codecs: serializer.NewCodecFactory(scheme), + paramCodec: runtime.NewParameterCodec(scheme), + resync: resync, + createListWatcher: createListWatcher, } return ip } @@ -60,9 +65,9 @@ type MapEntry struct { Reader CacheReader } -// InformersMap create and caches Informers for (runtime.Object, schema.GroupVersionKind) pairs. -//It uses a standard parameter codec constructed based on the given generated Scheme. -type InformersMap struct { +// specificInformersMap create and caches Informers for (runtime.Object, schema.GroupVersionKind) pairs. +// It uses a standard parameter codec constructed based on the given generated Scheme. +type specificInformersMap struct { // Scheme maps runtime.Objects to GroupVersionKinds Scheme *runtime.Scheme @@ -75,10 +80,6 @@ type InformersMap struct { // informersByGVK is the cache of informers keyed by groupVersionKind informersByGVK map[schema.GroupVersionKind]*MapEntry - // unstructuredInformerByGVK is a cache of informers for unstructured types - // keyed by groupVersionKind - unstructuredInformerByGVK map[schema.GroupVersionKind]*MapEntry - // codecs is used to create a new REST client codecs serializer.CodecFactory @@ -93,20 +94,22 @@ type InformersMap struct { // mu guards access to the map mu sync.RWMutex - // mu guards access to the unstructured map - unstructuredMu sync.RWMutex // start is true if the informers have been started started bool + + // createClient knows how to create a client and a list object, + // and allows for abstracting over the particulars of structured vs + // unstructured objects. + createListWatcher createListWatcherFunc } // Start calls Run on each of the informers and sets started to true. Blocks on the stop channel. -func (ip *InformersMap) Start(stop <-chan struct{}) error { +// It doesn't return start because it can't return an error, and it's not a runnable directly. +func (ip *specificInformersMap) Start(stop <-chan struct{}) { func() { ip.mu.Lock() - ip.unstructuredMu.Lock() defer ip.mu.Unlock() - defer ip.unstructuredMu.Unlock() // Set the stop channel so it can be passed to informers that are added later ip.stop = stop @@ -116,52 +119,31 @@ func (ip *InformersMap) Start(stop <-chan struct{}) error { go informer.Informer.Run(stop) } - // Start each unstructured informer - for _, informer := range ip.unstructuredInformerByGVK { - go informer.Informer.Run(stop) - } - // Set started to true so we immediately start any informers added later. ip.started = true }() <-stop - return nil } -// WaitForCacheSync waits until all the caches have been synced -func (ip *InformersMap) WaitForCacheSync(stop <-chan struct{}) bool { - syncedFuncs := make([]cache.InformerSynced, 0, len(ip.informersByGVK)+len(ip.unstructuredInformerByGVK)) +// HasSyncedFuncs returns all the HasSynced functions for the informers in this map. +func (ip *specificInformersMap) HasSyncedFuncs() []cache.InformerSynced { + syncedFuncs := make([]cache.InformerSynced, 0, len(ip.informersByGVK)) for _, informer := range ip.informersByGVK { syncedFuncs = append(syncedFuncs, informer.Informer.HasSynced) } - for _, informer := range ip.unstructuredInformerByGVK { - syncedFuncs = append(syncedFuncs, informer.Informer.HasSynced) - } - return cache.WaitForCacheSync(stop, syncedFuncs...) -} - -func (ip *InformersMap) getMapEntry(gvk schema.GroupVersionKind, isUnstructured bool) (*MapEntry, bool) { - if isUnstructured { - ip.unstructuredMu.RLock() - defer ip.unstructuredMu.RUnlock() - i, ok := ip.unstructuredInformerByGVK[gvk] - return i, ok - } - ip.mu.RLock() - defer ip.mu.RUnlock() - i, ok := ip.informersByGVK[gvk] - return i, ok - + return syncedFuncs } -// Get will create a new Informer and add it to the map of InformersMap if none exists. Returns +// Get will create a new Informer and add it to the map of specificInformersMap if none exists. Returns // the Informer from the map. -func (ip *InformersMap) Get(gvk schema.GroupVersionKind, obj runtime.Object) (*MapEntry, error) { - _, isUnstructured := obj.(*unstructured.Unstructured) - _, isUnstructuredList := obj.(*unstructured.UnstructuredList) - isUnstructured = isUnstructured || isUnstructuredList +func (ip *specificInformersMap) Get(gvk schema.GroupVersionKind, obj runtime.Object) (*MapEntry, error) { // Return the informer if it is found - i, ok := ip.getMapEntry(gvk, isUnstructured) + i, ok := func() (*MapEntry, bool) { + ip.mu.RLock() + defer ip.mu.RUnlock() + i, ok := ip.informersByGVK[gvk] + return i, ok + }() if ok { return i, nil } @@ -170,27 +152,21 @@ func (ip *InformersMap) Get(gvk schema.GroupVersionKind, obj runtime.Object) (*M // need to be locked var sync bool i, err := func() (*MapEntry, error) { - var ok bool - var i *MapEntry - // Check the caches to see if we already have an Informer. If we do, return the Informer. + ip.mu.Lock() + defer ip.mu.Unlock() + + // Check the cache to see if we already have an Informer. If we do, return the Informer. // This is for the case where 2 routines tried to get the informer when it wasn't in the map // so neither returned early, but the first one created it. - if isUnstructured { - ip.unstructuredMu.Lock() - defer ip.unstructuredMu.Unlock() - i, ok = ip.unstructuredInformerByGVK[gvk] - } else { - ip.mu.Lock() - defer ip.mu.Unlock() - i, ok = ip.informersByGVK[gvk] - } + var ok bool + i, ok := ip.informersByGVK[gvk] if ok { return i, nil } // Create a NewSharedIndexInformer and add it to the map. var lw *cache.ListWatch - lw, err := ip.newListWatch(gvk, isUnstructured) + lw, err := ip.createListWatcher(gvk, ip) if err != nil { return nil, err } @@ -201,7 +177,7 @@ func (ip *InformersMap) Get(gvk schema.GroupVersionKind, obj runtime.Object) (*M Informer: ni, Reader: CacheReader{indexer: ni.GetIndexer(), groupVersionKind: gvk}, } - ip.setMap(i, gvk, isUnstructured) + ip.informersByGVK[gvk] = i // Start the Informer if need by // TODO(seans): write thorough tests and document what happens here - can you add indexers? @@ -226,18 +202,8 @@ func (ip *InformersMap) Get(gvk schema.GroupVersionKind, obj runtime.Object) (*M return i, err } -// setMap - helper function to decide which map to add to. -func (ip *InformersMap) setMap(i *MapEntry, gvk schema.GroupVersionKind, isUnstructured bool) { - if isUnstructured { - ip.unstructuredInformerByGVK[gvk] = i - } else { - - ip.informersByGVK[gvk] = i - } -} - // newListWatch returns a new ListWatch object that can be used to create a SharedIndexInformer. -func (ip *InformersMap) newListWatch(gvk schema.GroupVersionKind, isUnstructured bool) (*cache.ListWatch, error) { +func createStructuredListWatch(gvk schema.GroupVersionKind, ip *specificInformersMap) (*cache.ListWatch, error) { // Kubernetes APIs work against Resources, not GroupVersionKinds. Map the // groupVersionKind to the Resource API we will use. mapping, err := ip.mapper.RESTMapping(gvk.GroupKind(), gvk.Version) @@ -245,26 +211,14 @@ func (ip *InformersMap) newListWatch(gvk schema.GroupVersionKind, isUnstructured return nil, err } - // Construct a RESTClient for the groupVersionKind that we will use to - // talk to the apiserver. - var client rest.Interface - var listObj runtime.Object - if isUnstructured { - listObj = &unstructured.UnstructuredList{} - client, err = apiutil.RESTUnstructuredClientForGVK(gvk, ip.config) - if err != nil { - return nil, err - } - } else { - client, err = apiutil.RESTClientForGVK(gvk, ip.config, ip.codecs) - if err != nil { - return nil, err - } - listGVK := gvk.GroupVersion().WithKind(gvk.Kind + "List") - listObj, err = ip.Scheme.New(listGVK) - if err != nil { - return nil, err - } + client, err := apiutil.RESTClientForGVK(gvk, ip.config, ip.codecs) + if err != nil { + return nil, err + } + listGVK := gvk.GroupVersion().WithKind(gvk.Kind + "List") + listObj, err := ip.Scheme.New(listGVK) + if err != nil { + return nil, err } // Create a new ListWatch for the obj @@ -282,3 +236,29 @@ func (ip *InformersMap) newListWatch(gvk schema.GroupVersionKind, isUnstructured }, }, nil } + +func createUnstructuredListWatch(gvk schema.GroupVersionKind, ip *specificInformersMap) (*cache.ListWatch, error) { + // Kubernetes APIs work against Resources, not GroupVersionKinds. Map the + // groupVersionKind to the Resource API we will use. + mapping, err := ip.mapper.RESTMapping(gvk.GroupKind(), gvk.Version) + if err != nil { + return nil, err + } + dynamicClient, err := dynamic.NewForConfig(ip.config) + if err != nil { + return nil, err + } + + // Create a new ListWatch for the obj + return &cache.ListWatch{ + ListFunc: func(opts metav1.ListOptions) (runtime.Object, error) { + return dynamicClient.Resource(mapping.Resource).List(opts) + }, + // Setup the watch function + WatchFunc: func(opts metav1.ListOptions) (watch.Interface, error) { + // Watch needs to be set to true separately + opts.Watch = true + return dynamicClient.Resource(mapping.Resource).Watch(opts) + }, + }, nil +} diff --git a/pkg/client/apiutil/apimachinery.go b/pkg/client/apiutil/apimachinery.go index c9419842fb..614d454f1f 100644 --- a/pkg/client/apiutil/apimachinery.go +++ b/pkg/client/apiutil/apimachinery.go @@ -17,20 +17,13 @@ limitations under the License. package apiutil import ( - "encoding/json" "fmt" - "io" - "strings" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "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/discovery" - "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "k8s.io/client-go/restmapper" ) @@ -77,22 +70,6 @@ func RESTClientForGVK(gvk schema.GroupVersionKind, baseConfig *rest.Config, code return rest.RESTClientFor(cfg) } -// RESTUnstructuredClientForGVK constructs a new rest.Interface for accessing unstructured resources. -func RESTUnstructuredClientForGVK(gvk schema.GroupVersionKind, baseConfig *rest.Config) (rest.Interface, error) { - cfg := createRestConfig(gvk, baseConfig) - var jsonInfo runtime.SerializerInfo - for _, info := range scheme.Codecs.SupportedMediaTypes() { - if info.MediaType == runtime.ContentTypeJSON { - jsonInfo = info - break - } - } - jsonInfo.Serializer = dynamicCodec{} - cfg.NegotiatedSerializer = serializer.NegotiatedSerializerWrapper(jsonInfo) - - return rest.RESTClientFor(cfg) -} - //createRestConfig copies the base config and updates needed fields for a new rest config func createRestConfig(gvk schema.GroupVersionKind, baseConfig *rest.Config) *rest.Config { gv := gvk.GroupVersion() @@ -108,32 +85,4 @@ func createRestConfig(gvk schema.GroupVersionKind, baseConfig *rest.Config) *res cfg.UserAgent = rest.DefaultKubernetesUserAgent() } return cfg - -} - -//Copied from deprecated-dynamic/bad_debt.go -// dynamicCodec is a codec that wraps the standard unstructured codec -// with special handling for Status objects. -// Deprecated only used by test code and its wrong -type dynamicCodec struct{} - -func (dynamicCodec) Decode(data []byte, gvk *schema.GroupVersionKind, obj runtime.Object) (runtime.Object, *schema.GroupVersionKind, error) { - obj, gvk, err := unstructured.UnstructuredJSONScheme.Decode(data, gvk, obj) - if err != nil { - return nil, nil, err - } - - if _, ok := obj.(*metav1.Status); !ok && strings.ToLower(gvk.Kind) == "status" { - obj = &metav1.Status{} - err := json.Unmarshal(data, obj) - if err != nil { - return nil, nil, err - } - } - - return obj, gvk, nil -} - -func (dynamicCodec) Encode(obj runtime.Object, w io.Writer) error { - return unstructured.UnstructuredJSONScheme.Encode(obj, w) } diff --git a/pkg/client/client.go b/pkg/client/client.go index 1ac8bd65e2..780c4386bf 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -158,7 +158,6 @@ func (c *client) List(ctx context.Context, opts *ListOptions, obj runtime.Object VersionedParams(opts.AsListOptions(), c.paramCodec). Do(). Into(obj) - return err } // Status implements client.StatusClient diff --git a/pkg/client/client_cache.go b/pkg/client/client_cache.go index 30b7240d1b..8d58c2e0c3 100644 --- a/pkg/client/client_cache.go +++ b/pkg/client/client_cache.go @@ -45,17 +45,18 @@ type clientCache struct { // codecs are used to create a REST client for a gvk codecs serializer.CodecFactory + muByType sync.RWMutex // resourceByType caches type metadata resourceByType map[reflect.Type]*resourceMeta + + muByGVK sync.RWMutex // resourceByGVK caches type metadata for unstructured unstructuredResourceByGVK map[schema.GroupVersionKind]*resourceMeta - muByType sync.RWMutex - muByGVK 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) { +func (c *clientCache) newResource(obj runtime.Object, isUnstructured bool) (*resourceMeta, error) { gvk, err := apiutil.GVKForObject(obj, c.scheme) if err != nil { return nil, err @@ -66,7 +67,6 @@ func (c *clientCache) newResource(obj runtime.Object) (*resourceMeta, error) { gvk.Kind = gvk.Kind[:len(gvk.Kind)-4] } - _, isUnstructured := obj.(*unstructured.Unstructured) var client rest.Interface if isUnstructured { client, err = apiutil.RESTUnstructuredClientForGVK(gvk, c.config) @@ -97,7 +97,7 @@ func (c *clientCache) getUnstructuredResourceByGVK(obj runtime.Object) (*resourc // Initialize a new Client c.muByGVK.Lock() defer c.muByGVK.Unlock() - r, err := c.newResource(obj) + r, err := c.newResource(obj, true) if err != nil { return nil, err } @@ -121,7 +121,7 @@ func (c *clientCache) getResourceByType(obj runtime.Object) (*resourceMeta, erro // Initialize a new Client c.muByType.Lock() defer c.muByType.Unlock() - r, err := c.newResource(obj) + r, err := c.newResource(obj, false) if err != nil { return nil, err } diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index 4d1f9beacb..14d424aa3b 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -1457,64 +1457,67 @@ var _ = Describe("Client", func() { close(done) }, serverSideTimeoutSeconds) - /* - It("should filter results by field selector", func(done Done) { - By("creating a Deployment with name deployment-frontend") - depFrontend := &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{Name: "deployment-frontend", Namespace: ns}, - Spec: appsv1.DeploymentSpec{ - Selector: &metav1.LabelSelector{ - MatchLabels: map[string]string{"app": "frontend"}, - }, - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "frontend"}}, - Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}}, - }, + It("should filter results by field selector", func(done Done) { + By("creating a Deployment with name deployment-frontend") + depFrontend := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: "deployment-frontend", Namespace: ns}, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "frontend"}, }, - } - depFrontend, err := clientset.AppsV1().Deployments(ns).Create(depFrontend) - Expect(err).NotTo(HaveOccurred()) - - By("creating a Deployment with name deployment-backend") - depBackend := &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{Name: "deployment-backend", Namespace: ns}, - Spec: appsv1.DeploymentSpec{ - Selector: &metav1.LabelSelector{ - MatchLabels: map[string]string{"app": "backend"}, - }, - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "backend"}}, - Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}}, - }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "frontend"}}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}}, }, - } - depBackend, err = clientset.AppsV1().Deployments(ns).Create(depBackend) - Expect(err).NotTo(HaveOccurred()) + }, + } + depFrontend, err := clientset.AppsV1().Deployments(ns).Create(depFrontend) + Expect(err).NotTo(HaveOccurred()) + + By("creating a Deployment with name deployment-backend") + depBackend := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: "deployment-backend", Namespace: ns}, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "backend"}, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "backend"}}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}}, + }, + }, + } + depBackend, err = clientset.AppsV1().Deployments(ns).Create(depBackend) + Expect(err).NotTo(HaveOccurred()) - cl, err := client.New(cfg, client.Options{}) - Expect(err).NotTo(HaveOccurred()) + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) - By("listing all Deployments with field metadata.name=deployment-backend") - deps := &appsv1.DeploymentList{} - lo := client.MatchingField("metadata.name", "deployment-backend") - Expect(cl.List(context.Background(), lo, deps)).NotTo(HaveOccurred()) + By("listing all Deployments with field metadata.name=deployment-backend") + deps := &unstructured.UnstructuredList{} + deps.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "apps", + Kind: "DeploymentList", + Version: "v1", + }) + lo := client.MatchingField("metadata.name", "deployment-backend") + Expect(cl.List(context.Background(), lo, deps)).NotTo(HaveOccurred()) - By("only the Deployment with the backend field is returned") - Expect(deps.Items).NotTo(BeEmpty()) - Expect(1).To(Equal(len(deps.Items))) - actual := deps.Items[0] - Expect(actual.Name).To(Equal("deployment-backend")) + By("only the Deployment with the backend field is returned") + Expect(deps.Items).NotTo(BeEmpty()) + Expect(1).To(Equal(len(deps.Items))) + actual := deps.Items[0] + Expect(actual.GetName()).To(Equal("deployment-backend")) - deleteDeployment(depFrontend, ns) - deleteDeployment(depBackend, ns) + deleteDeployment(depFrontend, ns) + deleteDeployment(depBackend, ns) - close(done) - }, serverSideTimeoutSeconds) + close(done) + }, serverSideTimeoutSeconds) - It("should fail if the object doesn't have meta", func() { + PIt("should fail if the object doesn't have meta", func() { - }) - */ + }) }) }) From 00c5c79fd54c2318e1b048e71d485894b8035f28 Mon Sep 17 00:00:00 2001 From: Shawn Hurley Date: Fri, 31 Aug 2018 13:59:56 -0400 Subject: [PATCH 7/8] Adding two types of client for unstructured and typed --- pkg/cache/cache_test.go | 62 +++++++----- pkg/cache/informer_cache.go | 5 - pkg/client/client.go | 107 ++++++++------------ pkg/client/client_cache.go | 63 ++---------- pkg/client/client_test.go | 161 +++++++++++++++++++++++++----- pkg/client/typed_client.go | 107 ++++++++++++++++++++ pkg/client/unstructured_client.go | 145 +++++++++++++++++++++++++++ 7 files changed, 475 insertions(+), 175 deletions(-) create mode 100644 pkg/client/typed_client.go create mode 100644 pkg/client/unstructured_client.go diff --git a/pkg/cache/cache_test.go b/pkg/cache/cache_test.go index badef27794..832cecde8d 100644 --- a/pkg/cache/cache_test.go +++ b/pkg/cache/cache_test.go @@ -117,7 +117,7 @@ var _ = Describe("Informer Cache", func() { It("should be able to list objects that haven't been watched previously", func() { By("listing all services in the cluster") listObj := &kcorev1.ServiceList{} - Expect(informerCache.List(context.Background(), listObj)).To(Succeed()) + Expect(informerCache.List(context.Background(), nil, listObj)).To(Succeed()) By("verifying that the returned list contains the Kubernetes service") // NB: kubernetes default service is automatically created in testenv. @@ -147,10 +147,10 @@ var _ = Describe("Informer Cache", func() { By("listing pods with a particular label") // NB: each pod has a "test-label": out := kcorev1.PodList{} - Expect(informerCache.List(context.Background(), &out, - client.InNamespace(testNamespaceTwo), - client.MatchingLabels(map[string]string{"test-label": "test-pod-2"}), - )).To(Succeed()) + lo := &client.ListOptions{} + lo.InNamespace(testNamespaceTwo) + lo.MatchingLabels(map[string]string{"test-label": "test-pod-2"}) + Expect(informerCache.List(context.Background(), lo, &out)).To(Succeed()) By("verifying the returned pods have the correct label") Expect(out.Items).NotTo(BeEmpty()) @@ -167,9 +167,9 @@ var _ = Describe("Informer Cache", func() { // NB: each pod has a "test-label": out := kcorev1.PodList{} labels := map[string]string{"test-label": "test-pod-2"} - Expect(informerCache.List(context.Background(), &out, - client.MatchingLabels(labels), - )).To(Succeed()) + lo := &client.ListOptions{} + lo.MatchingLabels(labels) + Expect(informerCache.List(context.Background(), lo, &out)).To(Succeed()) By("verifying multiple pods with the same label in different namespaces are returned") Expect(out.Items).NotTo(BeEmpty()) @@ -184,9 +184,9 @@ var _ = Describe("Informer Cache", func() { It("should be able to list objects by namespace", func() { By("listing pods in test-namespace-1") listObj := &kcorev1.PodList{} - Expect(informerCache.List(context.Background(), - client.InNamespace(testNamespaceOne), - listObj)).To(Succeed()) + lo := &client.ListOptions{} + lo.InNamespace(testNamespaceOne) + Expect(informerCache.List(context.Background(), lo, listObj)).To(Succeed()) By("verifying that the returned pods are in test-namespace-1") Expect(listObj.Items).NotTo(BeEmpty()) @@ -231,7 +231,8 @@ var _ = Describe("Informer Cache", func() { Version: "v1", Kind: "ServiceList", }) - Expect(informerCache.List(context.Background(), nil, listObj)).To(Succeed()) + err := informerCache.List(context.Background(), nil, listObj) + Expect(err).To(Succeed()) By("verifying that the returned list contains the Kubernetes service") // NB: kubernetes default service is automatically created in testenv. @@ -271,8 +272,11 @@ var _ = Describe("Informer Cache", func() { Version: "v1", Kind: "PodList", }) - Expect(informerCache.List(context.Background(), client.InNamespace(testNamespaceTwo). - MatchingLabels(map[string]string{"test-label": "test-pod-2"}), &out)).To(Succeed()) + lo := &client.ListOptions{} + lo.InNamespace(testNamespaceTwo) + lo.MatchingLabels(map[string]string{"test-label": "test-pod-2"}) + err := informerCache.List(context.Background(), lo, &out) + Expect(err).To(Succeed()) By("verifying the returned pods have the correct label") Expect(out.Items).NotTo(BeEmpty()) @@ -294,8 +298,10 @@ var _ = Describe("Informer Cache", func() { Kind: "PodList", }) labels := map[string]string{"test-label": "test-pod-2"} - Expect(informerCache.List(context.Background(), - client.MatchingLabels(labels), &out)).To(Succeed()) + lo := &client.ListOptions{} + lo.MatchingLabels(labels) + err := informerCache.List(context.Background(), lo, &out) + Expect(err).To(Succeed()) By("verifying multiple pods with the same label in different namespaces are returned") Expect(out.Items).NotTo(BeEmpty()) @@ -315,9 +321,10 @@ var _ = Describe("Informer Cache", func() { Version: "v1", Kind: "PodList", }) - Expect(informerCache.List(context.Background(), - client.InNamespace(testNamespaceOne), - listObj)).To(Succeed()) + lo := &client.ListOptions{} + lo.InNamespace(testNamespaceOne) + err := informerCache.List(context.Background(), lo, listObj) + Expect(err).To(Succeed()) By("verifying that the returned pods are in test-namespace-1") Expect(listObj.Items).NotTo(BeEmpty()) @@ -470,9 +477,9 @@ var _ = Describe("Informer Cache", func() { By("listing Pods with restartPolicyOnFailure") listObj := &kcorev1.PodList{} - Expect(informer.List(context.Background(), listObj, - client.MatchingField("spec.restartPolicy", "OnFailure"), - )).To(Succeed()) + lo := &client.ListOptions{} + lo.MatchingField("spec.restartPolicy", "OnFailure") + Expect(informer.List(context.Background(), lo, listObj)).To(Succeed()) By("verifying that the returned pods have correct restart policy") Expect(listObj.Items).NotTo(BeEmpty()) @@ -524,7 +531,7 @@ var _ = Describe("Informer Cache", func() { By("verifying the object is received on the channel") Eventually(out).Should(Receive(Equal(pod))) close(done) - }) + }, 3) It("should be able to index an object field then retrieve objects by that field", func() { By("creating the cache") @@ -565,16 +572,17 @@ var _ = Describe("Informer Cache", func() { Version: "v1", Kind: "PodList", }) - Expect(informer.List(context.Background(), - client.MatchingField("spec.restartPolicy", "OnFailure"), - listObj)).To(Succeed()) + lo := &client.ListOptions{} + lo.MatchingField("spec.restartPolicy", "OnFailure") + err = informer.List(context.Background(), lo, listObj) + Expect(err).To(Succeed()) By("verifying that the returned pods have correct restart policy") Expect(listObj.Items).NotTo(BeEmpty()) Expect(listObj.Items).Should(HaveLen(1)) actual := listObj.Items[0] Expect(actual.GetName()).To(Equal("test-pod-3")) - }) + }, 3) }) }) }) diff --git a/pkg/cache/informer_cache.go b/pkg/cache/informer_cache.go index 94d9e85cda..eedfcc6cf0 100644 --- a/pkg/cache/informer_cache.go +++ b/pkg/cache/informer_cache.go @@ -59,11 +59,6 @@ func (ip *informerCache) Get(ctx context.Context, key client.ObjectKey, out runt // List implements Reader func (ip *informerCache) List(ctx context.Context, opts *client.ListOptions, out runtime.Object) error { - itemsPtr, err := apimeta.GetItemsPtr(out) - if err != nil { - return nil - } - gvk, err := apiutil.GVKForObject(out, ip.Scheme) if err != nil { return err diff --git a/pkg/client/client.go b/pkg/client/client.go index 780c4386bf..fa2b09eaf5 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -22,9 +22,10 @@ import ( "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" @@ -59,16 +60,26 @@ func New(config *rest.Config, options Options) (Client, error) { } } + dynamicClient, err := dynamic.NewForConfig(config) + if err != nil { + return nil, err + } + c := &client{ - cache: clientCache{ - config: config, - scheme: options.Scheme, - mapper: options.Mapper, - codecs: serializer.NewCodecFactory(options.Scheme), - resourceByType: make(map[reflect.Type]*resourceMeta), - unstructuredResourceByGVK: make(map[schema.GroupVersionKind]*resourceMeta), + typedClient: typedClient{ + cache: clientCache{ + config: config, + scheme: options.Scheme, + mapper: options.Mapper, + codecs: serializer.NewCodecFactory(options.Scheme), + resourceByType: make(map[reflect.Type]*resourceMeta), + }, + paramCodec: runtime.NewParameterCodec(options.Scheme), + }, + unstructuredClient: unstructuredClient{ + client: dynamicClient, + restMapper: options.Mapper, }, - paramCodec: runtime.NewParameterCodec(options.Scheme), } return c, nil @@ -79,85 +90,53 @@ var _ Client = &client{} // 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 client struct { - cache clientCache - paramCodec runtime.ParameterCodec + typedClient typedClient + unstructuredClient unstructuredClient } // Create implements client.Client func (c *client) Create(ctx context.Context, obj runtime.Object) error { - o, err := c.cache.getObjMeta(obj) - if err != nil { - return err + _, ok := obj.(*unstructured.Unstructured) + if ok { + return c.unstructuredClient.Create(ctx, obj) } - return o.Post(). - NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()). - Resource(o.resource()). - Body(obj). - Do(). - Into(obj) + return c.typedClient.Create(ctx, obj) } // Update implements client.Client func (c *client) Update(ctx context.Context, obj runtime.Object) error { - o, err := c.cache.getObjMeta(obj) - if err != nil { - return err + _, ok := obj.(*unstructured.Unstructured) + if ok { + return c.unstructuredClient.Update(ctx, obj) } - return o.Put(). - NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()). - Resource(o.resource()). - Name(o.GetName()). - Body(obj). - Do(). - Into(obj) + return c.typedClient.Update(ctx, obj) } // Delete implements client.Client func (c *client) Delete(ctx context.Context, obj runtime.Object, opts ...DeleteOptionFunc) error { - o, err := c.cache.getObjMeta(obj) - if err != nil { - return err + _, ok := obj.(*unstructured.Unstructured) + if ok { + return c.unstructuredClient.Delete(ctx, obj, opts...) } - - deleteOpts := DeleteOptions{} - return o.Delete(). - NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()). - Resource(o.resource()). - Name(o.GetName()). - Body(deleteOpts.ApplyOptions(opts).AsDeleteOptions()). - Do(). - Error() + return c.typedClient.Delete(ctx, obj, opts...) } // Get implements client.Client func (c *client) Get(ctx context.Context, key ObjectKey, obj runtime.Object) error { - r, err := c.cache.getResource(obj) - if err != nil { - return err + _, ok := obj.(*unstructured.Unstructured) + if ok { + return c.unstructuredClient.Get(ctx, key, obj) } - return r.Get(). - NamespaceIfScoped(key.Namespace, r.isNamespaced()). - Resource(r.resource()). - Name(key.Name).Do().Into(obj) + return c.typedClient.Get(ctx, key, obj) } // List implements client.Client func (c *client) List(ctx context.Context, opts *ListOptions, obj runtime.Object) error { - r, err := c.cache.getResource(obj) - if err != nil { - return err + _, ok := obj.(*unstructured.UnstructuredList) + if ok { + return c.unstructuredClient.List(ctx, opts, obj) } - namespace := "" - if opts != nil { - namespace = opts.Namespace - } - return r.Get(). - NamespaceIfScoped(namespace, r.isNamespaced()). - Resource(r.resource()). - Body(obj). - VersionedParams(opts.AsListOptions(), c.paramCodec). - Do(). - Into(obj) + return c.typedClient.List(ctx, opts, obj) } // Status implements client.StatusClient @@ -175,7 +154,7 @@ var _ StatusWriter = &statusWriter{} // Update implements client.StatusWriter func (sw *statusWriter) Update(_ context.Context, obj runtime.Object) error { - o, err := sw.client.cache.getObjMeta(obj) + o, err := sw.client.typedClient.cache.getObjMeta(obj) if err != nil { return err } diff --git a/pkg/client/client_cache.go b/pkg/client/client_cache.go index 8d58c2e0c3..d6452ab62e 100644 --- a/pkg/client/client_cache.go +++ b/pkg/client/client_cache.go @@ -23,7 +23,6 @@ import ( "k8s.io/apimachinery/pkg/api/meta" "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/apimachinery/pkg/runtime/serializer" @@ -45,18 +44,14 @@ type clientCache struct { // codecs are used to create a REST client for a gvk codecs serializer.CodecFactory - muByType sync.RWMutex // resourceByType caches type metadata resourceByType map[reflect.Type]*resourceMeta - - muByGVK sync.RWMutex - // resourceByGVK caches type metadata for unstructured - unstructuredResourceByGVK 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, isUnstructured bool) (*resourceMeta, error) { +func (c *clientCache) newResource(obj runtime.Object) (*resourceMeta, error) { gvk, err := apiutil.GVKForObject(obj, c.scheme) if err != nil { return nil, err @@ -67,12 +62,7 @@ func (c *clientCache) newResource(obj runtime.Object, isUnstructured bool) (*res gvk.Kind = gvk.Kind[:len(gvk.Kind)-4] } - var client rest.Interface - if isUnstructured { - client, err = apiutil.RESTUnstructuredClientForGVK(gvk, c.config) - } else { - client, err = apiutil.RESTClientForGVK(gvk, c.config, c.codecs) - } + client, err := apiutil.RESTClientForGVK(gvk, c.config, c.codecs) if err != nil { return nil, err } @@ -83,45 +73,25 @@ func (c *clientCache) newResource(obj runtime.Object, isUnstructured bool) (*res return &resourceMeta{Interface: client, mapping: mapping, gvk: gvk}, nil } -func (c *clientCache) getUnstructuredResourceByGVK(obj runtime.Object) (*resourceMeta, error) { - // It's better to do creation work twice than to not let multiple - // people make requests at once - c.muByGVK.RLock() - r, known := c.unstructuredResourceByGVK[obj.GetObjectKind().GroupVersionKind()] - c.muByGVK.RUnlock() - - if known { - return r, nil - } - - // Initialize a new Client - c.muByGVK.Lock() - defer c.muByGVK.Unlock() - r, err := c.newResource(obj, true) - if err != nil { - return nil, err - } - c.unstructuredResourceByGVK[obj.GetObjectKind().GroupVersionKind()] = r - return r, err -} - -func (c *clientCache) getResourceByType(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) // It's better to do creation work twice than to not let multiple // people make requests at once - c.muByType.RLock() + c.mu.RLock() r, known := c.resourceByType[typ] - c.muByType.RUnlock() + c.mu.RUnlock() if known { return r, nil } // Initialize a new Client - c.muByType.Lock() - defer c.muByType.Unlock() - r, err := c.newResource(obj, false) + c.mu.Lock() + defer c.mu.Unlock() + r, err := c.newResource(obj) if err != nil { return nil, err } @@ -129,17 +99,6 @@ func (c *clientCache) getResourceByType(obj runtime.Object) (*resourceMeta, erro return r, err } -// 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) { - _, isUnstructured := obj.(*unstructured.Unstructured) - _, isUnstructuredList := obj.(*unstructured.UnstructuredList) - if isUnstructured || isUnstructuredList { - return c.getUnstructuredResourceByGVK(obj) - } - return c.getResourceByType(obj) -} - // getObjMeta returns objMeta containing both type and object metadata and state func (c *clientCache) getObjMeta(obj runtime.Object) (*objMeta, error) { r, err := c.getResource(obj) diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index 14d424aa3b..4389fd8daa 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -1101,9 +1101,9 @@ var _ = Describe("Client", func() { By("listing all Deployments with label app=backend") deps := &appsv1.DeploymentList{} labels := map[string]string{"app": "backend"} - err = cl.List(context.Background(), deps, - client.MatchingLabels(labels), - ) + lo := &client.ListOptions{} + lo.MatchingLabels(labels) + err = cl.List(context.Background(), lo, deps) Expect(err).NotTo(HaveOccurred()) By("only the Deployment with the backend label is returned") @@ -1162,7 +1162,9 @@ var _ = Describe("Client", func() { By("listing all Deployments in test-namespace-1") deps := &appsv1.DeploymentList{} - err = cl.List(context.Background(), deps, client.InNamespace("test-namespace-1")) + lo := &client.ListOptions{} + lo.InNamespace("test-namespace-1") + err = cl.List(context.Background(), lo, deps) Expect(err).NotTo(HaveOccurred()) By("only the Deployment in test-namespace-1 is returned") @@ -1217,7 +1219,9 @@ var _ = Describe("Client", func() { By("listing all Deployments with field metadata.name=deployment-backend") deps := &appsv1.DeploymentList{} - err = cl.List(context.Background(), deps, client.MatchingField("metadata.name", "deployment-backend")) + lo := &client.ListOptions{} + lo.MatchingField("metadata.name", "deployment-backend") + err = cl.List(context.Background(), lo, deps) Expect(err).NotTo(HaveOccurred()) By("only the Deployment with the backend field is returned") @@ -1305,10 +1309,10 @@ var _ = Describe("Client", func() { By("listing all Deployments in test-namespace-3 with label app=frontend") deps := &appsv1.DeploymentList{} labels := map[string]string{"app": "frontend"} - err = cl.List(context.Background(), deps, - client.InNamespace("test-namespace-3"), - client.MatchingLabels(labels), - ) + lo := &client.ListOptions{} + lo.InNamespace("test-namespace-3") + lo.MatchingLabels(labels) + err = cl.List(context.Background(), lo, deps) Expect(err).NotTo(HaveOccurred()) By("only the Deployment in test-namespace-3 with label app=frontend is returned") @@ -1356,7 +1360,7 @@ var _ = Describe("Client", func() { Kind: "DeploymentList", Version: "v1", }) - err = cl.List(context.Background(), deps) + err = cl.List(context.Background(), nil, deps) Expect(err).NotTo(HaveOccurred()) Expect(deps.Items).NotTo(BeEmpty()) @@ -1382,7 +1386,7 @@ var _ = Describe("Client", func() { Kind: "DeploymentList", Version: "v1", }) - Expect(cl.List(context.Background(), deps)).NotTo(HaveOccurred()) + Expect(cl.List(context.Background(), nil, deps)).NotTo(HaveOccurred()) By("validating no Deployments are returned") Expect(deps.Items).To(BeEmpty()) @@ -1391,12 +1395,12 @@ var _ = Describe("Client", func() { }, serverSideTimeoutSeconds) It("should filter results by namespace selector", func(done Done) { - By("creating a Deployment in test-namespace-3") - tns1 := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test-namespace-3"}} + By("creating a Deployment in test-namespace-5") + tns1 := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test-namespace-5"}} _, err := clientset.CoreV1().Namespaces().Create(tns1) Expect(err).NotTo(HaveOccurred()) depFrontend := &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{Name: "deployment-frontend", Namespace: "test-namespace-3"}, + ObjectMeta: metav1.ObjectMeta{Name: "deployment-frontend", Namespace: "test-namespace-5"}, Spec: appsv1.DeploymentSpec{ Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{"app": "frontend"}, @@ -1407,15 +1411,15 @@ var _ = Describe("Client", func() { }, }, } - depFrontend, err = clientset.AppsV1().Deployments("test-namespace-3").Create(depFrontend) + depFrontend, err = clientset.AppsV1().Deployments("test-namespace-5").Create(depFrontend) Expect(err).NotTo(HaveOccurred()) - By("creating a Deployment in test-namespace-4") - tns2 := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test-namespace-4"}} + By("creating a Deployment in test-namespace-6") + tns2 := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test-namespace-6"}} _, err = clientset.CoreV1().Namespaces().Create(tns2) Expect(err).NotTo(HaveOccurred()) depBackend := &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{Name: "deployment-backend", Namespace: "test-namespace-4"}, + ObjectMeta: metav1.ObjectMeta{Name: "deployment-backend", Namespace: "test-namespace-6"}, Spec: appsv1.DeploymentSpec{ Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{"app": "backend"}, @@ -1426,31 +1430,32 @@ var _ = Describe("Client", func() { }, }, } - depBackend, err = clientset.AppsV1().Deployments("test-namespace-4").Create(depBackend) + depBackend, err = clientset.AppsV1().Deployments("test-namespace-6").Create(depBackend) Expect(err).NotTo(HaveOccurred()) cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) - By("listing all Deployments in test-namespace-3") + By("listing all Deployments in test-namespace-5") deps := &unstructured.UnstructuredList{} deps.SetGroupVersionKind(schema.GroupVersionKind{ Group: "apps", Kind: "DeploymentList", Version: "v1", }) - lo := client.InNamespace("test-namespace-3") - err = cl.List(context.Background(), deps, client.InNamespace("test-namespace-3")).NotTo(HaveOccurred()) + lo := &client.ListOptions{} + lo.InNamespace("test-namespace-5") + err = cl.List(context.Background(), lo, deps) Expect(err).NotTo(HaveOccurred()) - By("only the Deployment in test-namespace-3 is returned") + By("only the Deployment in test-namespace-5 is returned") Expect(deps.Items).NotTo(BeEmpty()) Expect(1).To(Equal(len(deps.Items))) actual := deps.Items[0] Expect(actual.GetName()).To(Equal("deployment-frontend")) - deleteDeployment(depFrontend, "test-namespace-3") - deleteDeployment(depBackend, "test-namespace-4") + deleteDeployment(depFrontend, "test-namespace-5") + deleteDeployment(depBackend, "test-namespace-6") deleteNamespace(tns1) deleteNamespace(tns2) @@ -1500,8 +1505,10 @@ var _ = Describe("Client", func() { Kind: "DeploymentList", Version: "v1", }) - lo := client.MatchingField("metadata.name", "deployment-backend") - Expect(cl.List(context.Background(), lo, deps)).NotTo(HaveOccurred()) + lo := &client.ListOptions{} + lo.MatchingField("metadata.name", "deployment-backend") + err = cl.List(context.Background(), lo, deps) + Expect(err).NotTo(HaveOccurred()) By("only the Deployment with the backend field is returned") Expect(deps.Items).NotTo(BeEmpty()) @@ -1515,6 +1522,106 @@ var _ = Describe("Client", func() { close(done) }, serverSideTimeoutSeconds) + It("should filter results by namespace selector and label selector", func(done Done) { + By("creating a Deployment in test-namespace-7 with the app=frontend label") + tns3 := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test-namespace-7"}} + _, err := clientset.CoreV1().Namespaces().Create(tns3) + Expect(err).NotTo(HaveOccurred()) + depFrontend3 := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "deployment-frontend", + Namespace: "test-namespace-7", + Labels: map[string]string{"app": "frontend"}, + }, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "frontend"}, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "frontend"}}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}}, + }, + }, + } + depFrontend3, err = clientset.AppsV1().Deployments("test-namespace-7").Create(depFrontend3) + Expect(err).NotTo(HaveOccurred()) + + By("creating a Deployment in test-namespace-7 with the app=backend label") + depBackend3 := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "deployment-backend", + Namespace: "test-namespace-7", + Labels: map[string]string{"app": "backend"}, + }, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "backend"}, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "backend"}}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}}, + }, + }, + } + depBackend3, err = clientset.AppsV1().Deployments("test-namespace-7").Create(depBackend3) + Expect(err).NotTo(HaveOccurred()) + + By("creating a Deployment in test-namespace-8 with the app=frontend label") + tns4 := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test-namespace-8"}} + _, err = clientset.CoreV1().Namespaces().Create(tns4) + Expect(err).NotTo(HaveOccurred()) + depFrontend4 := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "deployment-frontend", + Namespace: "test-namespace-8", + Labels: map[string]string{"app": "frontend"}, + }, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "frontend"}, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "frontend"}}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}}, + }, + }, + } + depFrontend4, err = clientset.AppsV1().Deployments("test-namespace-8").Create(depFrontend4) + Expect(err).NotTo(HaveOccurred()) + + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + + By("listing all Deployments in test-namespace-8 with label app=frontend") + deps := &unstructured.UnstructuredList{} + deps.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "apps", + Kind: "DeploymentList", + Version: "v1", + }) + labels := map[string]string{"app": "frontend"} + lo := &client.ListOptions{} + lo.InNamespace("test-namespace-7") + lo.MatchingLabels(labels) + err = cl.List(context.Background(), lo, deps) + Expect(err).NotTo(HaveOccurred()) + + By("only the Deployment in test-namespace-7 with label app=frontend is returned") + Expect(deps.Items).NotTo(BeEmpty()) + Expect(1).To(Equal(len(deps.Items))) + actual := deps.Items[0] + Expect(actual.GetName()).To(Equal("deployment-frontend")) + Expect(actual.GetNamespace()).To(Equal("test-namespace-7")) + + deleteDeployment(depFrontend3, "test-namespace-7") + deleteDeployment(depBackend3, "test-namespace-7") + deleteDeployment(depFrontend4, "test-namespace-8") + deleteNamespace(tns3) + deleteNamespace(tns4) + + close(done) + }, serverSideTimeoutSeconds) + PIt("should fail if the object doesn't have meta", func() { }) diff --git a/pkg/client/typed_client.go b/pkg/client/typed_client.go new file mode 100644 index 0000000000..9834863e52 --- /dev/null +++ b/pkg/client/typed_client.go @@ -0,0 +1,107 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package client + +import ( + "context" + + "k8s.io/apimachinery/pkg/runtime" +) + +// 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 + paramCodec runtime.ParameterCodec +} + +// Create implements client.Client +func (c *typedClient) Create(ctx context.Context, obj runtime.Object) error { + o, err := c.cache.getObjMeta(obj) + if err != nil { + return err + } + return o.Post(). + NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()). + Resource(o.resource()). + Body(obj). + Do(). + Into(obj) +} + +// Update implements client.Client +func (c *typedClient) Update(ctx context.Context, obj runtime.Object) error { + o, err := c.cache.getObjMeta(obj) + if err != nil { + return err + } + return o.Put(). + NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()). + Resource(o.resource()). + Name(o.GetName()). + Body(obj). + Do(). + Into(obj) +} + +// Delete implements client.Client +func (c *typedClient) Delete(ctx context.Context, obj runtime.Object, opts ...DeleteOptionFunc) error { + o, err := c.cache.getObjMeta(obj) + if err != nil { + return err + } + + deleteOpts := DeleteOptions{} + return o.Delete(). + NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()). + Resource(o.resource()). + Name(o.GetName()). + Body(deleteOpts.ApplyOptions(opts).AsDeleteOptions()). + Do(). + Error() +} + +// Get implements client.Client +func (c *typedClient) Get(ctx context.Context, key ObjectKey, obj runtime.Object) error { + r, err := c.cache.getResource(obj) + if err != nil { + return err + } + return r.Get(). + NamespaceIfScoped(key.Namespace, r.isNamespaced()). + Resource(r.resource()). + Name(key.Name).Do().Into(obj) +} + +// List implements client.Client +func (c *typedClient) List(ctx context.Context, opts *ListOptions, obj runtime.Object) error { + r, err := c.cache.getResource(obj) + if err != nil { + return err + } + namespace := "" + if opts != nil { + namespace = opts.Namespace + } + return r.Get(). + NamespaceIfScoped(namespace, r.isNamespaced()). + Resource(r.resource()). + Body(obj). + VersionedParams(opts.AsListOptions(), c.paramCodec). + Do(). + Into(obj) +} diff --git a/pkg/client/unstructured_client.go b/pkg/client/unstructured_client.go new file mode 100644 index 0000000000..17494da252 --- /dev/null +++ b/pkg/client/unstructured_client.go @@ -0,0 +1,145 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package client + +import ( + "context" + "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 +} + +// Create implements client.Client +func (uc *unstructuredClient) Create(ctx context.Context, 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(), u.GetNamespace()) + if err != nil { + return err + } + i, err := r.Create(u) + if err != nil { + return err + } + u.Object = i.Object + return nil +} + +// Update implements client.Client +func (uc *unstructuredClient) Update(ctx context.Context, 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(), u.GetNamespace()) + if err != nil { + return err + } + i, err := r.Update(u) + if err != nil { + return err + } + u.Object = i.Object + return nil +} + +// Delete implements client.Client +func (uc *unstructuredClient) Delete(ctx context.Context, obj runtime.Object, opts ...DeleteOptionFunc) 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()) + if err != nil { + return err + } + deleteOpts := DeleteOptions{} + err = r.Delete(u.GetName(), deleteOpts.ApplyOptions(opts).AsDeleteOptions()) + return err +} + +// Get implements client.Client +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{}) + if err != nil { + return err + } + u.Object = i.Object + return nil +} + +// List implements client.Client +func (uc *unstructuredClient) List(ctx context.Context, opts *ListOptions, obj runtime.Object) 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] + } + namespace := "" + if opts != nil { + namespace = opts.Namespace + } + r, err := uc.getResourceInterface(gvk, namespace) + if err != nil { + return err + } + + i, err := r.List(*opts.AsListOptions()) + if err != nil { + return err + } + u.Items = i.Items + u.Object = i.Object + return nil +} + +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 +} From 7471369bdb842fa2a85bc8a3e3ac250e8346801c Mon Sep 17 00:00:00 2001 From: Shawn Hurley Date: Wed, 19 Sep 2018 15:09:59 -0400 Subject: [PATCH 8/8] Status Writer working with unstructured as well as typed objects --- pkg/client/client.go | 21 +-- pkg/client/client_test.go | 270 +++++++++++++++++++++--------- pkg/client/typed_client.go | 30 +++- pkg/client/unstructured_client.go | 27 ++- 4 files changed, 239 insertions(+), 109 deletions(-) diff --git a/pkg/client/client.go b/pkg/client/client.go index fa2b09eaf5..05b9eba2b4 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -153,21 +153,10 @@ type statusWriter struct { var _ StatusWriter = &statusWriter{} // Update implements client.StatusWriter -func (sw *statusWriter) Update(_ context.Context, obj runtime.Object) error { - o, err := sw.client.typedClient.cache.getObjMeta(obj) - if err != nil { - return err +func (sw *statusWriter) Update(ctx context.Context, obj runtime.Object) error { + _, ok := obj.(*unstructured.Unstructured) + if ok { + return sw.client.unstructuredClient.UpdateStatus(ctx, obj) } - // TODO(droot): examine the returned error and check if it error needs to be - // wrapped to improve the UX ? - // It will be nice to receive an error saying the object doesn't implement - // status subresource and check CRD definition - return o.Put(). - NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()). - Resource(o.resource()). - Name(o.GetName()). - SubResource("status"). - Body(obj). - Do(). - Into(obj) + return sw.client.typedClient.UpdateStatus(ctx, obj) } diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index 4389fd8daa..3ab5c12de7 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -541,114 +541,218 @@ var _ = Describe("Client", func() { }) Describe("StatusClient", func() { - It("should update status of an existing object", func(done Done) { - cl, err := client.New(cfg, client.Options{}) - Expect(err).NotTo(HaveOccurred()) - Expect(cl).NotTo(BeNil()) + Context("with structured objects", func() { + It("should update status of an existing object", 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("initially creating a Deployment") + dep, err := clientset.AppsV1().Deployments(ns).Create(dep) + Expect(err).NotTo(HaveOccurred()) - By("updating the status of Deployment") - dep.Status.Replicas = 1 - err = cl.Status().Update(context.TODO(), dep) - Expect(err).NotTo(HaveOccurred()) + By("updating the status of Deployment") + dep.Status.Replicas = 1 + err = cl.Status().Update(context.TODO(), dep) + Expect(err).NotTo(HaveOccurred()) - By("validating updated 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)) + By("validating updated 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) - }) + 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()) - Expect(cl).NotTo(BeNil()) + It("should not update spec of an existing object", 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("initially creating a Deployment") + dep, err := clientset.AppsV1().Deployments(ns).Create(dep) + Expect(err).NotTo(HaveOccurred()) - By("updating the spec and status of Deployment") - var rc int32 = 1 - dep.Status.Replicas = 1 - dep.Spec.Replicas = &rc - err = cl.Status().Update(context.TODO(), dep) - Expect(err).NotTo(HaveOccurred()) + By("updating the spec and status of Deployment") + var rc int32 = 1 + dep.Status.Replicas = 1 + dep.Spec.Replicas = &rc + err = cl.Status().Update(context.TODO(), dep) + Expect(err).NotTo(HaveOccurred()) - By("validating updated Deployment has new status and unchanged spec") - 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)) - Expect(*actual.Spec.Replicas).To(BeEquivalentTo(replicaCount)) + By("validating updated Deployment has new status and unchanged spec") + 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)) + Expect(*actual.Spec.Replicas).To(BeEquivalentTo(replicaCount)) - close(done) - }) + close(done) + }) - It("should update an existing object non-namespace object", func(done Done) { - cl, err := client.New(cfg, client.Options{}) - Expect(err).NotTo(HaveOccurred()) - Expect(cl).NotTo(BeNil()) + It("should update an existing object non-namespace object", func(done Done) { + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) - node, err := clientset.CoreV1().Nodes().Create(node) - Expect(err).NotTo(HaveOccurred()) + node, err := clientset.CoreV1().Nodes().Create(node) + Expect(err).NotTo(HaveOccurred()) - By("updating status of the object") - node.Status.Phase = corev1.NodeRunning - err = cl.Status().Update(context.TODO(), node) - Expect(err).NotTo(HaveOccurred()) + By("updating status of the object") + node.Status.Phase = corev1.NodeRunning + err = cl.Status().Update(context.TODO(), node) + Expect(err).NotTo(HaveOccurred()) - By("validate updated Node had new annotation") - actual, err := clientset.CoreV1().Nodes().Get(node.Name, metav1.GetOptions{}) - Expect(err).NotTo(HaveOccurred()) - Expect(actual).NotTo(BeNil()) - Expect(actual.Status.Phase).To(Equal(corev1.NodeRunning)) + By("validate updated Node had new annotation") + actual, err := clientset.CoreV1().Nodes().Get(node.Name, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(actual).NotTo(BeNil()) + Expect(actual.Status.Phase).To(Equal(corev1.NodeRunning)) - close(done) - }) + close(done) + }) - It("should fail if the object does not exists", func(done Done) { - cl, err := client.New(cfg, client.Options{}) - Expect(err).NotTo(HaveOccurred()) - Expect(cl).NotTo(BeNil()) + It("should fail if the object does not exists", func(done Done) { + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) - By("updating status of a non-existent object") - err = cl.Status().Update(context.TODO(), dep) - Expect(err).To(HaveOccurred()) + By("updating status of a non-existent object") + err = cl.Status().Update(context.TODO(), dep) + Expect(err).To(HaveOccurred()) - close(done) - }) + close(done) + }) - It("should fail if the object cannot be mapped to a GVK", func(done Done) { - By("creating client with empty Scheme") - emptyScheme := runtime.NewScheme() - cl, err := client.New(cfg, client.Options{Scheme: emptyScheme}) - Expect(err).NotTo(HaveOccurred()) - Expect(cl).NotTo(BeNil()) + It("should fail if the object cannot be mapped to a GVK", func(done Done) { + By("creating client with empty Scheme") + emptyScheme := runtime.NewScheme() + cl, err := client.New(cfg, client.Options{Scheme: emptyScheme}) + 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("initially creating a Deployment") + dep, err := clientset.AppsV1().Deployments(ns).Create(dep) + Expect(err).NotTo(HaveOccurred()) - By("updating status of the Deployment") - dep.Status.Replicas = 1 - err = cl.Status().Update(context.TODO(), dep) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("no kind is registered for the type")) + By("updating status of the Deployment") + dep.Status.Replicas = 1 + err = cl.Status().Update(context.TODO(), dep) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("no kind is registered for the type")) - close(done) - }) + close(done) + }) - PIt("should fail if the GVK cannot be mapped to a Resource", func() { + PIt("should fail if the GVK cannot be mapped to a Resource", func() { + + }) + PIt("should fail if an API does not implement Status subresource", func() { + + }) }) - PIt("should fail if an API does not implement Status subresource", func() { + Context("with unstructured objects", func() { + It("should update status of an existing object", 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 + scheme.Convert(dep, u, nil) + err = cl.Status().Update(context.TODO(), u) + Expect(err).NotTo(HaveOccurred()) + + By("validating updated 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()) + Expect(cl).NotTo(BeNil()) + + By("initially creating a Deployment") + dep, err := clientset.AppsV1().Deployments(ns).Create(dep) + Expect(err).NotTo(HaveOccurred()) + + By("updating the spec and status of Deployment") + u := &unstructured.Unstructured{} + var rc int32 = 1 + dep.Status.Replicas = 1 + dep.Spec.Replicas = &rc + scheme.Convert(dep, u, nil) + err = cl.Status().Update(context.TODO(), u) + Expect(err).NotTo(HaveOccurred()) + + By("validating updated Deployment has new status and unchanged spec") + 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)) + Expect(*actual.Spec.Replicas).To(BeEquivalentTo(replicaCount)) + + close(done) + }) + + It("should update an existing object non-namespace object", func(done Done) { + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) + + node, err := clientset.CoreV1().Nodes().Create(node) + Expect(err).NotTo(HaveOccurred()) + + By("updating status of the object") + u := &unstructured.Unstructured{} + node.Status.Phase = corev1.NodeRunning + scheme.Convert(node, u, nil) + err = cl.Status().Update(context.TODO(), u) + Expect(err).NotTo(HaveOccurred()) + + By("validate updated Node had new annotation") + actual, err := clientset.CoreV1().Nodes().Get(node.Name, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(actual).NotTo(BeNil()) + Expect(actual.Status.Phase).To(Equal(corev1.NodeRunning)) + + close(done) + }) + + It("should fail if the object does not exists", func(done Done) { + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) + + By("updating status of a non-existent object") + u := &unstructured.Unstructured{} + scheme.Convert(dep, u, nil) + err = cl.Status().Update(context.TODO(), u) + Expect(err).To(HaveOccurred()) + + close(done) + }) + + PIt("should fail if the GVK cannot be mapped to a Resource", func() { + + }) + + PIt("should fail if an API does not implement Status subresource", func() { + + }) }) }) diff --git a/pkg/client/typed_client.go b/pkg/client/typed_client.go index 9834863e52..63c232fa92 100644 --- a/pkg/client/typed_client.go +++ b/pkg/client/typed_client.go @@ -30,7 +30,7 @@ type typedClient struct { } // Create implements client.Client -func (c *typedClient) Create(ctx context.Context, obj runtime.Object) error { +func (c *typedClient) Create(_ context.Context, obj runtime.Object) error { o, err := c.cache.getObjMeta(obj) if err != nil { return err @@ -44,7 +44,7 @@ func (c *typedClient) Create(ctx context.Context, obj runtime.Object) error { } // Update implements client.Client -func (c *typedClient) Update(ctx context.Context, obj runtime.Object) error { +func (c *typedClient) Update(_ context.Context, obj runtime.Object) error { o, err := c.cache.getObjMeta(obj) if err != nil { return err @@ -59,7 +59,7 @@ func (c *typedClient) Update(ctx context.Context, obj runtime.Object) error { } // Delete implements client.Client -func (c *typedClient) Delete(ctx context.Context, obj runtime.Object, opts ...DeleteOptionFunc) error { +func (c *typedClient) Delete(_ context.Context, obj runtime.Object, opts ...DeleteOptionFunc) error { o, err := c.cache.getObjMeta(obj) if err != nil { return err @@ -76,7 +76,7 @@ func (c *typedClient) Delete(ctx context.Context, obj runtime.Object, opts ...De } // Get implements client.Client -func (c *typedClient) Get(ctx context.Context, key ObjectKey, obj runtime.Object) error { +func (c *typedClient) Get(_ context.Context, key ObjectKey, obj runtime.Object) error { r, err := c.cache.getResource(obj) if err != nil { return err @@ -88,7 +88,7 @@ func (c *typedClient) Get(ctx context.Context, key ObjectKey, obj runtime.Object } // List implements client.Client -func (c *typedClient) List(ctx context.Context, opts *ListOptions, obj runtime.Object) error { +func (c *typedClient) List(_ context.Context, opts *ListOptions, obj runtime.Object) error { r, err := c.cache.getResource(obj) if err != nil { return err @@ -105,3 +105,23 @@ func (c *typedClient) List(ctx context.Context, opts *ListOptions, obj runtime.O Do(). Into(obj) } + +// UpdateStatus used by StatusWriter to write status. +func (c *typedClient) UpdateStatus(_ context.Context, obj runtime.Object) error { + o, err := c.cache.getObjMeta(obj) + if err != nil { + return err + } + // TODO(droot): examine the returned error and check if it error needs to be + // wrapped to improve the UX ? + // It will be nice to receive an error saying the object doesn't implement + // status subresource and check CRD definition + return o.Put(). + NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()). + Resource(o.resource()). + Name(o.GetName()). + SubResource("status"). + Body(obj). + Do(). + Into(obj) +} diff --git a/pkg/client/unstructured_client.go b/pkg/client/unstructured_client.go index 17494da252..13217b07da 100644 --- a/pkg/client/unstructured_client.go +++ b/pkg/client/unstructured_client.go @@ -37,7 +37,7 @@ type unstructuredClient struct { } // Create implements client.Client -func (uc *unstructuredClient) Create(ctx context.Context, obj runtime.Object) error { +func (uc *unstructuredClient) Create(_ context.Context, obj runtime.Object) error { u, ok := obj.(*unstructured.Unstructured) if !ok { return fmt.Errorf("unstructured client did not understand object: %T", obj) @@ -55,7 +55,7 @@ func (uc *unstructuredClient) Create(ctx context.Context, obj runtime.Object) er } // Update implements client.Client -func (uc *unstructuredClient) Update(ctx context.Context, obj runtime.Object) error { +func (uc *unstructuredClient) Update(_ context.Context, obj runtime.Object) error { u, ok := obj.(*unstructured.Unstructured) if !ok { return fmt.Errorf("unstructured client did not understand object: %T", obj) @@ -73,7 +73,7 @@ func (uc *unstructuredClient) Update(ctx context.Context, obj runtime.Object) er } // Delete implements client.Client -func (uc *unstructuredClient) Delete(ctx context.Context, obj runtime.Object, opts ...DeleteOptionFunc) error { +func (uc *unstructuredClient) Delete(_ context.Context, obj runtime.Object, opts ...DeleteOptionFunc) error { u, ok := obj.(*unstructured.Unstructured) if !ok { return fmt.Errorf("unstructured client did not understand object: %T", obj) @@ -88,7 +88,7 @@ func (uc *unstructuredClient) Delete(ctx context.Context, obj runtime.Object, op } // Get implements client.Client -func (uc *unstructuredClient) Get(ctx context.Context, key ObjectKey, obj runtime.Object) error { +func (uc *unstructuredClient) Get(_ 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) @@ -106,7 +106,7 @@ func (uc *unstructuredClient) Get(ctx context.Context, key ObjectKey, obj runtim } // List implements client.Client -func (uc *unstructuredClient) List(ctx context.Context, opts *ListOptions, obj runtime.Object) error { +func (uc *unstructuredClient) List(_ context.Context, opts *ListOptions, obj runtime.Object) error { u, ok := obj.(*unstructured.UnstructuredList) if !ok { return fmt.Errorf("unstructured client did not understand object: %T", obj) @@ -133,6 +133,23 @@ func (uc *unstructuredClient) List(ctx context.Context, opts *ListOptions, obj r return nil } +func (uc *unstructuredClient) UpdateStatus(_ context.Context, 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(), u.GetNamespace()) + if err != nil { + return err + } + i, err := r.UpdateStatus(u) + if err != nil { + return err + } + u.Object = i.Object + return nil +} + func (uc *unstructuredClient) getResourceInterface(gvk schema.GroupVersionKind, ns string) (dynamic.ResourceInterface, error) { mapping, err := uc.restMapper.RESTMapping(gvk.GroupKind(), gvk.Version) if err != nil {