Skip to content

Commit c2b90f4

Browse files
authored
Only log incorrect k8s buttons definitions (#1413)
* Add unit-test, make error more descriptive
1 parent 97f6bf8 commit c2b90f4

File tree

9 files changed

+190
-28
lines changed

9 files changed

+190
-28
lines changed

CONTRIBUTING.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ For faster development, you can also build and run Botkube outside K8s cluster.
189189
> Each time you make a change to the [source](cmd/source) or [executors](cmd/executor) plugins re-run the above command.
190190
191191
> **Note**
192-
> To build specific plugin binaries, use `PLUGIN_TARGETS`. For example `PLUGIN_TARGETS="x, kubectl" make build-plugins-single`.
192+
> To build specific plugin binaries, use `PLUGIN_TARGETS`. For example `PLUGIN_TARGETS="kubernetes,echo" make build-plugins-single`.
193193
194194
## Making A Change
195195

Makefile

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
.DEFAULT_GOAL := build
2-
.PHONY: container-image test test-integration-slack test-integration-discord build pre-build publish lint lint-fix go-import-fmt system-check save-images load-and-push-images gen-grpc-resources gen-plugins-index build-plugins build-plugins-single gen-docs-cli gen-plugins-goreleaser
2+
.PHONY: container-image test test-integration-slack test-integration-discord build pre-build publish lint lint-fix go-import-fmt system-check save-images load-and-push-images gen-grpc-resources gen-plugins-index build-plugins build-plugins-single gen-docs-cli gen-plugins-goreleaser serve-local-plugins
33

44
# Show this help.
55
help:
66
@awk '/^#/{c=substr($$0,3);next}c&&/^[[:alpha:]][[:alnum:]_-]+:/{print substr($$1,1,index($$1,":")),c}1{c=0}' $(MAKEFILE_LIST) | column -s: -t
77

8+
serve-local-plugins: ## Serve local plugins
9+
go run hack/target/serve-plugins/main.go
10+
811
lint-fix: go-import-fmt
912
@go mod tidy
1013
@go mod verify

internal/source/kubernetes/config/config.go

+50-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package config
22

33
import (
4+
"errors"
45
"fmt"
56
"regexp"
67
"strings"
@@ -38,10 +39,40 @@ type (
3839
DisplayName string `yaml:"displayName"`
3940
}
4041
Trigger struct {
41-
Type []string `yaml:"type"`
42+
Type []EventType `yaml:"type"`
4243
}
4344
)
4445

46+
func (b *ExtraButtons) NormalizeAndValidate() error {
47+
// the multierr pkg is not used as it breaks the final error indent making it hard to read
48+
var issues []string
49+
if b.Button.DisplayName == "" {
50+
issues = append(issues, "displayName cannot be empty")
51+
}
52+
53+
if b.Button.CommandTpl == "" {
54+
issues = append(issues, "commandTpl cannot be empty")
55+
}
56+
57+
if b.Trigger.Type == nil {
58+
issues = append(issues, "trigger.type cannot be empty")
59+
}
60+
for idx, t := range b.Trigger.Type {
61+
// we can't normalize it on custom 'UnmarshalYAML' because koanf uses map[string]any
62+
// that causes it to drop Go struct as custom UnmarshalYAML.
63+
t = EventType(strings.ToLower(string(t)))
64+
if !t.IsValid() {
65+
issues = append(issues, fmt.Sprintf("unknown trigger.type[%q]", t))
66+
}
67+
b.Trigger.Type[idx] = t
68+
}
69+
70+
if len(issues) > 0 {
71+
return errors.New(strings.Join(issues, ", "))
72+
}
73+
return nil
74+
}
75+
4576
// Commands contains allowed verbs and resources
4677
type Commands struct {
4778
Verbs []string `yaml:"verbs"`
@@ -179,8 +210,24 @@ const (
179210
AllEvent EventType = "all"
180211
)
181212

182-
func (eventType EventType) String() string {
183-
return string(eventType)
213+
// IsValid checks if the event type is valid.
214+
func (eventType *EventType) IsValid() bool {
215+
if eventType == nil {
216+
return false
217+
}
218+
switch *eventType {
219+
case CreateEvent, UpdateEvent, DeleteEvent, ErrorEvent, WarningEvent, NormalEvent, InfoEvent, AllEvent:
220+
return true
221+
}
222+
return false
223+
}
224+
225+
// String returns the string representation of the event type.
226+
func (eventType *EventType) String() string {
227+
if eventType == nil {
228+
return ""
229+
}
230+
return string(*eventType)
184231
}
185232

186233
// Resource contains resources to watch

internal/source/kubernetes/filterengine/filterengine.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ func (f *DefaultFilterEngine) Run(ctx context.Context, event event.Event) event.
6969
// Register filter(s) to engine.
7070
func (f *DefaultFilterEngine) Register(filters ...RegisteredFilter) {
7171
for _, filter := range filters {
72-
f.log.Infof("Registering filter %q (enabled: %t)...", filter.Name(), filter.Enabled)
72+
f.log.Debugf("Registering filter %q (enabled: %t)...", filter.Name(), filter.Enabled)
7373
f.filters[filter.Name()] = filter
7474
}
7575
}

internal/source/kubernetes/msg.go

+20-10
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ import (
77

88
sprig "github.com/go-task/slim-sprig"
99
"github.com/sirupsen/logrus"
10-
"k8s.io/kubectl/pkg/util/slice"
10+
"golang.org/x/exp/slices"
1111

1212
"github.com/kubeshop/botkube/internal/source/kubernetes/commander"
1313
"github.com/kubeshop/botkube/internal/source/kubernetes/config"
1414
"github.com/kubeshop/botkube/internal/source/kubernetes/event"
1515
"github.com/kubeshop/botkube/pkg/api"
16+
multierrx "github.com/kubeshop/botkube/pkg/multierror"
1617
)
1718

1819
var emojiForLevel = map[config.Level]string{
@@ -53,12 +54,12 @@ func (m *MessageBuilder) FromEvent(event event.Event, actions []config.ExtraButt
5354

5455
cmdSection, err := m.getCommandSelectIfShould(event)
5556
if err != nil {
56-
return api.Message{}, err
57+
m.log.Errorf("Failed to get commands buttons assigned to %q event. Those buttons will be omitted. Issues:\n%s", event.Type.String(), err)
5758
}
5859

59-
btns, err := m.getExternalActions(actions, event)
60+
btns, err := m.getExtraButtonsAssignedToEvent(actions, event)
6061
if err != nil {
61-
return api.Message{}, err
62+
m.log.Errorf("Failed to convert extra buttons assigned to %q event. Those buttons will be omitted. Issues:\n%s", event.Type.String(), err)
6263
}
6364
if cmdSection != nil || len(btns) > 0 {
6465
msg.Sections = append(msg.Sections, api.Section{
@@ -70,24 +71,33 @@ func (m *MessageBuilder) FromEvent(event event.Event, actions []config.ExtraButt
7071
return msg, nil
7172
}
7273

73-
func (m *MessageBuilder) getExternalActions(actions []config.ExtraButtons, e event.Event) (api.Buttons, error) {
74+
func (m *MessageBuilder) getExtraButtonsAssignedToEvent(actions []config.ExtraButtons, e event.Event) (api.Buttons, error) {
7475
var actBtns api.Buttons
75-
for _, act := range actions {
76+
issues := multierrx.New()
77+
for idx, act := range actions {
7678
if !act.Enabled {
7779
continue
7880
}
79-
if !slice.ContainsString(act.Trigger.Type, e.Type.String(), nil) {
81+
82+
err := act.NormalizeAndValidate()
83+
if err != nil {
84+
issues = multierrx.Append(issues, fmt.Errorf("invalid extraButtons[%d]: %s", idx, err))
85+
continue
86+
}
87+
88+
if !slices.Contains(act.Trigger.Type, e.Type) {
8089
continue
8190
}
8291

8392
btn, err := m.renderActionButton(act, e)
8493
if err != nil {
85-
return nil, err
94+
issues = multierrx.Append(issues, fmt.Errorf("invalid extraButtons[%d].commandTpl: %s", idx, err))
95+
continue
8696
}
8797
actBtns = append(actBtns, btn)
8898
}
8999

90-
return actBtns, nil
100+
return actBtns, issues.ErrorOrNil()
91101
}
92102

93103
func ptrSection(s *api.Selects) api.Selects {
@@ -175,7 +185,7 @@ func (m *MessageBuilder) appendBulletListIfNotEmpty(bulletLists api.BulletLists,
175185
}
176186

177187
func (m *MessageBuilder) renderActionButton(act config.ExtraButtons, e event.Event) (api.Button, error) {
178-
tmpl, err := template.New("example").Funcs(sprig.FuncMap()).Parse(act.Button.CommandTpl)
188+
tmpl, err := template.New(act.Button.DisplayName).Funcs(sprig.FuncMap()).Parse(act.Button.CommandTpl)
179189
if err != nil {
180190
return api.Button{}, err
181191
}
+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package kubernetes
2+
3+
import (
4+
"testing"
5+
6+
"github.com/MakeNowJust/heredoc"
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
10+
"github.com/kubeshop/botkube/internal/source/kubernetes/config"
11+
"github.com/kubeshop/botkube/internal/source/kubernetes/event"
12+
)
13+
14+
func TestGetExtraButtonsAssignedToEvent(t *testing.T) {
15+
// given
16+
builder := MessageBuilder{}
17+
givenButtons := []config.ExtraButtons{
18+
{
19+
// This is fully valid
20+
Enabled: true,
21+
Trigger: config.Trigger{
22+
Type: []config.EventType{"error"},
23+
},
24+
Button: config.Button{
25+
DisplayName: "Ask AI",
26+
CommandTpl: "ai --resource={{ .Namespace }}/{{ .Kind | lower }}/{{ .Name }} --error={{ .Reason }} --bk-cmd-header='AI assistance'",
27+
},
28+
},
29+
{
30+
// This is valid, as the 'ERROR' type should be normalized to "error"
31+
Enabled: true,
32+
Trigger: config.Trigger{
33+
Type: []config.EventType{"ERROR"},
34+
},
35+
Button: config.Button{
36+
DisplayName: "Get",
37+
CommandTpl: "kubectl get {{ .Kind | lower }}",
38+
},
39+
},
40+
{
41+
// This is invalid, as we can't render `.Event`
42+
Enabled: true,
43+
Trigger: config.Trigger{
44+
Type: []config.EventType{"error"},
45+
},
46+
Button: config.Button{
47+
DisplayName: "Ask AI v2",
48+
CommandTpl: "ai {{.Event.Namespace}} this one is wrong",
49+
},
50+
},
51+
{
52+
// This is invalid, as the DisplayName and Trigger is not set
53+
Enabled: true,
54+
Trigger: config.Trigger{},
55+
Button: config.Button{
56+
CommandTpl: "ai {{.Event.Namespace}} this one is wrong",
57+
},
58+
},
59+
{
60+
// This is invalid, but should be ignored as it's disabled
61+
Enabled: false,
62+
Trigger: config.Trigger{},
63+
Button: config.Button{
64+
CommandTpl: "ai {{.Event.Namespace}} this one is wrong",
65+
},
66+
},
67+
}
68+
69+
// when
70+
gotBtns, err := builder.getExtraButtonsAssignedToEvent(givenButtons, event.Event{Type: "error"})
71+
assert.EqualError(t, err, heredoc.Doc(`
72+
2 errors occurred:
73+
* invalid extraButtons[2].commandTpl: template: Ask AI v2:1:11: executing "Ask AI v2" at <.Event.Namespace>: can't evaluate field Event in type event.Event
74+
* invalid extraButtons[3]: displayName cannot be empty, trigger.type cannot be empty`))
75+
76+
// then
77+
require.Len(t, gotBtns, 2)
78+
for idx, btn := range gotBtns {
79+
assert.Equal(t, givenButtons[idx].Button.DisplayName, btn.Name)
80+
}
81+
}

internal/source/kubernetes/source.go

+13-7
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,19 @@ func (*Source) Stream(ctx context.Context, input source.StreamInput) (source.Str
8484
if err != nil {
8585
return source.StreamOutput{}, fmt.Errorf("while merging input configs: %w", err)
8686
}
87+
88+
// In Kubernetes, we have an "info" level by default. We should aim to minimize info logging and consider using
89+
// the debug level instead. This approach will prevent flooding the Agent logs with irrelevant information,
90+
// as the Agent logs everything that plugin writes to stderr.
91+
log := loggerx.NewStderr(pkgConfig.Logger{
92+
Level: cfg.Log.Level,
93+
})
94+
8795
s := Source{
88-
startTime: time.Now(),
89-
eventCh: make(chan source.Event),
90-
config: cfg,
91-
logger: loggerx.New(pkgConfig.Logger{
92-
Level: cfg.Log.Level,
93-
}),
96+
startTime: time.Now(),
97+
eventCh: make(chan source.Event),
98+
config: cfg,
99+
logger: log,
94100
clusterName: input.Context.ClusterName,
95101
kubeConfig: input.Context.KubeConfig,
96102
isInteractivitySupported: input.Context.IsInteractivitySupported,
@@ -135,7 +141,7 @@ func consumeEvents(ctx context.Context, s Source) {
135141
}, func(resource string) (cache.SharedIndexInformer, error) {
136142
gvr, err := parseResourceArg(resource, client.mapper)
137143
if err != nil {
138-
s.logger.Infof("Unable to parse resource: %s to register with informer\n", resource)
144+
s.logger.Errorf("Unable to parse resource: %s to register with informer\n", resource)
139145
return nil, err
140146
}
141147
return dynamicKubeInformerFactory.ForResource(gvr).Informer(), nil

pkg/config/config.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -620,8 +620,8 @@ const (
620620
// FormatterText text formatter for logging
621621
FormatterText Formatter = "text"
622622

623-
// FormatterJson json formatter for logging
624-
FormatterJson Formatter = "json"
623+
// FormatterJSON json formatter for logging
624+
FormatterJSON Formatter = "json"
625625
)
626626

627627
// Logger holds logger configuration parameters.

pkg/loggerx/logger.go

+18-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package loggerx
22

33
import (
4+
"io"
45
"os"
56

67
"github.com/sirupsen/logrus"
@@ -9,10 +10,24 @@ import (
910
)
1011

1112
// New returns a new logger based on a given configuration.
13+
// It logs to stdout by default. It's a helper function to maintain backward compatibility.
1214
func New(cfg config.Logger) logrus.FieldLogger {
15+
return NewStdout(cfg)
16+
}
17+
18+
// NewStderr returns a new logger based on a given configuration. It logs to stderr.
19+
func NewStderr(cfg config.Logger) logrus.FieldLogger {
20+
return newWithOutput(cfg, os.Stderr)
21+
}
22+
23+
// NewStdout returns a new logger based on a given configuration. It logs to stdout.
24+
func NewStdout(cfg config.Logger) logrus.FieldLogger {
25+
return newWithOutput(cfg, os.Stdout)
26+
}
27+
28+
func newWithOutput(cfg config.Logger, output io.Writer) logrus.FieldLogger {
1329
logger := logrus.New()
14-
// Output to stdout instead of the default stderr
15-
logger.SetOutput(os.Stdout)
30+
logger.SetOutput(output)
1631

1732
// Only logger the warning severity or above.
1833
logLevel, err := logrus.ParseLevel(cfg.Level)
@@ -21,7 +36,7 @@ func New(cfg config.Logger) logrus.FieldLogger {
2136
logLevel = logrus.InfoLevel
2237
}
2338
logger.SetLevel(logLevel)
24-
if cfg.Formatter == config.FormatterJson {
39+
if cfg.Formatter == config.FormatterJSON {
2540
logger.Formatter = &logrus.JSONFormatter{}
2641
} else {
2742
logger.Formatter = &logrus.TextFormatter{FullTimestamp: true, DisableColors: cfg.DisableColors, ForceColors: true}

0 commit comments

Comments
 (0)