Skip to content

Commit 8590c99

Browse files
committed
refactoring and add support for annotation ttls
Signed-off-by: Markus Blaschke <[email protected]>
1 parent c1f2503 commit 8590c99

File tree

7 files changed

+214
-100
lines changed

7 files changed

+214
-100
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
[![Quay.io](https://img.shields.io/badge/Quay.io-webdevops%2Fkube--janitor-blue)](https://quay.io/repository/webdevops/kube-janitor)
66
[![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/kube-janitor)](https://artifacthub.io/packages/search?repo=kube-janitor)
77

8-
Kubernetes janitor which deletes resources by TTL label written in Golang
8+
Kubernetes janitor which deletes resources by TTL annotation or label written in Golang
99

1010
## Configuration
1111

example.yaml

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1-
label: ttl
1+
ttl:
2+
## checks all resources by annotation
3+
annotation: janitor/ttl
24

3-
resources:
4-
- {group: "", version: v1, kind: configmaps}
5-
- {group: "", version: v1, kind: secrets}
6-
- {group: apps, version: v1, kind: deployments}
5+
## checks all resources by label
6+
# label: janitor/ttl
7+
8+
resources:
9+
# definition of resources by group, version, kind (GVR)
10+
- {group: "", version: v1, kind: configmaps}
11+
- {group: "", version: v1, kind: secrets}
12+
- {group: apps, version: v1, kind: deployments}

kube_janitor/config.go

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,13 @@ import (
99

1010
type (
1111
Config struct {
12-
Label string `json:"label"`
13-
Resources []*ConfigResources `json:"resources"`
12+
Ttl *ConfigTtl `json:"ttl"`
13+
}
14+
15+
ConfigTtl struct {
16+
Annotation string `json:"annotation"`
17+
Label string `json:"label"`
18+
Resources []*ConfigResources `json:"resources"`
1419
}
1520

1621
ConfigResources struct {
@@ -22,30 +27,42 @@ type (
2227

2328
func NewConfig() *Config {
2429
return &Config{
25-
Resources: []*ConfigResources{},
30+
Ttl: &ConfigTtl{
31+
Resources: []*ConfigResources{},
32+
},
2633
}
2734
}
2835

29-
func (r *ConfigResources) String() string {
30-
return r.AsGVR().String()
31-
}
32-
33-
func (r *ConfigResources) AsGVR() schema.GroupVersionResource {
34-
return schema.GroupVersionResource{
35-
Group: r.Group,
36-
Version: r.Version,
37-
Resource: r.Kind,
36+
func (c *Config) Validate() error {
37+
if err := c.Ttl.Validate(); err != nil {
38+
return err
3839
}
40+
41+
return nil
3942
}
4043

41-
func (r *Config) Validate() error {
42-
if r.Label == "" {
43-
return errors.New("label is required")
44+
func (c *ConfigTtl) Validate() error {
45+
if c.Label == "" && c.Annotation == "" {
46+
return errors.New("label or annotation is required")
4447
}
4548

46-
if strings.Contains(r.Label, " ") {
47-
return errors.New("label must not contain spaces")
49+
if c.Label != "" {
50+
if strings.Contains(c.Label, " ") {
51+
return errors.New("label must not contain spaces")
52+
}
4853
}
4954

5055
return nil
5156
}
57+
58+
func (c *ConfigResources) String() string {
59+
return c.AsGVR().String()
60+
}
61+
62+
func (c *ConfigResources) AsGVR() schema.GroupVersionResource {
63+
return schema.GroupVersionResource{
64+
Group: c.Group,
65+
Version: c.Version,
66+
Resource: c.Kind,
67+
}
68+
}

kube_janitor/kube.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ const (
1212
KubeListLimit = 100
1313
)
1414

15-
func (j *Janitor) kubeEachResource(ctx context.Context, gvr schema.GroupVersionResource, callback func(unstructured unstructured.Unstructured) error) error {
15+
func (j *Janitor) kubeEachResource(ctx context.Context, gvr schema.GroupVersionResource, labelSelector string, callback func(unstructured unstructured.Unstructured) error) error {
1616
listOpts := metav1.ListOptions{
1717
Limit: KubeListLimit,
18-
LabelSelector: j.config.Label,
18+
LabelSelector: labelSelector,
1919
}
2020
for {
2121
result, err := j.dynClient.Resource(gvr).List(ctx, listOpts)

kube_janitor/manager.go

Lines changed: 2 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,7 @@ import (
99

1010
"github.com/go-logr/logr"
1111
yaml "github.com/goccy/go-yaml"
12-
"github.com/prometheus/client_golang/prometheus"
1312
"github.com/webdevops/go-common/log/slogger"
14-
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
15-
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
16-
17-
prometheusCommon "github.com/webdevops/go-common/prometheus"
1813
"k8s.io/client-go/dynamic"
1914
"k8s.io/client-go/rest"
2015
"k8s.io/client-go/tools/clientcmd"
@@ -146,63 +141,9 @@ func (j *Janitor) Start(interval time.Duration) {
146141
func (j *Janitor) Run() error {
147142
j.connect()
148143

149-
ctx := context.Background()
150-
151-
metricResourceTtl := prometheusCommon.NewMetricsList()
152-
153-
for _, resourceType := range j.config.Resources {
154-
gvkLogger := j.logger.With(slog.Any("gvk", resourceType))
155-
gvkLogger.Debug("checking resources")
156-
157-
err := j.kubeEachResource(ctx, resourceType.AsGVR(), func(resource unstructured.Unstructured) error {
158-
159-
if ttl := resource.GetLabels()[j.config.Label]; ttl != "" {
160-
resourceLogger := gvkLogger.With(
161-
slog.String("namespace", resource.GetNamespace()),
162-
slog.String("resource", resource.GetName()),
163-
slog.String("ttl", ttl),
164-
)
165-
166-
parsedDate, expired, err := j.checkExpiryDate(resource.GetCreationTimestamp().Time, ttl)
167-
if err != nil {
168-
resourceLogger.Error("unable to parse expiration date", slog.String("raw", ttl), slog.Any("error", err))
169-
return nil
170-
}
171-
172-
metricResourceTtl.AddTime(
173-
prometheus.Labels{
174-
"version": resource.GetAPIVersion(),
175-
"kind": resource.GetKind(),
176-
"namespace": resource.GetNamespace(),
177-
"name": resource.GetName(),
178-
"ttl": ttl,
179-
},
180-
*parsedDate,
181-
)
182-
183-
resourceLogger.Debug("found resource with valid TTL", slog.Time("expirationDate", *parsedDate))
184-
185-
if expired {
186-
if j.dryRun {
187-
resourceLogger.Info("resource is expired, would delete resource (DRY-RUN)", slog.Time("expirationDate", *parsedDate))
188-
} else {
189-
resourceLogger.Info("resource is expired, deleting", slog.Time("expirationDate", *parsedDate))
190-
err := j.dynClient.Resource(resourceType.AsGVR()).Namespace(resource.GetNamespace()).Delete(ctx, resource.GetName(), metav1.DeleteOptions{})
191-
if err != nil {
192-
return err
193-
}
194-
}
195-
}
196-
}
197-
198-
return nil
199-
})
200-
if err != nil {
201-
return err
202-
}
144+
if err := j.runTtlResources(); err != nil {
145+
return err
203146
}
204147

205-
metricResourceTtl.GaugeSet(j.prometheus.ttl)
206-
207148
return nil
208149
}

kube_janitor/task.ttl.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package kube_janitor
2+
3+
import (
4+
"context"
5+
"log/slog"
6+
"strings"
7+
8+
"github.com/prometheus/client_golang/prometheus"
9+
prometheusCommon "github.com/webdevops/go-common/prometheus"
10+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
11+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
12+
)
13+
14+
func (j *Janitor) runTtlResources() error {
15+
j.connect()
16+
17+
ctx := context.Background()
18+
19+
metricResourceTtl := prometheusCommon.NewMetricsList()
20+
21+
labelSelector := ""
22+
// we can use a label selector if ttl is configured by label
23+
if j.config.Ttl.Label != "" {
24+
labelSelector = j.config.Ttl.Label
25+
}
26+
27+
for _, resourceType := range j.config.Ttl.Resources {
28+
gvkLogger := j.logger.With(slog.Any("gvk", resourceType))
29+
gvkLogger.Debug("checking resources")
30+
31+
err := j.kubeEachResource(ctx, resourceType.AsGVR(), labelSelector, func(resource unstructured.Unstructured) error {
32+
var ttlValue string
33+
34+
if j.config.Ttl.Annotation != "" {
35+
// get from meta.annotations
36+
if val, exists := resource.GetAnnotations()[j.config.Ttl.Annotation]; exists {
37+
ttlValue = strings.TrimSpace(val)
38+
}
39+
} else if j.config.Ttl.Label != "" {
40+
// get from meta.labels
41+
if val, exists := resource.GetLabels()[j.config.Ttl.Label]; exists {
42+
ttlValue = strings.TrimSpace(val)
43+
}
44+
}
45+
46+
// check if we got a valid ttl value
47+
if ttlValue == "" {
48+
return nil
49+
}
50+
51+
resourceLogger := gvkLogger.With(
52+
slog.String("namespace", resource.GetNamespace()),
53+
slog.String("resource", resource.GetName()),
54+
slog.String("ttl", ttlValue),
55+
)
56+
57+
parsedDate, expired, err := j.checkExpiryDate(resource.GetCreationTimestamp().Time, ttlValue)
58+
if err != nil {
59+
resourceLogger.Error("unable to parse expiration date", slog.String("raw", ttlValue), slog.Any("error", err))
60+
return nil
61+
}
62+
63+
metricResourceTtl.AddTime(
64+
prometheus.Labels{
65+
"version": resource.GetAPIVersion(),
66+
"kind": resource.GetKind(),
67+
"namespace": resource.GetNamespace(),
68+
"name": resource.GetName(),
69+
"ttl": ttlValue,
70+
},
71+
*parsedDate,
72+
)
73+
74+
resourceLogger.Debug("found resource with valid TTL", slog.Time("expirationDate", *parsedDate))
75+
76+
if expired {
77+
if j.dryRun {
78+
resourceLogger.Info("resource is expired, would delete resource (DRY-RUN)", slog.Time("expirationDate", *parsedDate))
79+
} else {
80+
resourceLogger.Info("deleting expired resource", slog.Time("expirationDate", *parsedDate))
81+
err := j.dynClient.Resource(resourceType.AsGVR()).Namespace(resource.GetNamespace()).Delete(ctx, resource.GetName(), metav1.DeleteOptions{})
82+
if err != nil {
83+
return err
84+
}
85+
}
86+
}
87+
88+
return nil
89+
})
90+
if err != nil {
91+
return err
92+
}
93+
}
94+
95+
metricResourceTtl.GaugeSet(j.prometheus.ttl)
96+
97+
return nil
98+
}

0 commit comments

Comments
 (0)