Skip to content

Commit 51d8957

Browse files
deletions: add selector
Adds deletion `selector` to enable set and equality-based label requirements, see https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors Signed-off-by: Alexander Yastrebov <[email protected]>
1 parent 17b42c1 commit 51d8957

File tree

4 files changed

+170
-43
lines changed

4 files changed

+170
-43
lines changed

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,9 @@ pre_apply: # everything defined under here will be deleted before applying the m
140140
labels:
141141
foo: bar
142142
has_owner: false
143+
- namespace: kube-system
144+
kind: deployment
145+
selector: version != v1
143146
post_apply: # everything defined under here will be deleted after applying the manifests
144147
- namespace: kube-system
145148
kind: deployment
@@ -152,8 +155,9 @@ Whatever is defined in this file will be deleted pre/post applying the other
152155
manifest files, if the resource exists. If the resource has already been
153156
deleted previously it's treated as a no-op.
154157

155-
A resource can be identified either by `name` or `labels`.
156-
It is an error if both or none of them are defined.
158+
A resource can be identified either by `name`,
159+
[`selector`](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors) or
160+
`labels` and only one of them should be defined.
157161

158162
`namespace` can be left out, in which case it will default to `kube-system`.
159163

pkg/kubernetes/kubernetes.go

Lines changed: 45 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ type Resource struct {
6767
Name string `yaml:"name"`
6868
Namespace string `yaml:"namespace"`
6969
Kind string `yaml:"kind"`
70+
Selector string `yaml:"selector"`
7071
Labels Labels `yaml:"labels"`
7172
HasOwner *bool `yaml:"has_owner"`
7273

@@ -82,16 +83,23 @@ func (r *Resource) Options() metav1.DeleteOptions {
8283
}
8384
}
8485

