diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index a701562fa3..7822256aaa 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -66,7 +66,7 @@ var _ = Describe("Client", func() { BeforeEach(func(done Done) { atomic.AddUint64(&count, 1) dep = &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{Name: fmt.Sprintf("deployment-name-%v", count), Namespace: ns}, + ObjectMeta: metav1.ObjectMeta{Name: fmt.Sprintf("deployment-name-%v", count), Namespace: ns, Labels: map[string]string{"app": fmt.Sprintf("bar-%v", count)}}, Spec: appsv1.DeploymentSpec{ Replicas: &replicaCount, Selector: &metav1.LabelSelector{ @@ -885,6 +885,42 @@ var _ = Describe("Client", func() { PIt("should fail if the GVK cannot be mapped to a Resource", func() { }) + + It("should delete a collection of object", func(done Done) { + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) + + By("initially creating two Deployments") + + dep2 := dep.DeepCopy() + dep2.Name = dep2.Name + "-2" + + dep, err = clientset.AppsV1().Deployments(ns).Create(dep) + Expect(err).NotTo(HaveOccurred()) + dep2, err = clientset.AppsV1().Deployments(ns).Create(dep2) + Expect(err).NotTo(HaveOccurred()) + + depName := dep.Name + dep2Name := dep2.Name + + labelmatcher := client.CollectionOptions( + client.MatchingLabels(dep.ObjectMeta.Labels), + client.InNamespace(ns), + ) + + By("deleting Deployments") + err = cl.Delete(context.TODO(), dep, labelmatcher) + Expect(err).NotTo(HaveOccurred()) + + By("validating the Deployment no longer exists") + _, err = clientset.AppsV1().Deployments(ns).Get(depName, metav1.GetOptions{}) + Expect(err).To(HaveOccurred()) + _, err = clientset.AppsV1().Deployments(ns).Get(dep2Name, metav1.GetOptions{}) + Expect(err).To(HaveOccurred()) + + close(done) + }) }) Context("with unstructured objects", func() { It("should delete an existing object from a go struct", func(done Done) { @@ -961,6 +997,49 @@ var _ = Describe("Client", func() { close(done) }) + + It("should delete a collection of object", func(done Done) { + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) + + By("initially creating two Deployments") + + dep2 := dep.DeepCopy() + dep2.Name = dep2.Name + "-2" + + dep, err = clientset.AppsV1().Deployments(ns).Create(dep) + Expect(err).NotTo(HaveOccurred()) + dep2, err = clientset.AppsV1().Deployments(ns).Create(dep2) + Expect(err).NotTo(HaveOccurred()) + + depName := dep.Name + dep2Name := dep2.Name + + labelmatcher := client.CollectionOptions( + client.MatchingLabels(dep.ObjectMeta.Labels), + client.InNamespace(ns), + ) + + By("deleting Deployments") + u := &unstructured.Unstructured{} + scheme.Convert(dep, u, nil) + u.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "apps", + Kind: "Deployment", + Version: "v1", + }) + err = cl.Delete(context.TODO(), u, labelmatcher) + Expect(err).NotTo(HaveOccurred()) + + By("validating the Deployment no longer exists") + _, err = clientset.AppsV1().Deployments(ns).Get(depName, metav1.GetOptions{}) + Expect(err).To(HaveOccurred()) + _, err = clientset.AppsV1().Deployments(ns).Get(dep2Name, metav1.GetOptions{}) + Expect(err).To(HaveOccurred()) + + close(done) + }) }) }) @@ -1763,6 +1842,10 @@ var _ = Describe("Client", func() { PIt("should fail if the object doesn't have meta", func() { }) + + PIt("should filter results by namespace selector", func() { + + }) }) }) @@ -1812,6 +1895,12 @@ var _ = Describe("Client", func() { Expect(do.AsDeleteOptions()).To(Equal(&metav1.DeleteOptions{})) }) + It("should producte nil CollectionOptions if not present", func() { + do := &client.DeleteOptions{} + do.AsDeleteOptions() + Expect(do.CollectionOptions).To(BeNil()) + }) + It("should merge multiple options together", func() { gp := int64(1) pc := metav1.NewUIDPreconditions("uid") @@ -1821,10 +1910,13 @@ var _ = Describe("Client", func() { client.GracePeriodSeconds(gp), client.Preconditions(pc), client.PropagationPolicy(dp), + client.CollectionOptions(client.UseListOptions(&client.ListOptions{Namespace: "test"})), }) Expect(do.GracePeriodSeconds).To(Equal(&gp)) Expect(do.Preconditions).To(Equal(pc)) Expect(do.PropagationPolicy).To(Equal(&dp)) + Expect(do.CollectionOptions).NotTo(BeNil()) + Expect(do.CollectionOptions.Namespace).To(Equal("test")) }) }) @@ -1905,6 +1997,13 @@ var _ = Describe("Client", func() { Expect(lo).NotTo(BeNil()) Expect(lo.Namespace).To(Equal("test")) }) + + It("should produce empty metav1.ListOptions if nil", func() { + var do *client.ListOptions + Expect(do.AsListOptions()).To(Equal(&metav1.ListOptions{})) + do = &client.ListOptions{} + Expect(do.AsListOptions()).To(Equal(&metav1.ListOptions{})) + }) }) Describe("UpdateOptions", func() { diff --git a/pkg/client/example_test.go b/pkg/client/example_test.go index 9e29f5b3ff..a05c42c689 100644 --- a/pkg/client/example_test.go +++ b/pkg/client/example_test.go @@ -199,6 +199,27 @@ func ExampleClient_delete() { _ = c.Delete(context.Background(), u) } +// This example shows how to use the client with typed and unstrucurted objects to delete collections of objects. +func ExampleClient_deleteCollection() { + labelMatcher := client.CollectionOptions( + client.MatchingLabels(map[string]string{"app": "foo"}), + client.InNamespace("foo"), + ) + // Using a typed object. + pod := &corev1.Pod{} + // c is a created client. + _ = c.Delete(context.Background(), pod, labelMatcher) + + // Using an unstructured Object + u := &unstructured.UnstructuredList{} + u.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "apps", + Kind: "Deployment", + Version: "v1", + }) + _ = c.Delete(context.Background(), u, labelMatcher) +} + // This example shows how to set up and consume a field selector over a pod's volumes' secretName field. func ExampleFieldIndexer_secretName() { // someIndexer is a FieldIndexer over a Cache diff --git a/pkg/client/fake/client.go b/pkg/client/fake/client.go index 905f34cbb3..36c82e9c6d 100644 --- a/pkg/client/fake/client.go +++ b/pkg/client/fake/client.go @@ -169,10 +169,50 @@ func (c *fakeClient) Delete(ctx context.Context, obj runtime.Object, opts ...cli if err != nil { return err } + delOptions := client.DeleteOptions{} + delOptions.ApplyOptions(opts) + if delOptions.CollectionOptions != nil { + return c.deleteCollection(obj, delOptions) + } + //TODO: implement propagation return c.tracker.Delete(gvr, accessor.GetNamespace(), accessor.GetName()) } +func (c *fakeClient) deleteCollection(obj runtime.Object, dcOptions client.DeleteOptions) error { + + gvk, err := apiutil.GVKForObject(obj, scheme.Scheme) + if err != nil { + return err + } + + gvr, _ := meta.UnsafeGuessKindToResource(gvk) + o, err := c.tracker.List(gvr, gvk, dcOptions.CollectionOptions.Namespace) + if err != nil { + return err + } + + objs, err := meta.ExtractList(o) + if err != nil { + return err + } + filteredObjs, err := objectutil.FilterWithLabels(objs, dcOptions.CollectionOptions.LabelSelector) + if err != nil { + return err + } + for _, o := range filteredObjs { + accessor, err := meta.Accessor(o) + if err != nil { + return err + } + err = c.tracker.Delete(gvr, accessor.GetNamespace(), accessor.GetName()) + if err != nil { + return err + } + } + return nil +} + func (c *fakeClient) Update(ctx context.Context, obj runtime.Object, opts ...client.UpdateOptionFunc) error { updateOptions := &client.UpdateOptions{} updateOptions.ApplyOptions(opts) diff --git a/pkg/client/fake/client_test.go b/pkg/client/fake/client_test.go index c96f996a20..837cc32db8 100644 --- a/pkg/client/fake/client_test.go +++ b/pkg/client/fake/client_test.go @@ -156,6 +156,21 @@ var _ = Describe("Fake client", func() { Expect(list.Items).To(ConsistOf(*dep2)) }) + It("should be able to Delete a Collection", func() { + By("Deleting a deploymentList") + labelFilter := client.CollectionOptions( + client.InNamespace("ns1"), + ) + err := cl.Delete(nil, &appsv1.Deployment{}, labelFilter) + Expect(err).To(BeNil()) + + By("Listing all deployments in the namespace") + list := &appsv1.DeploymentList{} + err = cl.List(nil, list, client.InNamespace("ns1")) + Expect(err).To(BeNil()) + Expect(list.Items).To(BeEmpty()) + }) + Context("with the DryRun option", func() { It("should not create a new object", func() { By("Creating a new configmap with DryRun") diff --git a/pkg/client/interfaces.go b/pkg/client/interfaces.go index 1f075a7ec0..afe7462b95 100644 --- a/pkg/client/interfaces.go +++ b/pkg/client/interfaces.go @@ -180,6 +180,10 @@ type DeleteOptions struct { // foreground. PropagationPolicy *metav1.DeletionPropagation + // CollectionOptions is used by the DeleteCollection to determine the objects + // To be deleted. + CollectionOptions *ListOptions + // Raw represents raw DeleteOptions, as passed to the API server. Raw *metav1.DeleteOptions } @@ -239,6 +243,17 @@ func PropagationPolicy(p metav1.DeletionPropagation) DeleteOptionFunc { } } +// CollectionOptions is a functional option that sets the CollectionOptions +// field of a DeleteOptions struct +func CollectionOptions(listOpts ...ListOptionFunc) DeleteOptionFunc { + return func(opts *DeleteOptions) { + if opts.CollectionOptions == nil { + opts.CollectionOptions = &ListOptions{} + } + opts.CollectionOptions.ApplyOptions(listOpts) + } +} + // ListOptions contains options for limiting or filtering results. // It's generally a subset of metav1.ListOptions, with support for // pre-parsed selectors (since generally, selectors will be executed diff --git a/pkg/client/typed_client.go b/pkg/client/typed_client.go index 82d6cc12ef..6823ae4808 100644 --- a/pkg/client/typed_client.go +++ b/pkg/client/typed_client.go @@ -76,11 +76,30 @@ func (c *typedClient) Delete(ctx context.Context, obj runtime.Object, opts ...De } deleteOpts := DeleteOptions{} + deleteOpts.ApplyOptions(opts) + + if deleteOpts.CollectionOptions != nil { + return c.deleteCollection(ctx, o, deleteOpts) + } + return o.Delete(). NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()). Resource(o.resource()). Name(o.GetName()). - Body(deleteOpts.ApplyOptions(opts).AsDeleteOptions()). + Body(deleteOpts.AsDeleteOptions()). + Context(ctx). + Do(). + Error() +} + +// DeleteCollection implements client.Client +func (c *typedClient) deleteCollection(ctx context.Context, o *objMeta, deleteOpts DeleteOptions) error { + + return o.Delete(). + NamespaceIfScoped(deleteOpts.CollectionOptions.Namespace, o.isNamespaced()). + Resource(o.resource()). + VersionedParams(deleteOpts.CollectionOptions.AsListOptions(), c.paramCodec). + Body(deleteOpts.AsDeleteOptions()). Context(ctx). Do(). Error() diff --git a/pkg/client/unstructured_client.go b/pkg/client/unstructured_client.go index c7a199586e..d7474519df 100644 --- a/pkg/client/unstructured_client.go +++ b/pkg/client/unstructured_client.go @@ -87,7 +87,23 @@ func (uc *unstructuredClient) Delete(_ context.Context, obj runtime.Object, opts return err } deleteOpts := DeleteOptions{} - err = r.Delete(u.GetName(), deleteOpts.ApplyOptions(opts).AsDeleteOptions()) + deleteOpts.ApplyOptions(opts) + if deleteOpts.CollectionOptions != nil { + return uc.deleteCollection(u, deleteOpts) + } + err = r.Delete(u.GetName(), deleteOpts.AsDeleteOptions()) + return err +} + +func (uc *unstructuredClient) deleteCollection(u *unstructured.Unstructured, dcOpts DeleteOptions) error { + gvk := u.GroupVersionKind() + + r, err := uc.getResourceInterface(gvk, dcOpts.CollectionOptions.Namespace) + if err != nil { + return err + } + + err = r.DeleteCollection(dcOpts.AsDeleteOptions(), *dcOpts.CollectionOptions.AsListOptions()) return err }