Skip to content
Open
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 @@ -404,6 +404,10 @@ func (c *Config) UnmarshalYAML(unmarshal func(any) error) error {
return errors.New("at most one of rocketchat_token_id & rocketchat_token_id_file must be configured")
}

if c.Global.WeChatAPISecret != "" && len(c.Global.WeChatAPISecretFile) > 0 {
return errors.New("at most one of wechat_api_secret & wechat_api_secret_file must be configured")
}

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

for _, rcv := range c.Receivers {
Expand Down Expand Up @@ -518,11 +522,12 @@ func (c *Config) UnmarshalYAML(unmarshal func(any) error) error {
wcc.APIURL = c.Global.WeChatAPIURL
}

if wcc.APISecret == "" {
if c.Global.WeChatAPISecret == "" {
return errors.New("no global Wechat ApiSecret set")
if wcc.APISecret == "" && len(wcc.APISecretFile) == 0 {
if c.Global.WeChatAPISecret == "" && len(c.Global.WeChatAPISecretFile) == 0 {
return errors.New("no global Wechat Api Secret set either inline or in a file")
}
wcc.APISecret = c.Global.WeChatAPISecret
wcc.APISecretFile = c.Global.WeChatAPISecretFile
}

if wcc.CorpID == "" {
Expand Down Expand Up @@ -869,6 +874,7 @@ type GlobalConfig struct {
OpsGenieAPIKeyFile string `yaml:"opsgenie_api_key_file,omitempty" json:"opsgenie_api_key_file,omitempty"`
WeChatAPIURL *URL `yaml:"wechat_api_url,omitempty" json:"wechat_api_url,omitempty"`
WeChatAPISecret Secret `yaml:"wechat_api_secret,omitempty" json:"wechat_api_secret,omitempty"`
WeChatAPISecretFile string `yaml:"wechat_api_secret_file,omitempty" json:"wechat_api_secret_file,omitempty"`
WeChatAPICorpID string `yaml:"wechat_api_corp_id,omitempty" json:"wechat_api_corp_id,omitempty"`
VictorOpsAPIURL *URL `yaml:"victorops_api_url,omitempty" json:"victorops_api_url,omitempty"`
VictorOpsAPIKey Secret `yaml:"victorops_api_key,omitempty" json:"victorops_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 @@ -1490,3 +1490,48 @@ func TestInhibitRuleEqual(t *testing.T) {
r = c.InhibitRules[0]
require.Equal(t, []string{"qux🙂", "corge"}, r.Equal)
}

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

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

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

// no override
firstConfig := conf.Receivers[0].WechatConfigs[0]
if firstConfig.APISecretFile != "/global_file" || string(firstConfig.APISecret) != "" {
t.Fatalf("Invalid Wechat API Secret file: %s\nExpected: %s", firstConfig.APISecretFile, "/global_file")
}

// override the file
secondConfig := conf.Receivers[0].WechatConfigs[1]
if secondConfig.APISecretFile != "/override_file" || string(secondConfig.APISecret) != "" {
t.Fatalf("Invalid Wechat API Secret file: %s\nExpected: %s", secondConfig.APISecretFile, "/override_file")
}

// override the global file with an inline URL
thirdConfig := conf.Receivers[0].WechatConfigs[2]
if string(thirdConfig.APISecret) != "my_inline_secret" || thirdConfig.APISecretFile != "" {
t.Fatalf("Invalid Wechat API Secret: %s\nExpected: %s", string(thirdConfig.APISecret), "my_inline_secret")
}
}
23 changes: 14 additions & 9 deletions config/notifiers.go
Original file line number Diff line number Diff line change
Expand Up @@ -636,15 +636,16 @@ type WechatConfig struct {

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

APISecret Secret `yaml:"api_secret,omitempty" json:"api_secret,omitempty"`
CorpID string `yaml:"corp_id,omitempty" json:"corp_id,omitempty"`
Message string `yaml:"message,omitempty" json:"message,omitempty"`
APIURL *URL `yaml:"api_url,omitempty" json:"api_url,omitempty"`
ToUser string `yaml:"to_user,omitempty" json:"to_user,omitempty"`
ToParty string `yaml:"to_party,omitempty" json:"to_party,omitempty"`
ToTag string `yaml:"to_tag,omitempty" json:"to_tag,omitempty"`
AgentID string `yaml:"agent_id,omitempty" json:"agent_id,omitempty"`
MessageType string `yaml:"message_type,omitempty" json:"message_type,omitempty"`
APISecret Secret `yaml:"api_secret,omitempty" json:"api_secret,omitempty"`
APISecretFile string `yaml:"api_secret_file,omitempty" json:"api_secret_file,omitempty"`
CorpID string `yaml:"corp_id,omitempty" json:"corp_id,omitempty"`
Message string `yaml:"message,omitempty" json:"message,omitempty"`
APIURL *URL `yaml:"api_url,omitempty" json:"api_url,omitempty"`
ToUser string `yaml:"to_user,omitempty" json:"to_user,omitempty"`
ToParty string `yaml:"to_party,omitempty" json:"to_party,omitempty"`
ToTag string `yaml:"to_tag,omitempty" json:"to_tag,omitempty"`
AgentID string `yaml:"agent_id,omitempty" json:"agent_id,omitempty"`
MessageType string `yaml:"message_type,omitempty" json:"message_type,omitempty"`
}

const wechatValidTypesRe = `^(text|markdown)$`
Expand All @@ -667,6 +668,10 @@ func (c *WechatConfig) UnmarshalYAML(unmarshal func(any) error) error {
return fmt.Errorf("weChat message type %q does not match valid options %s", c.MessageType, wechatValidTypesRe)
}

if c.APISecret != "" && len(c.APISecretFile) > 0 {
return errors.New("at most one of api_secret & api_secret_file must be configured")
}

return nil
}

Expand Down
10 changes: 10 additions & 0 deletions config/testdata/conf.wechat-both-file-and-secret.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
global:
wechat_api_secret: "http://mysecret.example.com/"
wechat_api_secret_file: '/global_file'
route:
receiver: 'wechat-notifications'
group_by: [alertname, datacenter, app]
receivers:
- name: 'wechat-notifications'
wechat_configs:
- {}
15 changes: 15 additions & 0 deletions config/testdata/conf.wechat-default-api-secret-file.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
global:
wechat_api_secret_file: '/global_file'
wechat_api_corp_id: 'my_corp_id'
route:
receiver: 'wechat-notifications'
group_by: [alertname, datacenter, app]
receivers:
- name: 'wechat-notifications'
wechat_configs:
# Use global
- {}
# Override global with other file
- api_secret_file: '/override_file'
# Override global with inline API secret
- api_secret: 'my_inline_secret'
7 changes: 7 additions & 0 deletions config/testdata/conf.wechat-no-api-secret.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
route:
receiver: 'wechat-notifications'
group_by: [alertname, datacenter, app]
receivers:
- name: 'wechat-notifications'
wechat_configs:
- {}
8 changes: 5 additions & 3 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ global:
[ rocketchat_token_id_file: <filepath> ]
[ wechat_api_url: <string> | default = "https://qyapi.weixin.qq.com/cgi-bin/" ]
[ wechat_api_secret: <secret> ]
[ wechat_api_secret_file: <string> ]
[ wechat_api_corp_id: <string> ]
[ telegram_api_url: <string> | default = "https://api.telegram.org" ]
[ webex_api_url: <string> | default = "https://webexapis.com/v1/messages" ]
Expand Down Expand Up @@ -1155,7 +1156,7 @@ The default `jira.default.description` template only works with V2.
# The API Type to use for search requests, can be either auto, cloud or datacenter
# Example: cloud
[ api_type: <string> | default = auto ]

# The project key where issues are created.
project: <string>

Expand Down Expand Up @@ -1814,7 +1815,7 @@ Please be aware that if the payload exceeds incident.io's API limits (512KB), th
# The HTTP client's configuration.
[ http_config: <http_config> | default = global.http_config ]

# The URL to send the incident.io alert. This would typically be provided by the
# The URL to send the incident.io alert. This would typically be provided by the
# incident.io team when setting up an alert source.
# URL and URL_file are mutually exclusive.
url: <string>
Expand Down Expand Up @@ -1848,8 +1849,9 @@ API](https://developers.weixin.qq.com/doc/offiaccount/en/Message_Management/Serv
# Whether to notify about resolved alerts.
[ send_resolved: <boolean> | default = false ]

# The API key to use when talking to the WeChat API.
# The API key to use when talking to the WeChat API. Either api_secret or api_secret_file should be set.
[ api_secret: <secret> | default = global.wechat_api_secret ]
[ api_secret_file: <string> | default = global.wechat_api_secret_file ]

# The WeChat API URL.
[ api_url: <string> | default = global.wechat_api_url ]
Expand Down
19 changes: 18 additions & 1 deletion notify/wechat/wechat.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import (
"log/slog"
"net/http"
"net/url"
"os"
"strings"
"time"

commoncfg "github.com/prometheus/common/config"
Expand Down Expand Up @@ -97,7 +99,11 @@ func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error)
// Refresh AccessToken over 2 hours
if n.accessToken == "" || time.Since(n.accessTokenAt) > 2*time.Hour {
parameters := url.Values{}
parameters.Add("corpsecret", tmpl(string(n.conf.APISecret)))
apiSecret, err := n.getApiSecret()
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we want to read the file on each notification/expect it to change?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I must admit I just copied the pattern in https://github.com/prometheus/alertmanager/blob/main/notify/pagerduty/pagerduty.go#L228

But here this only run if accesToken is empty or older then 2 hours (line 100), so it should be fine.

if err != nil {
return false, err
}
parameters.Add("corpsecret", tmpl(apiSecret))
parameters.Add("corpid", tmpl(string(n.conf.CorpID)))
if err != nil {
return false, fmt.Errorf("templating error: %w", err)
Expand Down Expand Up @@ -194,3 +200,14 @@ func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error)

return false, errors.New(weResp.Error)
}

func (n *Notifier) getApiSecret() (string, error) {
if len(n.conf.APISecretFile) > 0 {
content, err := os.ReadFile(n.conf.APISecretFile)
if err != nil {
return "", err
}
return strings.TrimSpace(string(content)), nil
}
return string(n.conf.APISecret), nil
}
32 changes: 32 additions & 0 deletions notify/wechat/wechat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package wechat
import (
"fmt"
"net/http"
"os"
"testing"

commoncfg "github.com/prometheus/common/config"
Expand Down Expand Up @@ -90,3 +91,34 @@ func TestWechatMessageTypeSelector(t *testing.T) {

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

func TestGetApiSecretFromSecret(t *testing.T) {
n := &Notifier{conf: &config.WechatConfig{APISecret: config.Secret("shhh")}}
s, err := n.getApiSecret()
require.NoError(t, err)
require.Equal(t, "shhh", s)
}

func TestGetApiSecretFromFile(t *testing.T) {
tmpFile, err := os.CreateTemp(t.TempDir(), "wechat-secret-*")
require.NoError(t, err)
secretContent := "file-secret\n"
_, err = tmpFile.WriteString(secretContent)
require.NoError(t, err)
require.NoError(t, tmpFile.Close())

n := &Notifier{conf: &config.WechatConfig{APISecretFile: tmpFile.Name()}}
s, err := n.getApiSecret()
require.NoError(t, err)
require.Equal(t, "file-secret", s)
}

func TestGetApiSecretFromMissingFile(t *testing.T) {
n := &Notifier{conf: &config.WechatConfig{APISecretFile: "/non/existent/wechat-secret.txt"}}
s, err := n.getApiSecret()
var pathErr *os.PathError
require.ErrorAs(t, err, &pathErr)
require.Equal(t, "/non/existent/wechat-secret.txt", pathErr.Path)
require.ErrorIs(t, err, os.ErrNotExist)
require.Empty(t, s)
}
Loading