Skip to content

Commit 9b6a926

Browse files
authored
Merge pull request #655 from AlexanderYastrebov/deletion-selector
deletions: add selector
2 parents 0ad957d + 51d8957 commit 9b6a926

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
@@ -69,6 +69,7 @@ type Resource struct {
6969
Name string `yaml:"name"`
7070
Namespace string `yaml:"namespace"`
7171
Kind string `yaml:"kind"`
72+
Selector string `yaml:"selector"`
7273
Labels Labels `yaml:"labels"`
7374
HasOwner *bool `yaml:"has_owner"`
7475

@@ -84,16 +85,23 @@ func (r *Resource) Options() metav1.DeleteOptions {
8485
}
8586
}
8687

88+
func (r *Resource) LabelSelector() string {
89+
if r.Selector != "" {
90+
return r.Selector
91+
}
92+
return metav1.FormatLabelSelector(&metav1.LabelSelector{MatchLabels: r.Labels})
93+
}
94+
8795
func (r *Resource) LogFields() logrus.Fields {
8896
fields := logrus.Fields{
8997
"kind": r.Kind,
9098
}
9199
if r.Namespace != "" {
92100
fields["namespace"] = r.Namespace
93101
}
94-
if len(r.Labels) > 0 {
95-
fields["selector"] = metav1.FormatLabelSelector(&metav1.LabelSelector{MatchLabels: r.Labels})
96-
}
102+
103+
fields["selector"] = r.LabelSelector()
104+
97105
if r.HasOwner != nil {
98106
fields["has_owner"] = fmt.Sprintf("%t", *r.HasOwner)
99107
}
@@ -252,19 +260,27 @@ func (c *ClientsCollection) deleteIfFound(ctx context.Context, logger *logrus.En
252260
func (c *ClientsCollection) DeleteResource(ctx context.Context, logger *logrus.Entry, deletion *Resource) error {
253261
logger = logger.WithFields(deletion.LogFields())
254262

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

262-
if deletion.Name == "" && len(deletion.Labels) == 0 {
263-
return fmt.Errorf("either name or labels must be specified to identify a resource")
276+
if resourceIdentifiers == 0 {
277+
return fmt.Errorf("either 'name', 'selector' or 'labels' must be specified to identify a resource")
278+
} else if resourceIdentifiers > 1 {
279+
return fmt.Errorf("only one of 'name', 'selector' or 'labels' must be specified to identify a resource")
264280
}
265281

266-
if deletion.HasOwner != nil && len(deletion.Labels) == 0 {
267-
return fmt.Errorf("'has_owner' requires 'labels' to be specified")
282+
if deletion.HasOwner != nil && deletion.Selector == "" && len(deletion.Labels) == 0 {
283+
return fmt.Errorf("'has_owner' requires 'selector' or 'labels' to be specified")
268284
}
269285

270286
if deletion.Name != "" {
@@ -273,37 +289,33 @@ func (c *ClientsCollection) DeleteResource(ctx context.Context, logger *logrus.E
273289
return err
274290
}
275291
return c.deleteIfFound(ctx, logger, deletion.Kind, deletion.Namespace, deletion.Name, deletion.Options())
276-
} else if len(deletion.Labels) > 0 {
277-
items, err := c.ListResources(ctx, deletion)
292+
}
293+
294+
items, err := c.ListResources(ctx, deletion)
295+
if err != nil {
296+
return err
297+
}
298+
299+
if len(items) == 0 {
300+
logger.Infof("No matching %s resources found", deletion.Kind)
301+
}
302+
303+
for _, item := range items {
304+
err := c.overrideDeletionProtection(ctx, logger, deletion.Kind, deletion.Namespace, deletion.Name)
278305
if err != nil {
279306
return err
280307
}
281-
282-
if len(items) == 0 {
283-
logger.Infof("No matching %s resources found", deletion.Kind)
284-
}
285-
286-
for _, item := range items {
287-
err := c.overrideDeletionProtection(ctx, logger, deletion.Kind, deletion.Namespace, deletion.Name)
288-
if err != nil {
289-
return err
290-
}
291-
err = c.deleteIfFound(ctx, logger, deletion.Kind, item.GetNamespace(), item.GetName(), deletion.Options())
292-
if err != nil {
293-
return err
294-
}
308+
err = c.deleteIfFound(ctx, logger, deletion.Kind, item.GetNamespace(), item.GetName(), deletion.Options())
309+
if err != nil {
310+
return err
295311
}
296312
}
297313

298314
return nil
299315
}
300316

301317
func (c *ClientsCollection) ListResources(ctx context.Context, rsrc *Resource) ([]unstructured.Unstructured, error) {
302-
items, err := c.List(ctx, rsrc.Kind, rsrc.Namespace, metav1.ListOptions{
303-
LabelSelector: metav1.FormatLabelSelector(&metav1.LabelSelector{
304-
MatchLabels: rsrc.Labels,
305-
}),
306-
})
318+
items, err := c.List(ctx, rsrc.Kind, rsrc.Namespace, metav1.ListOptions{LabelSelector: rsrc.LabelSelector()})
307319
if err != nil {
308320
return nil, err
309321
}

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)