Skip to content

Commit dd782cd

Browse files
adracusMadVikingGod
andcommitted
✨ Implement delete collection via delete options
implement `CollectionOptions` in `DeleteOptions` that allow to issue a `DeleteCollection` call under the hood add tests + examples Co-Authored-By: Aaron Clawson <[email protected]>
1 parent 13ee2bc commit dd782cd

File tree

7 files changed

+226
-3
lines changed

7 files changed

+226
-3
lines changed

pkg/client/client_test.go

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ var _ = Describe("Client", func() {
7070
BeforeEach(func(done Done) {
7171
atomic.AddUint64(&count, 1)
7272
dep = &appsv1.Deployment{
73-
ObjectMeta: metav1.ObjectMeta{Name: fmt.Sprintf("deployment-name-%v", count), Namespace: ns},
73+
ObjectMeta: metav1.ObjectMeta{Name: fmt.Sprintf("deployment-name-%v", count), Namespace: ns, Labels: map[string]string{"app": fmt.Sprintf("bar-%v", count)}},
7474
Spec: appsv1.DeploymentSpec{
7575
Replicas: &replicaCount,
7676
Selector: &metav1.LabelSelector{
@@ -898,6 +898,42 @@ var _ = Describe("Client", func() {
898898
PIt("should fail if the GVK cannot be mapped to a Resource", func() {
899899

900900
})
901+
902+
It("should delete a collection of object", func(done Done) {
903+
cl, err := client.New(cfg, client.Options{})
904+
Expect(err).NotTo(HaveOccurred())
905+
Expect(cl).NotTo(BeNil())
906+
907+
By("initially creating two Deployments")
908+
909+
dep2 := dep.DeepCopy()
910+
dep2.Name = dep2.Name + "-2"
911+
912+
dep, err = clientset.AppsV1().Deployments(ns).Create(dep)
913+
Expect(err).NotTo(HaveOccurred())
914+
dep2, err = clientset.AppsV1().Deployments(ns).Create(dep2)
915+
Expect(err).NotTo(HaveOccurred())
916+
917+
depName := dep.Name
918+
dep2Name := dep2.Name
919+
920+
labelmatcher := client.CollectionOptions(
921+
client.MatchingLabels(dep.ObjectMeta.Labels),
922+
client.InNamespace(ns),
923+
)
924+
925+
By("deleting Deployments")
926+
err = cl.Delete(context.TODO(), dep, labelmatcher)
927+
Expect(err).NotTo(HaveOccurred())
928+
929+
By("validating the Deployment no longer exists")
930+
_, err = clientset.AppsV1().Deployments(ns).Get(depName, metav1.GetOptions{})
931+
Expect(err).To(HaveOccurred())
932+
_, err = clientset.AppsV1().Deployments(ns).Get(dep2Name, metav1.GetOptions{})
933+
Expect(err).To(HaveOccurred())
934+
935+
close(done)
936+
})
901937
})
902938
Context("with unstructured objects", func() {
903939
It("should delete an existing object from a go struct", func(done Done) {
@@ -974,6 +1010,49 @@ var _ = Describe("Client", func() {
9741010

9751011
close(done)
9761012
})
1013+
1014+
It("should delete a collection of object", func(done Done) {
1015+
cl, err := client.New(cfg, client.Options{})
1016+
Expect(err).NotTo(HaveOccurred())
1017+
Expect(cl).NotTo(BeNil())
1018+
1019+
By("initially creating two Deployments")
1020+
1021+
dep2 := dep.DeepCopy()
1022+
dep2.Name = dep2.Name + "-2"
1023+
1024+
dep, err = clientset.AppsV1().Deployments(ns).Create(dep)
1025+
Expect(err).NotTo(HaveOccurred())
1026+
dep2, err = clientset.AppsV1().Deployments(ns).Create(dep2)
1027+
Expect(err).NotTo(HaveOccurred())
1028+
1029+
depName := dep.Name
1030+
dep2Name := dep2.Name
1031+
1032+
labelmatcher := client.CollectionOptions(
1033+
client.MatchingLabels(dep.ObjectMeta.Labels),
1034+
client.InNamespace(ns),
1035+
)
1036+
1037+
By("deleting Deployments")
1038+
u := &unstructured.Unstructured{}
1039+
Expect(scheme.Convert(dep, u, nil)).To(Succeed())
1040+
u.SetGroupVersionKind(schema.GroupVersionKind{
1041+
Group: "apps",
1042+
Kind: "Deployment",
1043+
Version: "v1",
1044+
})
1045+
err = cl.Delete(context.TODO(), u, labelmatcher)
1046+
Expect(err).NotTo(HaveOccurred())
1047+
1048+
By("validating the Deployment no longer exists")
1049+
_, err = clientset.AppsV1().Deployments(ns).Get(depName, metav1.GetOptions{})
1050+
Expect(err).To(HaveOccurred())
1051+
_, err = clientset.AppsV1().Deployments(ns).Get(dep2Name, metav1.GetOptions{})
1052+
Expect(err).To(HaveOccurred())
1053+
1054+
close(done)
1055+
})
9771056
})
9781057
})
9791058

@@ -1994,6 +2073,10 @@ var _ = Describe("Client", func() {
19942073
PIt("should fail if the object doesn't have meta", func() {
19952074

19962075
})
2076+
2077+
PIt("should filter results by namespace selector", func() {
2078+
2079+
})
19972080
})
19982081
})
19992082

@@ -2043,6 +2126,12 @@ var _ = Describe("Client", func() {
20432126
Expect(do.AsDeleteOptions()).To(Equal(&metav1.DeleteOptions{}))
20442127
})
20452128

2129+
It("should producte nil CollectionOptions if not present", func() {
2130+
do := &client.DeleteOptions{}
2131+
do.AsDeleteOptions()
2132+
Expect(do.CollectionOptions).To(BeNil())
2133+
})
2134+
20462135
It("should merge multiple options together", func() {
20472136
gp := int64(1)
20482137
pc := metav1.NewUIDPreconditions("uid")
@@ -2052,10 +2141,13 @@ var _ = Describe("Client", func() {
20522141
client.GracePeriodSeconds(gp),
20532142
client.Preconditions(pc),
20542143
client.PropagationPolicy(dp),
2144+
client.CollectionOptions(client.UseListOptions(&client.ListOptions{Namespace: "test"})),
20552145
})
20562146
Expect(do.GracePeriodSeconds).To(Equal(&gp))
20572147
Expect(do.Preconditions).To(Equal(pc))
20582148
Expect(do.PropagationPolicy).To(Equal(&dp))
2149+
Expect(do.CollectionOptions).NotTo(BeNil())
2150+
Expect(do.CollectionOptions.Namespace).To(Equal("test"))
20592151
})
20602152
})
20612153

@@ -2136,6 +2228,13 @@ var _ = Describe("Client", func() {
21362228
Expect(lo).NotTo(BeNil())
21372229
Expect(lo.Namespace).To(Equal("test"))
21382230
})
2231+
2232+
It("should produce empty metav1.ListOptions if nil", func() {
2233+
var do *client.ListOptions
2234+
Expect(do.AsListOptions()).To(Equal(&metav1.ListOptions{}))
2235+
do = &client.ListOptions{}
2236+
Expect(do.AsListOptions()).To(Equal(&metav1.ListOptions{}))
2237+
})
21392238
})
21402239

21412240
Describe("UpdateOptions", func() {

pkg/client/example_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,27 @@ func ExampleClient_delete() {
199199
_ = c.Delete(context.Background(), u)
200200
}
201201

202+
// This example shows how to use the client with typed and unstrucurted objects to delete collections of objects.
203+
func ExampleClient_deleteCollection() {
204+
labelMatcher := client.CollectionOptions(
205+
client.MatchingLabels(map[string]string{"app": "foo"}),
206+
client.InNamespace("foo"),
207+
)
208+
// Using a typed object.
209+
pod := &corev1.Pod{}
210+
// c is a created client.
211+
_ = c.Delete(context.Background(), pod, labelMatcher)
212+
213+
// Using an unstructured Object
214+
u := &unstructured.UnstructuredList{}
215+
u.SetGroupVersionKind(schema.GroupVersionKind{
216+
Group: "apps",
217+
Kind: "Deployment",
218+
Version: "v1",
219+
})
220+
_ = c.Delete(context.Background(), u, labelMatcher)
221+
}
222+
202223
// This example shows how to set up and consume a field selector over a pod's volumes' secretName field.
203224
func ExampleFieldIndexer_secretName() {
204225
// someIndexer is a FieldIndexer over a Cache

pkg/client/fake/client.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,10 +161,49 @@ func (c *fakeClient) Delete(ctx context.Context, obj runtime.Object, opts ...cli
161161
if err != nil {
162162
return err
163163
}
164+
delOptions := client.DeleteOptions{}
165+
delOptions.ApplyOptions(opts)
166+
if delOptions.CollectionOptions != nil {
167+
return c.deleteCollection(obj, delOptions)
168+
}
169+
164170
//TODO: implement propagation
165171
return c.tracker.Delete(gvr, accessor.GetNamespace(), accessor.GetName())
166172
}
167173

174+
func (c *fakeClient) deleteCollection(obj runtime.Object, dcOptions client.DeleteOptions) error {
175+
gvk, err := apiutil.GVKForObject(obj, scheme.Scheme)
176+
if err != nil {
177+
return err
178+
}
179+
180+
gvr, _ := meta.UnsafeGuessKindToResource(gvk)
181+
o, err := c.tracker.List(gvr, gvk, dcOptions.CollectionOptions.Namespace)
182+
if err != nil {
183+
return err
184+
}
185+
186+
objs, err := meta.ExtractList(o)
187+
if err != nil {
188+
return err
189+
}
190+
filteredObjs, err := objectutil.FilterWithLabels(objs, dcOptions.CollectionOptions.LabelSelector)
191+
if err != nil {
192+
return err
193+
}
194+
for _, o := range filteredObjs {
195+
accessor, err := meta.Accessor(o)
196+
if err != nil {
197+
return err
198+
}
199+
err = c.tracker.Delete(gvr, accessor.GetNamespace(), accessor.GetName())
200+
if err != nil {
201+
return err
202+
}
203+
}
204+
return nil
205+
}
206+
168207
func (c *fakeClient) Update(ctx context.Context, obj runtime.Object, opts ...client.UpdateOptionFunc) error {
169208
updateOptions := &client.UpdateOptions{}
170209
updateOptions.ApplyOptions(opts)

pkg/client/fake/client_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,21 @@ var _ = Describe("Fake client", func() {
158158
Expect(list.Items).To(ConsistOf(*dep2))
159159
})
160160

161+
It("should be able to Delete a Collection", func() {
162+
By("Deleting a deploymentList")
163+
labelFilter := client.CollectionOptions(
164+
client.InNamespace("ns1"),
165+
)
166+
err := cl.Delete(nil, &appsv1.Deployment{}, labelFilter)
167+
Expect(err).To(BeNil())
168+
169+
By("Listing all deployments in the namespace")
170+
list := &appsv1.DeploymentList{}
171+
err = cl.List(nil, list, client.InNamespace("ns1"))
172+
Expect(err).To(BeNil())
173+
Expect(list.Items).To(BeEmpty())
174+
})
175+
161176
Context("with the DryRun option", func() {
162177
It("should not create a new object", func() {
163178
By("Creating a new configmap with DryRun")

pkg/client/options.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ type DeleteOptions struct {
9494
// foreground.
9595
PropagationPolicy *metav1.DeletionPropagation
9696

97+
// CollectionOptions is used by the DeleteCollection to determine the objects
98+
// To be deleted.
99+
CollectionOptions *ListOptions
100+
97101
// Raw represents raw DeleteOptions, as passed to the API server.
98102
Raw *metav1.DeleteOptions
99103
}
@@ -153,6 +157,17 @@ func PropagationPolicy(p metav1.DeletionPropagation) DeleteOptionFunc {
153157
}
154158
}
155159

160+
// CollectionOptions is a functional option that sets the CollectionOptions
161+
// field of a DeleteOptions struct
162+
func CollectionOptions(listOpts ...ListOptionFunc) DeleteOptionFunc {
163+
return func(opts *DeleteOptions) {
164+
if opts.CollectionOptions == nil {
165+
opts.CollectionOptions = &ListOptions{}
166+
}
167+
opts.CollectionOptions.ApplyOptions(listOpts)
168+
}
169+
}
170+
156171
// ListOptions contains options for limiting or filtering results.
157172
// It's generally a subset of metav1.ListOptions, with support for
158173
// pre-parsed selectors (since generally, selectors will be executed

pkg/client/typed_client.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,29 @@ func (c *typedClient) Delete(ctx context.Context, obj runtime.Object, opts ...De
7676
}
7777

7878
deleteOpts := DeleteOptions{}
79+
deleteOpts.ApplyOptions(opts)
80+
81+
if deleteOpts.CollectionOptions != nil {
82+
return c.deleteCollection(ctx, o, deleteOpts)
83+
}
84+
7985
return o.Delete().
8086
NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()).
8187
Resource(o.resource()).
8288
Name(o.GetName()).
83-
Body(deleteOpts.ApplyOptions(opts).AsDeleteOptions()).
89+
Body(deleteOpts.AsDeleteOptions()).
90+
Context(ctx).
91+
Do().
92+
Error()
93+
}
94+
95+
// DeleteCollection implements client.Client
96+
func (c *typedClient) deleteCollection(ctx context.Context, o *objMeta, deleteOpts DeleteOptions) error {
97+
return o.Delete().
98+
NamespaceIfScoped(deleteOpts.CollectionOptions.Namespace, o.isNamespaced()).
99+
Resource(o.resource()).
100+
VersionedParams(deleteOpts.CollectionOptions.AsListOptions(), c.paramCodec).
101+
Body(deleteOpts.AsDeleteOptions()).
84102
Context(ctx).
85103
Do().
86104
Error()

pkg/client/unstructured_client.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,23 @@ func (uc *unstructuredClient) Delete(_ context.Context, obj runtime.Object, opts
8787
return err
8888
}
8989
deleteOpts := DeleteOptions{}
90-
err = r.Delete(u.GetName(), deleteOpts.ApplyOptions(opts).AsDeleteOptions())
90+
deleteOpts.ApplyOptions(opts)
91+
if deleteOpts.CollectionOptions != nil {
92+
return uc.deleteCollection(u, deleteOpts)
93+
}
94+
err = r.Delete(u.GetName(), deleteOpts.AsDeleteOptions())
95+
return err
96+
}
97+
98+
func (uc *unstructuredClient) deleteCollection(u *unstructured.Unstructured, dcOpts DeleteOptions) error {
99+
gvk := u.GroupVersionKind()
100+
101+
r, err := uc.getResourceInterface(gvk, dcOpts.CollectionOptions.Namespace)
102+
if err != nil {
103+
return err
104+
}
105+
106+
err = r.DeleteCollection(dcOpts.AsDeleteOptions(), *dcOpts.CollectionOptions.AsListOptions())
91107
return err
92108
}
93109

0 commit comments

Comments
 (0)