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

feat: implement webhook_url_file for discord and msteams #3555

Merged
merged 2 commits into from
Feb 7, 2024
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
34 changes: 30 additions & 4 deletions config/notifiers.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,8 +216,9 @@ func (c *WebexConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
type DiscordConfig struct {
NotifierConfig `yaml:",inline" json:",inline"`

HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"`
WebhookURL *SecretURL `yaml:"webhook_url,omitempty" json:"webhook_url,omitempty"`
HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"`
WebhookURL *SecretURL `yaml:"webhook_url,omitempty" json:"webhook_url,omitempty"`
WebhookURLFile string `yaml:"webhook_url_file,omitempty" json:"webhook_url_file,omitempty"`

Title string `yaml:"title,omitempty" json:"title,omitempty"`
Message string `yaml:"message,omitempty" json:"message,omitempty"`
Expand All @@ -227,7 +228,19 @@ type DiscordConfig struct {
func (c *DiscordConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
*c = DefaultDiscordConfig
type plain DiscordConfig
return unmarshal((*plain)(c))
if err := unmarshal((*plain)(c)); err != nil {
return err
}

if c.WebhookURL == nil && c.WebhookURLFile == "" {
return fmt.Errorf("one of webhook_url or webhook_url_file must be configured")
}

if c.WebhookURL != nil && len(c.WebhookURLFile) > 0 {
return fmt.Errorf("at most one of webhook_url & webhook_url_file must be configured")
}

return nil
}

// EmailConfig configures notifications via mail.
Expand Down Expand Up @@ -787,6 +800,7 @@ type MSTeamsConfig struct {
NotifierConfig `yaml:",inline" json:",inline"`
HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"`
WebhookURL *SecretURL `yaml:"webhook_url,omitempty" json:"webhook_url,omitempty"`
WebhookURLFile string `yaml:"webhook_url_file,omitempty" json:"webhook_url_file,omitempty"`

Title string `yaml:"title,omitempty" json:"title,omitempty"`
Text string `yaml:"text,omitempty" json:"text,omitempty"`
Expand All @@ -795,5 +809,17 @@ type MSTeamsConfig struct {
func (c *MSTeamsConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
*c = DefaultMSTeamsConfig
type plain MSTeamsConfig
return unmarshal((*plain)(c))
if err := unmarshal((*plain)(c)); err != nil {
return err
}

if c.WebhookURL == nil && c.WebhookURLFile == "" {
return fmt.Errorf("one of webhook_url or webhook_url_file must be configured")
}

if c.WebhookURL != nil && len(c.WebhookURLFile) > 0 {
return fmt.Errorf("at most one of webhook_url & webhook_url_file must be configured")
}

return nil
}
4 changes: 4 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -666,7 +666,9 @@ Discord notifications are sent via the [Discord webhook API](https://discord.com
[ send_resolved: <boolean> | default = true ]

# The Discord webhook URL.
# webhook_url and webhook_url_file are mutually exclusive.
webhook_url: <secret>
webhook_url_file: <filepath>

# Message title template.
[ title: <tmpl_string> | default = '{{ template "discord.default.title" . }}' ]
Expand Down Expand Up @@ -731,7 +733,9 @@ Microsoft Teams notifications are sent via the [Incoming Webhooks](https://learn
[ send_resolved: <boolean> | default = true ]

# The incoming webhook URL.
# webhook_url and webhook_url_file are mutually exclusive.
[ webhook_url: <secret> ]
[ webhook_url_file: <filepath> ]

# Message title template.
[ title: <tmpl_string> | default = '{{ template "teams.default.title" . }}' ]
Expand Down
16 changes: 15 additions & 1 deletion notify/discord/discord.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"strings"

"github.com/go-kit/log"
"github.com/go-kit/log/level"
Expand Down Expand Up @@ -107,6 +110,17 @@ func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error)
color = colorGreen
}

var url string
if n.conf.WebhookURL != nil {
url = n.conf.WebhookURL.String()
} else {
content, err := os.ReadFile(n.conf.WebhookURLFile)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bit of a nit, but perhaps call this b instead of content. Two reasons:

  1. It's short.
  2. It's a somewhat idiomatic way of telling other maintainers that this variable is a byte slice instead of a string without having to check the docs for os.ReadFile.
Suggested change
content, err := os.ReadFile(n.conf.WebhookURLFile)
b, err := os.ReadFile(n.conf.WebhookURLFile)

if err != nil {
return false, fmt.Errorf("read webhook_url_file: %w", err)
}
url = strings.TrimSpace(string(content))
}

w := webhook{
Embeds: []webhookEmbed{{
Title: title,
Expand All @@ -120,7 +134,7 @@ func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error)
return false, err
}

resp, err := notify.PostJSON(ctx, n.client, n.webhookURL.String(), &payload)
resp, err := notify.PostJSON(ctx, n.client, url, &payload)
if err != nil {
return true, notify.RedactURL(err)
}
Expand Down
41 changes: 41 additions & 0 deletions notify/discord/discord_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"net/http"
"net/http/httptest"
"net/url"
"os"
"testing"
"time"

Expand Down Expand Up @@ -127,3 +128,43 @@ func TestDiscordTemplating(t *testing.T) {
})
}
}

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

secret := "secret"
notifier, err := New(
&config.DiscordConfig{
WebhookURL: &config.SecretURL{URL: u},
HTTPConfig: &commoncfg.HTTPClientConfig{},
},
test.CreateTmpl(t),
log.NewNopLogger(),
)
require.NoError(t, err)

test.AssertNotifyLeaksNoSecret(ctx, t, notifier, secret)
}

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

f, err := os.CreateTemp("", "webhook_url")
require.NoError(t, err, "creating temp file failed")
_, err = f.WriteString(u.String() + "\n")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good test, to check for removing whitespace! 👍

require.NoError(t, err, "writing to temp file failed")

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

test.AssertNotifyLeaksNoSecret(ctx, t, notifier, u.String())
}
16 changes: 15 additions & 1 deletion notify/msteams/msteams.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@ import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"

"github.com/go-kit/log"
"github.com/go-kit/log/level"
Expand Down Expand Up @@ -108,6 +111,17 @@ func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error)
color = colorGreen
}

