Skip to content

Commit 1436aaf

Browse files
committed
refactoring
- add custom timestamp using jmes path - rename jamespath to filterPath - improve logging Signed-off-by: Markus Blaschke <[email protected]>
1 parent e204372 commit 1436aaf

File tree

13 files changed

+260
-83
lines changed

13 files changed

+260
-83
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77

88
Kubernetes janitor which deletes resources by TTL annotations/labels and static rules written in Golang
99

10+
By default the janitor uses the creation timestamp from every resource but can also use a custom timestamp by using a JMES path with `timestampPath`.
11+
12+
1013
## Configuration
1114

1215
see [`example.yaml`](example.yaml) for example configurations
@@ -26,6 +29,7 @@ Application Options:
2629
--dry-run Dry run (no delete) [$JANITOR_DRYRUN]
2730
--once Run once and exit [$JANITOR_ONCE]
2831
--kubeconfig= Kuberentes config path (should be empty if in-cluster) [$KUBECONFIG]
32+
--kube.itemsperpage= Defines how many items per page janitor should process (default: 100) [$KUBE_ITEMSPERPAGE]
2933
--server.bind= Server address (default: :8080) [$SERVER_BIND]
3034
--server.timeout.read= Server read timeout (default: 5s) [$SERVER_TIMEOUT_READ]
3135
--server.timeout.write= Server write timeout (default: 10s) [$SERVER_TIMEOUT_WRITE]

