diff --git a/cmd/botkube/main.go b/cmd/botkube/main.go index c2099d707..f9cb783c3 100644 --- a/cmd/botkube/main.go +++ b/cmd/botkube/main.go @@ -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) diff --git a/config.yaml b/config.yaml index a5303c73e..7e6e82016 100644 --- a/config.yaml +++ b/config.yaml @@ -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 diff --git a/deploy-all-in-one-tls.yaml b/deploy-all-in-one-tls.yaml index 66f538861..5d1a215fe 100644 --- a/deploy-all-in-one-tls.yaml +++ b/deploy-all-in-one-tls.yaml @@ -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: diff --git a/deploy-all-in-one.yaml b/deploy-all-in-one.yaml index 30e3e983d..d475a9d75 100644 --- a/deploy-all-in-one.yaml +++ b/deploy-all-in-one.yaml @@ -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 diff --git a/helm/botkube/values.yaml b/helm/botkube/values.yaml index 34521aae2..cb89b26e0 100644 --- a/helm/botkube/values.yaml +++ b/helm/botkube/values.yaml @@ -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: diff --git a/pkg/config/config.go b/pkg/config/config.go index 14661c776..12327a562 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -74,6 +74,7 @@ type Communications struct { Slack Slack ElasticSearch ElasticSearch Mattermost Mattermost + Webhook Webhook } // Slack configuration to authentication and send notifications @@ -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 diff --git a/pkg/notify/webhook.go b/pkg/notify/webhook.go new file mode 100644 index 000000000..ce9695497 --- /dev/null +++ b/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 +} diff --git a/pkg/notify/webhook_test.go b/pkg/notify/webhook_test.go new file mode 100644 index 000000000..0964034e8 --- /dev/null +++ b/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) + }) + } +} diff --git a/test/e2e/command/botkube.go b/test/e2e/command/botkube.go index 2cdd36a46..b26900428 100644 --- a/test/e2e/command/botkube.go +++ b/test/e2e/command/botkube.go @@ -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" ) @@ -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 { @@ -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) @@ -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"}}, }, } @@ -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 @@ -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) diff --git a/test/e2e/command/kubectl.go b/test/e2e/command/kubectl.go index b0f9fed4b..770e7d6ba 100644 --- a/test/e2e/command/kubectl.go +++ b/test/e2e/command/kubectl.go @@ -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) diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 69339c487..edc2d1ded 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -22,14 +22,27 @@ func TestRun(t *testing.T) { testEnv := env.New() // Fake notifiers - fakeSlackNotifier := ¬ify.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 := ¬ify.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 := ¬ify.Webhook{ + URL: testEnv.WebhookServer.GetAPIURL(), + ClusterName: testEnv.Config.Settings.ClusterName, + } + notifiers = append(notifiers, fakeWebhookNotifier) } - notifiers := []notify.Notifier{fakeSlackNotifier} utils.KubeClient = testEnv.K8sClient utils.InitInformerMap() diff --git a/test/e2e/env/env.go b/test/e2e/env/env.go index 4142cb8fb..02145d0e9 100644 --- a/test/e2e/env/env.go +++ b/test/e2e/env/env.go @@ -7,6 +7,8 @@ import ( "testing" "github.com/infracloudio/botkube/pkg/config" + "github.com/infracloudio/botkube/test/e2e/utils" + "github.com/infracloudio/botkube/test/webhook" "github.com/nlopes/slack" "github.com/nlopes/slack/slacktest" "k8s.io/client-go/kubernetes" @@ -21,6 +23,7 @@ import ( type TestEnv struct { K8sClient kubernetes.Interface SlackServer *slacktest.Server + WebhookServer *webhook.Server SlackMessages chan (*slack.MessageEvent) Config *config.Config } @@ -45,6 +48,9 @@ func New() *TestEnv { testEnv.Config.Communications.Slack.Token = "ABCDEFG" testEnv.Config.Communications.Slack.Channel = "cloud-alerts" + //Set Webhook + testEnv.Config.Communications.Webhook.Enabled = true + // Add settings testEnv.Config.Settings.ClusterName = "test-cluster-1" testEnv.Config.Settings.AllowKubectl = true @@ -55,6 +61,7 @@ func New() *TestEnv { testEnv.K8sClient = fake.NewSimpleClientset() testEnv.SlackMessages = make(chan (*slack.MessageEvent), 1) testEnv.SetupFakeSlack() + testEnv.SetupFakeWebhook() return testEnv } @@ -69,10 +76,27 @@ func (e *TestEnv) SetupFakeSlack() { } // GetLastSeenSlackMessage return last message received by fake slack server -func (e TestEnv) GetLastSeenSlackMessage() string { +func (e TestEnv) GetLastSeenSlackMessage() *string { allSeenMessages := e.SlackServer.GetSeenOutboundMessages() if len(allSeenMessages) != 0 { - return allSeenMessages[len(allSeenMessages)-1] + return &allSeenMessages[len(allSeenMessages)-1] + } + return nil +} + +// SetupFakeWebhook create fake Slack server to mock Slack +func (e *TestEnv) SetupFakeWebhook() { + s := webhook.NewTestServer() + go s.Start() + + e.WebhookServer = s +} + +// GetLastReceivedPayload return last message received by fake webhook server +func (e TestEnv) GetLastReceivedPayload() *utils.WebhookPayload { + allSeenMessages := e.WebhookServer.GetReceivedPayloads() + if len(allSeenMessages) != 0 { + return &allSeenMessages[len(allSeenMessages)-1] } - return "" + return nil } diff --git a/test/e2e/filters/filters.go b/test/e2e/filters/filters.go index cb7a3a20c..2cd19d026 100644 --- a/test/e2e/filters/filters.go +++ b/test/e2e/filters/filters.go @@ -5,11 +5,12 @@ import ( "testing" "time" + "github.com/infracloudio/botkube/pkg/notify" "github.com/infracloudio/botkube/test/e2e/env" "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" extV1beta1 "k8s.io/api/extensions/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" @@ -27,27 +28,42 @@ func (c *context) testFilters(t *testing.T) { Kind: "pod", Namespace: "test", Specs: &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "nginx-pod", Labels: map[string]string{"env": "test"}}, Spec: v1.PodSpec{Containers: []v1.Container{{Name: "nginx", Image: "nginx:latest"}}}}, - Expected: utils.SlackMessage{ + ExpectedSlackMessage: utils.SlackMessage{ Attachments: []slack.Attachment{{Color: "good", Fields: []slack.AttachmentField{{Title: "Pod create", Value: "Pod `nginx-pod` in of cluster `test-cluster-1`, namespace `test` has been created:\n```Resource created\nRecommendations:\n- :latest tag used in image 'nginx:latest' of Container 'nginx' should be avoided.\n```", Short: false}}, Footer: "BotKube"}}, }, + ExpectedWebhookPayload: utils.WebhookPayload{ + EventMeta: notify.EventMeta{Kind: "Pod", Name: "nginx-pod", Namespace: "test", Cluster: "test-cluster-1"}, + EventStatus: notify.EventStatus{Type: "create", Level: "info", Reason: "", Error: "", Messages: []string{"Resource created\n"}}, + Summary: "Pod `nginx-pod` in of cluster `test-cluster-1`, namespace `test` has been created:\n```Resource created\nRecommendations:\n- :latest tag used in image 'nginx:latest' of Container 'nginx' should be avoided.\n```", + }, }, "test PodLabelChecker filter": { Kind: "pod", Namespace: "test", Specs: &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod-wo-label"}}, - Expected: utils.SlackMessage{ + ExpectedSlackMessage: utils.SlackMessage{ Attachments: []slack.Attachment{{Color: "good", Fields: []slack.AttachmentField{{Title: "Pod create", Value: "Pod `pod-wo-label` in of cluster `test-cluster-1`, namespace `test` has been created:\n```Resource created\nRecommendations:\n- pod 'pod-wo-label' creation without labels should be avoided.\n```", Short: false}}, Footer: "BotKube"}}, }, + ExpectedWebhookPayload: utils.WebhookPayload{ + EventMeta: notify.EventMeta{Kind: "Pod", Name: "pod-wo-label", Namespace: "test", Cluster: "test-cluster-1"}, + EventStatus: notify.EventStatus{Type: "create", Level: "info", Reason: "", Error: "", Messages: []string{"Resource created\n"}}, + Summary: "Pod `pod-wo-label` in of cluster `test-cluster-1`, namespace `test` has been created:\n```Resource created\nRecommendations:\n- pod 'pod-wo-label' creation without labels should be avoided.\n```", + }, }, "test IngressValidator filter": { Kind: "ingress", Namespace: "test", Specs: &extV1beta1.Ingress{ObjectMeta: metav1.ObjectMeta{Name: "ingress-with-service"}, Spec: extV1beta1.IngressSpec{Rules: []extV1beta1.IngressRule{{IngressRuleValue: extV1beta1.IngressRuleValue{HTTP: &extV1beta1.HTTPIngressRuleValue{Paths: []extV1beta1.HTTPIngressPath{{Path: "testpath", Backend: extV1beta1.IngressBackend{ServiceName: "test-service", ServicePort: intstr.FromInt(80)}}}}}}}}}, - Expected: utils.SlackMessage{ + ExpectedSlackMessage: utils.SlackMessage{ Attachments: []slack.Attachment{{Color: "good", Fields: []slack.AttachmentField{{Title: "Ingress create", Value: "Ingress `ingress-with-service` in of cluster `test-cluster-1`, namespace `test` has been created:\n```Resource created\nWarnings:\n- Service 'test-service' used in ingress 'ingress-with-service' config does not exist or port '80' not exposed\n```", Short: false}}, Footer: "BotKube"}}, }, + ExpectedWebhookPayload: utils.WebhookPayload{ + EventMeta: notify.EventMeta{Kind: "Ingress", Name: "ingress-with-service", Namespace: "test", Cluster: "test-cluster-1"}, + EventStatus: notify.EventStatus{Type: "create", Level: "info", Reason: "", Error: "", Messages: []string{"Resource created\n"}}, + Summary: "Ingress `ingress-with-service` in of cluster `test-cluster-1`, namespace `test` has been created:\n```Resource created\nWarnings:\n- Service 'test-service' used in ingress 'ingress-with-service' config does not exist or port '80' not exposed\n```", + }, }, } @@ -62,10 +78,16 @@ func (c *context) testFilters(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.Attachments, m.Attachments) + assert.Equal(t, test.ExpectedSlackMessage.Attachments, m.Attachments) + + // Get last seen webhook payload + lastSeenPayload := c.GetLastReceivedPayload() + assert.Equal(t, test.ExpectedWebhookPayload.EventMeta, lastSeenPayload.EventMeta) + assert.Equal(t, test.ExpectedWebhookPayload.EventStatus, lastSeenPayload.EventStatus) + assert.Equal(t, test.ExpectedWebhookPayload.Summary, lastSeenPayload.Summary) }) } } diff --git a/test/e2e/notifier/create/create.go b/test/e2e/notifier/create/create.go index 076aefbe9..85b4857a1 100644 --- a/test/e2e/notifier/create/create.go +++ b/test/e2e/notifier/create/create.go @@ -6,12 +6,13 @@ import ( "time" "github.com/infracloudio/botkube/pkg/config" + "github.com/infracloudio/botkube/pkg/notify" "github.com/infracloudio/botkube/pkg/utils" "github.com/infracloudio/botkube/test/e2e/env" testutils "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" ) @@ -27,17 +28,27 @@ func (c *context) testCreateResource(t *testing.T) { Kind: "pod", Namespace: "test", Specs: &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "test-pod"}}, - Expected: testutils.SlackMessage{ + ExpectedSlackMessage: testutils.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"}}, }, + ExpectedWebhookPayload: testutils.WebhookPayload{ + EventMeta: notify.EventMeta{Kind: "Pod", Name: "test-pod", Namespace: "test", Cluster: "test-cluster-1"}, + EventStatus: notify.EventStatus{Type: "create", Level: "info", Reason: "", Error: "", Messages: []string{"Resource created\n"}}, + Summary: "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```", + }, }, "create service in configured namespace": { Kind: "service", Namespace: "test", Specs: &v1.Service{ObjectMeta: metav1.ObjectMeta{Name: "test-service"}}, - Expected: testutils.SlackMessage{ + ExpectedSlackMessage: testutils.SlackMessage{ Attachments: []slack.Attachment{{Color: "good", Fields: []slack.AttachmentField{{Title: "Service create", Value: "Service `test-service` in of cluster `test-cluster-1`, namespace `test` has been created:\n```Resource created\n```", Short: false}}, Footer: "BotKube"}}, }, + ExpectedWebhookPayload: testutils.WebhookPayload{ + EventMeta: notify.EventMeta{Kind: "Service", Name: "test-service", Namespace: "test", Cluster: "test-cluster-1"}, + EventStatus: notify.EventStatus{Type: "create", Level: "info", Reason: "", Error: "", Messages: []string{"Resource created\n"}}, + Summary: "Service `test-service` in of cluster `test-cluster-1`, namespace `test` has been created:\n```Resource created\n```", + }, }, } @@ -52,10 +63,17 @@ func (c *context) testCreateResource(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.Attachments, m.Attachments) + assert.Equal(t, test.ExpectedSlackMessage.Attachments, m.Attachments) + + // Get last seen webhook payload + lastSeenPayload := c.GetLastReceivedPayload() + assert.Equal(t, test.ExpectedWebhookPayload.EventMeta, lastSeenPayload.EventMeta) + assert.Equal(t, test.ExpectedWebhookPayload.EventStatus, lastSeenPayload.EventStatus) + assert.Equal(t, test.ExpectedWebhookPayload.Summary, lastSeenPayload.Summary) + isAllowed := utils.AllowedEventKindsMap[utils.EventKind{Resource: test.Kind, Namespace: "all", EventType: config.CreateEvent}] || utils.AllowedEventKindsMap[utils.EventKind{Resource: test.Kind, Namespace: test.Namespace, EventType: config.CreateEvent}] assert.Equal(t, isAllowed, true) diff --git a/test/e2e/utils/utils.go b/test/e2e/utils/utils.go index d0804d541..02717c4fc 100644 --- a/test/e2e/utils/utils.go +++ b/test/e2e/utils/utils.go @@ -4,9 +4,10 @@ import ( "testing" "github.com/infracloudio/botkube/pkg/config" + "github.com/infracloudio/botkube/pkg/notify" "github.com/infracloudio/botkube/pkg/utils" "github.com/nlopes/slack" - "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" extV1beta1 "k8s.io/api/extensions/v1beta1" "k8s.io/apimachinery/pkg/runtime" ) @@ -17,13 +18,21 @@ type SlackMessage struct { Attachments []slack.Attachment } +// WebhookPayload structure +type WebhookPayload struct { + Summary string `json:summary` + EventMeta notify.EventMeta `json:"meta"` + EventStatus notify.EventStatus `json:"status"` +} + // CreateObjects stores specs for creating a k8s fake object and expected Slack response type CreateObjects struct { - Kind string - Namespace string - Specs runtime.Object - NotifType config.NotifType - Expected SlackMessage + Kind string + Namespace string + Specs runtime.Object + NotifType config.NotifType + ExpectedWebhookPayload WebhookPayload + ExpectedSlackMessage SlackMessage } // CreateResource with fake client diff --git a/test/e2e/welcome/welcome.go b/test/e2e/welcome/welcome.go index b89a22904..a826d2f0d 100644 --- a/test/e2e/welcome/welcome.go +++ b/test/e2e/welcome/welcome.go @@ -24,7 +24,7 @@ func (c *context) testWelcome(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.Env.Config.Communications.Slack.Channel, m.Channel) assert.Equal(t, expected, m.Text) diff --git a/test/webhook/server.go b/test/webhook/server.go new file mode 100644 index 000000000..112388c56 --- /dev/null +++ b/test/webhook/server.go @@ -0,0 +1,62 @@ +package webhook + +import ( + "encoding/json" + "log" + "net/http" + "net/http/httptest" + + "github.com/infracloudio/botkube/test/e2e/utils" +) + +// handle chat.postMessage +func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + + decoder := json.NewDecoder(r.Body) + + var t utils.WebhookPayload + + err := decoder.Decode(&t) + if err != nil { + panic(err) + } + + // update message in mutex + s.receivedPayloads.Lock() + s.receivedPayloads.messages = append(s.receivedPayloads.messages, t) + s.receivedPayloads.Unlock() + +} + +// NewTestServer returns a slacktest.Server ready to be started +func NewTestServer() *Server { + + s := &Server{ + receivedPayloads: &payloadCollection{}, + } + httpserver := httptest.NewUnstartedServer(s) + addr := httpserver.Listener.Addr().String() + s.ServerAddr = addr + s.server = httpserver + return s +} + +// Start starts the test server +func (s *Server) Start() { + log.Print("starting Mock Webhook server") + s.server.Start() +} + +// GetAPIURL returns the api url you can pass to webhook +func (s *Server) GetAPIURL() string { + return "http://" + s.ServerAddr + "/" +} + +// GetReceivedPayloads returns all messages received +func (s *Server) GetReceivedPayloads() []utils.WebhookPayload { + s.receivedPayloads.RLock() + m := s.receivedPayloads.messages + s.receivedPayloads.RUnlock() + return m +} diff --git a/test/webhook/types.go b/test/webhook/types.go new file mode 100644 index 000000000..37b092dcd --- /dev/null +++ b/test/webhook/types.go @@ -0,0 +1,21 @@ +package webhook + +import ( + "net/http/httptest" + "sync" + + "github.com/infracloudio/botkube/test/e2e/utils" +) + +// payloadCollection mutex to hold incoming json payloads +type payloadCollection struct { + sync.RWMutex + messages []utils.WebhookPayload +} + +// Server represents a Webhook Test server +type Server struct { + server *httptest.Server + ServerAddr string + receivedPayloads *payloadCollection +}