Skip to content

Commit

Permalink
✨ Implement delete collection via delete options
Browse files Browse the repository at this point in the history
implement `CollectionOptions` in `DeleteOptions` that allow to issue a
`DeleteCollection` call under the hood

add tests + examples

Co-Authored-By: Aaron Clawson <Aaron.Clawson@gmail.com>
  • Loading branch information
2 people authored and DirectXMan12 committed Jul 24, 2019
1 parent 0216acb commit acf910d
Show file tree
Hide file tree
Showing 7 changed files with 222 additions and 6 deletions.
91 changes: 90 additions & 1 deletion pkg/client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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 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")
err = cl.Delete(context.TODO(), dep, client.All(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) {
Expand Down Expand Up @@ -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.Delete(context.TODO(), u, client.All(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)
})
})
})

Expand Down Expand Up @@ -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() {

})
})
})

Expand Down Expand Up @@ -2056,6 +2129,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")
Expand All @@ -2065,10 +2144,13 @@ var _ = Describe("Client", func() {
client.GracePeriodSeconds(gp),
client.Preconditions(*pc),
client.PropagationPolicy(dp),
client.All(client.InNamespace("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"))
})
})

Expand Down Expand Up @@ -2105,6 +2187,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() {
Expand Down
16 changes: 16 additions & 0 deletions pkg/client/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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_deleteCollection() {
// Using a typed object.
// c is a created client.
_ = c.Delete(context.Background(), &corev1.Pod{}, client.All(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.Delete(context.Background(), u, client.All(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
Expand Down
39 changes: 39 additions & 0 deletions pkg/client/fake/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
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.UpdateOption) error {
updateOptions := &client.UpdateOptions{}
updateOptions.ApplyOptions(opts)
Expand Down
12 changes: 12 additions & 0 deletions pkg/client/fake/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.Delete(nil, &appsv1.Deployment{}, client.All(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")
Expand Down
32 changes: 29 additions & 3 deletions pkg/client/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,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

Expand Down Expand Up @@ -228,6 +232,28 @@ func (p PropagationPolicy) ApplyToDelete(opts *DeleteOptions) {
opts.PropagationPolicy = &policy
}

// NB(directxman12): there's two ways to spell this because the variadic
// parameters are often more ergonomic, but we want a public type.

// All indicates that this delete operation should affect all objects,
// matching any list options passed to the delete operation, instead of
// a single object.
func All(opts ...ListOption) Everything {
return Everything(opts)
}

// Everything indicates that this delete operation should affect all objects,
// matching any list options passed to the delete operation, instead of
// a single object.
type Everything []ListOption

func (e Everything) ApplyToDelete(opts *DeleteOptions) {
if opts.CollectionOptions == nil {
opts.CollectionOptions = &ListOptions{}
}
opts.CollectionOptions.ApplyOptions([]ListOption(e))
}

// }}}

// {{{ List Options
Expand Down Expand Up @@ -282,7 +308,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) {
Expand All @@ -299,7 +325,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

Expand All @@ -309,7 +335,7 @@ func (m MatchingFields) ApplyToList(opts *ListOptions) {
opts.FieldSelector = sel
}

// InNamespace restricts the given operation to the given namespace.
// InNamespace restricts the list/delete operation to the given namespace.
type InNamespace string

func (n InNamespace) ApplyToList(opts *ListOptions) {
Expand Down
20 changes: 19 additions & 1 deletion pkg/client/typed_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,29 @@ 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()
Expand Down
18 changes: 17 additions & 1 deletion pkg/client/unstructured_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down

0 comments on commit acf910d

Please sign in to comment.