Skip to content

Commit 746750c

Browse files
committed
allow selection of resources with wildcards
Signed-off-by: Markus Blaschke <[email protected]>
1 parent 4ed29b4 commit 746750c

File tree

9 files changed

+147
-17
lines changed

9 files changed

+147
-17
lines changed

example.yaml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ ttl:
88

99
resources:
1010
# definition of resources by group, version, kind (GVR)
11-
- {group: "", version: v1, kind: configmaps}
11+
# a wildcard ("*") will try to match as many possible resources
12+
# from the serverside GVK list, be careful with it!
13+
- {group: "", version: "v1", kind: "pods"}
1214
- {group: "", version: v1, kind: secrets}
13-
- {group: apps, version: v1, kind: deployments}
15+
- {group: apps, version: "*", kind: "*"}
1416

1517
# static rules (fixed TTLs without using ttl definition in labels/annotations)
1618
rules:
17-
# cleanup of compelted/failed pods which are not yet removed by the apiserver
19+
# cleanup of completed/failed pods which are not yet removed by the Kubernetes control plane
1820
- id: CleanupCompletedPods
1921
resources:
2022
- group: ""
@@ -56,4 +58,4 @@ rules:
5658
matchLabels:
5759
kubernetes.io/metadata.name: default
5860

59-
ttl: 1m
61+
ttl: 1h # resources expires 1 hour after creation

kube_janitor/config.go

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package kube_janitor
22

