Navigation Menu

Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Webhooks #169

Merged
merged 2 commits into from Sep 2, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 3 additions & 1 deletion cmd/botkube/main.go
Expand Up @@ -43,7 +43,9 @@ func main() {
if Config.Communications.ElasticSearch.Enabled {
notifiers = append(notifiers, notify.NewElasticSearch(Config))
}

if Config.Communications.Webhook.Enabled {
notifiers = append(notifiers, notify.NewWebhook(Config))
}
if Config.Settings.UpgradeNotifier {
log.Logger.Info("Starting upgrade notifier")
go controller.UpgradeNotifier(Config, notifiers)
Expand Down
5 changes: 5 additions & 0 deletions config.yaml
Expand Up @@ -210,6 +210,11 @@ communications:
shards: 1
replicas: 0

# Settings for Webhook
webhook:
enabled: false
url: 'WEBHOOK_URL' # e.g https://example.com:80

# Setting to support multiple clusters
settings:
# Cluster name to differentiate incoming messages
Expand Down
6 changes: 6 additions & 0 deletions deploy-all-in-one-tls.yaml
Expand Up @@ -215,6 +215,12 @@ data:
type: botkube-event
shards: 1
replicas: 0

# Settings for Webhook
webhook:
enabled: false
url: 'WEBHOOK_URL' # e.g https://example.com:80


# Setting to support multiple clusters
settings:
Expand Down
7 changes: 6 additions & 1 deletion deploy-all-in-one.yaml
Expand Up @@ -216,7 +216,12 @@ data:
type: botkube-event
shards: 1
replicas: 0


# Settings for Webhook
webhook:
enabled: false
url: 'WEBHOOK_URL' # e.g https://example.com:80

# Setting to support multiple clusters
settings:
# Cluster name to differentiate incoming messages
Expand Down
6 changes: 6 additions & 0 deletions helm/botkube/values.yaml
Expand Up @@ -235,6 +235,12 @@ config:
type: botkube-event
shards: 1
replicas: 0

# Settings for Webhook
webhook:
enabled: false
url: 'WEBHOOK_URL' # e.g https://example.com:80


# Setting to support multiple clusters
settings:
Expand Down
7 changes: 7 additions & 0 deletions pkg/config/config.go
Expand Up @@ -74,6 +74,7 @@ type Communications struct {
Slack Slack
ElasticSearch ElasticSearch
Mattermost Mattermost
Webhook Webhook
}

// Slack configuration to authentication and send notifications
Expand Down Expand Up @@ -111,6 +112,12 @@ type Mattermost struct {
NotifType NotifType `yaml:",omitempty"`
}

// Webhook configuration to send notifications
type Webhook struct {
Enabled bool
URL string
}

// Settings for multicluster support
type Settings struct {
ClusterName string
Expand Down
121 changes: 121 additions & 0 deletions pkg/notify/webhook.go
@@ -0,0 +1,121 @@
package notify

import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"time"

"github.com/infracloudio/botkube/pkg/config"
"github.com/infracloudio/botkube/pkg/events"
log "github.com/infracloudio/botkube/pkg/logging"
)

// Webhook contains URL and ClusterName
type Webhook struct {
URL string
ClusterName string
}

// WebhookPayload contains json payload to be sent to webhook url
type WebhookPayload struct {
EventMeta EventMeta `json:"meta"`
EventStatus EventStatus `json:"status"`
EventSummary string `json:"summary"`
TimeStamp time.Time `json:"timestamp"`
Recommendations []string `json:"recommendations,omitempty"`
Warnings []string `json:"warnings,omitempty"`
}

// EventMeta contains the meta data about the event occurred
type EventMeta struct {
Kind string `json:"kind"`
Name string `json:"name"`
Namespace string `json:"namespace"`
Cluster string `json:"cluster,omitempty"`
}

// EventStatus contains the status about the event occurred
type EventStatus struct {
Type config.EventType `json:"type"`
Level events.Level `json:"level"`
Reason string `json:"reason,omitempty"`
Error string `json:"error,omitempty"`
Messages []string `json:"messages,omitempty"`
}

// NewWebhook returns new Webhook object
func NewWebhook(c *config.Config) Notifier {
return &Webhook{
URL: c.Communications.Webhook.URL,
ClusterName: c.Settings.ClusterName,
}
}

// SendEvent sends event notification to Webhook url
func (w *Webhook) SendEvent(event events.Event) (err error) {

// set missing cluster name to event object
event.Cluster = w.ClusterName

jsonPayload := &WebhookPayload{
EventMeta: EventMeta{
Kind: event.Kind,
Name: event.Name,
Namespace: event.Namespace,
Cluster: event.Cluster,
},
EventStatus: EventStatus{
Type: event.Type,
Level: event.Level,
Reason: event.Reason,
Error: event.Error,
Messages: event.Messages,
},
EventSummary: event.Message(),
TimeStamp: event.TimeStamp,
Recommendations: event.Recommendations,
Warnings: event.Warnings,
}

err = w.PostWebhook(jsonPayload)
if err != nil {
log.Logger.Error(err.Error())
log.Logger.Debugf("Event Not Sent to Webhook %v", event)
}

log.Logger.Debugf("Event successfully sent to Webhook %v", event)
return nil
}

// SendMessage sends message to Webhook url
func (w *Webhook) SendMessage(msg string) error {
return nil
}

// PostWebhook posts webhook to listener
func (w *Webhook) PostWebhook(jsonPayload *WebhookPayload) error {

message, err := json.Marshal(jsonPayload)
if err != nil {
return err
}

req, err := http.NewRequest("POST", w.URL, bytes.NewBuffer(message))
if err != nil {
return err
}
req.Header.Add("Content-Type", "application/json")

client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("Error Posting Webhook: %s", string(resp.StatusCode))
}

return nil
}
46 changes: 46 additions & 0 deletions pkg/notify/webhook_test.go
@@ -0,0 +1,46 @@
package notify

import (
"fmt"
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/assert"
)

// Unit test PostWebhook
func TestPostWebhook(t *testing.T) {
tests := map[string]struct {
server *httptest.Server
expected error
}{
`Status Not Ok`: {
httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable)
})),
fmt.Errorf("Error Posting Webhook: %s", string(http.StatusServiceUnavailable)),
},
`Status Ok`: {
httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})),
nil,
},
}
for name, test := range tests {
name, test := name, test
t.Run(name, func(t *testing.T) {
ts := test.server
defer ts.Close()
// create a dummy webhook object to test
w := &Webhook{
URL: ts.URL,
ClusterName: "test",
}

err := w.PostWebhook(&WebhookPayload{})
assert.Equal(t, test.expected, err)
})
}
}
14 changes: 7 additions & 7 deletions test/e2e/command/botkube.go
Expand Up @@ -13,7 +13,7 @@ import (
"github.com/infracloudio/botkube/test/e2e/utils"
"github.com/nlopes/slack"
"github.com/stretchr/testify/assert"
"k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

Expand Down Expand Up @@ -55,7 +55,7 @@ func (c *context) testBotkubeCommand(t *testing.T) {

// Convert text message into Slack message structure
m := slack.Message{}
err := json.Unmarshal([]byte(lastSeenMsg), &m)
err := json.Unmarshal([]byte(*lastSeenMsg), &m)
assert.NoError(t, err, "message should decode properly")
assert.Equal(t, c.Config.Communications.Slack.Channel, m.Channel)
switch test.command {
Expand Down Expand Up @@ -106,7 +106,7 @@ func (c *context) testNotifierCommand(t *testing.T) {

// Convert text message into Slack message structure
m := slack.Message{}
err := json.Unmarshal([]byte(lastSeenMsg), &m)
err := json.Unmarshal([]byte(*lastSeenMsg), &m)
assert.NoError(t, err, "message should decode properly")
assert.Equal(t, c.Config.Communications.Slack.Channel, m.Channel)
assert.Equal(t, fmt.Sprintf("```Sure! I won't send you notifications from cluster '%s' anymore.```", c.Config.Settings.ClusterName), m.Text)
Expand All @@ -118,7 +118,7 @@ func (c *context) testNotifierCommand(t *testing.T) {
Kind: "pod",
Namespace: "test",
Specs: &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "test-pod-notifier"}},
Expected: utils.SlackMessage{
ExpectedSlackMessage: utils.SlackMessage{
Attachments: []slack.Attachment{{Color: "good", Fields: []slack.AttachmentField{{Title: "Pod create", Value: "Pod `test-pod` in of cluster `test-cluster-1`, namespace `test` has been created:\n```Resource created\nRecommendations:\n- pod 'test-pod' creation without labels should be avoided.\n```", Short: false}}, Footer: "BotKube"}},
},
}
Expand All @@ -132,10 +132,10 @@ func (c *context) testNotifierCommand(t *testing.T) {

// Convert text message into Slack message structure
m := slack.Message{}
err := json.Unmarshal([]byte(lastSeenMsg), &m)
err := json.Unmarshal([]byte(*lastSeenMsg), &m)
assert.NoError(t, err, "message should decode properly")
assert.Equal(t, c.Config.Communications.Slack.Channel, m.Channel)
assert.NotEqual(t, pod.Expected.Attachments, m.Attachments)
assert.NotEqual(t, pod.ExpectedSlackMessage.Attachments, m.Attachments)
})

// Revert and Enable notifier
Expand All @@ -149,7 +149,7 @@ func (c *context) testNotifierCommand(t *testing.T) {

// Convert text message into Slack message structure
m := slack.Message{}
err := json.Unmarshal([]byte(lastSeenMsg), &m)
err := json.Unmarshal([]byte(*lastSeenMsg), &m)
assert.NoError(t, err, "message should decode properly")
assert.Equal(t, c.Config.Communications.Slack.Channel, m.Channel)
assert.Equal(t, fmt.Sprintf("```Brace yourselves, notifications are coming from cluster '%s'.```", c.Config.Settings.ClusterName), m.Text)
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/command/kubectl.go
Expand Up @@ -42,7 +42,7 @@ func (c *context) testKubectlCommand(t *testing.T) {

// Convert text message into Slack message structure
m := slack.Message{}
err := json.Unmarshal([]byte(lastSeenMsg), &m)
err := json.Unmarshal([]byte(*lastSeenMsg), &m)
assert.NoError(t, err, "message should decode properly")
assert.Equal(t, c.Config.Communications.Slack.Channel, m.Channel)
assert.Equal(t, test.expected, m.Text)
Expand Down
27 changes: 20 additions & 7 deletions test/e2e/e2e_test.go
Expand Up @@ -22,14 +22,27 @@ func TestRun(t *testing.T) {
testEnv := env.New()

// Fake notifiers
fakeSlackNotifier := &notify.Slack{
Token: testEnv.Config.Communications.Slack.Token,
Channel: testEnv.Config.Communications.Slack.Channel,
ClusterName: testEnv.Config.Settings.ClusterName,
NotifType: testEnv.Config.Communications.Slack.NotifType,
SlackURL: testEnv.SlackServer.GetAPIURL(),
notifiers := []notify.Notifier{}

if testEnv.Config.Communications.Slack.Enabled {
fakeSlackNotifier := &notify.Slack{
Token: testEnv.Config.Communications.Slack.Token,
Channel: testEnv.Config.Communications.Slack.Channel,
ClusterName: testEnv.Config.Settings.ClusterName,
NotifType: testEnv.Config.Communications.Slack.NotifType,
SlackURL: testEnv.SlackServer.GetAPIURL(),
}

notifiers = append(notifiers, fakeSlackNotifier)
}

if testEnv.Config.Communications.Webhook.Enabled {
fakeWebhookNotifier := &notify.Webhook{
URL: testEnv.WebhookServer.GetAPIURL(),
ClusterName: testEnv.Config.Settings.ClusterName,
}
notifiers = append(notifiers, fakeWebhookNotifier)
}
notifiers := []notify.Notifier{fakeSlackNotifier}

utils.KubeClient = testEnv.K8sClient
utils.InitInformerMap()
Expand Down