Skip to content

Commit 33d2f82

Browse files
committed
add support for fixed ttl by rules
Signed-off-by: Markus Blaschke <[email protected]>
1 parent 8590c99 commit 33d2f82

File tree

10 files changed

+327
-70
lines changed

10 files changed

+327
-70
lines changed

example.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
## TTL removal by annotation or label
12
ttl:
23
## checks all resources by annotation
34
annotation: janitor/ttl
@@ -10,3 +11,18 @@ ttl:
1011
- {group: "", version: v1, kind: configmaps}
1112
- {group: "", version: v1, kind: secrets}
1213
- {group: apps, version: v1, kind: deployments}
14+
15+
# static rules (fixed TTLs without using ttl definition in labels/annotations)
16+
rules:
17+
- id: default
18+
resources:
19+
- group: ""
20+
version: v1
21+
kind: configmaps
22+
selector:
23+
matchLabels:
24+
foo: bar
25+
namespaceSelector:
26+
matchLabels:
27+
kubernetes.io/metadata.name: default
28+
ttl: 1d

go.mod

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ require (
99
github.com/jessevdk/go-flags v1.6.1
1010
github.com/prometheus/client_golang v1.23.2
1111
github.com/webdevops/go-common v0.0.0-20251205143010-9409efa47a03
12+
k8s.io/api v0.34.3
1213
k8s.io/apimachinery v0.34.3
1314
k8s.io/client-go v0.34.3
1415
sigs.k8s.io/controller-runtime v0.22.4
@@ -20,8 +21,25 @@ require (
2021
github.com/cespare/xxhash/v2 v2.3.0 // indirect
2122
github.com/davecgh/go-spew v1.1.1 // indirect
2223
github.com/dustin/go-humanize v1.0.1 // indirect
24+
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
2325
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
26+
github.com/go-openapi/jsonpointer v0.22.4 // indirect
27+
github.com/go-openapi/jsonreference v0.21.4 // indirect
28+
github.com/go-openapi/swag v0.25.4 // indirect
29+
github.com/go-openapi/swag/cmdutils v0.25.4 // indirect
30+
github.com/go-openapi/swag/conv v0.25.4 // indirect
31+
github.com/go-openapi/swag/fileutils v0.25.4 // indirect
32+
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
33+
github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
34+
github.com/go-openapi/swag/loading v0.25.4 // indirect
35+
github.com/go-openapi/swag/mangling v0.25.4 // indirect
36+
github.com/go-openapi/swag/netutils v0.25.4 // indirect
37+
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
38+
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
39+
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
2440
github.com/gogo/protobuf v1.3.2 // indirect
41+
github.com/google/gnostic-models v0.7.1 // indirect
42+
github.com/google/uuid v1.6.0 // indirect
2543
github.com/json-iterator/go v1.1.12 // indirect
2644
github.com/kr/text v0.2.0 // indirect
2745
github.com/lmittmann/tint v1.1.2 // indirect
@@ -38,15 +56,18 @@ require (
3856
github.com/x448/float16 v0.8.4 // indirect
3957
go.uber.org/automaxprocs v1.6.0 // indirect
4058
go.yaml.in/yaml/v2 v2.4.3 // indirect
59+
go.yaml.in/yaml/v3 v3.0.4 // indirect
4160
golang.org/x/net v0.48.0 // indirect
4261
golang.org/x/oauth2 v0.34.0 // indirect
4362
golang.org/x/sys v0.39.0 // indirect
4463
golang.org/x/term v0.38.0 // indirect
4564
golang.org/x/text v0.32.0 // indirect
4665
golang.org/x/time v0.14.0 // indirect
4766
google.golang.org/protobuf v1.36.11 // indirect
67+
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
4868
gopkg.in/inf.v0 v0.9.1 // indirect
4969
k8s.io/klog/v2 v2.130.1 // indirect
70+
k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e // indirect
5071
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
5172
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
5273
sigs.k8s.io/randfill v1.0.0 // indirect

go.sum

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa
1818
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
1919
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
2020
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
21-
github.com/go-openapi/jsonpointer v0.22.3 h1:dKMwfV4fmt6Ah90zloTbUKWMD+0he+12XYAsPotrkn8=
22-
github.com/go-openapi/jsonpointer v0.22.3/go.mod h1:0lBbqeRsQ5lIanv3LHZBrmRGHLHcQoOXQnf88fHlGWo=
23-
github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc=
24-
github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4=
21+
github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
22+
github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
23+
github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8=
24+
github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4=
2525
github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU=
2626
github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ=
2727
github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4=
@@ -34,6 +34,8 @@ github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ98
3434
github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
3535
github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA=
3636
github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY=
37+
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo=
38+
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM=
3739
github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s=
3840
github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE=
3941
github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48=
@@ -46,6 +48,10 @@ github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv4
4648
github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE=
4749
github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw=
4850
github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc=
51+
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4=
52+
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg=
53+
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
54+
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
4955
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
5056
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
5157
github.com/goccy/go-yaml v1.19.0 h1:EmkZ9RIsX+Uq4DYFowegAuJo8+xdX3T/2dwNPXbxEYE=

kube_janitor/config.go

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import (
44
"errors"
55
"strings"
66

7+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
78
"k8s.io/apimachinery/pkg/runtime/schema"
89
)
910

1011
type (
1112
Config struct {
12-
Ttl *ConfigTtl `json:"ttl"`
13+
Ttl *ConfigTtl `json:"ttl"`
14+
Rules []*ConfigRule `json:"rules"`
1315
}
1416

1517
ConfigTtl struct {
@@ -19,9 +21,17 @@ type (
1921
}
2022

2123
ConfigResources struct {
22-
Group string `json:"group"`
23-
Version string `json:"version"`
24-
Kind string `json:"kind"`
24+
Group string `json:"group"`
25+
Version string `json:"version"`
26+
Kind string `json:"kind"`
27+
Selector *metav1.LabelSelector `json:"selector"`
28+
}
29+
30+
ConfigRule struct {
31+
Id string `json:"id"`
32+
Resources []*ConfigResources `json:"resources"`
33+
NamespaceSelector *metav1.LabelSelector `json:"namespaceSelector"`
34+
Ttl string `json:"ttl"`
2535
}
2636
)
2737

@@ -30,6 +40,7 @@ func NewConfig() *Config {
3040
Ttl: &ConfigTtl{
3141
Resources: []*ConfigResources{},
3242
},
43+
Rules: []*ConfigRule{},
3344
}
3445
}
3546

@@ -38,14 +49,16 @@ func (c *Config) Validate() error {
3849
return err
3950
}
4051

52+
for _, rule := range c.Rules {
53+
if err := rule.Validate(); err != nil {
54+
return err
55+
}
56+
}
57+
4158
return nil
4259
}
4360

4461
func (c *ConfigTtl) Validate() error {
45-
if c.Label == "" && c.Annotation == "" {
46-
return errors.New("label or annotation is required")
47-
}
48-
4962
if c.Label != "" {
5063
if strings.Contains(c.Label, " ") {
5164
return errors.New("label must not contain spaces")
@@ -55,6 +68,18 @@ func (c *ConfigTtl) Validate() error {
5568
return nil
5669
}
5770

71+
func (c *ConfigRule) Validate() error {
72+
if c.Id == "" {
73+
return errors.New("rules requires an id")
74+
}
75+
76+
if len(c.Resources) == 0 {
77+
return errors.New("rules requires at least one resource")
78+
}
79+
80+
return nil
81+
}
82+
5883
func (c *ConfigResources) String() string {
5984
return c.AsGVR().String()
6085
}
@@ -66,3 +91,7 @@ func (c *ConfigResources) AsGVR() schema.GroupVersionResource {
6691
Resource: c.Kind,
6792
}
6893
}
94+
95+
func (c *ConfigRule) String() string {
96+
return c.Id
97+
}

kube_janitor/kube.go

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,100 @@ package kube_janitor
22

33
import (
44
"context"
5+
"fmt"
6+
"strings"
57

8+
corev1 "k8s.io/api/core/v1"
69
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
710
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
811
"k8s.io/apimachinery/pkg/runtime/schema"
912
)
1013

1114
const (
1215
KubeListLimit = 100
16+
17+
KubeNoNamespace = ""
18+
19+
KubeSelectorError = "<error>"
20+
KubeSelectorNone = "<none>"
1321
)
1422

15-
func (j *Janitor) kubeEachResource(ctx context.Context, gvr schema.GroupVersionResource, labelSelector string, callback func(unstructured unstructured.Unstructured) error) error {
23+
func (j *Janitor) kubeBuildLabelSelector(selector *metav1.LabelSelector) (string, error) {
24+
// no selector
25+
if selector == nil {
26+
return "", nil
27+
}
28+
29+
compiledSelector := metav1.FormatLabelSelector(selector)
30+
if strings.EqualFold(compiledSelector, KubeSelectorError) {
31+
return "", fmt.Errorf(`unable to compile Kubernetes selector for resource: %v`, selector)
32+
}
33+
34+
if !strings.EqualFold(compiledSelector, KubeSelectorNone) {
35+
return compiledSelector, nil
36+
}
37+
38+
return "", nil
39+
}
40+
41+
func (j *Janitor) kubeEachNamespace(ctx context.Context, selector *metav1.LabelSelector, callback func(namespace corev1.Namespace) error) error {
42+
labelSelector, err := j.kubeBuildLabelSelector(selector)
43+
if err != nil {
44+
return err
45+
}
46+
47+
listOpts := metav1.ListOptions{
48+
Limit: KubeListLimit,
49+
LabelSelector: labelSelector,
50+
}
51+
for {
52+
result, err := j.kubeClient.CoreV1().Namespaces().List(ctx, listOpts)
53+
if err != nil {
54+
return err
55+
}
56+
57+
for _, item := range result.Items {
58+
err := callback(item)
59+
if err != nil {
60+
return err
61+
}
62+
}
63+
64+
if result.GetContinue() != "" {
65+
listOpts.Continue = result.GetContinue()
66+
continue
67+
}
68+
69+
break
70+
}
71+
72+
return nil
73+
}
74+
75+
func (j *Janitor) kubeEachResource(ctx context.Context, gvr schema.GroupVersionResource, namespace string, selector *metav1.LabelSelector, callback func(unstructured unstructured.Unstructured) error) error {
76+
labelSelector, err := j.kubeBuildLabelSelector(selector)
77+
if err != nil {
78+
return err
79+
}
80+
1681
listOpts := metav1.ListOptions{
1782
Limit: KubeListLimit,
1883
LabelSelector: labelSelector,
1984
}
2085
for {
21-
result, err := j.dynClient.Resource(gvr).List(ctx, listOpts)
86+
var (
87+
result *unstructured.UnstructuredList
88+
err error
89+
)
90+
91+
if namespace != KubeNoNamespace {
92+
// get by namespace
93+
result, err = j.dynClient.Resource(gvr).Namespace(namespace).List(ctx, listOpts)
94+
} else {
95+
// get all
96+
result, err = j.dynClient.Resource(gvr).List(ctx, listOpts)
97+
}
98+
2299
if err != nil {
23100
return err
24101
}

kube_janitor/manager.go

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
yaml "github.com/goccy/go-yaml"
1212
"github.com/webdevops/go-common/log/slogger"
1313
"k8s.io/client-go/dynamic"
14+
"k8s.io/client-go/kubernetes"
1415
"k8s.io/client-go/rest"
1516
"k8s.io/client-go/tools/clientcmd"
1617
kubelog "sigs.k8s.io/controller-runtime/pkg/log"
@@ -22,7 +23,8 @@ type (
2223

2324
config *Config
2425

25-
dynClient *dynamic.DynamicClient
26+
kubeClient *kubernetes.Clientset
27+
dynClient *dynamic.DynamicClient
2628

2729
logger *slogger.Logger
2830

@@ -64,6 +66,11 @@ func (j *Janitor) connect() {
6466
}
6567
}
6668

69+
j.kubeClient, err = kubernetes.NewForConfig(config)
70+
if err != nil {
71+
panic(err.Error())
72+
}
73+
6774
j.dynClient, err = dynamic.NewForConfig(config)
6875
if err != nil {
6976
panic(err)
@@ -140,9 +147,22 @@ func (j *Janitor) Start(interval time.Duration) {
140147

141148
func (j *Janitor) Run() error {
142149
j.connect()
150+
ctx := context.Background()
151+
152+
if j.config.Ttl.Label != "" || j.config.Ttl.Annotation != "" {
153+
if err := j.runTtlResources(ctx); err != nil {
154+
return err
155+
}
156+
} else {
157+
j.logger.Debug("skipping TTL run, no label or annotation defined")
158+
}
143159

144-
if err := j.runTtlResources(); err != nil {
145-
return err
160+
if len(j.config.Rules) > 0 {
161+
if err := j.runRules(ctx); err != nil {
162+
return err
163+
}
164+
} else {
165+
j.logger.Debug("skipping rules run, no rules defined")
146166
}
147167

148168
return nil

0 commit comments

Comments
 (0)