Skip to content
Draft
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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
## main / (unreleased)

* [CHANGE] ...
* [CHANGE] slack: Derive the `chat.update` URL from the configured `api_url` base instead of using the hardcoded `https://slack.com/api/chat.update`. Standard configurations are unaffected; deployments that point `api_url` at a Slack-compatible proxy/forwarder will now have their update requests routed to that same host. #5225
* [FEATURE] ...
* [ENHANCEMENT] ...

Expand Down
8 changes: 6 additions & 2 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (
"github.com/prometheus/alertmanager/notify/incidentio"
"github.com/prometheus/alertmanager/notify/jira"
"github.com/prometheus/alertmanager/notify/mattermost"
"github.com/prometheus/alertmanager/notify/slack"
"github.com/prometheus/alertmanager/notify/webhook"
"github.com/prometheus/alertmanager/timeinterval"
"github.com/prometheus/alertmanager/tracing"
Expand Down Expand Up @@ -411,7 +412,7 @@ func (c *Config) UnmarshalYAML(unmarshal func(any) error) error {
}
for _, sc := range rcv.SlackConfigs {
if sc == nil {
sc = &SlackConfig{}
sc = &slack.SlackConfig{}
}
sc.AppURL = cmp.Or(sc.AppURL, c.Global.SlackAppURL)
if sc.AppURL == nil {
Expand All @@ -429,6 +430,9 @@ func (c *Config) UnmarshalYAML(unmarshal func(any) error) error {
if sc.APIURL == nil && len(sc.APIURLFile) == 0 && sc.AppToken == "" && len(sc.AppTokenFile) == 0 {
return errors.New("no Slack API URL nor App token set either inline or in a file")
}
if err := sc.ValidateMessageStrategy(); err != nil {
return err
}
if sc.HTTPConfig == nil {
// we don't want to change the global http config when setting the receiver's http config, do we do a copy
httpconfig := *c.Global.HTTPConfig
Expand Down Expand Up @@ -947,7 +951,7 @@ type Receiver struct {
EmailConfigs []*EmailConfig `yaml:"email_configs,omitempty" json:"email_configs,omitempty"`
IncidentioConfigs []*incidentio.IncidentioConfig `yaml:"incidentio_configs,omitempty" json:"incidentio_configs,omitempty"`
PagerdutyConfigs []*PagerdutyConfig `yaml:"pagerduty_configs,omitempty" json:"pagerduty_configs,omitempty"`
SlackConfigs []*SlackConfig `yaml:"slack_configs,omitempty" json:"slack_configs,omitempty"`
SlackConfigs []*slack.SlackConfig `yaml:"slack_configs,omitempty" json:"slack_configs,omitempty"`
WebhookConfigs []*webhook.WebhookConfig `yaml:"webhook_configs,omitempty" json:"webhook_configs,omitempty"`
OpsGenieConfigs []*OpsGenieConfig `yaml:"opsgenie_configs,omitempty" json:"opsgenie_configs,omitempty"`
WechatConfigs []*WechatConfig `yaml:"wechat_configs,omitempty" json:"wechat_configs,omitempty"`
Expand Down
103 changes: 98 additions & 5 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1072,13 +1072,106 @@ func TestSlackBothAppTokenAndAPIURL(t *testing.T) {
}
}

func TestSlackUpdateMessageWebhookURL(t *testing.T) {
_, err := LoadFile("testdata/conf.slack-update-message-and-webhook.yml")
func TestSlackMessageStrategyWithWrongAPIURL(t *testing.T) {
t.Parallel()

tests := []struct {
name string
file string
strategy string
}{
{
name: "update strategy with webhook URL",
file: "testdata/conf.slack-update-message-and-webhook.yml",
strategy: `update`,
},
{
name: "update strategy with webhook URL (new field)",
file: "testdata/conf.slack-update-message-and-webhook-with-new-field.yml",
strategy: `update`,
},
{
name: "thread strategy with webhook URL",
file: "testdata/conf.slack-thread-message-and-webhook.yml",
strategy: `thread`,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

_, err := LoadFile(tt.file)
if err == nil {
t.Fatal("Expected an error")
}
expectedErrMsg := fmt.Sprintf("message_strategy %q requires a bot token; api_url must be https://slack.com/api/chat.postMessage", tt.strategy)
if err.Error() != expectedErrMsg {
t.Errorf("Expected: %s\nGot: %s", expectedErrMsg, err.Error())
}
})
}
}

func TestSlackMessageStrategyWithGlobalAPIURL(t *testing.T) {
t.Parallel()

_, err := LoadFile("testdata/conf.slack-thread-strategy-with-global-api-url.yml")
if err != nil {
t.Fatalf("Expected no error when message_strategy uses global slack_api_url, got: %s", err)
}
}

func TestSlackMessageStrategyWithoutAPIURL(t *testing.T) {
t.Parallel()

_, err := LoadFile("testdata/conf.slack-thread-strategy-without-api-url.yml")
if err == nil {
t.Fatalf("Expected an error parsing %s: %s", "testdata/conf.slack-update-message-and-webhook", err)
t.Fatal("Expected an error when message_strategy: thread has no api_url at any level")
}
expectedErrMsg := `no Slack API URL nor App token set either inline or in a file`
if err.Error() != expectedErrMsg {
t.Errorf("Expected: %s\nGot: %s", expectedErrMsg, err.Error())
}
}

func TestSlackThreadedOptionsValidation(t *testing.T) {
t.Parallel()

tests := []struct {
name string
file string
expectedErrMsg string
}{
{
name: "threaded options without thread strategy (resolve emoji)",
file: "testdata/conf.slack-thread-resolve-emoji-without-thread-message.yml",
expectedErrMsg: `threaded_options requires message_strategy to be "thread"`,
},
{
name: "threaded options without thread strategy (update parent)",
file: "testdata/conf.slack-thread-update-parent-without-thread-message.yml",
expectedErrMsg: `threaded_options requires message_strategy to be "thread"`,
},
{
name: "resolve color without summary header",
file: "testdata/conf.slack-resolve-color-without-summary-header.yml",
expectedErrMsg: `threaded_options.summary_header requires use_summary_header to be enabled`,
},
}
if err.Error() != "update_message can only be used with bot tokens. api_url must be set to https://slack.com/api/chat.postMessage" {
t.Errorf("Expected: %s\nGot: %s", "update_message can only be used with bot tokens. api_url must be set to https://slack.com/api/chat.postMessage", err.Error())

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

_, err := LoadFile(tt.file)
if err == nil {
t.Fatal("Expected an error")
}
if err.Error() != tt.expectedErrMsg {
t.Errorf("Expected: %s\nGot: %s", tt.expectedErrMsg, err.Error())
}
})
}
}

Expand Down
171 changes: 0 additions & 171 deletions config/notifiers.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,23 +69,6 @@ var (
ClientURL: `{{ template "pagerduty.default.clientURL" . }}`,
}

// DefaultSlackConfig defines default values for Slack configurations.
DefaultSlackConfig = SlackConfig{
NotifierConfig: amcommoncfg.NotifierConfig{
VSendResolved: false,
},
Color: `{{ template "slack.default.color" . }}`,
Username: `{{ template "slack.default.username" . }}`,
Title: `{{ template "slack.default.title" . }}`,
TitleLink: `{{ template "slack.default.titlelink" . }}`,
IconEmoji: `{{ template "slack.default.iconemoji" . }}`,
IconURL: `{{ template "slack.default.iconurl" . }}`,
Pretext: `{{ template "slack.default.pretext" . }}`,
Text: `{{ template "slack.default.text" . }}`,
Fallback: `{{ template "slack.default.fallback" . }}`,
CallbackID: `{{ template "slack.default.callbackid" . }}`,
Footer: `{{ template "slack.default.footer" . }}`,
}
// DefaultRocketchatConfig defines default values for Rocketchat configurations.
DefaultRocketchatConfig = RocketchatConfig{
NotifierConfig: amcommoncfg.NotifierConfig{
Expand Down Expand Up @@ -352,160 +335,6 @@ func (c *PagerdutyConfig) UnmarshalYAML(unmarshal func(any) error) error {
return nil
}

// SlackAction configures a single Slack action that is sent with each notification.
// See https://api.slack.com/docs/message-attachments#action_fields and https://api.slack.com/docs/message-buttons
// for more information.
type SlackAction struct {
Type string `yaml:"type,omitempty" json:"type,omitempty"`
Text string `yaml:"text,omitempty" json:"text,omitempty"`
URL string `yaml:"url,omitempty" json:"url,omitempty"`
Style string `yaml:"style,omitempty" json:"style,omitempty"`
Name string `yaml:"name,omitempty" json:"name,omitempty"`
Value string `yaml:"value,omitempty" json:"value,omitempty"`
ConfirmField *SlackConfirmationField `yaml:"confirm,omitempty" json:"confirm,omitempty"`
}

// UnmarshalYAML implements the yaml.Unmarshaler interface for SlackAction.
func (c *SlackAction) UnmarshalYAML(unmarshal func(any) error) error {
type plain SlackAction
if err := unmarshal((*plain)(c)); err != nil {
return err
}
if c.Type == "" {
return errors.New("missing type in Slack action configuration")
}
if c.Text == "" {
return errors.New("missing text in Slack action configuration")
}
if c.URL != "" {
// Clear all message action fields.
c.Name = ""
c.Value = ""
c.ConfirmField = nil
} else if c.Name != "" {
c.URL = ""
} else {
return errors.New("missing name or url in Slack action configuration")
}
return nil
}

// SlackConfirmationField protect users from destructive actions or particularly distinguished decisions
// by asking them to confirm their button click one more time.
// See https://api.slack.com/docs/interactive-message-field-guide#confirmation_fields for more information.
type SlackConfirmationField struct {
Text string `yaml:"text,omitempty" json:"text,omitempty"`
Title string `yaml:"title,omitempty" json:"title,omitempty"`
OkText string `yaml:"ok_text,omitempty" json:"ok_text,omitempty"`
DismissText string `yaml:"dismiss_text,omitempty" json:"dismiss_text,omitempty"`
}

// UnmarshalYAML implements the yaml.Unmarshaler interface for SlackConfirmationField.
func (c *SlackConfirmationField) UnmarshalYAML(unmarshal func(any) error) error {
type plain SlackConfirmationField
if err := unmarshal((*plain)(c)); err != nil {
return err
}
if c.Text == "" {
return errors.New("missing text in Slack confirmation configuration")
}
return nil
}

// SlackField configures a single Slack field that is sent with each notification.
// Each field must contain a title, value, and optionally, a boolean value to indicate if the field
// is short enough to be displayed next to other fields designated as short.
// See https://api.slack.com/docs/message-attachments#fields for more information.
type SlackField struct {
Title string `yaml:"title,omitempty" json:"title,omitempty"`
Value string `yaml:"value,omitempty" json:"value,omitempty"`
Short *bool `yaml:"short,omitempty" json:"short,omitempty"`
}

// UnmarshalYAML implements the yaml.Unmarshaler interface for SlackField.
func (c *SlackField) UnmarshalYAML(unmarshal func(any) error) error {
type plain SlackField
if err := unmarshal((*plain)(c)); err != nil {
return err
}
if c.Title == "" {
return errors.New("missing title in Slack field configuration")
}
if c.Value == "" {
return errors.New("missing value in Slack field configuration")
}
return nil
}

// SlackConfig configures notifications via Slack.
type SlackConfig struct {
amcommoncfg.NotifierConfig `yaml:",inline" json:",inline"`

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

APIURL *amcommoncfg.SecretURL `yaml:"api_url,omitempty" json:"api_url,omitempty"`
APIURLFile string `yaml:"api_url_file,omitempty" json:"api_url_file,omitempty"`
AppToken commoncfg.Secret `yaml:"app_token,omitempty" json:"app_token,omitempty"`
AppTokenFile string `yaml:"app_token_file,omitempty" json:"app_token_file,omitempty"`
AppURL *amcommoncfg.URL `yaml:"app_url,omitempty" json:"app_url,omitempty"`

// Slack channel override, (like #other-channel or @username).
Channel string `yaml:"channel,omitempty" json:"channel,omitempty"`
Username string `yaml:"username,omitempty" json:"username,omitempty"`
Color string `yaml:"color,omitempty" json:"color,omitempty"`

Title string `yaml:"title,omitempty" json:"title,omitempty"`
TitleLink string `yaml:"title_link,omitempty" json:"title_link,omitempty"`
Pretext string `yaml:"pretext,omitempty" json:"pretext,omitempty"`
Text string `yaml:"text,omitempty" json:"text,omitempty"`
MessageText string `yaml:"message_text,omitempty" json:"message_text,omitempty"`
Fields []*SlackField `yaml:"fields,omitempty" json:"fields,omitempty"`
ShortFields bool `yaml:"short_fields" json:"short_fields,omitempty"`
Footer string `yaml:"footer,omitempty" json:"footer,omitempty"`
Fallback string `yaml:"fallback,omitempty" json:"fallback,omitempty"`
CallbackID string `yaml:"callback_id,omitempty" json:"callback_id,omitempty"`
IconEmoji string `yaml:"icon_emoji,omitempty" json:"icon_emoji,omitempty"`
IconURL string `yaml:"icon_url,omitempty" json:"icon_url,omitempty"`
ImageURL string `yaml:"image_url,omitempty" json:"image_url,omitempty"`
ThumbURL string `yaml:"thumb_url,omitempty" json:"thumb_url,omitempty"`
LinkNames bool `yaml:"link_names" json:"link_names,omitempty"`
MrkdwnIn []string `yaml:"mrkdwn_in,omitempty" json:"mrkdwn_in,omitempty"`
Actions []*SlackAction `yaml:"actions,omitempty" json:"actions,omitempty"`

// UpdateMessage enables updating existing Slack messages instead of creating new ones.
// Requires bot token with chat:write scope. Webhook URLs do not support updates.

UpdateMessage bool `yaml:"update_message" json:"update_message,omitempty"`
// Timeout is the maximum time allowed to invoke the slack. Setting this to 0
// does not impose a timeout.
Timeout time.Duration `yaml:"timeout" json:"timeout"`
}

// UnmarshalYAML implements the yaml.Unmarshaler interface.
func (c *SlackConfig) UnmarshalYAML(unmarshal func(any) error) error {
*c = DefaultSlackConfig
type plain SlackConfig
if err := unmarshal((*plain)(c)); err != nil {
return err
}

if c.APIURL != nil && len(c.APIURLFile) > 0 {
return errors.New("at most one of api_url & api_url_file must be configured")
}
if c.AppToken != "" && len(c.AppTokenFile) > 0 {
return errors.New("at most one of app_token & app_token_file must be configured")
}
if (c.APIURL != nil || len(c.APIURLFile) > 0) && (c.AppToken != "" || len(c.AppTokenFile) > 0) {
return errors.New("at most one of api_url/api_url_file & app_token/app_token_file must be configured")
}

if c.UpdateMessage && c.APIURL.String() != "https://slack.com/api/chat.postMessage" {
return errors.New("update_message can only be used with bot tokens. api_url must be set to https://slack.com/api/chat.postMessage")
}

return nil
}

// WechatConfig configures notifications via Wechat.
type WechatConfig struct {
amcommoncfg.NotifierConfig `yaml:",inline" json:",inline"`
Expand Down
Loading