Skip to content

Commit 85f7bae

Browse files
authored
Merge pull request #1 from cmaster11/freeform-rules
Freeform rules
2 parents 1c5b627 + ac63281 commit 85f7bae

File tree

17 files changed

+695
-113
lines changed

17 files changed

+695
-113
lines changed

.gitignore

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
1-
.idea
2-
config.yaml
1+
.idea

README.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# k8s-event-watcher
2+
3+
This library lets you watch for Kubernetes events, and triggers a callback whenever one matching the criteria is found.
4+
5+
An example of configuration is:
6+
7+
```yaml
8+
sinceNow: true
9+
filters:
10+
- rules:
11+
involvedObject.kind: Job
12+
involvedObject.name: "^*.fail"
13+
reason: BackoffLimitExceeded
14+
```
15+
16+
This would match a `Job`-failed `Event`:
17+
18+
```yaml
19+
apiVersion: v1
20+
kind: Event
21+
...
22+
involvedObject:
23+
apiVersion: batch/v1
24+
kind: Job
25+
name: job-fail
26+
...
27+
lastTimestamp: "2019-10-14T07:11:45Z"
28+
message: Job has reached the specified backoff limit
29+
metadata:
30+
...
31+
reason: BackoffLimitExceeded
32+
...
33+
type: Warning
34+
```
35+
36+
## Configuration
37+
38+
Configurable keys:
39+
40+
* `sinceNow`: if `true`, only processes events generated after the program starts.
41+
* `filters`: a list of `rules`. Each `rules` object is evaluated **independently**, in an `OR` fashion. Any event is
42+
evaluated on all sets of `rules`. The first matching filter will cause the trigger of the callback.
43+
* `filters.[*].rules`: a map of regular expressions. Each regular expression is evaluated against the provided object key.
44+
If **all** the regular expressions match, then the event will be sent to the callback.
45+
46+
## Local test
47+
48+
You can test the execution of this library by running the example program:
49+
50+
```bash
51+
go run ./example --kubeconfig PATH_TO_A_KUBECONFIG_FILE --config ./example/config.yaml
52+
```
53+
54+
While the example program is running, you can then start a failing job with:
55+
56+
```bash
57+
kubectl apply -f ./example/job-fail-k8s.yaml
58+
```
59+
60+
The example program will then pick up the failure and show the matching event.

config.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package k8seventwatcher
33
import (
44
"errors"
55
"gopkg.in/yaml.v2"
6-
"k8s.io/api/core/v1"
76
"log"
87
)
98

@@ -28,14 +27,18 @@ func (c *Config) Validate() error {
2827
return nil
2928
}
3029