33
import (
4+
"bytes"
5+
"encoding/gob"
46
"errors"
57
"strings"
68

@@ -15,11 +17,13 @@ type (
1517
}
1618

1719
ConfigTtl struct {
18-
Annotation string `json:"annotation"`
19-
Label string `json:"label"`
20-
Resources []*ConfigResource `json:"resources"`
20+
Annotation string `json:"annotation"`
21+
Label string `json:"label"`
22+
Resources ConfigResourceList `json:"resources"`
2123
}
2224

25+
ConfigResourceList []*ConfigResource
26+
2327
ConfigResource struct {
2428
Group string `json:"group"`
2529
Version string `json:"version"`
@@ -31,7 +35,7 @@ type (
3135

3236
ConfigRule struct {
3337
Id string `json:"id"`
34-
Resources []*ConfigResource `json:"resources"`
38+
Resources ConfigResourceList `json:"resources"`
3539
NamespaceSelector *metav1.LabelSelector `json:"namespaceSelector"`
3640
Ttl string `json:"ttl"`
3741
}
@@ -82,6 +86,20 @@ func (c *ConfigRule) Validate() error {
8286
return nil
8387
}
8488

89+
func (c *ConfigResource) Clone() *ConfigResource {
90+
ret := ConfigResource{}
91+
buf := bytes.Buffer{}
92+
err := gob.NewEncoder(&buf).Encode(c)
93+
if err != nil {
94+
panic(err)
95+
}
96+
err = gob.NewDecoder(&buf).Decode(&ret)
97+
if err != nil {
98+
panic(err)
99+
}
100+
return &ret
101+
}
102+
85103
func (c *ConfigResource) String() string {
86104
return c.AsGVR().String()
87105
}

kube_janitor/init.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@ import (
55
)
66

77
func init() {
8-
yaml.RegisterCustomUnmarshalerContext(UmarshallJmesPath)
8+
yaml.RegisterCustomUnmarshalerContext(UnmarshallJmesPath)
99
}

kube_janitor/jmespath.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ func (path *JmesPath) IsEmpty() bool {
2323
return path == nil || path.compiledPath == nil
2424
}
2525

26-
func UmarshallJmesPath(ctx context.Context, path *JmesPath, data []byte) error {
26+
func UnmarshallJmesPath(ctx context.Context, path *JmesPath, data []byte) error {
2727
var valString string
2828

2929
err := yaml.UnmarshalContext(ctx, data, &valString, yaml.Strict())
@@ -116,7 +116,6 @@ func (j *Janitor) checkResourceIsSkippedFromJmesPath(resource unstructured.Unstr
116116

117117
func (j *Janitor) parseResourceTimestampFromJmesPath(resource unstructured.Unstructured, jmesPath *JmesPath) (*time.Time, error) {
118118
result, err := j.fetchResourceValueByFromJmesPath(resource, jmesPath)
119-
fmt.Println(jmesPath.Path, result, err)
120119
if err != nil {
121120
return nil, err
122121
}

kube_janitor/kube.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package kube_janitor
33
import (
44
"context"
55
"fmt"
6+
"slices"
67
"strings"
78
"time"
89

@@ -21,6 +22,93 @@ const (
2122
KubeSelectorNone = "<none>"
2223
)
2324

25+
type (
26+
KubeServerGvrList []metav1.GroupVersionKind
27+
)
28+
29+
func (j *Janitor) kubeDiscoverGVKs() (KubeServerGvrList, error) {
30+
cacheKey := "kube.servergroups"
31+
32+
// from cache
33+
if val, ok := j.cache.Get(cacheKey); ok {
34+
if v, ok := val.(KubeServerGvrList); ok {
35+
return v, nil
36+
}
37+
}
38+
39+
ret := KubeServerGvrList{}
40+
41+
groupsResult, resourcesResult, err := j.kubeClient.Discovery().ServerGroupsAndResources()
42+
if err != nil {
43+
return nil, err
44+
}
45+
46+
// build GVK list
47+
for _, serverGroup := range groupsResult {
48+
for _, resourceGroup := range resourcesResult {
49+
if resourceGroup.GroupVersion == serverGroup.PreferredVersion.GroupVersion {
50+
for _, resource := range resourceGroup.APIResources {
51+
if slices.Contains([]string(resource.Verbs), "list") && slices.Contains([]string(resource.Verbs), "delete") {
52+
ret = append(ret, metav1.GroupVersionKind{
53+
Group: serverGroup.Name,
54+
Version: serverGroup.PreferredVersion.Version,
55+
Kind: resource.Name,
56+
})
57+
}
58+
}
59+
}
60+
}
61+
}
62+
63+
j.cache.SetDefault(cacheKey, ret)
64+
65+
return ret, nil
66+
}
67+
68+
func (j *Janitor) kubeLookupGvrs(list ConfigResourceList) (ConfigResourceList, error) {
69+
var (
70+
gvrList KubeServerGvrList
71+
err error
72+
)
73+
ret := []*ConfigResource{}
74+
75+
for _, resource := range list {
76+
if resource.Group == "*" || resource.Version == "*" || resource.Kind == "*" {
77+
// lookup possible types
78+
if gvrList == nil {
79+
gvrList, err = j.kubeDiscoverGVKs()
80+
if err != nil {
81+
return nil, err
82+
}
83+
}
84+
85+
for _, row := range gvrList {
86+
if resource.Group != "*" && !strings.EqualFold(resource.Group, row.Group) {
87+
continue
88+
}
89+
if resource.Version != "*" && !strings.EqualFold(resource.Version, row.Version) {
90+
continue
91+
}
92+
if resource.Kind != "*" && !strings.EqualFold(resource.Kind, row.Kind) {
93+
continue
94+
}
95+
96+
clone := resource.Clone()
97+
clone.Group = row.Group
98+
clone.Version = row.Version
99+
clone.Kind = row.Kind
100+
101+
ret = append(ret, clone)
102+
}
103+
} else {
104+
// no lookup needed
105+
ret = append(ret, resource)
106+
}
107+
}
108+
109+
return ret, nil
110+
}
111+
24112
func (j *Janitor) kubeBuildLabelSelector(selector *metav1.LabelSelector) (string, error) {
25113
// no selector
26114
if selector == nil {

kube_janitor/manager.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
"github.com/go-logr/logr"
1010
yaml "github.com/goccy/go-yaml"
11+
"github.com/patrickmn/go-cache"
1112
"github.com/webdevops/go-common/log/slogger"
1213
"k8s.io/client-go/dynamic"
1314
"k8s.io/client-go/kubernetes"
@@ -22,6 +23,8 @@ type (
2223

2324
config *Config
2425

26+
cache *cache.Cache
27+
2528
kubeClient *kubernetes.Clientset
2629
dynClient *dynamic.DynamicClient
2730

@@ -43,6 +46,7 @@ func New() *Janitor {
4346

4447
func (j *Janitor) init() {
4548
j.setupMetrics()
49+
j.cache = cache.New(1*time.Hour, 5*time.Minute)
4650
j.kubePageLimit = KubeDefaultListLimit
4751
}
4852

@@ -97,6 +101,8 @@ func (j *Janitor) GetConfigFromFile(path string) *Janitor {
97101
j.config = NewConfig()
98102
}
99103

104+
parserCtx := context.Background()
105+
100106
logger := j.logger.With(slog.String("path", path))
101107

102108
logger.Info("reading configuration from file")
@@ -108,7 +114,7 @@ func (j *Janitor) GetConfigFromFile(path string) *Janitor {
108114
}
109115

110116
logger.Info("parsing configuration")
111-
err = yaml.UnmarshalWithOptions(data, j.config, yaml.Strict(), yaml.UseJSONUnmarshaler())
117+
err = yaml.UnmarshalContext(parserCtx, data, j.config, yaml.Strict(), yaml.UseJSONUnmarshaler())
112118
if err != nil {
113119
logger.Fatal("failed to parse config file")
114120
}
@@ -135,6 +141,11 @@ func (j *Janitor) SetKubePageSize(val int64) *Janitor {
135141
return j
136142
}
137143

144+
func (j *Janitor) Connect() *Janitor {
145+
j.connect()
146+
return j
147+
}
148+
138149
func (j *Janitor) Start(interval time.Duration) *Janitor {
139150
go func() {
140151
// wait for settle down
@@ -158,7 +169,6 @@ func (j *Janitor) Start(interval time.Duration) *Janitor {
158169
}
159170

160171
func (j *Janitor) Run() error {
161-
j.connect()
162172
ctx := context.Background()
163173

164174
if j.config.Ttl.Label != "" || j.config.Ttl.Annotation != "" {

kube_janitor/task.rules.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,17 @@ func (j *Janitor) runRules(ctx context.Context) error {
1919
)
2020
ruleLogger.Info("running rule")
2121

22-
err := j.kubeEachNamespace(ctx, rule.NamespaceSelector, func(namespace corev1.Namespace) error {
22+
resourceList, err := j.kubeLookupGvrs(rule.Resources)
23+
if err != nil {
24+
return err
25+
}
26+
27+
// TODO: check if namespace selector is empty and if, use the cluster list instead
28+
29+
err = j.kubeEachNamespace(ctx, rule.NamespaceSelector, func(namespace corev1.Namespace) error {
2330
namespaceLogger := ruleLogger
2431

25-
for _, resourceType := range rule.Resources {
32+
for _, resourceType := range resourceList {
2633
gvkLogger := namespaceLogger.With(slog.Any("gvk", resourceType))
2734
err := j.kubeEachResource(ctx, resourceType.AsGVR(), namespace.GetName(), resourceType.Selector, func(resource unstructured.Unstructured) error {
2835
resourceLogger := gvkLogger.With(

kube_janitor/task.ttl.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,14 @@ import (
1313
func (j *Janitor) runTtlResources(ctx context.Context) error {
1414
metricResourceTtl := prometheusCommon.NewMetricsList()
1515

16-
for _, resourceType := range j.config.Ttl.Resources {
16+
resourceList, err := j.kubeLookupGvrs(j.config.Ttl.Resources)
17+
if err != nil {
18+
return err
19+
}
20+
21+
for _, resourceType := range resourceList {
1722
gvkLogger := j.logger.With(slog.Any("gvk", resourceType))
18-
gvkLogger.Debug("checking resources")
23+
gvkLogger.Info("checking resources")
1924

2025
err := j.kubeEachResource(ctx, resourceType.AsGVR(), KubeNoNamespace, resourceType.Selector, func(resource unstructured.Unstructured) error {
2126
var ttlValue string

main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ func main() {
4242
janitor := kube_janitor.New()
4343
janitor.SetKubeconfig(Opts.Kubernetes.Config).
4444
SetLogger(logger).
45+
Connect().
4546
SetKubePageSize(Opts.Kubernetes.ItemsPerPage).
4647
GetConfigFromFile(Opts.Janitor.Config).
4748
SetDryRun(Opts.Janitor.DryRun)

0 commit comments

Comments
 (0)