From 32e94b6ab3ff09ab80da63bc6fbfbe6e734fc9f5 Mon Sep 17 00:00:00 2001 From: Varsha Date: Thu, 22 Oct 2020 07:47:36 -0700 Subject: [PATCH] :sparkles: Add namespace enforcing wrapper for client.Client (#1088) * This commit adds a namespace enforcing wrapper for client.Client. This helps while dealing with namespace-scoped objects, where the namespace value need not be specified in every operation. * Modify namespaced client to use RestMapper for finding the scope of object. * Address review comments --- pkg/client/namespaced_client.go | 253 ++++++++++++ pkg/client/namespaced_client_test.go | 583 +++++++++++++++++++++++++++ 2 files changed, 836 insertions(+) create mode 100644 pkg/client/namespaced_client.go create mode 100644 pkg/client/namespaced_client_test.go diff --git a/pkg/client/namespaced_client.go b/pkg/client/namespaced_client.go new file mode 100644 index 0000000000..5ed8baca9b --- /dev/null +++ b/pkg/client/namespaced_client.go @@ -0,0 +1,253 @@ +/* +Copyright 2020 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" + "errors" + "fmt" + + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/client/apiutil" +) + +// NewNamespacedClient wraps an existing client enforcing the namespace value. +// All functions using this client will have the same namespace declared here. +func NewNamespacedClient(c Client, ns string) Client { + return &namespacedClient{ + client: c, + namespace: ns, + } +} + +var _ Client = &namespacedClient{} + +// namespacedClient is a Client that wraps another Client in order to enforce the specified namespace value. +type namespacedClient struct { + namespace string + client Client +} + +// Scheme returns the scheme this client is using. +func (n *namespacedClient) Scheme() *runtime.Scheme { + return n.client.Scheme() +} + +// RESTMapper returns the scheme this client is using. +func (n *namespacedClient) RESTMapper() meta.RESTMapper { + return n.client.RESTMapper() +} + +// isNamespaced returns true if the object is namespace scoped. +// For unstructured objects the gvk is found from the object itself. +func isNamespaced(c Client, obj runtime.Object) (bool, error) { + var gvk schema.GroupVersionKind + var err error + + _, isUnstructured := obj.(*unstructured.Unstructured) + _, isUnstructuredList := obj.(*unstructured.UnstructuredList) + + isUnstructured = isUnstructured || isUnstructuredList + if isUnstructured { + gvk = obj.GetObjectKind().GroupVersionKind() + } else { + gvk, err = apiutil.GVKForObject(obj, c.Scheme()) + if err != nil { + return false, err + } + } + + gk := schema.GroupKind{ + Group: gvk.Group, + Kind: gvk.Kind, + } + restmapping, err := c.RESTMapper().RESTMapping(gk) + if err != nil { + return false, fmt.Errorf("failed to get restmapping: %w", err) + } + scope := restmapping.Scope.Name() + + if scope == "" { + return false, errors.New("Scope cannot be identified. Empty scope returned") + } + + if scope != meta.RESTScopeNameRoot { + return true, nil + } + return false, nil +} + +// Create implements clinet.Client +func (n *namespacedClient) Create(ctx context.Context, obj Object, opts ...CreateOption) error { + isNamespaceScoped, err := isNamespaced(n.client, obj) + if err != nil { + return fmt.Errorf("error finding the scope of the object: %v", err) + } + + objectNamespace := obj.GetNamespace() + if objectNamespace != n.namespace && objectNamespace != "" { + return fmt.Errorf("Namespace %s of the object %s does not match the namespace %s on the client", objectNamespace, obj.GetName(), n.namespace) + } + + if isNamespaceScoped && objectNamespace == "" { + obj.SetNamespace(n.namespace) + } + return n.client.Create(ctx, obj, opts...) +} + +// Update implements client.Client +func (n *namespacedClient) Update(ctx context.Context, obj Object, opts ...UpdateOption) error { + isNamespaceScoped, err := isNamespaced(n.client, obj) + if err != nil { + return fmt.Errorf("error finding the scope of the object: %v", err) + } + + objectNamespace := obj.GetNamespace() + if objectNamespace != n.namespace && objectNamespace != "" { + return fmt.Errorf("Namespace %s of the object %s does not match the namespace %s on the client", objectNamespace, obj.GetName(), n.namespace) + } + + if isNamespaceScoped && objectNamespace == "" { + obj.SetNamespace(n.namespace) + } + return n.client.Update(ctx, obj, opts...) +} + +// Delete implements client.Client +func (n *namespacedClient) Delete(ctx context.Context, obj Object, opts ...DeleteOption) error { + isNamespaceScoped, err := isNamespaced(n.client, obj) + if err != nil { + return fmt.Errorf("error finding the scope of the object: %v", err) + } + + objectNamespace := obj.GetNamespace() + if objectNamespace != n.namespace && objectNamespace != "" { + return fmt.Errorf("Namespace %s of the object %s does not match the namespace %s on the client", objectNamespace, obj.GetName(), n.namespace) + } + + if isNamespaceScoped && objectNamespace == "" { + obj.SetNamespace(n.namespace) + } + return n.client.Delete(ctx, obj, opts...) +} + +// DeleteAllOf implements client.Client +func (n *namespacedClient) DeleteAllOf(ctx context.Context, obj Object, opts ...DeleteAllOfOption) error { + isNamespaceScoped, err := isNamespaced(n.client, obj) + if err != nil { + return fmt.Errorf("error finding the scope of the object: %v", err) + } + + if isNamespaceScoped { + opts = append(opts, InNamespace(n.namespace)) + } + return n.client.DeleteAllOf(ctx, obj, opts...) +} + +// Patch implements client.Client +func (n *namespacedClient) Patch(ctx context.Context, obj Object, patch Patch, opts ...PatchOption) error { + isNamespaceScoped, err := isNamespaced(n.client, obj) + if err != nil { + return fmt.Errorf("error finding the scope of the object: %v", err) + } + + objectNamespace := obj.GetNamespace() + if objectNamespace != n.namespace && objectNamespace != "" { + return fmt.Errorf("Namespace %s of the object %s does not match the namespace %s on the client", objectNamespace, obj.GetName(), n.namespace) + } + + if isNamespaceScoped && objectNamespace == "" { + obj.SetNamespace(n.namespace) + } + return n.client.Patch(ctx, obj, patch, opts...) +} + +// Get implements client.Client +func (n *namespacedClient) Get(ctx context.Context, key ObjectKey, obj Object) error { + isNamespaceScoped, err := isNamespaced(n.client, obj) + if err != nil { + return fmt.Errorf("error finding the scope of the object: %v", err) + } + if isNamespaceScoped { + if key.Namespace != "" && key.Namespace != n.namespace { + return fmt.Errorf("Namespace %s provided for the object %s does not match the namesapce %s on the client", key.Namespace, obj.GetName(), n.namespace) + } + key.Namespace = n.namespace + } + return n.client.Get(ctx, key, obj) +} + +// List implements client.Client +func (n *namespacedClient) List(ctx context.Context, obj ObjectList, opts ...ListOption) error { + if n.namespace != "" { + opts = append(opts, InNamespace(n.namespace)) + } + return n.client.List(ctx, obj, opts...) +} + +// Status implements client.StatusClient +func (n *namespacedClient) Status() StatusWriter { + return &namespacedClientStatusWriter{StatusClient: n.client.Status(), namespace: n.namespace, namespacedclient: n} +} + +// ensure namespacedClientStatusWriter implements client.StatusWriter +var _ StatusWriter = &namespacedClientStatusWriter{} + +type namespacedClientStatusWriter struct { + StatusClient StatusWriter + namespace string + namespacedclient Client +} + +// Update implements client.StatusWriter +func (nsw *namespacedClientStatusWriter) Update(ctx context.Context, obj Object, opts ...UpdateOption) error { + isNamespaceScoped, err := isNamespaced(nsw.namespacedclient, obj) + if err != nil { + return fmt.Errorf("error finding the scope of the object: %v", err) + } + + objectNamespace := obj.GetNamespace() + if objectNamespace != nsw.namespace && objectNamespace != "" { + return fmt.Errorf("Namespace %s of the object %s does not match the namespace %s on the client", objectNamespace, obj.GetName(), nsw.namespace) + } + + if isNamespaceScoped && objectNamespace == "" { + obj.SetNamespace(nsw.namespace) + } + return nsw.StatusClient.Update(ctx, obj, opts...) +} + +// Patch implements client.StatusWriter +func (nsw *namespacedClientStatusWriter) Patch(ctx context.Context, obj Object, patch Patch, opts ...PatchOption) error { + isNamespaceScoped, err := isNamespaced(nsw.namespacedclient, obj) + if err != nil { + return fmt.Errorf("error finding the scope of the object: %v", err) + } + + objectNamespace := obj.GetNamespace() + if objectNamespace != nsw.namespace && objectNamespace != "" { + return fmt.Errorf("Namespace %s of the object %s does not match the namespace %s on the client", objectNamespace, obj.GetName(), nsw.namespace) + } + + if isNamespaceScoped && objectNamespace == "" { + obj.SetNamespace(nsw.namespace) + } + return nsw.StatusClient.Patch(ctx, obj, patch, opts...) +} diff --git a/pkg/client/namespaced_client_test.go b/pkg/client/namespaced_client_test.go new file mode 100644 index 0000000000..5b8f3388c8 --- /dev/null +++ b/pkg/client/namespaced_client_test.go @@ -0,0 +1,583 @@ +/* +Copyright 2020 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_test + +import ( + "context" + "encoding/json" + "fmt" + "sync/atomic" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + rbacv1 "k8s.io/api/rbac/v1" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var _ = Describe("NamespacedClient", func() { + var dep *appsv1.Deployment + var ns = "default" + ctx := context.Background() + var count uint64 = 0 + var replicaCount int32 = 2 + + getClient := func() client.Client { + var sch = runtime.NewScheme() + + err := rbacv1.AddToScheme(sch) + Expect(err).To(BeNil()) + err = appsv1.AddToScheme(sch) + Expect(err).To(BeNil()) + + nonNamespacedClient, err := client.New(cfg, client.Options{Scheme: sch}) + Expect(err).NotTo(HaveOccurred()) + Expect(nonNamespacedClient).NotTo(BeNil()) + return client.NewNamespacedClient(nonNamespacedClient, ns) + } + + BeforeEach(func() { + atomic.AddUint64(&count, 1) + dep = &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("namespaced-deployment-%v", count), + Labels: map[string]string{"name": fmt.Sprintf("namespaced-deployment-%v", count)}, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: &replicaCount, + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"foo": "bar"}, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"foo": "bar"}}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}}, + }, + }, + } + }) + + Describe("Get", func() { + + BeforeEach(func() { + var err error + dep, err = clientset.AppsV1().Deployments(ns).Create(ctx, dep, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + }) + + AfterEach(func() { + deleteDeployment(ctx, dep, ns) + }) + It("should successfully Get a namespace-scoped object", func() { + name := types.NamespacedName{Name: dep.Name} + result := &appsv1.Deployment{} + + Expect(getClient().Get(ctx, name, result)).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo(dep)) + }) + + It("should error when namespace provided in the object is different than the one "+ + "specified in client", func() { + name := types.NamespacedName{Name: dep.Name, Namespace: "non-default"} + result := &appsv1.Deployment{} + + Expect(getClient().Get(ctx, name, result)).To(HaveOccurred()) + }) + }) + + Describe("List", func() { + BeforeEach(func() { + var err error + dep, err = clientset.AppsV1().Deployments(ns).Create(ctx, dep, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + }) + + AfterEach(func() { + deleteDeployment(ctx, dep, ns) + }) + + It("should successfully List objects when namespace is not specified with the object", func() { + result := &appsv1.DeploymentList{} + opts := client.MatchingLabels(dep.Labels) + + Expect(getClient().List(ctx, result, opts)).NotTo(HaveOccurred()) + Expect(len(result.Items)).To(BeEquivalentTo(1)) + Expect(result.Items[0]).To(BeEquivalentTo(*dep)) + }) + + It("should List objects from the namespace specified in the client", func() { + result := &appsv1.DeploymentList{} + opts := client.InNamespace("non-default") + + Expect(getClient().List(ctx, result, opts)).NotTo(HaveOccurred()) + Expect(len(result.Items)).To(BeEquivalentTo(1)) + Expect(result.Items[0]).To(BeEquivalentTo(*dep)) + }) + }) + + Describe("Create", func() { + AfterEach(func() { + deleteDeployment(ctx, dep, ns) + }) + + It("should successfully create object in the right namespace", func() { + By("creating the object initially") + err := getClient().Create(ctx, dep) + Expect(err).NotTo(HaveOccurred()) + + By("checking if the object was created in the right namespace") + res, err := clientset.AppsV1().Deployments(ns).Get(ctx, dep.Name, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(res.GetNamespace()).To(BeEquivalentTo(ns)) + }) + + It("should not create object if the namespace of the object is different", func() { + By("creating the object initially") + dep.SetNamespace("non-default") + err := getClient().Create(ctx, dep) + Expect(err).To(HaveOccurred()) + }) + It("should create a cluster scoped object", func() { + cr := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("clusterRole-%v", count), + Labels: map[string]string{"name": fmt.Sprintf("clusterRole-%v", count)}, + }, + } + cr.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "rbac.authorization.k8s.io", + Version: "v1", + Kind: "ClusterRole", + }) + + By("creating the object initially") + err := getClient().Create(ctx, cr) + Expect(err).NotTo(HaveOccurred()) + + By("checking if the object was created") + res, err := clientset.RbacV1().ClusterRoles().Get(ctx, cr.Name, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(res).NotTo(BeNil()) + + // Delete the clusterRole Resource + deleteClusterRole(ctx, cr) + }) + }) + + Describe("Update", func() { + var err error + BeforeEach(func() { + dep, err = clientset.AppsV1().Deployments(ns).Create(ctx, dep, metav1.CreateOptions{}) + dep.Annotations = map[string]string{"foo": "bar"} + Expect(err).NotTo(HaveOccurred()) + }) + AfterEach(func() { + deleteDeployment(ctx, dep, ns) + }) + + It("should successfully update the provided object", func() { + By("updating the Deployment") + err = getClient().Update(ctx, dep) + Expect(err).NotTo(HaveOccurred()) + + By("validating if the updated Deployment has new annotation") + actual, err := clientset.AppsV1().Deployments(ns).Get(ctx, dep.Name, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(actual).NotTo(BeNil()) + Expect(actual.GetNamespace()).To(Equal(ns)) + Expect(actual.Annotations["foo"]).To(Equal("bar")) + }) + + It("should successfully update the provided object when namespace is not provided", func() { + By("updating the Deployment") + dep.SetNamespace("") + err = getClient().Update(ctx, dep) + Expect(err).NotTo(HaveOccurred()) + + By("validating if the updated Deployment has new annotation") + actual, err := clientset.AppsV1().Deployments(ns).Get(ctx, dep.Name, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(actual).NotTo(BeNil()) + Expect(actual.GetNamespace()).To(Equal(ns)) + Expect(actual.Annotations["foo"]).To(Equal("bar")) + }) + + It("should not update when object namespace is different", func() { + By("updating the Deployment") + dep.SetNamespace("non-default") + err = getClient().Update(ctx, dep) + Expect(err).To(HaveOccurred()) + }) + + It("should not update any object from other namespace", func() { + By("creating a new namespace") + tns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "non-default-1"}} + _, err := clientset.CoreV1().Namespaces().Create(ctx, tns, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + changedDep := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "changed-dep", + Namespace: tns.Name, + Labels: map[string]string{"name": "changed-dep"}, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: &replicaCount, + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"foo": "bar"}, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"foo": "bar"}}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}}, + }, + }, + } + changedDep.Annotations = map[string]string{"foo": "bar"} + + By("creating the object initially") + _, err = clientset.AppsV1().Deployments(tns.Name).Create(ctx, changedDep, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + By("updating the object") + err = getClient().Update(ctx, changedDep) + Expect(err).To(HaveOccurred()) + + deleteDeployment(ctx, changedDep, tns.Name) + deleteNamespace(ctx, tns) + }) + + It("should update a cluster scoped resource", func() { + changedCR := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("clusterRole-%v", count), + Labels: map[string]string{"name": fmt.Sprintf("clusterRole-%v", count)}, + }, + } + + changedCR.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "rbac.authorization.k8s.io", + Version: "v1", + Kind: "ClusterRole", + }) + + By("Setting annotations and creating the resource") + changedCR.Annotations = map[string]string{"foo": "bar"} + changedCR, err = clientset.RbacV1().ClusterRoles().Create(ctx, changedCR, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + By("updating the deployment") + err = getClient().Update(ctx, changedCR) + + By("validating if the cluster role was update") + actual, err := clientset.RbacV1().ClusterRoles().Get(ctx, changedCR.Name, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(actual).NotTo(BeNil()) + Expect(actual.Annotations["foo"]).To(Equal("bar")) + + // delete cluster role resource + deleteClusterRole(ctx, changedCR) + }) + + }) + + Describe("Patch", func() { + var err error + BeforeEach(func() { + dep, err = clientset.AppsV1().Deployments(ns).Create(ctx, dep, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + }) + + AfterEach(func() { + deleteDeployment(ctx, dep, ns) + }) + + It("should successfully modify the object using Patch", func() { + By("Applying Patch") + err = getClient().Patch(ctx, dep, client.RawPatch(types.MergePatchType, generatePatch())) + Expect(err).NotTo(HaveOccurred()) + + By("validating patched Deployment has new annotations") + actual, err := clientset.AppsV1().Deployments(ns).Get(ctx, dep.Name, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(actual.Annotations["foo"]).To(Equal("bar")) + Expect(actual.GetNamespace()).To(Equal(ns)) + }) + + It("should successfully modify the object using Patch when namespace is not provided", func() { + By("Applying Patch") + dep.SetNamespace("") + err = getClient().Patch(ctx, dep, client.RawPatch(types.MergePatchType, generatePatch())) + Expect(err).NotTo(HaveOccurred()) + + By("validating patched Deployment has new annotations") + actual, err := clientset.AppsV1().Deployments(ns).Get(ctx, dep.Name, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(actual.Annotations["foo"]).To(Equal("bar")) + Expect(actual.GetNamespace()).To(Equal(ns)) + }) + + It("should not modify the object when namespace of the object is different", func() { + dep.SetNamespace("non-default") + err = getClient().Patch(ctx, dep, client.RawPatch(types.MergePatchType, generatePatch())) + Expect(err).To(HaveOccurred()) + }) + + It("should not modify an object from a different namespace", func() { + By("creating a new namespace") + tns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "non-default-2"}} + _, err := clientset.CoreV1().Namespaces().Create(ctx, tns, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + changedDep := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "changed-dep", + Namespace: tns.Name, + Labels: map[string]string{"name": "changed-dep"}, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: &replicaCount, + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"foo": "bar"}, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"foo": "bar"}}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}}, + }, + }, + } + + By("creating the object initially") + changedDep, err = clientset.AppsV1().Deployments(tns.Name).Create(ctx, changedDep, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + err = getClient().Patch(ctx, changedDep, client.RawPatch(types.MergePatchType, generatePatch())) + Expect(err).To(HaveOccurred()) + + deleteDeployment(ctx, changedDep, tns.Name) + deleteNamespace(ctx, tns) + }) + + It("should successfully modify cluster scoped resource", func() { + cr := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("clusterRole-%v", count), + Labels: map[string]string{"name": fmt.Sprintf("clusterRole-%v", count)}, + }, + } + + cr.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "rbac.authorization.k8s.io", + Version: "v1", + Kind: "ClusterRole", + }) + + By("creating the resource") + cr, err = clientset.RbacV1().ClusterRoles().Create(ctx, cr, metav1.CreateOptions{}) + Expect(err).ToNot(HaveOccurred()) + + By("Applying Patch") + err = getClient().Patch(ctx, cr, client.RawPatch(types.MergePatchType, generatePatch())) + Expect(err).NotTo(HaveOccurred()) + + By("Validating the patch") + actual, err := clientset.RbacV1().ClusterRoles().Get(ctx, cr.Name, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(actual.Annotations["foo"]).To(Equal("bar")) + + // delete the resource + deleteClusterRole(ctx, cr) + }) + }) + + Describe("Delete and DeleteAllOf", func() { + var err error + BeforeEach(func() { + dep, err = clientset.AppsV1().Deployments(ns).Create(ctx, dep, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + }) + + AfterEach(func() { + deleteDeployment(ctx, dep, ns) + }) + It("should successfully delete an object when namespace is not specified", func() { + By("deleting the object") + dep.SetNamespace("") + err = getClient().Delete(ctx, dep) + Expect(err).NotTo(HaveOccurred()) + + By("validating the Deployment no longer exists") + _, err = clientset.AppsV1().Deployments(ns).Get(ctx, dep.Name, metav1.GetOptions{}) + Expect(err).To(HaveOccurred()) + }) + + It("should successfully delete all of the deployments in the given namespace", func() { + By("Deleting all objects in the namespace") + err = getClient().DeleteAllOf(ctx, dep) + Expect(err).NotTo(HaveOccurred()) + + By("validating the Deployment no longer exists") + _, err = clientset.AppsV1().Deployments(ns).Get(ctx, dep.Name, metav1.GetOptions{}) + Expect(err).To(HaveOccurred()) + }) + + It("should not delete deployments in other namespaces", func() { + tns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "non-default-3"}} + _, err = clientset.CoreV1().Namespaces().Create(ctx, tns, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + changedDep := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "changed-dep", + Namespace: tns.Name, + Labels: map[string]string{"name": "changed-dep"}, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: &replicaCount, + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"foo": "bar"}, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"foo": "bar"}}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}}, + }, + }, + } + + By("creating the object initially in other namespace") + changedDep, err = clientset.AppsV1().Deployments(tns.Name).Create(ctx, changedDep, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + err = getClient().DeleteAllOf(ctx, dep) + Expect(err).NotTo(HaveOccurred()) + + By("validating the Deployment exists") + actual, err := clientset.AppsV1().Deployments(tns.Name).Get(ctx, changedDep.Name, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(actual).To(BeEquivalentTo(changedDep)) + + deleteDeployment(ctx, changedDep, tns.Name) + deleteNamespace(ctx, tns) + }) + }) + + Describe("StatusWriter", func() { + var err error + BeforeEach(func() { + dep, err = clientset.AppsV1().Deployments(ns).Create(ctx, dep, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + }) + + AfterEach(func() { + deleteDeployment(ctx, dep, ns) + }) + + It("should change objects via update status", func() { + changedDep := dep.DeepCopy() + changedDep.Status.Replicas = 99 + + Expect(getClient().Status().Update(ctx, changedDep)).NotTo(HaveOccurred()) + + actual, err := clientset.AppsV1().Deployments(ns).Get(ctx, dep.Name, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(actual).NotTo(BeNil()) + Expect(actual.GetNamespace()).To(BeEquivalentTo(ns)) + Expect(actual.Status.Replicas).To(BeEquivalentTo(99)) + }) + + It("should not change objects via update status when object namespace is different", func() { + changedDep := dep.DeepCopy() + changedDep.SetNamespace("test") + changedDep.Status.Replicas = 99 + + Expect(getClient().Status().Update(ctx, changedDep)).To(HaveOccurred()) + }) + + It("should change objects via status patch", func() { + changedDep := dep.DeepCopy() + changedDep.Status.Replicas = 99 + + Expect(getClient().Status().Patch(ctx, changedDep, client.MergeFrom(dep))).NotTo(HaveOccurred()) + + actual, err := clientset.AppsV1().Deployments(ns).Get(ctx, dep.Name, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(actual).NotTo(BeNil()) + Expect(actual.GetNamespace()).To(BeEquivalentTo(ns)) + Expect(actual.Status.Replicas).To(BeEquivalentTo(99)) + }) + + It("should not change objects via status patch when object namespace is different", func() { + changedDep := dep.DeepCopy() + changedDep.Status.Replicas = 99 + changedDep.SetNamespace("test") + + Expect(getClient().Status().Patch(ctx, changedDep, client.MergeFrom(dep))).To(HaveOccurred()) + }) + }) + + Describe("Test on invalid objects", func() { + It("should refuse to perform operations on invalid object", func() { + err := getClient().Create(ctx, nil) + Expect(err).To(HaveOccurred()) + + err = getClient().List(ctx, nil) + Expect(err).To(HaveOccurred()) + + err = getClient().Patch(ctx, nil, client.MergeFrom(dep)) + Expect(err).To(HaveOccurred()) + + err = getClient().Update(ctx, nil) + Expect(err).To(HaveOccurred()) + + err = getClient().Delete(ctx, nil) + Expect(err).To(HaveOccurred()) + + err = getClient().Status().Patch(ctx, nil, client.MergeFrom(dep)) + Expect(err).To(HaveOccurred()) + + err = getClient().Status().Update(ctx, nil) + Expect(err).To(HaveOccurred()) + + }) + + }) +}) + +func generatePatch() []byte { + mergePatch, err := json.Marshal(map[string]interface{}{ + "metadata": map[string]interface{}{ + "annotations": map[string]interface{}{ + "foo": "bar", + }, + }, + }) + Expect(err).NotTo(HaveOccurred()) + return mergePatch +} + +func deleteClusterRole(ctx context.Context, cr *rbacv1.ClusterRole) { + _, err := clientset.RbacV1().ClusterRoles().Get(ctx, cr.Name, metav1.GetOptions{}) + if err == nil { + err = clientset.RbacV1().ClusterRoles().Delete(ctx, cr.Name, metav1.DeleteOptions{}) + Expect(err).NotTo(HaveOccurred()) + } +}