Skip to content

Commit 60b32cd

Browse files
committed
feat: add config directive to pass wechat api secret via file
Ref: #2498 Signed-off-by: Christoph Maser <christoph.maser+github@gmail.com>
1 parent 29d491e commit 60b32cd

File tree

9 files changed

+155
-16
lines changed

9 files changed

+155
-16
lines changed

config/config.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,10 @@ func (c *Config) UnmarshalYAML(unmarshal func(any) error) error {
404404
return errors.New("at most one of rocketchat_token_id & rocketchat_token_id_file must be configured")
405405
}
406406

407+
if c.Global.WeChatAPISecret != "" && len(c.Global.WeChatAPISecretFile) > 0 {
408+
return errors.New("at most one of wechat_api_secret & wechat_api_secret_file must be configured")
409+
}
410+
407411
names := map[string]struct{}{}
408412

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

521-
if wcc.APISecret == "" {
522-
if c.Global.WeChatAPISecret == "" {
523-
return errors.New("no global Wechat ApiSecret set")
525+
if wcc.APISecret == "" && len(wcc.APISecretFile) == 0 {
526+
if c.Global.WeChatAPISecret == "" && len(c.Global.WeChatAPISecretFile) == 0 {
527+
return errors.New("no global Wechat Api Secret set either inline or in a file")
524528
}
525529
wcc.APISecret = c.Global.WeChatAPISecret
530+
wcc.APISecretFile = c.Global.WeChatAPISecretFile
526531
}
527532

528533
if wcc.CorpID == "" {
@@ -869,6 +874,7 @@ type GlobalConfig struct {
869874
OpsGenieAPIKeyFile string `yaml:"opsgenie_api_key_file,omitempty" json:"opsgenie_api_key_file,omitempty"`
870875
WeChatAPIURL *URL `yaml:"wechat_api_url,omitempty" json:"wechat_api_url,omitempty"`
871876
WeChatAPISecret Secret `yaml:"wechat_api_secret,omitempty" json:"wechat_api_secret,omitempty"`
877+
WeChatAPISecretFile string `yaml:"wechat_api_secret_file,omitempty" json:"wechat_api_secret_file,omitempty"`
872878
WeChatAPICorpID string `yaml:"wechat_api_corp_id,omitempty" json:"wechat_api_corp_id,omitempty"`
873879
VictorOpsAPIURL *URL `yaml:"victorops_api_url,omitempty" json:"victorops_api_url,omitempty"`
874880
VictorOpsAPIKey Secret `yaml:"victorops_api_key,omitempty" json:"victorops_api_key,omitempty"`

config/config_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1490,3 +1490,48 @@ func TestInhibitRuleEqual(t *testing.T) {
14901490
r = c.InhibitRules[0]
14911491
require.Equal(t, []string{"qux🙂", "corge"}, r.Equal)
14921492
}
1493+
1494+
func TestWechatNoAPIURL(t *testing.T) {
1495+
_, err := LoadFile("testdata/conf.wechat-no-api-secret.yml")
1496+
if err == nil {
1497+
t.Fatalf("Expected an error parsing %s: %s", "testdata/conf.wechat-no-api-url.yml", err)
1498+
}
1499+
if err.Error() != "no global Wechat Api Secret set either inline or in a file" {
1500+
t.Errorf("Expected: %s\nGot: %s", "no global Wechat Api Secret set either inline or in a file", err.Error())
1501+
}
1502+
}
1503+
1504+
func TestWechatBothAPIURLAndFile(t *testing.T) {
1505+
_, err := LoadFile("testdata/conf.wechat-both-file-and-secret.yml")
1506+
if err == nil {
1507+
t.Fatalf("Expected an error parsing %s: %s", "testdata/conf.wechat-both-file-and-secret.yml", err)
1508+
}
1509+
if err.Error() != "at most one of wechat_api_secret & wechat_api_secret_file must be configured" {
1510+
t.Errorf("Expected: %s\nGot: %s", "at most one of wechat_api_secret & wechat_api_secret_file must be configured", err.Error())
1511+
}
1512+
}
1513+
1514+
func TestWechatGlobalAPISecretFile(t *testing.T) {
1515+
conf, err := LoadFile("testdata/conf.wechat-default-api-secret-file.yml")
1516+
if err != nil {
1517+
t.Fatalf("Error parsing %s: %s", "testdata/conf.wechat-default-api-secret-file.yml", err)
1518+
}
1519+
1520+
// no override
1521+
firstConfig := conf.Receivers[0].WechatConfigs[0]
1522+
if firstConfig.APISecretFile != "/global_file" || string(firstConfig.APISecret) != "" {
1523+
t.Fatalf("Invalid Wechat API Secret file: %s\nExpected: %s", firstConfig.APISecretFile, "/global_file")
1524+
}
1525+
1526+
// override the file
1527+
secondConfig := conf.Receivers[0].WechatConfigs[1]
1528+
if secondConfig.APISecretFile != "/override_file" || string(secondConfig.APISecret) != "" {
1529+
t.Fatalf("Invalid Wechat API Secret file: %s\nExpected: %s", secondConfig.APISecretFile, "/override_file")
1530+
}
1531+
1532+
// override the global file with an inline URL
1533+
thirdConfig := conf.Receivers[0].WechatConfigs[2]
1534+
if string(thirdConfig.APISecret) != "http://mysecret.example.com/" || thirdConfig.APISecretFile != "" {
1535+
t.Fatalf("Invalid Wechat API Secret: %s\nExpected: %s", string(thirdConfig.APISecret), "http://mysecret.example.com/")
1536+
}
1537+
}

config/notifiers.go

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -636,15 +636,16 @@ type WechatConfig struct {
636636

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

639-
APISecret Secret `yaml:"api_secret,omitempty" json:"api_secret,omitempty"`
640-
CorpID string `yaml:"corp_id,omitempty" json:"corp_id,omitempty"`
641-
Message string `yaml:"message,omitempty" json:"message,omitempty"`
642-
APIURL *URL `yaml:"api_url,omitempty" json:"api_url,omitempty"`
643-
ToUser string `yaml:"to_user,omitempty" json:"to_user,omitempty"`
644-
ToParty string `yaml:"to_party,omitempty" json:"to_party,omitempty"`
645-
ToTag string `yaml:"to_tag,omitempty" json:"to_tag,omitempty"`
646-
AgentID string `yaml:"agent_id,omitempty" json:"agent_id,omitempty"`
647-
MessageType string `yaml:"message_type,omitempty" json:"message_type,omitempty"`
639+
APISecret Secret `yaml:"api_secret,omitempty" json:"api_secret,omitempty"`
640+
APISecretFile string `yaml:"api_secret_file,omitempty" json:"api_secret_file,omitempty"`
641+
CorpID string `yaml:"corp_id,omitempty" json:"corp_id,omitempty"`
642+
Message string `yaml:"message,omitempty" json:"message,omitempty"`
643+
APIURL *URL `yaml:"api_url,omitempty" json:"api_url,omitempty"`
644+
ToUser string `yaml:"to_user,omitempty" json:"to_user,omitempty"`
645+
ToParty string `yaml:"to_party,omitempty" json:"to_party,omitempty"`
646+
ToTag string `yaml:"to_tag,omitempty" json:"to_tag,omitempty"`
647+
AgentID string `yaml:"agent_id,omitempty" json:"agent_id,omitempty"`
648+
MessageType string `yaml:"message_type,omitempty" json:"message_type,omitempty"`
648649
}
649650

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

671+
if c.APISecret != "" && len(c.APISecretFile) > 0 {
672+
return errors.New("at most one of api_secret & api_secret_file must be configured")
673+
}
674+
670675
return nil
671676
}
672677

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
global:
2+
wechat_api_secret: "http://mysecret.example.com/"
3+
wechat_api_secret_file: '/global_file'
4+
route:
5+
receiver: 'wechat-notifications'
6+
group_by: [alertname, datacenter, app]
7+
receivers:
8+
- name: 'wechat-notifications'
9+
wechat_configs:
10+
- {}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
global:
2+
wechat_api_secret_file: '/global_file'
3+
wechat_api_corp_id: 'my_corp_id'
4+
route:
5+
receiver: 'wechat-notifications'
6+
group_by: [alertname, datacenter, app]
7+
receivers:
8+
- name: 'wechat-notifications'
9+
wechat_configs:
10+
# Use global
11+
- {}
12+
# Override global with other file
13+
- api_secret_file: '/override_file'
14+
# Override global with inline URL
15+
- api_secret: 'http://mysecret.example.com/'
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
route:
2+
receiver: 'wechat-notifications'
3+
group_by: [alertname, datacenter, app]
4+
receivers:
5+
- name: 'wechat-notifications'
6+
wechat_configs:
7+
- {}

docs/configuration.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ global:
115115
[ rocketchat_token_id_file: <filepath> ]
116116
[ wechat_api_url: <string> | default = "https://qyapi.weixin.qq.com/cgi-bin/" ]
117117
[ wechat_api_secret: <secret> ]
118+
[ wechat_api_secret_file: <string> ]
118119
[ wechat_api_corp_id: <string> ]
119120
[ telegram_api_url: <string> | default = "https://api.telegram.org" ]
120121
[ webex_api_url: <string> | default = "https://webexapis.com/v1/messages" ]
@@ -1155,7 +1156,7 @@ The default `jira.default.description` template only works with V2.
11551156
# The API Type to use for search requests, can be either auto, cloud or datacenter
11561157
# Example: cloud
11571158
[ api_type: <string> | default = auto ]
1158-
1159+
11591160
# The project key where issues are created.
11601161
project: <string>
11611162
@@ -1814,7 +1815,7 @@ Please be aware that if the payload exceeds incident.io's API limits (512KB), th
18141815
# The HTTP client's configuration.
18151816
[ http_config: <http_config> | default = global.http_config ]
18161817
1817-
# The URL to send the incident.io alert. This would typically be provided by the
1818+
# The URL to send the incident.io alert. This would typically be provided by the
18181819
# incident.io team when setting up an alert source.
18191820
# URL and URL_file are mutually exclusive.
18201821
url: <string>
@@ -1848,8 +1849,9 @@ API](https://developers.weixin.qq.com/doc/offiaccount/en/Message_Management/Serv
18481849
# Whether to notify about resolved alerts.
18491850
[ send_resolved: <boolean> | default = false ]
18501851
1851-
# The API key to use when talking to the WeChat API.
1852+
# The API key to use when talking to the WeChat API. Either api_secret or api_secret_file should be set.
18521853
[ api_secret: <secret> | default = global.wechat_api_secret ]
1854+
[ api_secret_file: <string> | default = global.wechat_api_secret_file ]
18531855
18541856
# The WeChat API URL.
18551857
[ api_url: <string> | default = global.wechat_api_url ]

notify/wechat/wechat.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import (
2323
"log/slog"
2424
"net/http"
2525
"net/url"
26+
"os"
27+
"strings"
2628
"time"
2729

2830
commoncfg "github.com/prometheus/common/config"
@@ -97,7 +99,11 @@ func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error)
9799
// Refresh AccessToken over 2 hours
98100
if n.accessToken == "" || time.Since(n.accessTokenAt) > 2*time.Hour {
99101
parameters := url.Values{}
100-
parameters.Add("corpsecret", tmpl(string(n.conf.APISecret)))
102+
apiSecret, err := n.getApiSecret()
103+
if err != nil {
104+
return false, err
105+
}
106+
parameters.Add("corpsecret", tmpl(apiSecret))
101107
parameters.Add("corpid", tmpl(string(n.conf.CorpID)))
102108
if err != nil {
103109
return false, fmt.Errorf("templating error: %w", err)
@@ -194,3 +200,14 @@ func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error)
194200

195201
return false, errors.New(weResp.Error)
196202
}
203+
204+
func (n *Notifier) getApiSecret() (string, error) {
205+
if len(n.conf.APISecretFile) > 0 {
206+
content, err := os.ReadFile(n.conf.APISecretFile)
207+
if err != nil {
208+
return "", err
209+
}
210+
return strings.TrimSpace(string(content)), nil
211+
}
212+
return string(n.conf.APISecret), nil
213+
}

notify/wechat/wechat_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package wechat
1616
import (
1717
"fmt"
1818
"net/http"
19+
"os"
1920
"testing"
2021

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

9192
test.AssertNotifyLeaksNoSecret(ctx, t, notifier, secret, token)
9293
}
94+
95+
func TestGetApiSecretFromSecret(t *testing.T) {
96+
n := &Notifier{conf: &config.WechatConfig{APISecret: config.Secret("shhh")}}
97+
s, err := n.getApiSecret()
98+
require.NoError(t, err)
99+
require.Equal(t, "shhh", s)
100+
}
101+
102+
func TestGetApiSecretFromFile(t *testing.T) {
103+
tmpFile, err := os.CreateTemp(t.TempDir(), "wechat-secret-*")
104+
require.NoError(t, err)
105+
secretContent := "file-secret\n"
106+
_, err = tmpFile.WriteString(secretContent)
107+
require.NoError(t, err)
108+
require.NoError(t, tmpFile.Close())
109+
110+
n := &Notifier{conf: &config.WechatConfig{APISecretFile: tmpFile.Name()}}
111+
s, err := n.getApiSecret()
112+
require.NoError(t, err)
113+
require.Equal(t, "file-secret", s)
114+
}
115+
116+
func TestGetApiSecretFromMissingFile(t *testing.T) {
117+
n := &Notifier{conf: &config.WechatConfig{APISecretFile: "/non/existent/wechat-secret.txt"}}
118+
s, err := n.getApiSecret()
119+
var pathErr *os.PathError
120+
require.ErrorAs(t, err, &pathErr)
121+
require.Equal(t, "/non/existent/wechat-secret.txt", pathErr.Path)
122+
require.ErrorIs(t, err, os.ErrNotExist)
123+
require.Empty(t, s)
124+
}

0 commit comments

Comments
 (0)