var url string
if n.conf.WebhookURL != nil {
url = n.conf.WebhookURL.String()
} else {
content, err := os.ReadFile(n.conf.WebhookURLFile)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
content, err := os.ReadFile(n.conf.WebhookURLFile)
b, err := os.ReadFile(n.conf.WebhookURLFile)

if err != nil {
return false, fmt.Errorf("read webhook_url_file: %w", err)
}
url = strings.TrimSpace(string(content))
}

t := teamsMessage{
Context: "http://schema.org/extensions",
Type: "MessageCard",
Expand All @@ -121,7 +135,7 @@ func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error)
return false, err
}

resp, err := n.postJSONFunc(ctx, n.client, n.webhookURL.String(), &payload)
resp, err := n.postJSONFunc(ctx, n.client, url, &payload)
if err != nil {
return true, notify.RedactURL(err)
}
Expand Down
41 changes: 41 additions & 0 deletions notify/msteams/msteams_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"net/http"
"net/http/httptest"
"net/url"
"os"
"testing"
"time"

Expand Down Expand Up @@ -182,3 +183,43 @@ func TestNotifier_Notify_WithReason(t *testing.T) {
})
}
}

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

secret := "secret"
notifier, err := New(
&config.MSTeamsConfig{
WebhookURL: &config.SecretURL{URL: u},
HTTPConfig: &commoncfg.HTTPClientConfig{},
},
test.CreateTmpl(t),
log.NewNopLogger(),
)
require.NoError(t, err)

test.AssertNotifyLeaksNoSecret(ctx, t, notifier, secret)
}

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

f, err := os.CreateTemp("", "webhook_url")
require.NoError(t, err, "creating temp file failed")
_, err = f.WriteString(u.String() + "\n")
require.NoError(t, err, "writing to temp file failed")

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

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