Repository: influxdata/kapacitor (go)

Issue:
channel() with slack does not work any more
When trying to send an alert to slack channel, it always goes to default channel of the slack webhook.
Ie. calling slack().channel('#xyz') won't have any effect.

I think this is because slack has deprecated legacy webhooks. Incoming webhooks don't support channel property.

Pull request:
feat: enable new-style slack apps
Closes #2470

###### Required for all non-trivial PRs
- [x] Rebased/mergable
- [x] Tests pass
- [x] CHANGELOG.md updated


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0152b763..b41239f8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@
 - [#2544](https://github.com/influxdata/kapacitor/pull/2544): flux tasks skeleton in Kapacitor
 - [#2555](https://github.com/influxdata/kapacitor/pull/2555): run flux tasks with built-in flux engine
 - [#2559](https://github.com/influxdata/kapacitor/pull/2559): kapacitor cli supports flux tasks
+- [#2560](https://github.com/influxdata/kapacitor/pull/2560): enable new-style slack apps
 
 ## v1.5.9 [2021-04-01]
 
diff --git a/services/slack/config.go b/services/slack/config.go
index 3034649a..1153d161 100644
--- a/services/slack/config.go
+++ b/services/slack/config.go
@@ -16,8 +16,15 @@ type Config struct {
 	Default bool `toml:"default" override:"default"`
 	// ID assigned if multiple slack configs are given
 	Workspace string `toml:"workspace"  override:"workspace"`
-	// The Slack webhook URL, can be obtained by adding Incoming Webhook integration.
+	// The Slack webhook URL.
+	// For new-style slack apps, use "https://slack.com/api/chat.postMessage".
+	// For legacy webhooks (e.g. created at https://slack.com/services/new/incoming-webhook), use the webhook link.
 	URL string `toml:"url" override:"url,redact"`
+	// The Slack OAuth token.
+	// For new-style slack apps, go to https://api.slack.com/apps -> your app -> 'OAuth & Permissions' to find
+	// the token. Ensure your app has 'chat:write' and 'chat:write.public' permissions.
+	// For legacy webhooks this can be left blank.
+	Token string `toml:"token" override:"token,redact"`
 	// The default channel, can be overridden per alert.
 	Channel string `toml:"channel" override:"channel"`
 	// The username of the Slack bot.
diff --git a/services/slack/service.go b/services/slack/service.go
index a98fcfbc..a36c1104 100644
--- a/services/slack/service.go
+++ b/services/slack/service.go
@@ -7,7 +7,6 @@ import (
 	"io"
 	"io/ioutil"
 	"net/http"
-
 	"sync"
 
 	"github.com/influxdata/kapacitor/alert"
@@ -196,7 +195,7 @@ func (s *Service) Global() bool {
 }
 
 func (s *Service) StateChangesOnly() bool {
-	return s.defaultConfig().Global
+	return s.defaultConfig().StateChangesOnly
 }
 
 // slack attachment info
@@ -235,7 +234,7 @@ func (s *Service) Test(options interface{}) error {
 }
 
 func (s *Service) Alert(workspace, channel, message, username, iconEmoji string, level alert.Level) error {
-	url, post, err := s.preparePost(workspace, channel, message, username, iconEmoji, level)
+	url, token, post, err := s.preparePost(workspace, channel, message, username, iconEmoji, level)
 	if err != nil {
 		return err
 	}
@@ -245,11 +244,20 @@ func (s *Service) Alert(workspace, channel, message, username, iconEmoji string,
 		return err
 	}
 
-	resp, err := client.Post(url, "application/json", post)
+	req, err := http.NewRequest("POST", url, post)
+	if err != nil {
+		return err
+	}
+	req.Header.Set("Content-Type", "application/json")
+	if token != "" {
+		req.Header.Set("Authorization", "Bearer "+token)
+	}
+	resp, err := client.Do(req)
 	if err != nil {
 		return err
 	}
 	defer resp.Body.Close()
+
 	if resp.StatusCode != http.StatusOK {
 		body, err := ioutil.ReadAll(resp.Body)
 		if err != nil {
@@ -267,13 +275,13 @@ func (s *Service) Alert(workspace, channel, message, username, iconEmoji string,
 	return nil
 }
 
-func (s *Service) preparePost(workspace, channel, message, username, iconEmoji string, level alert.Level) (string, io.Reader, error) {
+func (s *Service) preparePost(workspace, channel, message, username, iconEmoji string, level alert.Level) (string, string, io.Reader, error) {
 	c, err := s.config(workspace)
 	if err != nil {
-		return "", nil, err
+		return "", "", nil, err
 	}
 	if !c.Enabled {
-		return "", nil, errors.New("service is not enabled")
+		return "", "", nil, errors.New("service is not enabled")
 	}
 	if channel == "" {
 		channel = c.Channel
@@ -294,7 +302,6 @@ func (s *Service) preparePost(workspace, channel, message, username, iconEmoji s
 		Mrkdwn_in: []string{"text"},
 	}
 	postData := make(map[string]interface{})
-	postData["as_user"] = false
 	postData["channel"] = channel
 	postData["text"] = ""
 	postData["attachments"] = []attachment{a}
@@ -313,10 +320,10 @@ func (s *Service) preparePost(workspace, channel, message, username, iconEmoji s
 	enc := json.NewEncoder(&post)
 	err = enc.Encode(postData)
 	if err != nil {
-		return "", nil, err
+		return "", "", nil, err
 	}
 
-	return c.URL, &post, nil
+	return c.URL, c.Token, &post, nil
 }
 
 type HandlerConfig struct {


Test patch:
diff --git a/integrations/streamer_test.go b/integrations/streamer_test.go
index ec9c205a..a0bf8782 100644
--- a/integrations/streamer_test.go
+++ b/integrations/streamer_test.go
@@ -22,9 +22,6 @@ import (
 	"text/template"
 	"time"
 
-	"github.com/influxdata/kapacitor/services/discord"
-	"github.com/influxdata/kapacitor/services/discord/discordtest"
-
 	"github.com/davecgh/go-spew/spew"
 	"github.com/docker/docker/api/types/swarm"
 	"github.com/google/go-cmp/cmp"
@@ -45,6 +42,8 @@ import (
 	"github.com/influxdata/kapacitor/services/bigpanda"
 	"github.com/influxdata/kapacitor/services/bigpanda/bigpandatest"
 	"github.com/influxdata/kapacitor/services/diagnostic"
+	"github.com/influxdata/kapacitor/services/discord"
+	"github.com/influxdata/kapacitor/services/discord/discordtest"
 	"github.com/influxdata/kapacitor/services/hipchat"
 	"github.com/influxdata/kapacitor/services/hipchat/hipchattest"
 	"github.com/influxdata/kapacitor/services/httppost"
@@ -8713,6 +8712,7 @@ stream
 		c2.Workspace = "company_private"
 		c2.Enabled = true
 		c2.URL = ts.URL + "/test/slack/url2"
+		c2.Token = "my_secret_token"
 		c2.Channel = "#channel"
 		d := diagService.NewSlackHandler().WithContext(keyvalue.KV("test", "slack"))
 		sl, err := slack.NewService([]slack.Config{c1, c2}, d)
@@ -8725,7 +8725,8 @@ stream
 
 	exp := []interface{}{
 		slacktest.Request{
-			URL: "/test/slack/url",
+			URL:        "/test/slack/url",
+			AuthHeader: "",
 			PostData: slacktest.PostData{
 				Channel:  "@jim",
 				Username: "kapacitor",
@@ -8741,7 +8742,8 @@ stream
 			},
 		},
 		slacktest.Request{
-			URL: "/test/slack/url2",
+			URL:        "/test/slack/url2",
+			AuthHeader: "Bearer my_secret_token",
 			PostData: slacktest.PostData{
 				Channel:  "#alerts",
 				Username: "kapacitor",
diff --git a/server/server_test.go b/server/server_test.go
index b9103ad2..da1fcec5 100644
--- a/server/server_test.go
+++ b/server/server_test.go
@@ -7982,6 +7982,7 @@ func TestServer_UpdateConfig(t *testing.T) {
 						"global":               true,
 						"icon-emoji":           "",
 						"state-changes-only":   false,
+						"token":                false,
 						"url":                  false,
 						"username":             "kapacitor",
 						"ssl-ca":               "",
@@ -7991,6 +7992,7 @@ func TestServer_UpdateConfig(t *testing.T) {
 					},
 					Redacted: []string{
 						"url",
+						"token",
 					},
 				}},
 			},
@@ -8005,6 +8007,7 @@ func TestServer_UpdateConfig(t *testing.T) {
 					"icon-emoji":           "",
 					"state-changes-only":   false,
 					"url":                  false,
+					"token":                false,
 					"username":             "kapacitor",
 					"ssl-ca":               "",
 					"ssl-cert":             "",
@@ -8013,6 +8016,7 @@ func TestServer_UpdateConfig(t *testing.T) {
 				},
 				Redacted: []string{
 					"url",
+					"token",
 				},
 			},
 			updates: []updateAction{
@@ -8025,6 +8029,7 @@ func TestServer_UpdateConfig(t *testing.T) {
 							"channel":   "#general",
 							"username":  slack.DefaultUsername,
 							"url":       "http://slack.example.com/secret-token",
+							"token":     "my_other_secret",
 						},
 					},
 					element: "company_private",
@@ -8041,6 +8046,7 @@ func TestServer_UpdateConfig(t *testing.T) {
 								"icon-emoji":           "",
 								"state-changes-only":   false,
 								"url":                  false,
+								"token":                false,
 								"username":             "kapacitor",
 								"ssl-ca":               "",
 								"ssl-cert":             "",
@@ -8049,6 +8055,7 @@ func TestServer_UpdateConfig(t *testing.T) {
 							},
 							Redacted: []string{
 								"url",
+								"token",
 							},
 						},
 							{
@@ -8062,6 +8069,7 @@ func TestServer_UpdateConfig(t *testing.T) {
 									"icon-emoji":           "",
 									"state-changes-only":   false,
 									"url":                  true,
+									"token":                true,
 									"username":             "kapacitor",
 									"ssl-ca":               "",
 									"ssl-cert":             "",
@@ -8070,6 +8078,7 @@ func TestServer_UpdateConfig(t *testing.T) {
 								},
 								Redacted: []string{
 									"url",
+									"token",
 								},
 							}},
 					},
@@ -8084,6 +8093,7 @@ func TestServer_UpdateConfig(t *testing.T) {
 							"icon-emoji":           "",
 							"state-changes-only":   false,
 							"url":                  true,
+							"token":                true,
 							"username":             "kapacitor",
 							"ssl-ca":               "",
 							"ssl-cert":             "",
@@ -8092,6 +8102,7 @@ func TestServer_UpdateConfig(t *testing.T) {
 						},
 						Redacted: []string{
 							"url",
+							"token",
 						},
 					},
 				},
@@ -8121,6 +8132,7 @@ func TestServer_UpdateConfig(t *testing.T) {
 									"icon-emoji":           "",
 									"state-changes-only":   false,
 									"url":                  false,
+									"token":                false,
 									"username":             "kapacitor",
 									"ssl-ca":               "",
 									"ssl-cert":             "",
@@ -8129,6 +8141,7 @@ func TestServer_UpdateConfig(t *testing.T) {
 								},
 								Redacted: []string{
 									"url",
+									"token",
 								},
 							},
 							{
@@ -8142,6 +8155,7 @@ func TestServer_UpdateConfig(t *testing.T) {
 									"icon-emoji":           "",
 									"state-changes-only":   false,
 									"url":                  true,
+									"token":                true,
 									"username":             "kapacitor",
 									"ssl-ca":               "",
 									"ssl-cert":             "",
@@ -8150,6 +8164,7 @@ func TestServer_UpdateConfig(t *testing.T) {
 								},
 								Redacted: []string{
 									"url",
+									"token",
 								},
 							},
 							{
@@ -8163,6 +8178,7 @@ func TestServer_UpdateConfig(t *testing.T) {
 									"icon-emoji":           "",
 									"state-changes-only":   false,
 									"url":                  true,
+									"token":                false,
 									"username":             "kapacitor",
 									"ssl-ca":               "",
 									"ssl-cert":             "",
@@ -8171,6 +8187,7 @@ func TestServer_UpdateConfig(t *testing.T) {
 								},
 								Redacted: []string{
 									"url",
+									"token",
 								},
 							},
 						},
@@ -8186,6 +8203,7 @@ func TestServer_UpdateConfig(t *testing.T) {
 							"icon-emoji":           "",
 							"state-changes-only":   false,
 							"url":                  true,
+							"token":                false,
 							"username":             "kapacitor",
 							"ssl-ca":               "",
 							"ssl-cert":             "",
@@ -8194,6 +8212,7 @@ func TestServer_UpdateConfig(t *testing.T) {
 						},
 						Redacted: []string{
 							"url",
+							"token",
 						},
 					},
 				},
@@ -8219,6 +8238,7 @@ func TestServer_UpdateConfig(t *testing.T) {
 									"icon-emoji":           "",
 									"state-changes-only":   false,
 									"url":                  false,
+									"token":                false,
 									"username":             "kapacitor",
 									"ssl-ca":               "",
 									"ssl-cert":             "",
@@ -8227,6 +8247,7 @@ func TestServer_UpdateConfig(t *testing.T) {
 								},
 								Redacted: []string{
 									"url",
+									"token",
 								},
 							},
 							{
@@ -8240,6 +8261,7 @@ func TestServer_UpdateConfig(t *testing.T) {
 									"icon-emoji":           "",
 									"state-changes-only":   false,
 									"url":                  true,
+									"token":                true,
 									"username":             "kapacitor",
 									"ssl-ca":               "",
 									"ssl-cert":             "",
@@ -8248,6 +8270,7 @@ func TestServer_UpdateConfig(t *testing.T) {
 								},
 								Redacted: []string{
 									"url",
+									"token",
 								},
 							},
 							{
@@ -8261,6 +8284,7 @@ func TestServer_UpdateConfig(t *testing.T) {
 									"icon-emoji":           "",
 									"state-changes-only":   false,
 									"url":                  true,
+									"token":                false,
 									"username":             "testbot",
 									"ssl-ca":               "",
 									"ssl-cert":             "",
@@ -8269,6 +8293,7 @@ func TestServer_UpdateConfig(t *testing.T) {
 								},
 								Redacted: []string{
 									"url",
+									"token",
 								},
 							},
 						},
@@ -8284,6 +8309,7 @@ func TestServer_UpdateConfig(t *testing.T) {
 							"icon-emoji":           "",
 							"state-changes-only":   false,
 							"url":                  true,
+							"token":                false,
 							"username":             "testbot",
 							"ssl-ca":               "",
 							"ssl-cert":             "",
@@ -8292,6 +8318,7 @@ func TestServer_UpdateConfig(t *testing.T) {
 						},
 						Redacted: []string{
 							"url",
+							"token",
 						},
 					},
 				},
@@ -8314,6 +8341,7 @@ func TestServer_UpdateConfig(t *testing.T) {
 									"icon-emoji":           "",
 									"state-changes-only":   false,
 									"url":                  false,
+									"token":                false,
 									"username":             "kapacitor",
 									"ssl-ca":               "",
 									"ssl-cert":             "",
@@ -8322,6 +8350,7 @@ func TestServer_UpdateConfig(t *testing.T) {
 								},
 								Redacted: []string{
 									"url",
+									"token",
 								},
 							},
 							{
@@ -8335,6 +8364,7 @@ func TestServer_UpdateConfig(t *testing.T) {
 									"icon-emoji":           "",
 									"state-changes-only":   false,
 									"url":                  true,
+									"token":                true,
 									"username":             "kapacitor",
 									"ssl-ca":               "",
 									"ssl-cert":             "",
@@ -8343,6 +8373,7 @@ func TestServer_UpdateConfig(t *testing.T) {
 								},
 								Redacted: []string{
 									"url",
+									"token",
 								},
 							},
 							{
@@ -8356,6 +8387,7 @@ func TestServer_UpdateConfig(t *testing.T) {
 									"icon-emoji":           "",
 									"state-changes-only":   false,
 									"url":                  true,
+									"token":                false,
 									"username":             "",
 									"ssl-ca":               "",
 									"ssl-cert":             "",
@@ -8364,6 +8396,7 @@ func TestServer_UpdateConfig(t *testing.T) {
 								},
 								Redacted: []string{
 									"url",
+									"token",
 								},
 							},
 						},
@@ -8379,6 +8412,7 @@ func TestServer_UpdateConfig(t *testing.T) {
 							"icon-emoji":           "",
 							"state-changes-only":   false,
 							"url":                  true,
+							"token":                false,
 							"username":             "",
 							"ssl-ca":               "",
 							"ssl-cert":             "",
@@ -8387,6 +8421,7 @@ func TestServer_UpdateConfig(t *testing.T) {
 						},
 						Redacted: []string{
 							"url",
+							"token",
 						},
 					},
 				},
diff --git a/services/slack/slacktest/slacktest.go b/services/slack/slacktest/slacktest.go
index 9157de67..2b9f00bd 100644
--- a/services/slack/slacktest/slacktest.go
+++ b/services/slack/slacktest/slacktest.go
@@ -19,7 +19,8 @@ func NewServer() *Server {
 	s := new(Server)
 	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		sr := Request{
-			URL: r.URL.String(),
+			URL:        r.URL.String(),
+			AuthHeader: r.Header.Get("Authorization"),
 		}
 		dec := json.NewDecoder(r.Body)
 		dec.Decode(&sr.PostData)
@@ -45,8 +46,9 @@ func (s *Server) Close() {
 }
 
 type Request struct {
-	URL      string
-	PostData PostData
+	URL        string
+	AuthHeader string
+	PostData   PostData
 }
 
 type PostData struct {