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 @@ -419,6 +419,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 @@ -557,11 +561,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 @@ -912,6 +917,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 @@ -1593,3 +1593,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 @@ -665,15 +665,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 @@ -696,6 +697,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:
- {}
4 changes: 3 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,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 @@ -1875,8 +1876,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 @@ -99,7 +101,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()
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 @@ -196,3 +202,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)
}