86+
func (r *Resource) LabelSelector() string {
87+
if r.Selector != "" {
88+
return r.Selector
89+
}
90+
return metav1.FormatLabelSelector(&metav1.LabelSelector{MatchLabels: r.Labels})
91+
}
92+
8593
func (r *Resource) LogFields() logrus.Fields {
8694
fields := logrus.Fields{
8795
"kind": r.Kind,
8896
}
8997
if r.Namespace != "" {
9098
fields["namespace"] = r.Namespace
9199
}
92-
if len(r.Labels) > 0 {
93-
fields["selector"] = metav1.FormatLabelSelector(&metav1.LabelSelector{MatchLabels: r.Labels})
94-
}
100+
101+
fields["selector"] = r.LabelSelector()
102+
95103
if r.HasOwner != nil {
96104
fields["has_owner"] = fmt.Sprintf("%t", *r.HasOwner)
97105
}
@@ -250,19 +258,27 @@ func (c *ClientsCollection) deleteIfFound(ctx context.Context, logger *logrus.En
250258
func (c *ClientsCollection) DeleteResource(ctx context.Context, logger *logrus.Entry, deletion *Resource) error {
251259
logger = logger.WithFields(deletion.LogFields())
252260

253-
// identify the resource to be deleted either by name or
254-
// labels. name AND labels cannot be defined at the same time,
255-
// but one of them MUST be defined.
256-
if deletion.Name != "" && len(deletion.Labels) > 0 {
257-
return fmt.Errorf("only one of 'name' or 'labels' must be specified")
261+
// identify the resource to be deleted either by name, selector or labels.
262+
// Only one of them must be defined.
263+
resourceIdentifiers := 0
264+
if deletion.Name != "" {
265+
resourceIdentifiers++
266+
}
267+
if deletion.Selector != "" {
268+
resourceIdentifiers++
269+
}
270+
if len(deletion.Labels) > 0 {
271+
resourceIdentifiers++
258272
}
259273

260-
if deletion.Name == "" && len(deletion.Labels) == 0 {
261-
return fmt.Errorf("either name or labels must be specified to identify a resource")
274+
if resourceIdentifiers == 0 {
275+
return fmt.Errorf("either 'name', 'selector' or 'labels' must be specified to identify a resource")
276+
} else if resourceIdentifiers > 1 {
277+
return fmt.Errorf("only one of 'name', 'selector' or 'labels' must be specified to identify a resource")
262278
}
263279

264-
if deletion.HasOwner != nil && len(deletion.Labels) == 0 {
265-
return fmt.Errorf("'has_owner' requires 'labels' to be specified")
280+
if deletion.HasOwner != nil && deletion.Selector == "" && len(deletion.Labels) == 0 {
281+
return fmt.Errorf("'has_owner' requires 'selector' or 'labels' to be specified")
266282
}
267283

268284
if deletion.Name != "" {
@@ -271,37 +287,33 @@ func (c *ClientsCollection) DeleteResource(ctx context.Context, logger *logrus.E
271287
return err
272288
}
273289
return c.deleteIfFound(ctx, logger, deletion.Kind, deletion.Namespace, deletion.Name, deletion.Options())
274-
} else if len(deletion.Labels) > 0 {
275-
items, err := c.ListResources(ctx, deletion)
290+
}
291+
292+
items, err := c.ListResources(ctx, deletion)
293+
if err != nil {
294+
return err
295+
}
296+
297+
if len(items) == 0 {
298+
logger.Infof("No matching %s resources found", deletion.Kind)
299+
}
300+
301+
for _, item := range items {
302+
err := c.overrideDeletionProtection(ctx, logger, deletion.Kind, deletion.Namespace, deletion.Name)
276303
if err != nil {
277304
return err
278305
}
279-
280-
if len(items) == 0 {
281-
logger.Infof("No matching %s resources found", deletion.Kind)
282-
}
283-
284-
for _, item := range items {
285-
err := c.overrideDeletionProtection(ctx, logger, deletion.Kind, deletion.Namespace, deletion.Name)
286-
if err != nil {
287-
return err
288-
}
289-
err = c.deleteIfFound(ctx, logger, deletion.Kind, item.GetNamespace(), item.GetName(), deletion.Options())
290-
if err != nil {
291-
return err
292-
}
306+
err = c.deleteIfFound(ctx, logger, deletion.Kind, item.GetNamespace(), item.GetName(), deletion.Options())
307+
if err != nil {
308+
return err
293309
}
294310
}
295311

296312
return nil
297313
}
298314

299315
func (c *ClientsCollection) ListResources(ctx context.Context, rsrc *Resource) ([]unstructured.Unstructured, error) {
300-
items, err := c.List(ctx, rsrc.Kind, rsrc.Namespace, metav1.ListOptions{
301-
LabelSelector: metav1.FormatLabelSelector(&metav1.LabelSelector{
302-
MatchLabels: rsrc.Labels,
303-
}),
304-
})
316+
items, err := c.List(ctx, rsrc.Kind, rsrc.Namespace, metav1.ListOptions{LabelSelector: rsrc.LabelSelector()})
305317
if err != nil {
306318
return nil, err
307319
}

pkg/kubernetes/kubernetes_test.go

Lines changed: 109 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,64 @@ func TestPerformDeletion(t *testing.T) {
196196
},
197197
expectDeleted: []string{},
198198
},
199+
{
200+
name: "delete by selector (single label)",
201+
deletion: &Resource{
202+
Namespace: "ns-foo",
203+
Kind: "Deployment",
204+
Selector: "app=foo",
205+
},
206+
expectDeleted: []string{
207+
"/namespaces/ns-foo/deployments/foo-app",
208+
"/namespaces/ns-foo/deployments/foo-app-com",
209+
"/namespaces/ns-foo/deployments/foo-app-com-env",
210+
},
211+
},
212+
{
213+
name: "delete by selector (multiple labels)",
214+
deletion: &Resource{
215+
Namespace: "ns-foo",
216+
Kind: "Deployment",
217+
Selector: "app=foo,com=foo",
218+
},
219+
expectDeleted: []string{
220+
"/namespaces/ns-foo/deployments/foo-app-com",
221+
"/namespaces/ns-foo/deployments/foo-app-com-env",
222+
},
223+
},
224+
{
225+
name: "delete by selector (label not equal)",
226+
deletion: &Resource{
227+
Namespace: "ns-foo",
228+
Kind: "Deployment",
229+
Selector: "app=foo, env != foo",
230+
},
231+
expectDeleted: []string{
232+
"/namespaces/ns-foo/deployments/foo-app",
233+
"/namespaces/ns-foo/deployments/foo-app-com",
234+
},
235+
},
236+
{
237+
name: "delete by selector (label not in)",
238+
deletion: &Resource{
239+
Namespace: "ns-foo",
240+
Kind: "Deployment",
241+
Selector: "app=foo, env notin (foo, bar)",
242+
},
243+
expectDeleted: []string{
244+
"/namespaces/ns-foo/deployments/foo-app",
245+
"/namespaces/ns-foo/deployments/foo-app-com",
246+
},
247+
},
248+
{
249+
name: "delete by selector (no match)",
250+
deletion: &Resource{
251+
Namespace: "ns-foo",
252+
Kind: "Deployment",
253+
Selector: "app=nomatch",
254+
},
255+
expectDeleted: []string{},
256+
},
199257
{
200258
name: "delete replicasets without owner",
201259
deletion: &Resource{
@@ -210,7 +268,7 @@ func TestPerformDeletion(t *testing.T) {
210268
},
211269
},
212270
{
213-
name: "delete replicasets with owner",
271+
name: "delete replicasets with owner and labels",
214272
deletion: &Resource{
215273
Namespace: "ns-foo",
216274
Kind: "ReplicaSet",
@@ -222,6 +280,19 @@ func TestPerformDeletion(t *testing.T) {
222280
"/namespaces/ns-foo/replicasets/foo-app-com-env-0002",
223281
},
224282
},
283+
{
284+
name: "delete replicasets with owner and selector",
285+
deletion: &Resource{
286+
Namespace: "ns-foo",
287+
Kind: "ReplicaSet",
288+
Selector: "app=foo,com=foo",
289+
HasOwner: &yes,
290+
},
291+
expectDeleted: []string{
292+
"/namespaces/ns-foo/replicasets/foo-app-com-0002",
293+
"/namespaces/ns-foo/replicasets/foo-app-com-env-0002",
294+
},
295+
},
225296
{
226297
name: "delete replicasets regardless owner",
227298
deletion: &Resource{
@@ -239,32 +310,63 @@ func TestPerformDeletion(t *testing.T) {
239310
},
240311
// Errors
241312
{
242-
name: "both name or labels are specified",
313+
name: "all of name, selector and labels are specified",
314+
deletion: &Resource{
315+
Name: "foo",
316+
Labels: map[string]string{"foo": "bar"},
317+
Selector: "foo=bar",
318+
Namespace: "default",
319+
Kind: "Deployment",
320+
},
321+
expectError: "only one of 'name', 'selector' or 'labels' must be specified to identify a resource",
322+
},
323+
{
324+
name: "both name and labels are specified",
325+
deletion: &Resource{
326+
Name: "foo",
327+
Labels: map[string]string{"foo": "bar"},
328+
Namespace: "default",
329+
Kind: "Deployment",
330+
},
331+
expectError: "only one of 'name', 'selector' or 'labels' must be specified to identify a resource",
332+
},
333+
{
334+
name: "both name and selector are specified",
243335
deletion: &Resource{
244336
Name: "foo",
337+
Selector: "foo=bar",
338+
Namespace: "default",
339+
Kind: "Deployment",
340+
},
341+
expectError: "only one of 'name', 'selector' or 'labels' must be specified to identify a resource",
342+
},
343+
{
344+
name: "both selector and labels are specified",
345+
deletion: &Resource{
245346
Labels: map[string]string{"foo": "bar"},
347+
Selector: "foo=bar",
246348
Namespace: "default",
247349
Kind: "Deployment",
248350
},
249-
expectError: "only one of 'name' or 'labels' must be specified",
351+
expectError: "only one of 'name', 'selector' or 'labels' must be specified to identify a resource",
250352
},
251353
{
252-
name: "neither name or labels are specified",
354+
name: "none of name, selector or labels are specified",
253355
deletion: &Resource{
254356
Namespace: "default",
255357
Kind: "Deployment",
256358
},
257-
expectError: "either name or labels must be specified to identify a resource",
359+
expectError: "either 'name', 'selector' or 'labels' must be specified to identify a resource",
258360
},
259361
{
260-
name: "has_owner without labels",
362+
name: "has_owner without selector or labels",
261363
deletion: &Resource{
262364
Name: "foo",
263365
Namespace: "default",
264366
Kind: "Deployment",
265367
HasOwner: &yes,
266368
},
267-
expectError: "'has_owner' requires 'labels' to be specified",
369+
expectError: "'has_owner' requires 'selector' or 'labels' to be specified",
268370
},
269371
{
270372
name: "list error",

provisioner/clusterpy_test.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,13 @@ pre_apply:
6868
baz: qux
6969
has_owner: true
7070
`
71+
72+
deletionsContent4 = `
73+
pre_apply:
74+
- namespace: kube-system
75+
kind: Deployment
76+
selector: version != v1
77+
`
7178
)
7279

7380
func TestGetInfrastructureID(t *testing.T) {
@@ -249,7 +256,7 @@ func TestPropagateConfigItemsToNodePool(tt *testing.T) {
249256
} {
250257
cluster := &api.Cluster{
251258
ConfigItems: tc.cluster,
252-
NodePools: []*api.NodePool{&api.NodePool{ConfigItems: tc.nodePool}},
259+
NodePools: []*api.NodePool{{ConfigItems: tc.nodePool}},
253260
}
254261

255262
p := clusterpyProvisioner{}
@@ -305,6 +312,7 @@ func TestParseDeletions(t *testing.T) {
305312
{Path: "deletions.yaml", Contents: []byte(deletionsContent)},
306313
{Path: "deletions.yaml", Contents: []byte(deletionsContent2)},
307314
{Path: "deletions.yaml", Contents: []byte(deletionsContent3)},
315+
{Path: "deletions.yaml", Contents: []byte(deletionsContent4)},
308316
},
309317
}
310318

@@ -322,6 +330,7 @@ func TestParseDeletions(t *testing.T) {
322330
{Name: "foobar-pre", Namespace: "templated", Kind: "deployment"},
323331
{Name: "has-no-owner-pre", HasOwner: &no, Namespace: "kube-system", Kind: "ReplicaSet", Labels: map[string]string{"foo": "bar", "baz": "qux"}},
324332
{Name: "require-owner-pre", HasOwner: &yes, Namespace: "kube-system", Kind: "ReplicaSet", Labels: map[string]string{"foo": "bar", "baz": "qux"}},
333+
{Namespace: "kube-system", Kind: "Deployment", Selector: "version != v1"},
325334
},
326335
PostApply: []*kubernetes.Resource{
327336
{Name: "secretary-post", Namespace: "kube-system", Kind: "deployment"},

0 commit comments

Comments
 (0)