Skip to content

Commit

Permalink
Add support to set the Slack URL in a file
Browse files Browse the repository at this point in the history
- Added support for the file in both the global and the lower level
- Tried to follow configuration patterns I saw in prometheus
- The slack file is read on every request as mentioned in the prometheus issue to enable seamless switches

prometheus#2498
  • Loading branch information
julienduchesne committed Apr 2, 2021
1 parent bc7b16d commit 61b7b7c
Show file tree
Hide file tree
Showing 8 changed files with 149 additions and 8 deletions.
12 changes: 9 additions & 3 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,10 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
*c.Global = DefaultGlobalConfig()
}

if c.Global.SlackAPIURL != nil && len(c.Global.SlackAPIURLFile) > 0 {
return fmt.Errorf("at most one of slack_api_url & slack_api_url_file must be configured")
}

names := map[string]struct{}{}

for _, rcv := range c.Receivers {
Expand Down Expand Up @@ -349,11 +353,12 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
if sc.HTTPConfig == nil {
sc.HTTPConfig = c.Global.HTTPConfig
}
if sc.APIURL == nil {
if c.Global.SlackAPIURL == nil {
return fmt.Errorf("no global Slack API URL set")
if sc.APIURL == nil && len(sc.APIURLFile) == 0 {
if c.Global.SlackAPIURL == nil && len(c.Global.SlackAPIURLFile) == 0 {
return fmt.Errorf("no global Slack API URL set either inline or in a file")
}
sc.APIURL = c.Global.SlackAPIURL
sc.APIURLFile = c.Global.SlackAPIURLFile
}
}
for _, poc := range rcv.PushoverConfigs {
Expand Down Expand Up @@ -632,6 +637,7 @@ type GlobalConfig struct {
SMTPAuthIdentity string `yaml:"smtp_auth_identity,omitempty" json:"smtp_auth_identity,omitempty"`
SMTPRequireTLS bool `yaml:"smtp_require_tls" json:"smtp_require_tls,omitempty"`
SlackAPIURL *SecretURL `yaml:"slack_api_url,omitempty" json:"slack_api_url,omitempty"`
SlackAPIURLFile string `yaml:"slack_api_url_file,omitempty" json:"slack_api_url_file,omitempty"`
PagerdutyURL *URL `yaml:"pagerduty_url,omitempty" json:"pagerduty_url,omitempty"`
OpsGenieAPIURL *URL `yaml:"opsgenie_api_url,omitempty" json:"opsgenie_api_url,omitempty"`
OpsGenieAPIKey Secret `yaml:"opsgenie_api_key,omitempty" json:"opsgenie_api_key,omitempty"`
Expand Down
45 changes: 45 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -859,6 +859,51 @@ func TestOpsGenieDeprecatedTeamSpecified(t *testing.T) {
}
}

func TestSlackBothAPIURLAndFile(t *testing.T) {
_, err := LoadFile("testdata/conf.slack-both-file-and-url.yml")
if err == nil {
t.Fatalf("Expected an error parsing %s: %s", "testdata/conf.slack-both-file-and-url.yml", err)
}
if err.Error() != "at most one of slack_api_url & slack_api_url_file must be configured" {
t.Errorf("Expected: %s\nGot: %s", "at most one of slack_api_url & slack_api_url_file must be configured", err.Error())
}
}

func TestSlackNoAPIURL(t *testing.T) {
_, err := LoadFile("testdata/conf.slack-no-api-url.yml")
if err == nil {
t.Fatalf("Expected an error parsing %s: %s", "testdata/conf.slack-no-api-url.yml", err)
}
if err.Error() != "no global Slack API URL set either inline or in a file" {
t.Errorf("Expected: %s\nGot: %s", "no global Slack API URL set either inline or in a file", err.Error())
}
}

func TestSlackGlobalAPIURLFile(t *testing.T) {
conf, err := LoadFile("testdata/conf.slack-default-api-url-file.yml")
if err != nil {
t.Fatalf("Error parsing %s: %s", "testdata/conf.slack-default-api-url-file.yml", err)
}

// no override
firstConfig := conf.Receivers[0].SlackConfigs[0]
if firstConfig.APIURLFile != "/global_file" || firstConfig.APIURL != nil {
t.Fatalf("Invalid Slack URL file: %s\nExpected: %s", firstConfig.APIURLFile, "/global_file")
}

// override the file
secondConfig := conf.Receivers[0].SlackConfigs[1]
if secondConfig.APIURLFile != "/override_file" || secondConfig.APIURL != nil {
t.Fatalf("Invalid Slack URL file: %s\nExpected: %s", secondConfig.APIURLFile, "/override_file")
}

// override the global file with an inline URL
thirdConfig := conf.Receivers[0].SlackConfigs[2]
if thirdConfig.APIURL.String() != "http://mysecret.example.com/" || thirdConfig.APIURLFile != "" {
t.Fatalf("Invalid Slack URL: %s\nExpected: %s", thirdConfig.APIURL.String(), "http://mysecret.example.com/")
}
}

func TestUnmarshalHostPort(t *testing.T) {
for _, tc := range []struct {
in string
Expand Down
16 changes: 13 additions & 3 deletions config/notifiers.go
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,8 @@ type SlackConfig struct {

HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"`

APIURL *SecretURL `yaml:"api_url,omitempty" json:"api_url,omitempty"`
APIURL *SecretURL `yaml:"api_url,omitempty" json:"api_url,omitempty"`
APIURLFile string `yaml:"api_url_file,omitempty" json:"api_url_file,omitempty"`

// Slack channel override, (like #other-channel or @username).
Channel string `yaml:"channel,omitempty" json:"channel,omitempty"`
Expand Down Expand Up @@ -357,7 +358,15 @@ type SlackConfig struct {
func (c *SlackConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
*c = DefaultSlackConfig
type plain SlackConfig
return unmarshal((*plain)(c))
if err := unmarshal((*plain)(c)); err != nil {
return err
}

if c.APIURL != nil && len(c.APIURLFile) > 0 {
return fmt.Errorf("at most one of api_url & api_url_file must be configured")
}

return nil
}

// WebhookConfig configures notifications via a generic webhook.
Expand Down Expand Up @@ -490,7 +499,8 @@ type VictorOpsConfig struct {

HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"`

APIKey Secret `yaml:"api_key" json:"api_key"`
APIKey Secret `yaml:"api_key,omitempty" json:"api_key,omitempty"`
APIKeyFile Secret `yaml:"api_key_file,omitempty" json:"api_key_file,omitempty"`
APIURL *URL `yaml:"api_url" json:"api_url"`
RoutingKey string `yaml:"routing_key" json:"routing_key"`
MessageType string `yaml:"message_type" json:"message_type"`
Expand Down
14 changes: 14 additions & 0 deletions config/testdata/conf.slack-both-file-and-url.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
global:
slack_api_url: "http://mysecret.example.com/"
slack_api_url_file: '/global_file'

route:
receiver: 'slack-notifications'
group_by: [alertname, datacenter, app]

receivers:
- name: 'slack-notifications'
slack_configs:
- channel: '#alerts1'
text: 'test'

22 changes: 22 additions & 0 deletions config/testdata/conf.slack-default-api-url-file.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
global:
slack_api_url_file: '/global_file'

route:
receiver: 'slack-notifications'
group_by: [alertname, datacenter, app]

receivers:
- name: 'slack-notifications'
slack_configs:
# Use global
- channel: '#alerts1'
text: 'test'
# Override global with other file
- channel: '#alerts2'
text: 'test'
api_url_file: '/override_file'
# Override global with inline URL
- channel: '#alerts3'
text: 'test'
api_url: 'http://mysecret.example.com/'

9 changes: 9 additions & 0 deletions config/testdata/conf.slack-no-api-url.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
route:
receiver: 'slack-notifications'
group_by: [alertname, datacenter, app]

receivers:
- name: 'slack-notifications'
slack_configs:
- channel: '#alerts'
text: 'test'
16 changes: 14 additions & 2 deletions notify/slack/slack.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/pkg/errors"
"io/ioutil"
"net/http"

"github.com/pkg/errors"

"github.com/go-kit/kit/log"
commoncfg "github.com/prometheus/common/config"

Expand Down Expand Up @@ -175,7 +177,17 @@ func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error)
return false, err
}

u := n.conf.APIURL.String()
var u string
if n.conf.APIURL != nil {
u = n.conf.APIURL.String()
} else {
content, err := ioutil.ReadFile(n.conf.APIURLFile)
if err != nil {
return false, err
}
u = string(content)
}

resp, err := notify.PostJSON(ctx, n.client, u, &buf)
if err != nil {
return true, notify.RedactURL(err)
Expand Down
23 changes: 23 additions & 0 deletions notify/slack/slack_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ package slack

import (
"fmt"
"io/ioutil"
"testing"

"github.com/go-kit/kit/log"
Expand Down Expand Up @@ -57,3 +58,25 @@ func TestSlackRedactedURL(t *testing.T) {

test.AssertNotifyLeaksNoSecret(t, ctx, notifier, u.String())
}

func TestGettingSlackURLFromFile(t *testing.T) {
ctx, u, fn := test.GetContextWithCancelingURL()
defer fn()

f, err := ioutil.TempFile("", "slack_test")
require.NoError(t, err, "creating temp file failed")
_, err = f.WriteString(u.String())
require.NoError(t, err, "writing to temp file failed")

notifier, err := New(
&config.SlackConfig{
APIURLFile: f.Name(),
HTTPConfig: &commoncfg.HTTPClientConfig{},
},
test.CreateTmpl(t),
log.NewNopLogger(),
)
require.NoError(t, err)

test.AssertNotifyLeaksNoSecret(t, ctx, notifier, u.String())
}

0 comments on commit 61b7b7c

Please sign in to comment.