Skip to content

Commit 31184a4

Browse files
committed
add jmespath support
Signed-off-by: Markus Blaschke <[email protected]>
1 parent 33d2f82 commit 31184a4

File tree

8 files changed

+102
-22
lines changed

8 files changed

+102
-22
lines changed

README.md

Lines changed: 5 additions & 4 deletions
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 annotation or label written in Golang
8+
Kubernetes janitor which deletes resources by TTL annotations/labels and static rules written in Golang
99

1010
## Configuration
1111

@@ -61,6 +61,7 @@ Supported relative timestamps ([`time.Duration`](https://pkg.go.dev/time) and [`
6161

6262
## Metrics
6363

64-
| Metric | Description |
65-
|--------------------------------------------------|--------------------------------------------------------------------|
66-
| `kube_janitor_resource_expiry_timestamp_seconds` | Expiry date (unix timestamp) for every resource which was detected |
64+
| Metric | Description |
65+
|-------------------------------------------------------|-----------------------------------------------------------------------------------------------------|
66+
| `kube_janitor_resource_ttl_expiry_timestamp_seconds` | Expiry date (unix timestamp) for every resource which was detected matching the TTL expiry |
67+
| `kube_janitor_resource_rule_expiry_timestamp_seconds` | Expiry date (unix timestamp) for every resource which was detected matching the static expiry rules |

example.yaml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,18 @@ rules:
1919
- group: ""
2020
version: v1
2121
kind: configmaps
22+
# JMESpath for additional selector, should return true if resource should be used for TTL checks, optional
23+
jmespath: |-
24+
!(metadata.annotations."kubernetes.io/description")
25+
26+
# kubernetes selector (matchLabels, matchExpressions), optional
2227
selector:
2328
matchLabels:
2429
foo: bar
30+
31+
# kubernetes selector (matchLabels, matchExpressions), optional
2532
namespaceSelector:
2633
matchLabels:
2734
kubernetes.io/metadata.name: default
28-
ttl: 1d
35+
36+
ttl: 1m

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ require (
77
github.com/go-logr/logr v1.4.3
88
github.com/goccy/go-yaml v1.19.0
99
github.com/jessevdk/go-flags v1.6.1
10+
github.com/jmespath-community/go-jmespath v1.1.1
1011
github.com/prometheus/client_golang v1.23.2
1112
github.com/webdevops/go-common v0.0.0-20251205143010-9409efa47a03
1213
k8s.io/api v0.34.3
@@ -57,6 +58,7 @@ require (
5758
go.uber.org/automaxprocs v1.6.0 // indirect
5859
go.yaml.in/yaml/v2 v2.4.3 // indirect
5960
go.yaml.in/yaml/v3 v3.0.4 // indirect
61+
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
6062
golang.org/x/net v0.48.0 // indirect
6163
golang.org/x/oauth2 v0.34.0 // indirect
6264
golang.org/x/sys v0.39.0 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
6969
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
7070
github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4=
7171
github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc=
72+
github.com/jmespath-community/go-jmespath v1.1.1 h1:bFikPhsi/FdmlZhVgSCd2jj1e7G/rw+zyQfyg5UF+L4=
73+
github.com/jmespath-community/go-jmespath v1.1.1/go.mod h1:4gOyFJsR/Gk+05RgTKYrifT7tBPWD8Lubtb5jRrfy9I=
7274
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
7375
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
7476
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
@@ -140,6 +142,8 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
140142
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
141143
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
142144
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
145+
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
146+
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
143147
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
144148
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
145149
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=

kube_janitor/config.go

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

33
import (
44
"errors"
5+
"fmt"
56
"strings"
67

8+
jmespath "github.com/jmespath-community/go-jmespath"
79
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
810
"k8s.io/apimachinery/pkg/runtime/schema"
911
)
@@ -15,21 +17,23 @@ type (
1517
}
1618

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

23-
ConfigResources struct {
24-
Group string `json:"group"`
25-
Version string `json:"version"`
26-
Kind string `json:"kind"`
27-
Selector *metav1.LabelSelector `json:"selector"`
25+
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
2832
}
2933

3034
ConfigRule struct {
3135
Id string `json:"id"`
32-
Resources []*ConfigResources `json:"resources"`
36+
Resources []*ConfigResource `json:"resources"`
3337
NamespaceSelector *metav1.LabelSelector `json:"namespaceSelector"`
3438
Ttl string `json:"ttl"`
3539
}
@@ -38,7 +42,7 @@ type (
3842
func NewConfig() *Config {
3943
return &Config{
4044
Ttl: &ConfigTtl{
41-
Resources: []*ConfigResources{},
45+
Resources: []*ConfigResource{},
4246
},
4347
Rules: []*ConfigRule{},
4448
}
@@ -80,11 +84,24 @@ func (c *ConfigRule) Validate() error {
8084
return nil
8185
}
8286

83-
func (c *ConfigResources) String() string {
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+
100+
func (c *ConfigResource) String() string {
84101
return c.AsGVR().String()
85102
}
86103

87-
func (c *ConfigResources) AsGVR() schema.GroupVersionResource {
104+
func (c *ConfigResource) AsGVR() schema.GroupVersionResource {
88105
return schema.GroupVersionResource{
89106
Group: c.Group,
90107
Version: c.Version,

kube_janitor/task.common.go

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

33
import (
44
"context"
5+
"encoding/json"
56
"log/slog"
67

8+
jmespath "github.com/jmespath-community/go-jmespath"
79
"github.com/prometheus/client_golang/prometheus"
810
"github.com/webdevops/go-common/log/slogger"
911
prometheusCommon "github.com/webdevops/go-common/prometheus"
1012
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1113
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
12-
"k8s.io/apimachinery/pkg/runtime/schema"
1314
)
1415

15-
func (j *Janitor) checkResourceTtlAndTriggerDeleteIfExpired(ctx context.Context, logger *slogger.Logger, gvr schema.GroupVersionResource, resource unstructured.Unstructured, ttlValue string, metricResourceTtl *prometheusCommon.MetricList, labels prometheus.Labels) error {
16+
func (j *Janitor) checkResourceIsSkippedByJmesPath(resource unstructured.Unstructured, jmesPath jmespath.JMESPath) (bool, error) {
17+
18+
resourceRaw, err := resource.MarshalJSON()
19+
if err != nil {
20+
return true, err
21+
}
22+
var data any
23+
err = json.Unmarshal(resourceRaw, &data)
24+
if err != nil {
25+
return true, err
26+
}
27+
28+
// check if resource is valid by JMES path
29+
result, err := jmesPath.Search(data)
30+
if err != nil {
31+
return true, err
32+
}
33+
34+
switch v := result.(type) {
35+
case string:
36+
// skip if string is empty
37+
if len(v) == 0 {
38+
return true, nil
39+
}
40+
case bool:
41+
// skip if false (not selected)
42+
return !v, nil
43+
case nil:
44+
// nil? jmes path didn't find anything? better skip the resource
45+
return true, nil
46+
}
47+
48+
return false, nil
49+
}
50+
51+
func (j *Janitor) checkResourceTtlAndTriggerDeleteIfExpired(ctx context.Context, logger *slogger.Logger, resourceConfig *ConfigResource, resource unstructured.Unstructured, ttlValue string, metricResourceTtl *prometheusCommon.MetricList, labels prometheus.Labels) error {
1652
resourceLogger := logger.With(
1753
slog.String("namespace", resource.GetNamespace()),
1854
slog.String("resource", resource.GetName()),
1955
slog.String("ttl", ttlValue),
2056
)
2157

58+
if resourceConfig.JmesPath != "" {
59+
skipped, err := j.checkResourceIsSkippedByJmesPath(resource, resourceConfig.CompiledJmesPath())
60+
if err != nil {
61+
return err
62+
}
63+
64+
if skipped {
65+
resourceLogger.Debug("resource skipped by JMES path")
66+
return nil
67+
}
68+
}
69+
2270
parsedDate, expired, err := j.checkExpiryDate(resource.GetCreationTimestamp().Time, ttlValue)
2371
if err != nil {
2472
resourceLogger.Error("unable to parse expiration date", slog.String("raw", ttlValue), slog.Any("error", err))
@@ -39,7 +87,7 @@ func (j *Janitor) checkResourceTtlAndTriggerDeleteIfExpired(ctx context.Context,
3987
resourceLogger.Info("resource is expired, would delete resource (DRY-RUN)", slog.Time("expirationDate", *parsedDate))
4088
} else {
4189
resourceLogger.Info("deleting expired resource", slog.Time("expirationDate", *parsedDate))
42-
err := j.dynClient.Resource(gvr).Namespace(resource.GetNamespace()).Delete(ctx, resource.GetName(), metav1.DeleteOptions{})
90+
err := j.dynClient.Resource(resourceConfig.AsGVR()).Namespace(resource.GetNamespace()).Delete(ctx, resource.GetName(), metav1.DeleteOptions{})
4391
if err != nil {
4492
return err
4593
}

kube_janitor/task.rules.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func (j *Janitor) runRules(ctx context.Context) error {
3434
return j.checkResourceTtlAndTriggerDeleteIfExpired(
3535
ctx,
3636
gvkLogger,
37-
resourceType.AsGVR(),
37+
resourceType,
3838
resource,
3939
rule.Ttl,
4040
metricResourceRule,

kube_janitor/task.ttl.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ func (j *Janitor) runTtlResources(ctx context.Context) error {
4040
return j.checkResourceTtlAndTriggerDeleteIfExpired(
4141
ctx,
4242
gvkLogger,
43-
resourceType.AsGVR(),
43+
resourceType,
4444
resource,
4545
ttlValue,
4646
metricResourceTtl,

0 commit comments

Comments
 (0)