Add support for reading VictorOps API key from file (#3111)

* Read VictorOps API key from file

* Update documentation

* Improve tests

Signed-off-by: Oktarian T-B <oktariantilneybassett@improbable.io>
This commit is contained in:
Oktarian T-B 2022-10-25 14:40:17 +01:00 committed by GitHub
parent 893ad67978
commit dd7214e51f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 174 additions and 17 deletions

View File

@ -335,6 +335,10 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
return fmt.Errorf("at most one of opsgenie_api_key & opsgenie_api_key_file must be configured") return fmt.Errorf("at most one of opsgenie_api_key & opsgenie_api_key_file must be configured")
} }
if c.Global.VictorOpsAPIKey != "" && len(c.Global.VictorOpsAPIKeyFile) > 0 {
return fmt.Errorf("at most one of victorops_api_key & victorops_api_key_file must be configured")
}
if len(c.Global.SMTPAuthPassword) > 0 && len(c.Global.SMTPAuthPasswordFile) > 0 { if len(c.Global.SMTPAuthPassword) > 0 && len(c.Global.SMTPAuthPasswordFile) > 0 {
return fmt.Errorf("at most one of smtp_auth_password & smtp_auth_password_file must be configured") return fmt.Errorf("at most one of smtp_auth_password & smtp_auth_password_file must be configured")
} }
@ -476,11 +480,12 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
if !strings.HasSuffix(voc.APIURL.Path, "/") { if !strings.HasSuffix(voc.APIURL.Path, "/") {
voc.APIURL.Path += "/" voc.APIURL.Path += "/"
} }
if voc.APIKey == "" { if voc.APIKey == "" && len(voc.APIKeyFile) == 0 {
if c.Global.VictorOpsAPIKey == "" { if c.Global.VictorOpsAPIKey == "" && len(c.Global.VictorOpsAPIKeyFile) == 0 {
return fmt.Errorf("no global VictorOps API Key set") return fmt.Errorf("no global VictorOps API Key set")
} }
voc.APIKey = c.Global.VictorOpsAPIKey voc.APIKey = c.Global.VictorOpsAPIKey
voc.APIKeyFile = c.Global.VictorOpsAPIKeyFile
} }
} }
for _, sns := range rcv.SNSConfigs { for _, sns := range rcv.SNSConfigs {
@ -718,6 +723,7 @@ type GlobalConfig struct {
WeChatAPICorpID string `yaml:"wechat_api_corp_id,omitempty" json:"wechat_api_corp_id,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"` VictorOpsAPIURL *URL `yaml:"victorops_api_url,omitempty" json:"victorops_api_url,omitempty"`
VictorOpsAPIKey Secret `yaml:"victorops_api_key,omitempty" json:"victorops_api_key,omitempty"` VictorOpsAPIKey Secret `yaml:"victorops_api_key,omitempty" json:"victorops_api_key,omitempty"`
VictorOpsAPIKeyFile string `yaml:"victorops_api_key_file,omitempty" json:"victorops_api_key_file,omitempty"`
TelegramAPIUrl *URL `yaml:"telegram_api_url,omitempty" json:"telegram_api_url,omitempty"` TelegramAPIUrl *URL `yaml:"telegram_api_url,omitempty" json:"telegram_api_url,omitempty"`
} }

View File

@ -1016,11 +1016,38 @@ func TestVictorOpsDefaultAPIKey(t *testing.T) {
} }
defaultKey := conf.Global.VictorOpsAPIKey defaultKey := conf.Global.VictorOpsAPIKey
overrideKey := Secret("qwe456")
if defaultKey != conf.Receivers[0].VictorOpsConfigs[0].APIKey { if defaultKey != conf.Receivers[0].VictorOpsConfigs[0].APIKey {
t.Fatalf("Invalid victorops key: %s\nExpected: %s", conf.Receivers[0].VictorOpsConfigs[0].APIKey, defaultKey) t.Fatalf("Invalid victorops key: %s\nExpected: %s", conf.Receivers[0].VictorOpsConfigs[0].APIKey, defaultKey)
} }
if defaultKey == conf.Receivers[1].VictorOpsConfigs[0].APIKey { if overrideKey != conf.Receivers[1].VictorOpsConfigs[0].APIKey {
t.Errorf("Invalid victorops key: %s\nExpected: %s", conf.Receivers[0].VictorOpsConfigs[0].APIKey, "qwe456") t.Errorf("Invalid victorops key: %s\nExpected: %s", conf.Receivers[0].VictorOpsConfigs[0].APIKey, string(overrideKey))
}
}
func TestVictorOpsDefaultAPIKeyFile(t *testing.T) {
conf, err := LoadFile("testdata/conf.victorops-default-apikey-file.yml")
if err != nil {
t.Fatalf("Error parsing %s: %s", "testdata/conf.victorops-default-apikey-file.yml", err)
}
defaultKey := conf.Global.VictorOpsAPIKeyFile
overrideKey := "/override_file"
if defaultKey != conf.Receivers[0].VictorOpsConfigs[0].APIKeyFile {
t.Fatalf("Invalid VictorOps key_file: %s\nExpected: %s", conf.Receivers[0].VictorOpsConfigs[0].APIKeyFile, defaultKey)
}
if overrideKey != conf.Receivers[1].VictorOpsConfigs[0].APIKeyFile {
t.Errorf("Invalid VictorOps key_file: %s\nExpected: %s", conf.Receivers[0].VictorOpsConfigs[0].APIKeyFile, overrideKey)
}
}
func TestVictorOpsBothAPIKeyAndFile(t *testing.T) {
_, err := LoadFile("testdata/conf.victorops-both-file-and-apikey.yml")
if err == nil {
t.Fatalf("Expected an error parsing %s: %s", "testdata/conf.victorops-both-file-and-apikey.yml", err)
}
if err.Error() != "at most one of victorops_api_key & victorops_api_key_file must be configured" {
t.Errorf("Expected: %s\nGot: %s", "at most one of victorops_api_key & victorops_api_key_file must be configured", err.Error())
} }
} }

