diff --git a/pkg/cache/cache_test.go b/pkg/cache/cache_test.go index 82441777d8..832cecde8d 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,477 @@ 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(), 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 + } } - } - 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{} + 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()) + 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"} + 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()) + 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{} + 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()) + 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", + }) + 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. + 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", + }) + 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()) + 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"} + 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()) + 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", + }) + 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()) + 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{} + 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()) + 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) + }, 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 := &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", + }) + 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 03298baeeb..eedfcc6cf0 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" @@ -58,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 @@ -73,13 +69,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/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 4564fc95e1..1e3776f7ae 100644 --- a/pkg/cache/internal/informers_map.go +++ b/pkg/cache/internal/informers_map.go @@ -27,24 +27,31 @@ import ( "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), - 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 } @@ -58,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 @@ -90,10 +97,16 @@ type InformersMap struct { // 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() defer ip.mu.Unlock() @@ -110,21 +123,20 @@ func (ip *InformersMap) Start(stop <-chan struct{}) error { ip.started = true }() <-stop - return nil } -// WaitForCacheSync waits until all the caches have been synced -func (ip *InformersMap) WaitForCacheSync(stop <-chan struct{}) bool { +// 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) } - return cache.WaitForCacheSync(stop, syncedFuncs...) + 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) { +func (ip *specificInformersMap) Get(gvk schema.GroupVersionKind, obj runtime.Object) (*MapEntry, error) { // Return the informer if it is found i, ok := func() (*MapEntry, bool) { ip.mu.RLock() @@ -154,7 +166,7 @@ func (ip *InformersMap) Get(gvk schema.GroupVersionKind, obj runtime.Object) (*M // Create a NewSharedIndexInformer and add it to the map. var lw *cache.ListWatch - lw, err := ip.newListWatch(gvk) + lw, err := ip.createListWatcher(gvk, ip) if err != nil { return nil, err } @@ -191,14 +203,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 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) @@ -206,7 +211,10 @@ func (ip *InformersMap) newListWatch(gvk schema.GroupVersionKind) (*cache.ListWa return nil, err } - // Get a listObject for listing that the ListWatch can DeepCopy + 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 { @@ -228,3 +236,29 @@ func (ip *InformersMap) newListWatch(gvk schema.GroupVersionKind) (*cache.ListWa }, }, 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 9365eb2af0..614d454f1f 100644 --- a/pkg/client/apiutil/apimachinery.go +++ b/pkg/client/apiutil/apimachinery.go @@ -65,6 +65,13 @@ 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) +} + +//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 +81,8 @@ 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..05b9eba2b4 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -22,8 +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/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" @@ -58,15 +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), + 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 @@ -77,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 - } - namespace := "" - if opts != nil { - namespace = opts.Namespace + _, ok := obj.(*unstructured.UnstructuredList) + if ok { + return c.unstructuredClient.List(ctx, opts, obj) } - 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 @@ -172,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.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 072322c8ed..3ab5c12de7 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -25,9 +25,11 @@ 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" + "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/client" kscheme "k8s.io/client-go/kubernetes/scheme" @@ -160,821 +162,1573 @@ 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()) - - 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("writing the result back to the go struct") - Expect(dep).To(Equal(actual)) + 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()) - close(done) - }) + By("creating the object") + err = cl.Create(context.TODO(), dep) + Expect(err).NotTo(HaveOccurred()) - 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()) + 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") - var u runtime.Unstructured = &unstructured.Unstructured{} - scheme.Convert(dep, u, nil) + By("writing the result back to the go struct") + Expect(dep).To(Equal(actual)) - 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()) + 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("initially creating a Deployment") - dep, err := clientset.AppsV1().Deployments(ns).Create(dep) - 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 Deployment") - dep.Annotations = map[string]string{"foo": "bar"} - err = cl.Update(context.TODO(), dep) - Expect(err).NotTo(HaveOccurred()) + node, err := clientset.CoreV1().Nodes().Create(node) + 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")) + 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 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()) + close(done) + }) - By("initially creating a Deployment") - dep, err := clientset.AppsV1().Deployments(ns).Create(dep) - 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("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 non-existent object") + err = cl.Update(context.TODO(), dep) + Expect(err).To(HaveOccurred()) - By("updating the Deployment") - err = cl.Update(context.TODO(), u) - Expect(err).NotTo(HaveOccurred()) + 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")) + PIt("should fail if the object does not pass server-side validation", func() { - close(done) - }) + }) - 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()) + PIt("should fail if the object doesn't have meta", func() { - node, err := clientset.CoreV1().Nodes().Create(node) - Expect(err).NotTo(HaveOccurred()) + }) - By("updating the object") - node.Annotations = map[string]string{"foo": "bar"} - err = cl.Update(context.TODO(), node) - 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("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("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")) - 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 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 object does not pass server-side validation", 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) + }) }) + }) - PIt("should fail if the object doesn't have meta", func() { + Describe("StatusClient", func() { + 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("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)) + + 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") + 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)) + + 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 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()) - By("initially creating a Deployment") - dep, err := clientset.AppsV1().Deployments(ns).Create(dep) - Expect(err).NotTo(HaveOccurred()) + node, err := clientset.CoreV1().Nodes().Create(node) + Expect(err).NotTo(HaveOccurred()) - 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("updating status of the object") + node.Status.Phase = corev1.NodeRunning + err = cl.Status().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.Status.Phase).To(Equal(corev1.NodeRunning)) - PIt("should fail if the GVK cannot be mapped to a Resource", 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()) - 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()) + By("updating status of a non-existent object") + err = cl.Status().Update(context.TODO(), dep) + Expect(err).To(HaveOccurred()) - By("initially creating a Deployment") - dep, err := clientset.AppsV1().Deployments(ns).Create(dep) - Expect(err).NotTo(HaveOccurred()) + close(done) + }) - By("updating the status of Deployment") - dep.Status.Replicas = 1 - err = cl.Status().Update(context.TODO(), 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("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("initially creating a Deployment") + dep, err := clientset.AppsV1().Deployments(ns).Create(dep) + Expect(err).NotTo(HaveOccurred()) - close(done) - }) + 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")) - 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()) + close(done) + }) - By("initially creating a Deployment") - dep, err := clientset.AppsV1().Deployments(ns).Create(dep) - Expect(err).NotTo(HaveOccurred()) + PIt("should fail if the GVK cannot be mapped to a Resource", func() { - 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)) + PIt("should fail if an API does not implement Status subresource", func() { - 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()) + 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) + }) - By("updating status of the object") - node.Status.Phase = corev1.NodeRunning - err = cl.Status().Update(context.TODO(), node) - Expect(err).NotTo(HaveOccurred()) + 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) + }) - 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)) + 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()) - close(done) - }) + 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("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("updating status of a non-existent object") - err = cl.Status().Update(context.TODO(), dep) - Expect(err).To(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) - }) + 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 does not exists", 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 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()) - 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() { + }) }) }) 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()) - - By("initially creating a Deployment") - 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) - - By("deleting the unstructured Deployment") - depName := dep.Name - err = cl.Delete(context.TODO(), u) - Expect(err).NotTo(HaveOccurred()) + 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("fetching newly created unstructured Deployment") - _, err = clientset.AppsV1().Deployments(ns).Get(depName, metav1.GetOptions{}) - Expect(err).To(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()) - close(done) - }) + By("initially creating a Node") + node, err := clientset.CoreV1().Nodes().Create(node) + 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("deleting the Node") + nodeName := node.Name + err = cl.Delete(context.TODO(), node) + Expect(err).NotTo(HaveOccurred()) - By("initially creating a Node") - node, err := clientset.CoreV1().Nodes().Create(node) - Expect(err).NotTo(HaveOccurred()) + By("validating the Node no longer exists") + _, err = clientset.CoreV1().Nodes().Get(nodeName, metav1.GetOptions{}) + Expect(err).To(HaveOccurred()) - By("deleting the Node") - nodeName := node.Name - err = cl.Delete(context.TODO(), node) - Expect(err).NotTo(HaveOccurred()) + close(done) + }) - By("validating the Node no longer exists") - _, err = clientset.CoreV1().Nodes().Get(nodeName, metav1.GetOptions{}) - 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("Deleting node before it is ever created") + err = cl.Delete(context.TODO(), node) + 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("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()) - - 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("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 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()) + 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) + }) - 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()) - Expect(node).To(Equal(&actual)) + 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()) - close(done) - }) + Expect(node).To(Equal(&actual)) - 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()) - - 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()) + close(done) + }) - By("fetching the created Node") - 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()) + 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 fetched Node equals the created one") - Expect(node).To(Equal(&actual)) + 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) - }) + 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()) + PIt("should fail if the object doesn't have meta", func() { - 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 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 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, + 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"} + 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") + 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{} + 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") + 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{} + 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()) + 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"} + 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") + 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(), 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 { + 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(), nil, 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-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-5"}, + 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-5").Create(depFrontend) + Expect(err).NotTo(HaveOccurred()) + + 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-6"}, + 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-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-5") + deps := &unstructured.UnstructuredList{} + deps.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "apps", + Kind: "DeploymentList", + Version: "v1", + }) + 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-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-5") + deleteDeployment(depBackend, "test-namespace-6") + 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 := &unstructured.UnstructuredList{} + deps.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "apps", + Kind: "DeploymentList", + Version: "v1", + }) + 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()) + Expect(1).To(Equal(len(deps.Items))) + actual := deps.Items[0] + Expect(actual.GetName()).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-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() { + }) }) }) @@ -1092,3 +1846,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 +} 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/client/typed_client.go b/pkg/client/typed_client.go new file mode 100644 index 0000000000..63c232fa92 --- /dev/null +++ b/pkg/client/typed_client.go @@ -0,0 +1,127 @@ +/* +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(_ 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(_ 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(_ 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(_ 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(_ 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) +} + +// 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 new file mode 100644 index 0000000000..13217b07da --- /dev/null +++ b/pkg/client/unstructured_client.go @@ -0,0 +1,162 @@ +/* +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(_ 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(_ 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(_ 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(_ 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(_ 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) 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 { + 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 +} 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,