From b141c7c687fd0578daefaa2bfca1639cdd225503 Mon Sep 17 00:00:00 2001
From: Abdelrahman Ahmed
diff --git a/alertmanager/slack/slack.go b/alertmanager/slack/slack.go index 4cefc1d5..06694706 100644 --- a/alertmanager/slack/slack.go +++ b/alertmanager/slack/slack.go @@ -1,6 +1,7 @@ package slack import ( + "context" "fmt" "strings" @@ -17,41 +18,60 @@ const ( ) type Slack struct { - webhook string title string text string - - // used by legacy webhook to send messages to specific channel, - // instead of default one channel string + appCfg *config.App - // reference for general app configuration - appCfg *config.App + // webhook mode + webhook string + send func(url string, msg *slackClient.WebhookMessage) error - send func(url string, msg *slackClient.WebhookMessage) error + // token mode + token string + apiClient *slackClient.Client } // NewSlack returns new Slack instance func NewSlack(config map[string]interface{}, appCfg *config.App) *Slack { + title, _ := config["title"].(string) + text, _ := config["text"].(string) + + // token mode: requires token + channel + token, hasToken := config["token"].(string) + channel, hasChannel := config["channel"].(string) + if hasToken && len(token) > 0 { + if !hasChannel || len(channel) == 0 { + logrus.Warnf("initializing slack with token but missing channel") + return nil + } + logrus.Infof("initializing slack with token and channel: %s", channel) + return &Slack{ + token: token, + channel: channel, + title: title, + text: text, + appCfg: appCfg, + apiClient: slackClient.New(token), + } + } + + // webhook mode: requires webhook webhook, ok := config["webhook"].(string) if !ok || len(webhook) == 0 { - logrus.Warnf("initializing slack with empty webhook url") + logrus.Warnf("initializing slack with empty webhook url and no token") return nil } logrus.Infof("initializing slack with webhook url: %s", webhook) - channel, _ := config["channel"].(string) - title, _ := config["title"].(string) - text, _ := config["text"].(string) - return &Slack{ webhook: webhook, channel: channel, title: title, text: text, - send: slackClient.PostWebhook, appCfg: appCfg, + send: slackClient.PostWebhook, } } @@ -132,12 +152,31 @@ func (s *Slack) SendMessage(msg string) error { } func (s *Slack) sendAPI(msg *slackClient.WebhookMessage) error { + if s.apiClient != nil { + return s.sendAPIWithToken(msg) + } if len(s.channel) > 0 { msg.Channel = s.channel } return s.send(s.webhook, msg) } +func (s *Slack) sendAPIWithToken(msg *slackClient.WebhookMessage) error { + opts := []slackClient.MsgOption{} + if len(msg.Text) > 0 { + opts = append(opts, slackClient.MsgOptionText(msg.Text, false)) + } + if msg.Blocks != nil { + opts = append(opts, slackClient.MsgOptionBlocks(msg.Blocks.BlockSet...)) + } + _, _, err := s.apiClient.PostMessageContext( + context.Background(), + s.channel, + opts..., + ) + return err +} + func chunks(s string, chunkSize int) []string { if chunkSize >= len(s) { return []string{s} diff --git a/alertmanager/slack/slack_test.go b/alertmanager/slack/slack_test.go index 895283eb..929d0d61 100644 --- a/alertmanager/slack/slack_test.go +++ b/alertmanager/slack/slack_test.go @@ -12,6 +12,9 @@ import ( func mockedSend(url string, msg *slackClient.WebhookMessage) error { return nil } + +// --- webhook mode tests --- + func TestSlackEmptyConfig(t *testing.T) { assert := assert.New(t) @@ -19,7 +22,7 @@ func TestSlackEmptyConfig(t *testing.T) { assert.Nil(s) } -func TestSlack(t *testing.T) { +func TestSlackWebhook(t *testing.T) { assert := assert.New(t) configMap := map[string]interface{}{ @@ -27,11 +30,22 @@ func TestSlack(t *testing.T) { } s := NewSlack(configMap, &config.App{ClusterName: "dev"}) assert.NotNil(s) + assert.Equal("Slack", s.Name()) +} - assert.Equal(s.Name(), "Slack") +func TestSlackWebhookWithChannel(t *testing.T) { + assert := assert.New(t) + + configMap := map[string]interface{}{ + "webhook": "testtest", + "channel": "#alerts", + } + s := NewSlack(configMap, &config.App{ClusterName: "dev"}) + assert.NotNil(s) + assert.Equal("#alerts", s.channel) } -func TestSendMessage(t *testing.T) { +func TestSendMessageWebhook(t *testing.T) { assert := assert.New(t) s := NewSlack(map[string]interface{}{ @@ -44,7 +58,7 @@ func TestSendMessage(t *testing.T) { assert.Nil(s.SendMessage("test")) } -func TestSendEvent(t *testing.T) { +func TestSendEventWebhook(t *testing.T) { assert := assert.New(t) s := NewSlack(map[string]interface{}{ @@ -54,50 +68,151 @@ func TestSendEvent(t *testing.T) { s.send = mockedSend - ev := event.Event{ + ev := &event.Event{ NodeName: "test-node", PodName: "test-pod", ContainerName: "test-container", Namespace: "default", Reason: "OOMKILLED", - Logs: "Nam quis nulla. Integer malesuada. In in enim a arcu " + - "imperdiet malesuada. Sed vel lectus. Donec odio urna, tempus " + - "molestie, porttitor ut, iaculis quis, sem. Phasellus rhoncus.\n" + - "Nam quis nulla. Integer malesuada. In in enim a arcu " + - "imperdiet malesuada. Sed vel lectus. Donec odio urna, tempus " + - "molestie, porttitor ut, iaculis quis, sem. Phasellus rhoncus.\n" + - "Nam quis nulla. Integer malesuada. In in enim a arcu " + - "imperdiet malesuada. Sed vel lectus. Donec odio urna, tempus " + - "molestie, porttitor ut, iaculis quis, sem. Phasellus rhoncus.\n" + - "Nam quis nulla. Integer malesuada. In in enim a arcu " + - "imperdiet malesuada. Sed vel lectus. Donec odio urna, tempus " + - "molestie, porttitor ut, iaculis quis, sem. Phasellus rhoncus.\n" + - "Nam quis nulla. Integer malesuada. In in enim a arcu " + - "imperdiet malesuada. Sed vel lectus. Donec odio urna, tempus " + - "molestie, porttitor ut, iaculis quis, sem. Phasellus rhoncus.\n" + - "Nam quis nulla. Integer malesuada. In in enim a arcu " + - "imperdiet malesuada. Sed vel lectus. Donec odio urna, tempus " + - "molestie, porttitor ut, iaculis quis, sem. Phasellus rhoncus.\n" + - "Nam quis nulla. Integer malesuada. In in enim a arcu " + - "imperdiet malesuada. Sed vel lectus. Donec odio urna, tempus " + - "molestie, porttitor ut, iaculis quis, sem. Phasellus rhoncus.\n" + - "Nam quis nulla. Integer malesuada. In in enim a arcu " + - "imperdiet malesuada. Sed vel lectus. Donec odio urna, tempus " + - "molestie, porttitor ut, iaculis quis, sem. Phasellus rhoncus.\n" + - "Nam quis nulla. Integer malesuada. In in enim a arcu " + - "imperdiet malesuada. Sed vel lectus. Donec odio urna, tempus " + - "molestie, porttitor ut, iaculis quis, sem. Phasellus rhoncus.\n" + - "Nam quis nulla. Integer malesuada. In in enim a arcu " + - "imperdiet malesuada. Sed vel lectus. Donec odio urna, tempus " + - "molestie, porttitor ut, iaculis quis, sem. Phasellus rhoncus.\n" + - "Nam quis nulla. Integer malesuada. In in enim a arcu " + - "imperdiet malesuada. Sed vel lectus. Donec odio urna, tempus " + - "molestie, porttitor ut, iaculis quis, sem. Phasellus rhoncus.\n" + - "Nam quis nulla. Integer malesuada. In in enim a arcu " + - "imperdiet malesuada. Sed vel lectus. Donec odio urna, tempus " + - "molestie, porttitor ut, iaculis quis, sem. Phasellus rhoncus.\n", - Events: "BackOff Back-off restarting failed container\n" + - "event3\nevent5\nevent6-event8-event11-event12", + Logs: "some log line 1\nsome log line 2\nsome log line 3", + Events: "BackOff Back-off restarting failed container\nevent3\nevent5", + } + assert.Nil(s.SendEvent(ev)) +} + +func TestSendEventWebhookWithLargeLogs(t *testing.T) { + assert := assert.New(t) + + s := NewSlack(map[string]interface{}{ + "webhook": "testtest", + }, &config.App{ClusterName: "dev"}) + assert.NotNil(s) + + s.send = mockedSend + + // generate logs larger than chunkSize (2000) + longLog := "" + for i := 0; i < 500; i++ { + longLog += "Nam quis nulla. Integer malesuada. In in enim a arcu imperdiet.\n" } - assert.Nil(s.SendEvent(&ev)) + + ev := &event.Event{ + NodeName: "test-node", + PodName: "test-pod", + ContainerName: "test-container", + Namespace: "default", + Reason: "OOMKILLED", + Logs: longLog, + } + assert.Nil(s.SendEvent(ev)) +} + +// --- token mode tests --- + +func TestSlackTokenMode(t *testing.T) { + assert := assert.New(t) + + configMap := map[string]interface{}{ + "token": "xoxb-test-token", + "channel": "#alerts", + } + s := NewSlack(configMap, &config.App{ClusterName: "dev"}) + assert.NotNil(s) + assert.Equal("Slack", s.Name()) + assert.Equal("#alerts", s.channel) + assert.NotNil(s.apiClient) + assert.Empty(s.webhook) +} + +func TestSlackTokenMissingChannel(t *testing.T) { + assert := assert.New(t) + + configMap := map[string]interface{}{ + "token": "xoxb-test-token", + } + s := NewSlack(configMap, &config.App{ClusterName: "dev"}) + assert.Nil(s) +} + +func TestSlackTokenEmptyChannel(t *testing.T) { + assert := assert.New(t) + + configMap := map[string]interface{}{ + "token": "xoxb-test-token", + "channel": "", + } + s := NewSlack(configMap, &config.App{ClusterName: "dev"}) + assert.Nil(s) +} + +func TestSlackWebhookPreferWebhookOverToken(t *testing.T) { + assert := assert.New(t) + + configMap := map[string]interface{}{ + "webhook": "https://hooks.slack.com/test", + "token": "", + "channel": "#alerts", + } + s := NewSlack(configMap, &config.App{ClusterName: "dev"}) + assert.NotNil(s) + // Empty token should fall through to webhook mode + assert.Equal("https://hooks.slack.com/test", s.webhook) + assert.Nil(s.apiClient) +} + +func TestSendMessageTokenMode(t *testing.T) { + assert := assert.New(t) + + s := NewSlack(map[string]interface{}{ + "token": "xoxb-test-token", + "channel": "#alerts", + }, &config.App{ClusterName: "dev"}) + assert.NotNil(s) + + // sendAPIWithToken will fail because the token is fake, + // but we're testing the dispatch path + err := s.SendMessage("test message") + assert.Error(err) // fake token, API call fails +} + +func TestSendMessageWebhookMode(t *testing.T) { + assert := assert.New(t) + + s := NewSlack(map[string]interface{}{ + "webhook": "testtest", + }, &config.App{ClusterName: "dev"}) + assert.NotNil(s) + + s.send = mockedSend + assert.Nil(s.SendMessage("test message")) +} + +// --- helper tests --- + +func TestChunks(t *testing.T) { + assert := assert.New(t) + + result := chunks("abc", 5) + assert.Equal([]string{"abc"}, result) + + result = chunks("abcdef", 3) + assert.Equal([]string{"abc", "def"}, result) + + result = chunks("abcdefg", 3) + assert.Equal([]string{"abc", "def", "g"}, result) +} + +func TestMarkdownSection(t *testing.T) { + block := markdownSection("test") + assert.Equal(t, slackClient.MBTSection, block.Type) +} + +func TestPlainSection(t *testing.T) { + block := plainSection("test") + assert.Equal(t, slackClient.MBTSection, block.Type) +} + +func TestMarkdownF(t *testing.T) { + obj := markdownF("*%s*", "test") + assert.NotNil(t, obj) }