Skip to content

Commit

Permalink
Add Object Annotation filter
Browse files Browse the repository at this point in the history
This commit, 
- enables filtering of events based on annotations present in objects at run time.
- annotation `botkube.io/disable: true` disables event notifications for the annotated object
- annotation `botkube.io/channel: <channel_name>` sends events notifications of the annotated object to the mentioned channel.
- adds func `ExtractAnnotations()`. It extract annotations from Event.InvolvedObject and adds them to event.Metadata.Annotations
- implements individual actions using internal functions.
- adds unit tests for internal functions.
- replaces Init() with InitialiseKubeClient() to decouple config.yaml and KubeClinet dependencies from unit testing
  • Loading branch information
codenio committed Aug 7, 2019
1 parent 5013c3b commit b1c42ba
Show file tree
Hide file tree
Showing 7 changed files with 326 additions and 11 deletions.
4 changes: 4 additions & 0 deletions cmd/botkube/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/infracloudio/botkube/pkg/config"
"github.com/infracloudio/botkube/pkg/controller"
log "github.com/infracloudio/botkube/pkg/logging"
"github.com/infracloudio/botkube/pkg/utils"
)

func main() {
Expand All @@ -16,6 +17,9 @@ func main() {
log.Logger.Fatal(fmt.Sprintf("Error in loading configuration. Error:%s", err.Error()))
}

// Initialise KubeClient,RtObjectMap,ResourceGetterMap,ClusterNamespaces
utils.InitialiseKubeClient()

if Config.Communications.Slack.Enabled {
log.Logger.Info("Starting slack bot")
sb := bot.NewSlackBot()
Expand Down
3 changes: 2 additions & 1 deletion pkg/events/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type Event struct {
Error string
Level Level
Cluster string
Channel string
TimeStamp time.Time
Count int32
Action string
Expand Down Expand Up @@ -204,7 +205,7 @@ func (event *Event) Message() (msg string) {
switch event.Type {
case config.CreateEvent, config.DeleteEvent, config.UpdateEvent:
msg = fmt.Sprintf(
"%s `%s` in of cluster `%s`, namespace `%s` has been %s:\n```%s```",
"%s `%s` of cluster `%s`, namespace `%s` has been %s:\n```%s```",
event.Kind,
event.Name,
event.Cluster,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package filters

import (
"github.com/infracloudio/botkube/pkg/events"
"github.com/infracloudio/botkube/pkg/filterengine"
log "github.com/infracloudio/botkube/pkg/logging"
"github.com/infracloudio/botkube/pkg/utils"
metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

const (
// DisableAnnotation is the object disable annotation
DisableAnnotation string = "botkube.io/disable"
// ChannelAnnotation is the multichannel support annotation
ChannelAnnotation string = "botkube.io/channel"
)

// ObjectAnnotationChecker add recommendations to the event object if pod created without any labels
type ObjectAnnotationChecker struct {
Description string
}

// Register filter
func init() {
filterengine.DefaultFilterEngine.Register(ObjectAnnotationChecker{
Description: "Checks if annotations botkube.io/* present in object specs and filters them.",
})
}

// Run filters and modifies event struct
func (f ObjectAnnotationChecker) Run(object interface{}, event *events.Event) {

// get objects metadata
obj := utils.GetObjectMetaData(object)

// if eventObj, ok := object.(*apiV1.Event); ok {
// // check annotations of the involved object
// eventObj = utils.ExtractAnnotaions(eventObj)
// obj = eventObj.ObjectMeta
// log.Logger.Debugf("Event Object >>> %+v",eventObj)
// }

// Check annotations in object
if isObjectNotifDisabled(obj) {
event.Skip = true
log.Logger.Debug("Object Notification Disable through annotations")
}

if channel, ok := reconfigureChannel(obj); ok {
event.Channel = channel
log.Logger.Debugf("Redirecting Event Notifications to channel: %s", channel)
}

log.Logger.Debug("Object annotations filter successful!")
}

// Describe filter
func (f ObjectAnnotationChecker) Describe() string {
return f.Description
}

// isObjectNotifDisabled checks annotation botkube.io/disable
// annotation botkube.io/disable disables the event notifications from objects
func isObjectNotifDisabled(obj metaV1.ObjectMeta) bool {

if obj.Annotations[DisableAnnotation] == "true" {
log.Logger.Debug("Skipping Disabled Event Notifications!")
return true
}
return false
}

// reconfigureChannel checks annotation botkube.io/channel
// annotation botkube.io/channel directs event notifications to channels
// based on the channel names present in them
// Note: Add botkube app into the desired channel to receive notifications
func reconfigureChannel(obj metaV1.ObjectMeta) (string, bool) {
// redirect messages to channels based on annotations
if channel, ok := obj.Annotations[ChannelAnnotation]; ok {
return channel, true
}
return "", false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package filters

import (
"testing"

metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestIsObjectNotifDisabled(t *testing.T) {
tests := map[string]struct {
annotaion metaV1.ObjectMeta
expected bool
}{
`Empty ObjectMeta`: {metaV1.ObjectMeta{}, false},
`ObjectMeta with some annotations`: {metaV1.ObjectMeta{Annotations: map[string]string{"foo": "bar"}}, false},
`ObjectMeta with disable false`: {metaV1.ObjectMeta{Annotations: map[string]string{"botkube.io/disable": "false"}}, false},
`ObjectMeta with disable true`: {metaV1.ObjectMeta{Annotations: map[string]string{"botkube.io/disable": "true"}}, true},
}
for name, test := range tests {
name, test := name, test
t.Run(name, func(t *testing.T) {
if actual := isObjectNotifDisabled(test.annotaion); actual != test.expected {
t.Errorf("expected: %+v != actual: %+v\n", test.expected, actual)
}
})
}
}

func TestReconfigureChannel(t *testing.T) {
tests := map[string]struct {
objectMeta metaV1.ObjectMeta
expectedChannel string
expectedBool bool
}{
`Empty ObjectMeta`: {metaV1.ObjectMeta{}, "", false},
`ObjectMeta with some annotations`: {metaV1.ObjectMeta{Annotations: map[string]string{"foo": "bar"}}, "", false},
`ObjectMeta with channel ""`: {metaV1.ObjectMeta{Annotations: map[string]string{"botkube.io/channel": ""}}, "", false},
`ObjectMeta with channel foo-channel`: {metaV1.ObjectMeta{Annotations: map[string]string{"botkube.io/channel": "foo-channel"}}, "foo-channel", true},
}
for name, test := range tests {
name, test := name, test
t.Run(name, func(t *testing.T) {
if actualChannel, actualBool := reconfigureChannel(test.objectMeta); actualBool != test.expectedBool {
if actualChannel != test.expectedChannel {
t.Errorf("expected: %+v != actual: %+v\n", test.expectedChannel, actualChannel)
}
}
})
}
}
28 changes: 25 additions & 3 deletions pkg/notify/mattermost.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,12 +142,34 @@ func (m *Mattermost) SendEvent(event events.Event) error {
}}

post := &model.Post{}
post.ChannelId = m.Channel
post.Props = map[string]interface{}{
"attachments": attachment,
}
if _, resp := m.Client.CreatePost(post); resp.Error != nil {
log.Logger.Error("Failed to send message. Error: ", resp.Error)

// non empty value in event.channel demands redirection of events to a different channel
if event.Channel != "" {
post.ChannelId = event.Channel

if _, resp := m.Client.CreatePost(post); resp.Error != nil {
log.Logger.Error("Failed to send message. Error: ", resp.Error)
// send error message to default channel
msg := fmt.Sprintf("Unable to send message to Channel `%s`: `%s`\n```add Botkube app to the Channel %s\nMissed events follows below:```", event.Channel, resp.Error, event.Channel)
go m.SendMessage(msg)
// sending missed event to default channel
// reset event.Channel and send event
event.Channel = ""
go m.SendEvent(event)
return resp.Error
}
log.Logger.Debugf("Event successfully sent to channel %s", post.ChannelId)
} else {
post.ChannelId = m.Channel
// empty value in event.channel sends notifications to default channel.
if _, resp := m.Client.CreatePost(post); resp.Error != nil {
log.Logger.Error("Failed to send message. Error: ", resp.Error)
return resp.Error
}
log.Logger.Debugf("Event successfully sent to channel %s", post.ChannelId)
}
return nil
}
Expand Down
31 changes: 25 additions & 6 deletions pkg/notify/slack.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,13 +145,32 @@ func (s *Slack) SendEvent(event events.Event) error {
attachment.Color = attachmentColor[event.Level]
params.Attachments = []slack.Attachment{attachment}

channelID, timestamp, err := api.PostMessage(s.Channel, "", params)
if err != nil {
log.Logger.Errorf("Error in sending slack message %s", err.Error())
return err
// non empty value in event.channel demands redirection of events to a different channel
if event.Channel != "" {
channelID, timestamp, err := api.PostMessage(event.Channel, "", params)
if err != nil {
log.Logger.Errorf("Error in sending slack message %s", err.Error())
// send error message to default channel
if err.Error() == "channel_not_found" {
msg := fmt.Sprintf("Unable to send message to Channel `%s`: `%s`\n```add Botkube app to the Channel %s\nMissed events follows below:```", event.Channel, err.Error(), event.Channel)
go s.SendMessage(msg)
// sending missed event to default channel
// reset event.Channel and send event
event.Channel = ""
go s.SendEvent(event)
}
return err
}
log.Logger.Debugf("Event successfully sent to channel %s at %s", channelID, timestamp)
} else {
// empty value in event.channel sends notifications to default channel.
channelID, timestamp, err := api.PostMessage(s.Channel, "", params)
if err != nil {
log.Logger.Errorf("Error in sending slack message %s", err.Error())
return err
}
log.Logger.Debugf("Event successfully sent to channel %s at %s", channelID, timestamp)
}

log.Logger.Debugf("Event successfully sent to channel %s at %s", channelID, timestamp)
return nil
}

Expand Down
Loading

0 comments on commit b1c42ba

Please sign in to comment.