1
0
mirror of https://github.com/prometheus/alertmanager.git synced 2026-02-05 15:45:34 +01:00

feat: add config directive to pass wechat api secret via file (#4734)

Ref: #2498

Signed-off-by: Christoph Maser <christoph.maser+github@gmail.com>
This commit is contained in:
Christoph Maser
2026-01-05 18:13:23 +01:00
committed by GitHub
parent b114dd40b7
commit cae1590129
9 changed files with 153 additions and 14 deletions

View File

@@ -499,6 +499,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 {
@@ -637,11 +641,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 == "" {
@@ -992,6 +997,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"`

View File

@@ -1703,3 +1703,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")
}
}

View File

@@ -664,15 +664,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)$`
@@ -695,6 +696,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
}

View File

@@ -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:
- {}

View File

@@ -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'

View File

@@ -0,0 +1,7 @@
route:
receiver: 'wechat-notifications'
group_by: [alertname, datacenter, app]
receivers:
- name: 'wechat-notifications'
wechat_configs:
- {}

View File

@@ -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" ]
@@ -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 ]

View File

@@ -23,6 +23,8 @@ import (
"log/slog"
"net/http"
"net/url"
"os"
"strings"
"time"
commoncfg "github.com/prometheus/common/config"
@@ -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)
@@ -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
}

View File

@@ -16,6 +16,7 @@ package wechat
import (
"fmt"
"net/http"
"os"
"testing"
commoncfg "github.com/prometheus/common/config"
@@ -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)
}