31-
func (c *Config) MatchingEventFilter(event *v1.Event) *EventFilter {
30+
func (c *Config) MatchingEventFilter(event map[string]interface{}) (*EventFilter, error) {
3231
for _, filter := range c.Filters {
33-
if filter.Matches(event) {
34-
return filter
32+
matches, err := filter.Matches(event)
33+
if err != nil {
34+
return nil, errorf("error matching filter: %s", err)
35+
}
36+
if matches {
37+
return filter, nil
3538
}
3639
}
3740

38-
return nil
41+
return nil, nil
3942
}
4043

4144
func (c *Config) Dump() string {

config.yaml

Lines changed: 0 additions & 4 deletions
This file was deleted.

example/config.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
sinceNow: true
2+
filters:
3+
- rules:
4+
involvedObject.kind: Job
5+
involvedObject.name: "^*.fail"
6+
reason: BackoffLimitExceeded

example/job-fail-k8s.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
apiVersion: batch/v1
2+
kind: Job
3+
metadata:
4+
name: job-fail
5+
spec:
6+
backoffLimit: 0
7+
template:
8+
spec:
9+
containers:
10+
- command:
11+
- sh
12+
- exit
13+
- "1"
14+
image: alpine
15+
name: main
16+
restartPolicy: Never
17+
terminationGracePeriodSeconds: 30

example/main.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import (
1212
)
1313

1414
func main() {
15-
kubeConfigPath := flag.String("kubeconfig", "", "path of k8s k8sConfig file to use")
16-
configPath := flag.String("config", "config.yaml", "path of k8sConfig file to use")
15+
kubeConfigPath := flag.String("kubeconfig", "", "path of kubeconfig file to use")
16+
configPath := flag.String("config", "config.yaml", "path of event watcher config file to use")
1717

1818
flag.Parse()
1919

filter.go

Lines changed: 16 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -3,106 +3,44 @@ package k8seventwatcher
33
import (
44
"errors"
55
"fmt"
6+
"github.com/cmaster11/k8s-event-watcher/lookup"
67
"gopkg.in/yaml.v2"
7-
"k8s.io/api/core/v1"
88
"strings"
99
)
1010

1111
type EventFilter struct {
12-
ObjectNamespace *Regexp `yaml:"objectNamespace,omitempty"`
13-
ObjectKind *Regexp `yaml:"objectKind,omitempty"`
14-
ObjectName *Regexp `yaml:"objectName,omitempty"`
15-
EventType *Regexp `yaml:"eventType,omitempty"`
16-
EventReason *Regexp `yaml:"eventReason,omitempty"`
12+
Rules map[string]*Regexp `yaml:"rules"`
1713
}
1814

1915
func (f *EventFilter) Validate() error {
2016
// At least one filter must exist
21-
if f.ObjectNamespace != nil {
17+
if len(f.Rules) > 0 {
2218
return nil
2319
}
24-
if f.ObjectKind != nil {
25-
return nil
26-
}
27-
if f.ObjectName != nil {
28-
return nil
29-
}
30-
if f.EventType != nil {
31-
return nil
32-
}
33-
if f.EventReason != nil {
34-
return nil
35-
}
36-
return errors.New("no filter attributes provided")
20+
return errors.New("no rules provided")
3721
}
3822

39-
func (f *EventFilter) Matches(event *v1.Event) bool {
40-
if f.ObjectNamespace != nil {
41-
if !f.ObjectNamespace.MatchString(event.InvolvedObject.Namespace) {
42-
return false
23+
func (f *EventFilter) Matches(event map[string]interface{}) (bool, error) {
24+
for path, regex := range f.Rules {
25+
value, err := lookup.LookupString(event, path)
26+
if err != nil {
27+
return false, errorf("lookup error: %s", err)
4328
}
44-
}
45-
if f.ObjectKind != nil {
46-
if !f.ObjectKind.MatchString(event.InvolvedObject.Kind) {
47-
return false
48-
}
49-
}
50-
if f.ObjectName != nil {
51-
if !f.ObjectName.MatchString(event.InvolvedObject.Name) {
52-
return false
53-
}
54-
}
55-
if f.EventType != nil {
56-
if !f.EventType.MatchString(event.Type) {
57-
return false
58-
}
59-
}
60-
if f.EventReason != nil {
61-
if !f.EventReason.MatchString(event.Reason) {
62-
return false
29+
30+
valueStr := fmt.Sprintf("%v", value.Interface())
31+
if !regex.MatchString(valueStr) {
32+
return false, nil
6333
}
6434
}
6535

66-
return true
36+
return true, nil
6737
}
6838

6939
func (f *EventFilter) String() string {
7040
var elements []string
71-
if f.EventReason != nil {
72-
elements = append(elements, fmt.Sprintf("eventReason=%s", f.EventReason.String()))
73-
}
74-
if f.EventType != nil {
75-
elements = append(elements, fmt.Sprintf("eventType=%s", f.EventType.String()))
76-
}
77-
if f.ObjectNamespace != nil {
78-
elements = append(elements, fmt.Sprintf("objectNamespace=%s", f.ObjectNamespace.String()))
79-
}
80-
if f.ObjectKind != nil {
81-
elements = append(elements, fmt.Sprintf("objectKind=%s", f.ObjectKind.String()))
82-
}
83-
if f.ObjectName != nil {
84-
elements = append(elements, fmt.Sprintf("objectName=%s", f.ObjectName.String()))
85-
}
86-
87-
return strings.Join(elements, ",")
88-
}
8941

90-
func (f *EventFilter) StringShort() string {
91-
var elements []string
92-
if f.EventReason != nil {
93-
elements = append(elements, fmt.Sprintf("reason=%s", f.EventReason.String()))
94-
}
95-
if f.EventType != nil {
96-
elements = append(elements, fmt.Sprintf("evtType=%s", f.EventType.String()))
97-
}
98-
if f.ObjectNamespace != nil {
99-
elements = append(elements, fmt.Sprintf("objNS=%s", f.ObjectNamespace.String()))
100-
}
101-
if f.ObjectKind != nil {
102-
elements = append(elements, fmt.Sprintf("objKind=%s", f.ObjectKind.String()))
103-
}
104-
if f.ObjectName != nil {
105-
elements = append(elements, fmt.Sprintf("objName=%s", f.ObjectName.String()))
42+
for path, regex := range f.Rules {
43+
elements = append(elements, fmt.Sprintf("%s=%s", path, regex.String()))
10644
}
10745

10846
return strings.Join(elements, ",")

filter_test.go

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@ import (
88

99
func TestEventFilter(t *testing.T) {
1010
input := `
11-
objectKind: Job
12-
objectNamespace: default
13-
objectName: test.*
14-
eventType: Warning
15-
eventReason: BackoffLimitExceeded
11+
rules:
12+
involvedObject.kind: Job
13+
involvedObject.namespace: default
14+
involvedObject.name: test.*
15+
type: Warning
16+
reason: BackoffLimitExceeded
1617
`
1718

1819
filter := &EventFilter{}
@@ -21,6 +22,10 @@ eventReason: BackoffLimitExceeded
2122
t.Fatal(err)
2223
}
2324

25+
if err := filter.Validate(); err != nil {
26+
t.Fatalf("invalid rules: %s", err)
27+
}
28+
2429
evt := v1.Event{}
2530
evt.InvolvedObject = v1.ObjectReference{
2631
Kind: "Job",
@@ -30,8 +35,18 @@ eventReason: BackoffLimitExceeded
3035
evt.Type = "Warning"
3136
evt.Reason = "BackoffLimitExceeded"
3237

33-
if !filter.Matches(&evt) {
34-
t.Fatal("expected match")
38+
// Marshal to JSON
39+
obj, err := eventToMap(&evt)
40+
if err != nil {
41+
t.Fatalf("failed to cast event to map: %s", err)
42+
}
43+
44+
match, err := filter.Matches(obj)
45+
if err != nil {
46+
t.Fatalf("match error: %s", err)
47+
}
48+
if !match {
49+
t.Fatalf("no match")
3550
}
3651

3752
output := filter.ToYAML()
@@ -43,19 +58,19 @@ eventReason: BackoffLimitExceeded
4358
t.Fatal(err)
4459
}
4560

46-
if filter2.EventReason.String() != filter.EventReason.String() {
47-
t.Fatal("wrong EventReason")
48-
}
49-
if filter2.EventType.String() != filter.EventType.String() {
50-
t.Fatal("wrong EventType")
51-
}
52-
if filter2.ObjectKind.String() != filter.ObjectKind.String() {
53-
t.Fatal("wrong ObjectKind")
54-
}
55-
if filter2.ObjectName.String() != filter.ObjectName.String() {
56-
t.Fatal("wrong ObjectName")
57-
}
58-
if filter2.ObjectNamespace.String() != filter.ObjectNamespace.String() {
59-
t.Fatal("wrong ObjectNamespace")
61+
// Test marshal to string
62+
for path, regex := range filter.Rules {
63+
found := false
64+
for path2, regex2 := range filter2.Rules {
65+
if path == path2 {
66+
found = true
67+
if regex.String() != regex2.String() {
68+
t.Fatalf("wrong regex for %s", path)
69+
}
70+
}
71+
}
72+
if !found {
73+
t.Fatalf("path %s not found", path)
74+
}
6075
}
6176
}

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ module github.com/cmaster11/k8s-event-watcher
33
go 1.13
44

55
require (
6+
github.com/mitchellh/mapstructure v1.1.2
7+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405
68
gopkg.in/yaml.v2 v2.2.4
79
k8s.io/api v0.0.0-20190620084959-7cf5895f2711
810
k8s.io/apimachinery v0.0.0-20190612205821-1799e75a0719

0 commit comments

Comments
 (0)