config/opts.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ type (
2525

2626
// kubernetes settings
2727
Kubernetes struct {
28-
Config string `long:"kubeconfig" env:"KUBECONFIG" description:"Kuberentes config path (should be empty if in-cluster)"`
28+
Config string `long:"kubeconfig" env:"KUBECONFIG" description:"Kuberentes config path (should be empty if in-cluster)"`
29+
ItemsPerPage int64 `long:"kube.itemsperpage" env:"KUBE_ITEMSPERPAGE" description:"Defines how many items per page janitor should process" default:"100"`
2930
}
3031

3132
// general options

example.yaml

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,36 @@ ttl:
1414

1515
# static rules (fixed TTLs without using ttl definition in labels/annotations)
1616
rules:
17-
- id: default
17+
# cleanup of compelted/failed pods which are not yet removed by the apiserver
18+
- id: CleanupCompletedPods
19+
resources:
20+
- group: ""
21+
version: v1
22+
kind: pods
23+
24+
## get timestampo from containerStatuses if pod is terminated
25+
timestampPath: |-
26+
max(containerStatuses[*].state.terminated.finishedAt)
27+
28+
## filter only pods which are in phase "Failed" or "Succeeded"
29+
filterPath: |-
30+
phase == 'Failed' || phase == 'Succeeded'
31+
32+
selector: {}
33+
34+
## run on all namespaces
35+
namespaceSelector: {}
36+
37+
## ttl for rule (based on terminated.finishedAt)
38+
ttl: 1h
39+
40+
- id: example
1841
resources:
1942
- group: ""
2043
version: v1
2144
kind: configmaps
2245
# JMESpath for additional selector, should return true if resource should be used for TTL checks, optional
23-
jmespath: |-
46+
filterPath: |-
2447
!(metadata.annotations."kubernetes.io/description")
2548
2649
# kubernetes selector (matchLabels, matchExpressions), optional

kube_janitor/config.go

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,8 @@ package kube_janitor
22

33
import (
44
"errors"
5-
"fmt"
65
"strings"
76

8-
jmespath "github.com/jmespath-community/go-jmespath"
97
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
108
"k8s.io/apimachinery/pkg/runtime/schema"
119
)
@@ -23,12 +21,12 @@ type (
2321
}
2422

2523
ConfigResource struct {
26-
Group string `json:"group"`
27-
Version string `json:"version"`
28-
Kind string `json:"kind"`
29-
Selector *metav1.LabelSelector `json:"selector"`
30-
JmesPath string `json:"jmespath"`
31-
jmesPathcompiled jmespath.JMESPath
24+
Group string `json:"group"`
25+
Version string `json:"version"`
26+
Kind string `json:"kind"`
27+
Selector *metav1.LabelSelector `json:"selector"`
28+
TimestampPath *JmesPath `json:"timestampPath"`
29+
FilterPath *JmesPath `json:"filterPath"`
3230
}
3331

3432
ConfigRule struct {
@@ -84,19 +82,6 @@ func (c *ConfigRule) Validate() error {
8482
return nil
8583
}
8684

87-
func (c *ConfigResource) CompiledJmesPath() jmespath.JMESPath {
88-
if c.jmesPathcompiled == nil {
89-
compiledPath, err := jmespath.Compile(c.JmesPath)
90-
if err != nil {
91-
panic(fmt.Errorf(`failed to compile jmespath "%s": %w`, c.JmesPath, err))
92-
}
93-
94-
c.jmesPathcompiled = compiledPath
95-
}
96-
97-
return c.jmesPathcompiled
98-
}
99-
10085
func (c *ConfigResource) String() string {
10186
return c.AsGVR().String()
10287
}

kube_janitor/expiry.go

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,27 @@ var (
3232
}
3333
)
3434

35+
func (j *Janitor) parseTimestamp(val string) *time.Time {
36+
val = strings.TrimSpace(val)
37+
if val == "" || val == "0" {
38+
return nil
39+
}
40+
41+
// parse as unix timestamp
42+
if unixTimestamp, err := strconv.ParseInt(val, 10, 64); err == nil && unixTimestamp > 0 {
43+
timestamp := time.Unix(unixTimestamp, 0)
44+
return &timestamp
45+
}
46+
47+
for _, timeFormat := range janitorTimeFormats {
48+
if timestamp, parseErr := time.Parse(timeFormat, val); parseErr == nil && timestamp.Unix() > 0 {
49+
return &timestamp
50+
}
51+
}
52+
53+
return nil
54+
}
55+
3556
func (j *Janitor) checkExpiryDate(createdAt time.Time, expiry string) (parsedTime *time.Time, expired bool, err error) {
3657
expired = false
3758

@@ -41,21 +62,21 @@ func (j *Janitor) checkExpiryDate(createdAt time.Time, expiry string) (parsedTim
4162
return
4263
}
4364

44-
// parse as unix timestamp
45-
if unixTimestamp, err := strconv.ParseInt(expiry, 10, 64); err == nil {
65+
// first: parse as unix timestamp
66+
if unixTimestamp, err := strconv.ParseInt(expiry, 10, 64); err == nil && unixTimestamp > 0 {
4667
expiryTime := time.Unix(unixTimestamp, 0)
4768
parsedTime = &expiryTime
4869
}
4970

50-
// parse duration
71+
// second: parse duration
5172
if !createdAt.IsZero() {
5273
if expiryDuration, err := duration.Parse(expiry); err == nil && expiryDuration.Seconds() > 1 {
5374
expiryTime := createdAt.Add(expiryDuration)
5475
parsedTime = &expiryTime
5576
}
5677
}
5778

58-
// parse time
79+
// third: parse time
5980
if parsedTime == nil {
6081
for _, timeFormat := range janitorTimeFormats {
6182
if parseVal, parseErr := time.Parse(timeFormat, expiry); parseErr == nil && parseVal.Unix() > 0 {

kube_janitor/init.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package kube_janitor
2+
3+
import (
4+
"github.com/goccy/go-yaml"
5+
)
6+
7+
func init() {
8+
yaml.RegisterCustomUnmarshalerContext(UmarshallJmesPath)
9+
}

kube_janitor/jmespath.go

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package kube_janitor
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"strings"
8+
"time"
9+
10+
"github.com/goccy/go-yaml"
11+
"github.com/jmespath-community/go-jmespath"
12+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
13+
)
14+
15+
type (
16+
JmesPath struct {
17+
Path string
18+
compiledPath jmespath.JMESPath
19+
}
20+
)
21+
22+
func (path *JmesPath) IsEmpty() bool {
23+
return path == nil || path.compiledPath == nil
24+
}
25+
26+
func UmarshallJmesPath(ctx context.Context, path *JmesPath, data []byte) error {
27+
var valString string
28+
29+
err := yaml.UnmarshalContext(ctx, data, &valString, yaml.Strict())
30+
if err != nil {
31+
return fmt.Errorf(`failed to parse jmespath as string: %w`, err)
32+
}
33+
34+
valString = strings.TrimSpace(valString)
35+
36+
if valString != "" {
37+
compiledPath, err := jmespath.Compile(valString)
38+
if err != nil {
39+
return fmt.Errorf(`failed to compile jmespath "%s": %w`, valString, err)
40+
}
41+
42+
path.Path = valString
43+
path.compiledPath = compiledPath
44+
}
45+
46+
return nil
47+
}
48+
49+
//
50+
// func (path *JmesPath) UnmarshalJSON(data []byte) error {
51+
// var valString string
52+
//
53+
// err := json.Unmarshal(data, &valString)
54+
// if err != nil {
55+
// return fmt.Errorf(`failed to parse jmespath as string: %w`, err)
56+
// }
57+
//
58+
// valString = strings.TrimSpace(valString)
59+
//
60+
// if valString != "" {
61+
// compiledPath, err := jmespath.Compile(valString)
62+
// if err != nil {
63+
// return fmt.Errorf(`failed to compile jmespath "%s": %w`, valString, err)
64+
// }
65+
//
66+
// path.Path = valString
67+
// path.compiledPath = compiledPath
68+
// }
69+
//
70+
// return nil
71+
// }
72+
73+
func (j *Janitor) fetchResourceValueByFromJmesPath(resource unstructured.Unstructured, jmesPath *JmesPath) (interface{}, error) {
74+
resourceRaw, err := resource.MarshalJSON()
75+
if err != nil {
76+
return true, err
77+
}
78+
79+
var data any
80+
err = json.Unmarshal(resourceRaw, &data)
81+
if err != nil {
82+
return true, err
83+
}
84+
85+
// check if resource is valid by JMES path
86+
result, err := jmesPath.compiledPath.Search(data)
87+
if err != nil {
88+
return true, err
89+
}
90+
91+
return result, nil
92+
}
93+
94+
func (j *Janitor) checkResourceIsSkippedFromJmesPath(resource unstructured.Unstructured, jmesPath *JmesPath) (bool, error) {
95+
result, err := j.fetchResourceValueByFromJmesPath(resource, jmesPath)
96+
if err != nil {
97+
return true, err
98+
}
99+
100+
switch v := result.(type) {
101+
case string:
102+
// skip if string is empty
103+
if len(v) == 0 {
104+
return true, nil
105+
}
106+
case bool:
107+
// skip if false (not selected)
108+
return !v, nil
109+
case nil:
110+
// nil? jmes path didn't find anything? better skip the resource
111+
return true, nil
112+
}
113+
114+
return false, nil
115+
}
116+
117+
func (j *Janitor) parseResourceTimestampFromJmesPath(resource unstructured.Unstructured, jmesPath *JmesPath) (*time.Time, error) {
118+
result, err := j.fetchResourceValueByFromJmesPath(resource, jmesPath)
119+
fmt.Println(jmesPath.Path, result, err)
120+
if err != nil {
121+
return nil, err
122+
}
123+
124+
switch v := result.(type) {
125+
case string:
126+
// skip if string is empty
127+
if timestamp := j.parseTimestamp(v); timestamp != nil {
128+
return timestamp, nil
129+
}
130+
}
131+
132+
return nil, nil
133+
}

kube_janitor/kube.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import (
1313
)
1414

1515
const (
16-
KubeListLimit = 100
16+
KubeDefaultListLimit = 100
1717

1818
KubeNoNamespace = ""
1919

@@ -46,7 +46,7 @@ func (j *Janitor) kubeEachNamespace(ctx context.Context, selector *metav1.LabelS
4646
}
4747

4848
listOpts := metav1.ListOptions{
49-
Limit: KubeListLimit,
49+
Limit: j.kubePageLimit,
5050
LabelSelector: labelSelector,
5151
}
5252
for {
@@ -80,7 +80,7 @@ func (j *Janitor) kubeEachResource(ctx context.Context, gvr schema.GroupVersionR
8080
}
8181

8282
listOpts := metav1.ListOptions{
83-
Limit: KubeListLimit,
83+
Limit: j.kubePageLimit,
8484
LabelSelector: labelSelector,
8585
}
8686
for {

0 commit comments

Comments
 (0)