diff --git a/pkg/client/client.go b/pkg/client/client.go index 820dc92b09..1a81c9e4ca 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -130,6 +130,15 @@ func (c *client) Delete(ctx context.Context, obj runtime.Object, opts ...DeleteO return c.typedClient.Delete(ctx, obj, opts...) } +// DeleteAllOf implements client.Client +func (c *client) DeleteAllOf(ctx context.Context, obj runtime.Object, opts ...DeleteAllOfOption) error { + _, ok := obj.(*unstructured.Unstructured) + if ok { + return c.unstructuredClient.DeleteAllOf(ctx, obj, opts...) + } + return c.typedClient.DeleteAllOf(ctx, obj, opts...) +} + // Patch implements client.Client func (c *client) Patch(ctx context.Context, obj runtime.Object, patch Patch, opts ...PatchOption) error { _, ok := obj.(*unstructured.Unstructured) diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index 9e613d56d6..1f7d5d64b5 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -70,7 +70,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{ @@ -898,6 +898,37 @@ var _ = Describe("Client", func() { PIt("should fail if the GVK cannot be mapped to a Resource", func() { }) + + It("should delete a collection of objects", 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 + + By("deleting Deployments") + err = cl.DeleteAllOf(context.TODO(), dep, client.InNamespace(ns), client.MatchingLabels(dep.ObjectMeta.Labels)) + 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) { @@ -974,6 +1005,44 @@ 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 + + By("deleting Deployments") + u := &unstructured.Unstructured{} + Expect(scheme.Convert(dep, u, nil)).To(Succeed()) + u.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "apps", + Kind: "Deployment", + Version: "v1", + }) + err = cl.DeleteAllOf(context.TODO(), u, client.InNamespace(ns), client.MatchingLabels(dep.ObjectMeta.Labels)) + 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) + }) }) }) @@ -1994,6 +2063,10 @@ var _ = Describe("Client", func() { PIt("should fail if the object doesn't have meta", func() { }) + + PIt("should filter results by namespace selector", func() { + + }) }) }) @@ -2072,6 +2145,34 @@ var _ = Describe("Client", func() { }) }) + Describe("DeleteCollectionOptions", func() { + It("should be convertable to list options", func() { + gp := int64(1) + do := &client.DeleteAllOfOptions{} + do.ApplyOptions([]client.DeleteAllOfOption{ + client.GracePeriodSeconds(gp), + client.MatchingLabels{"foo": "bar"}, + }) + + listOpts := do.AsListOptions() + Expect(listOpts).NotTo(BeNil()) + Expect(listOpts.LabelSelector).To(Equal("foo=bar")) + }) + + It("should be convertable to delete options", func() { + gp := int64(1) + do := &client.DeleteAllOfOptions{} + do.ApplyOptions([]client.DeleteAllOfOption{ + client.GracePeriodSeconds(gp), + client.MatchingLabels{"foo": "bar"}, + }) + + deleteOpts := do.AsDeleteOptions() + Expect(deleteOpts).NotTo(BeNil()) + Expect(deleteOpts.GracePeriodSeconds).To(Equal(&gp)) + }) + }) + Describe("ListOptions", func() { It("should be convertable to metav1.ListOptions", func() { lo := (&client.ListOptions{}).ApplyOptions([]client.ListOption{ @@ -2105,6 +2206,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 f861199b67..f6ddc7d843 100644 --- a/pkg/client/example_test.go +++ b/pkg/client/example_test.go @@ -199,6 +199,22 @@ 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_deleteAllOf() { + // Using a typed object. + // c is a created client. + _ = c.DeleteAllOf(context.Background(), &corev1.Pod{}, client.InNamespace("foo"), client.MatchingLabels{"app": "foo"}) + + // Using an unstructured Object + u := &unstructured.UnstructuredList{} + u.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "apps", + Kind: "Deployment", + Version: "v1", + }) + _ = c.DeleteAllOf(context.Background(), u, client.InNamespace("foo"), client.MatchingLabels{"app": "foo"}) +} + // 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 ba1d85a04d..6fb65bc36e 100644 --- a/pkg/client/fake/client.go +++ b/pkg/client/fake/client.go @@ -161,10 +161,49 @@ func (c *fakeClient) Delete(ctx context.Context, obj runtime.Object, opts ...cli if err != nil { return err } + delOptions := client.DeleteOptions{} + delOptions.ApplyOptions(opts) + //TODO: implement propagation return c.tracker.Delete(gvr, accessor.GetNamespace(), accessor.GetName()) } +func (c *fakeClient) DeleteAllOf(ctx context.Context, obj runtime.Object, opts ...client.DeleteAllOfOption) error { + gvk, err := apiutil.GVKForObject(obj, scheme.Scheme) + if err != nil { + return err + } + + dcOptions := client.DeleteAllOfOptions{} + dcOptions.ApplyOptions(opts) + + gvr, _ := meta.UnsafeGuessKindToResource(gvk) + o, err := c.tracker.List(gvr, gvk, dcOptions.Namespace) + if err != nil { + return err + } + + objs, err := meta.ExtractList(o) + if err != nil { + return err + } + filteredObjs, err := objectutil.FilterWithLabels(objs, dcOptions.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.UpdateOption) error { updateOptions := &client.UpdateOptions{} updateOptions.ApplyOptions(opts) diff --git a/pkg/client/fake/client_test.go b/pkg/client/fake/client_test.go index ad173fae01..9299e6f499 100644 --- a/pkg/client/fake/client_test.go +++ b/pkg/client/fake/client_test.go @@ -158,6 +158,18 @@ var _ = Describe("Fake client", func() { Expect(list.Items).To(ConsistOf(*dep2)) }) + It("should be able to Delete a Collection", func() { + By("Deleting a deploymentList") + err := cl.DeleteAllOf(nil, &appsv1.Deployment{}, client.InNamespace("ns1")) + 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 601c304052..00b43caee7 100644 --- a/pkg/client/interfaces.go +++ b/pkg/client/interfaces.go @@ -76,6 +76,9 @@ type Writer interface { // Patch patches the given obj in the Kubernetes cluster. obj must be a // struct pointer so that obj can be updated with the content returned by the Server. Patch(ctx context.Context, obj runtime.Object, patch Patch, opts ...PatchOption) error + + // DeleteAllOf deletes all objects of the given type matching the given options. + DeleteAllOf(ctx context.Context, obj runtime.Object, opts ...DeleteAllOfOption) error } // StatusClient knows how to create a client which can update status subresource diff --git a/pkg/client/options.go b/pkg/client/options.go index 48dd382c5e..4007a67657 100644 --- a/pkg/client/options.go +++ b/pkg/client/options.go @@ -30,30 +30,36 @@ type CreateOption interface { ApplyToCreate(*CreateOptions) } -// DeleteOption is some configuration that modifies options for a create request. +// DeleteOption is some configuration that modifies options for a delete request. type DeleteOption interface { // ApplyToDelete applies this configuration to the given delete options. ApplyToDelete(*DeleteOptions) } -// ListOption is some configuration that modifies options for a create request. +// ListOption is some configuration that modifies options for a list request. type ListOption interface { // ApplyToList applies this configuration to the given list options. ApplyToList(*ListOptions) } -// UpdateOption is some configuration that modifies options for a create request. +// UpdateOption is some configuration that modifies options for a update request. type UpdateOption interface { // ApplyToUpdate applies this configuration to the given update options. ApplyToUpdate(*UpdateOptions) } -// PatchOption is some configuration that modifies options for a create request. +// PatchOption is some configuration that modifies options for a patch request. type PatchOption interface { // ApplyToPatch applies this configuration to the given patch options. ApplyToPatch(*PatchOptions) } +// DeleteAllOfOption is some configuration that modifies options for a delete request. +type DeleteAllOfOption interface { + // ApplyToDeleteAllOf applies this configuration to the given deletecollection options. + ApplyToDeleteAllOf(*DeleteAllOfOptions) +} + // }}} // {{{ Multi-Type Options @@ -214,6 +220,10 @@ func (s GracePeriodSeconds) ApplyToDelete(opts *DeleteOptions) { opts.GracePeriodSeconds = &secs } +func (s GracePeriodSeconds) ApplyToDeleteAllOf(opts *DeleteAllOfOptions) { + s.ApplyToDelete(&opts.DeleteOptions) +} + type Preconditions metav1.Preconditions func (p Preconditions) ApplyToDelete(opts *DeleteOptions) { @@ -221,6 +231,10 @@ func (p Preconditions) ApplyToDelete(opts *DeleteOptions) { opts.Preconditions = &preconds } +func (p Preconditions) ApplyToDeleteAllOf(opts *DeleteAllOfOptions) { + p.ApplyToDelete(&opts.DeleteOptions) +} + type PropagationPolicy metav1.DeletionPropagation func (p PropagationPolicy) ApplyToDelete(opts *DeleteOptions) { @@ -228,6 +242,10 @@ func (p PropagationPolicy) ApplyToDelete(opts *DeleteOptions) { opts.PropagationPolicy = &policy } +func (p PropagationPolicy) ApplyToDeleteAllOf(opts *DeleteAllOfOptions) { + p.ApplyToDelete(&opts.DeleteOptions) +} + // }}} // {{{ List Options @@ -282,7 +300,7 @@ func (o *ListOptions) ApplyOptions(opts []ListOption) *ListOptions { return o } -// MatchingLabels filters the list operation on the given set of labels. +// MatchingLabels filters the list/delete operation on the given set of labels. type MatchingLabels map[string]string func (m MatchingLabels) ApplyToList(opts *ListOptions) { @@ -291,6 +309,10 @@ func (m MatchingLabels) ApplyToList(opts *ListOptions) { opts.LabelSelector = sel } +func (m MatchingLabels) ApplyToDeleteAllOf(opts *DeleteAllOfOptions) { + m.ApplyToList(&opts.ListOptions) +} + // MatchingField filters the list operation on the given field selector // (or index in the case of cached lists). // @@ -299,7 +321,7 @@ func MatchingField(name, val string) MatchingFields { return MatchingFields{name: val} } -// MatchingField filters the list operation on the given field selector +// MatchingField filters the list/delete operation on the given field selector // (or index in the case of cached lists). type MatchingFields fields.Set @@ -309,13 +331,21 @@ func (m MatchingFields) ApplyToList(opts *ListOptions) { opts.FieldSelector = sel } -// InNamespace restricts the given operation to the given namespace. +func (m MatchingFields) ApplyToDeleteAllOf(opts *DeleteAllOfOptions) { + m.ApplyToList(&opts.ListOptions) +} + +// InNamespace restricts the list/delete operation to the given namespace. type InNamespace string func (n InNamespace) ApplyToList(opts *ListOptions) { opts.Namespace = string(n) } +func (n InNamespace) ApplyToDeleteAllOf(opts *DeleteAllOfOptions) { + n.ApplyToList(&opts.ListOptions) +} + // }}} // {{{ Update Options @@ -437,3 +467,25 @@ func (forceOwnership) ApplyToPatch(opts *PatchOptions) { var PatchDryRunAll = DryRunAll // }}} + +// {{{ DeleteAllOf Options + +// these are all just delete options and list options + +// DeleteAllOfOptions contains options for deletecollection (deleteallof) requests. +// It's just list and delete options smooshed together. +type DeleteAllOfOptions struct { + ListOptions + DeleteOptions +} + +// ApplyOptions applies the given deleteallof options on these options, +// and then returns itself (for convenient chaining). +func (o *DeleteAllOfOptions) ApplyOptions(opts []DeleteAllOfOption) *DeleteAllOfOptions { + for _, opt := range opts { + opt.ApplyToDeleteAllOf(o) + } + return o +} + +// }}} diff --git a/pkg/client/typed_client.go b/pkg/client/typed_client.go index 2124b0e991..6ddc1171d3 100644 --- a/pkg/client/typed_client.go +++ b/pkg/client/typed_client.go @@ -76,11 +76,33 @@ func (c *typedClient) Delete(ctx context.Context, obj runtime.Object, opts ...De } deleteOpts := DeleteOptions{} + deleteOpts.ApplyOptions(opts) + 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() +} + +// DeleteAllOf implements client.Client +func (c *typedClient) DeleteAllOf(ctx context.Context, obj runtime.Object, opts ...DeleteAllOfOption) error { + o, err := c.cache.getObjMeta(obj) + if err != nil { + return err + } + + deleteAllOfOpts := DeleteAllOfOptions{} + deleteAllOfOpts.ApplyOptions(opts) + + return o.Delete(). + NamespaceIfScoped(deleteAllOfOpts.ListOptions.Namespace, o.isNamespaced()). + Resource(o.resource()). + VersionedParams(deleteAllOfOpts.AsListOptions(), c.paramCodec). + Body(deleteAllOfOpts.AsDeleteOptions()). Context(ctx). Do(). Error() diff --git a/pkg/client/unstructured_client.go b/pkg/client/unstructured_client.go index d5bb9a47a7..440ad2f97c 100644 --- a/pkg/client/unstructured_client.go +++ b/pkg/client/unstructured_client.go @@ -87,7 +87,25 @@ 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) + err = r.Delete(u.GetName(), deleteOpts.AsDeleteOptions()) + return err +} + +// DeleteAllOf implements client.Client +func (uc *unstructuredClient) DeleteAllOf(_ context.Context, obj runtime.Object, opts ...DeleteAllOfOption) 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 + } + + deleteAllOfOpts := DeleteAllOfOptions{} + deleteAllOfOpts.ApplyOptions(opts) + err = r.DeleteCollection(deleteAllOfOpts.AsDeleteOptions(), *deleteAllOfOpts.AsListOptions()) return err }