View File

@ -540,7 +540,7 @@ type VictorOpsConfig struct {
HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"`
APIKey Secret `yaml:"api_key,omitempty" json:"api_key,omitempty"` APIKey Secret `yaml:"api_key,omitempty" json:"api_key,omitempty"`
APIKeyFile Secret `yaml:"api_key_file,omitempty" json:"api_key_file,omitempty"` APIKeyFile string `yaml:"api_key_file,omitempty" json:"api_key_file,omitempty"`
APIURL *URL `yaml:"api_url" json:"api_url"` APIURL *URL `yaml:"api_url" json:"api_url"`
RoutingKey string `yaml:"routing_key" json:"routing_key"` RoutingKey string `yaml:"routing_key" json:"routing_key"`
MessageType string `yaml:"message_type" json:"message_type"` MessageType string `yaml:"message_type" json:"message_type"`
@ -560,6 +560,9 @@ func (c *VictorOpsConfig) UnmarshalYAML(unmarshal func(interface{}) error) error
if c.RoutingKey == "" { if c.RoutingKey == "" {
return fmt.Errorf("missing Routing key in VictorOps config") return fmt.Errorf("missing Routing key in VictorOps config")
} }
if c.APIKey != "" && len(c.APIKeyFile) > 0 {
return fmt.Errorf("at most one of api_key & api_key_file must be configured")
}
reservedFields := []string{"routing_key", "message_type", "state_message", "entity_display_name", "monitoring_tool", "entity_id", "entity_state"} reservedFields := []string{"routing_key", "message_type", "state_message", "entity_display_name", "monitoring_tool", "entity_id", "entity_state"}

View File

@ -291,21 +291,54 @@ http_config:
} }
} }
func TestVictorOpsRoutingKeyIsPresent(t *testing.T) { func TestVictorOpsConfiguration(t *testing.T) {
in := ` t.Run("valid configuration", func(t *testing.T) {
in := `
routing_key: test
api_key_file: /global_file
`
var cfg VictorOpsConfig
err := yaml.UnmarshalStrict([]byte(in), &cfg)
if err != nil {
t.Fatalf("no error was expected:\n%v", err)
}
})
t.Run("routing key is missing", func(t *testing.T) {
in := `
routing_key: '' routing_key: ''
` `
var cfg VictorOpsConfig var cfg VictorOpsConfig
err := yaml.UnmarshalStrict([]byte(in), &cfg) err := yaml.UnmarshalStrict([]byte(in), &cfg)
expected := "missing Routing key in VictorOps config" expected := "missing Routing key in VictorOps config"
if err == nil { if err == nil {
t.Fatalf("no error returned, expected:\n%v", expected) t.Fatalf("no error returned, expected:\n%v", expected)
} }
if err.Error() != expected { if err.Error() != expected {
t.Errorf("\nexpected:\n%v\ngot:\n%v", expected, err.Error()) t.Errorf("\nexpected:\n%v\ngot:\n%v", expected, err.Error())
} }
})
t.Run("api_key and api_key_file both defined", func(t *testing.T) {
in := `
routing_key: test
api_key: xyz
api_key_file: /global_file
`
var cfg VictorOpsConfig
err := yaml.UnmarshalStrict([]byte(in), &cfg)
expected := "at most one of api_key & api_key_file must be configured"
if err == nil {
t.Fatalf("no error returned, expected:\n%v", expected)
}
if err.Error() != expected {
t.Errorf("\nexpected:\n%v\ngot:\n%v", expected, err.Error())
}
})
} }
func TestVictorOpsCustomFieldsValidation(t *testing.T) { func TestVictorOpsCustomFieldsValidation(t *testing.T) {

View File

@ -0,0 +1,21 @@
global:
victorops_api_key: asd132
victorops_api_key_file: '/global_file'
route:
group_by: ['alertname', 'cluster', 'service']
group_wait: 30s
group_interval: 5m
repeat_interval: 3h
receiver: team-Y-victorops
routes:
- match:
service: foo
receiver: team-X-victorops
receivers:
- name: 'team-X-victorops'
victorops_configs:
- routing_key: 'team-X'
- name: 'team-Y-victorops'
victorops_configs:
- routing_key: 'team-Y'
api_key: qwe456

View File

@ -0,0 +1,20 @@
global:
victorops_api_key_file: '/global_file'
route:
group_by: ['alertname', 'cluster', 'service']
group_wait: 30s
group_interval: 5m
repeat_interval: 3h
receiver: team-Y-victorops
routes:
- match:
service: foo
receiver: team-X-victorops
receivers:
- name: 'team-X-victorops'
victorops_configs:
- routing_key: 'team-X'
- name: 'team-Y-victorops'
victorops_configs:
- routing_key: 'team-Y'
api_key_file: /override_file

View File

@ -85,6 +85,7 @@ global:
[ slack_api_url: <secret> ] [ slack_api_url: <secret> ]
[ slack_api_url_file: <filepath> ] [ slack_api_url_file: <filepath> ]
[ victorops_api_key: <secret> ] [ victorops_api_key: <secret> ]
[ victorops_api_key_file: <filepath> ]
[ victorops_api_url: <string> | default = "https://alert.victorops.com/integrations/generic/20131114/alert/" ] [ victorops_api_url: <string> | default = "https://alert.victorops.com/integrations/generic/20131114/alert/" ]
[ pagerduty_url: <string> | default = "https://events.pagerduty.com/v2/enqueue" ] [ pagerduty_url: <string> | default = "https://events.pagerduty.com/v2/enqueue" ]
[ opsgenie_api_key: <secret> ] [ opsgenie_api_key: <secret> ]
@ -973,8 +974,13 @@ VictorOps notifications are sent out via the [VictorOps API](https://help.victor
[ send_resolved: <boolean> | default = true ] [ send_resolved: <boolean> | default = true ]
# The API key to use when talking to the VictorOps API. # The API key to use when talking to the VictorOps API.
# It is mutually exclusive with `api_key_file`.
[ api_key: <secret> | default = global.victorops_api_key ] [ api_key: <secret> | default = global.victorops_api_key ]
# Reads the API key to use when talking to the VictorOps API from a file.
# It is mutually exclusive with `api_key`.
[ api_key_file: <filepath> | default = global.victorops_api_key_file ]
# The VictorOps API URL. # The VictorOps API URL.
[ api_url: <string> | default = global.victorops_api_url ] [ api_url: <string> | default = global.victorops_api_url ]

View File

@ -19,9 +19,12 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"os"
"strings"
"github.com/go-kit/log" "github.com/go-kit/log"
"github.com/go-kit/log/level" "github.com/go-kit/log/level"
"github.com/pkg/errors"
commoncfg "github.com/prometheus/common/config" commoncfg "github.com/prometheus/common/config"
"github.com/prometheus/common/model" "github.com/prometheus/common/model"
@ -70,7 +73,19 @@ func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error)
tmpl = notify.TmplText(n.tmpl, data, &err) tmpl = notify.TmplText(n.tmpl, data, &err)
apiURL = n.conf.APIURL.Copy() apiURL = n.conf.APIURL.Copy()
) )
apiURL.Path += fmt.Sprintf("%s/%s", n.conf.APIKey, tmpl(n.conf.RoutingKey))
var apiKey string
if n.conf.APIKey != "" {
apiKey = string(n.conf.APIKey)
} else {
content, fileErr := os.ReadFile(n.conf.APIKeyFile)
if fileErr != nil {
return false, errors.Wrap(fileErr, "failed to read API key from file")
}
apiKey = strings.TrimSpace(string(content))
}
apiURL.Path += fmt.Sprintf("%s/%s", apiKey, tmpl(n.conf.RoutingKey))
if err != nil { if err != nil {
return false, fmt.Errorf("templating error: %s", err) return false, fmt.Errorf("templating error: %s", err)
} }

