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 to set the Slack URL in a file #2534

Merged
merged 2 commits into from
Apr 13, 2021
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
13 changes: 13 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,13 @@
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'
21 changes: 21 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,21 @@
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'
5 changes: 4 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ global:

# The API URL to use for Slack notifications.
[ slack_api_url: <secret> ]
[ slack_api_url_file: <filepath> ]
[ victorops_api_key: <secret> ]
[ victorops_api_url: <string> | default = "https://alert.victorops.com/integrations/generic/20131114/alert/" ]
[ pagerduty_url: <string> | default = "https://events.pagerduty.com/v2/enqueue" ]
Expand Down Expand Up @@ -567,8 +568,10 @@ an [attachment](https://api.slack.com/docs/message-attachments).
# Whether or not to notify about resolved alerts.
[ send_resolved: <boolean> | default = false ]

# The Slack webhook URL.
# The Slack webhook URL. Either api_url or api_url_file should be set.
# Defaults to global settings if none are set here.
[ api_url: <secret> | default = global.slack_api_url ]
[ api_url_file: <filepath> | default = global.slack_api_url_file ]

# The channel or user to send notifications to.
channel: <tmpl_string>
Expand Down
2 changes: 2 additions & 0 deletions docs/notification_examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ In this example we've customised our Slack notification to send a URL to our org

```
global:
# Also possible to place this URL in a file.
# Ex: `slack_api_url_file: '/etc/alertmanager/slack_url'`
slack_api_url: '<slack_webhook_url>'

route:
Expand Down
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())
}