View File

@ -20,6 +20,7 @@ import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url" "net/url"
"os"
"testing" "testing"
"time" "time"
@ -119,6 +120,30 @@ func TestVictorOpsRedactedURL(t *testing.T) {
test.AssertNotifyLeaksNoSecret(ctx, t, notifier, secret) test.AssertNotifyLeaksNoSecret(ctx, t, notifier, secret)
} }
func TestVictorOpsReadingApiKeyFromFile(t *testing.T) {
key := "key"
f, err := os.CreateTemp("", "victorops_test")
require.NoError(t, err, "creating temp file failed")
_, err = f.WriteString(key)
require.NoError(t, err, "writing to temp file failed")
ctx, u, fn := test.GetContextWithCancelingURL()
defer fn()
notifier, err := New(
&config.VictorOpsConfig{
APIURL: &config.URL{URL: u},
APIKeyFile: f.Name(),
HTTPConfig: &commoncfg.HTTPClientConfig{},
},
test.CreateTmpl(t),
log.NewNopLogger(),
)
require.NoError(t, err)
test.AssertNotifyLeaksNoSecret(ctx, t, notifier, key)
}
func TestVictorOpsTemplating(t *testing.T) { func TestVictorOpsTemplating(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
dec := json.NewDecoder(r.Body) dec := json.NewDecoder(r.Body)
@ -181,6 +206,7 @@ func TestVictorOpsTemplating(t *testing.T) {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
tc.cfg.HTTPConfig = &commoncfg.HTTPClientConfig{} tc.cfg.HTTPConfig = &commoncfg.HTTPClientConfig{}
tc.cfg.APIURL = &config.URL{URL: u} tc.cfg.APIURL = &config.URL{URL: u}
tc.cfg.APIKey = "test"
vo, err := New(tc.cfg, test.CreateTmpl(t), log.NewNopLogger()) vo, err := New(tc.cfg, test.CreateTmpl(t), log.NewNopLogger())
require.NoError(t, err) require.NoError(t, err)
ctx := context.Background() ctx := context.Background()