Add support for reading PagerDuty secrets from files (#3107)
* Add support for reading PagerDuty secrets from files * Update documentation Signed-off-by: Oktarian Tilney-Bassett <oktariantilneybassett@improbable.io>
This commit is contained in:
parent
d034f116d5
commit
1045dc0f21
|
@ -208,20 +208,22 @@ type PagerdutyConfig struct {
|
||||||
|
|
||||||
HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"`
|
HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"`
|
||||||
|
|
||||||
ServiceKey Secret `yaml:"service_key,omitempty" json:"service_key,omitempty"`
|
ServiceKey Secret `yaml:"service_key,omitempty" json:"service_key,omitempty"`
|
||||||
RoutingKey Secret `yaml:"routing_key,omitempty" json:"routing_key,omitempty"`
|
ServiceKeyFile string `yaml:"service_key_file,omitempty" json:"service_key_file,omitempty"`
|
||||||
URL *URL `yaml:"url,omitempty" json:"url,omitempty"`
|
RoutingKey Secret `yaml:"routing_key,omitempty" json:"routing_key,omitempty"`
|
||||||
Client string `yaml:"client,omitempty" json:"client,omitempty"`
|
RoutingKeyFile string `yaml:"routing_key_file,omitempty" json:"routing_key_file,omitempty"`
|
||||||
ClientURL string `yaml:"client_url,omitempty" json:"client_url,omitempty"`
|
URL *URL `yaml:"url,omitempty" json:"url,omitempty"`
|
||||||
Description string `yaml:"description,omitempty" json:"description,omitempty"`
|
Client string `yaml:"client,omitempty" json:"client,omitempty"`
|
||||||
Details map[string]string `yaml:"details,omitempty" json:"details,omitempty"`
|
ClientURL string `yaml:"client_url,omitempty" json:"client_url,omitempty"`
|
||||||
Images []PagerdutyImage `yaml:"images,omitempty" json:"images,omitempty"`
|
Description string `yaml:"description,omitempty" json:"description,omitempty"`
|
||||||
Links []PagerdutyLink `yaml:"links,omitempty" json:"links,omitempty"`
|
Details map[string]string `yaml:"details,omitempty" json:"details,omitempty"`
|
||||||
Source string `yaml:"source,omitempty" json:"source,omitempty"`
|
Images []PagerdutyImage `yaml:"images,omitempty" json:"images,omitempty"`
|
||||||
Severity string `yaml:"severity,omitempty" json:"severity,omitempty"`
|
Links []PagerdutyLink `yaml:"links,omitempty" json:"links,omitempty"`
|
||||||
Class string `yaml:"class,omitempty" json:"class,omitempty"`
|
Source string `yaml:"source,omitempty" json:"source,omitempty"`
|
||||||
Component string `yaml:"component,omitempty" json:"component,omitempty"`
|
Severity string `yaml:"severity,omitempty" json:"severity,omitempty"`
|
||||||
Group string `yaml:"group,omitempty" json:"group,omitempty"`
|
Class string `yaml:"class,omitempty" json:"class,omitempty"`
|
||||||
|
Component string `yaml:"component,omitempty" json:"component,omitempty"`
|
||||||
|
Group string `yaml:"group,omitempty" json:"group,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PagerdutyLink is a link
|
// PagerdutyLink is a link
|
||||||
|
@ -244,9 +246,15 @@ func (c *PagerdutyConfig) UnmarshalYAML(unmarshal func(interface{}) error) error
|
||||||
if err := unmarshal((*plain)(c)); err != nil {
|
if err := unmarshal((*plain)(c)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if c.RoutingKey == "" && c.ServiceKey == "" {
|
if c.RoutingKey == "" && c.ServiceKey == "" && c.RoutingKeyFile == "" && c.ServiceKeyFile == "" {
|
||||||
return fmt.Errorf("missing service or routing key in PagerDuty config")
|
return fmt.Errorf("missing service or routing key in PagerDuty config")
|
||||||
}
|
}
|
||||||
|
if len(c.RoutingKey) > 0 && len(c.RoutingKeyFile) > 0 {
|
||||||
|
return fmt.Errorf("at most one of routing_key & routing_key_file must be configured")
|
||||||
|
}
|
||||||
|
if len(c.ServiceKey) > 0 && len(c.ServiceKeyFile) > 0 {
|
||||||
|
return fmt.Errorf("at most one of service_key & service_key_file must be configured")
|
||||||
|
}
|
||||||
if c.Details == nil {
|
if c.Details == nil {
|
||||||
c.Details = make(map[string]string)
|
c.Details = make(map[string]string)
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,38 +59,78 @@ headers:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPagerdutyRoutingKeyIsPresent(t *testing.T) {
|
func TestPagerdutyTestRoutingKey(t *testing.T) {
|
||||||
in := `
|
t.Run("error if no routing key or key file", func(t *testing.T) {
|
||||||
|
in := `
|
||||||
routing_key: ''
|
routing_key: ''
|
||||||
`
|
`
|
||||||
var cfg PagerdutyConfig
|
var cfg PagerdutyConfig
|
||||||
err := yaml.UnmarshalStrict([]byte(in), &cfg)
|
err := yaml.UnmarshalStrict([]byte(in), &cfg)
|
||||||
|
|
||||||
expected := "missing service or routing key in PagerDuty config"
|
expected := "missing service or routing key in PagerDuty 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("error if both routing key and key file", func(t *testing.T) {
|
||||||
|
in := `
|
||||||
|
routing_key: 'xyz'
|
||||||
|
routing_key_file: 'xyz'
|
||||||
|
`
|
||||||
|
var cfg PagerdutyConfig
|
||||||
|
err := yaml.UnmarshalStrict([]byte(in), &cfg)
|
||||||
|
|
||||||
|
expected := "at most one of routing_key & routing_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 TestPagerdutyServiceKeyIsPresent(t *testing.T) {
|
func TestPagerdutyServiceKey(t *testing.T) {
|
||||||
in := `
|
t.Run("error if no service key or key file", func(t *testing.T) {
|
||||||
|
in := `
|
||||||
service_key: ''
|
service_key: ''
|
||||||
`
|
`
|
||||||
var cfg PagerdutyConfig
|
var cfg PagerdutyConfig
|
||||||
err := yaml.UnmarshalStrict([]byte(in), &cfg)
|
err := yaml.UnmarshalStrict([]byte(in), &cfg)
|
||||||
|
|
||||||
expected := "missing service or routing key in PagerDuty config"
|
expected := "missing service or routing key in PagerDuty 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("error if both service key and key file", func(t *testing.T) {
|
||||||
|
in := `
|
||||||
|
service_key: 'xyz'
|
||||||
|
service_key_file: 'xyz'
|
||||||
|
`
|
||||||
|
var cfg PagerdutyConfig
|
||||||
|
err := yaml.UnmarshalStrict([]byte(in), &cfg)
|
||||||
|
|
||||||
|
expected := "at most one of service_key & service_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 TestPagerdutyDetails(t *testing.T) {
|
func TestPagerdutyDetails(t *testing.T) {
|
||||||
|
|
|
@ -640,11 +640,19 @@ PagerDuty provides [documentation](https://www.pagerduty.com/docs/guides/prometh
|
||||||
# Whether to notify about resolved alerts.
|
# Whether to notify about resolved alerts.
|
||||||
[ send_resolved: <boolean> | default = true ]
|
[ send_resolved: <boolean> | default = true ]
|
||||||
|
|
||||||
# The following two options are mutually exclusive.
|
# The routing and service keys are mutually exclusive.
|
||||||
# The PagerDuty integration key (when using PagerDuty integration type `Events API v2`).
|
# The PagerDuty integration key (when using PagerDuty integration type `Events API v2`).
|
||||||
|
# It is mutually exclusive with `routing_key_file`.
|
||||||
routing_key: <tmpl_secret>
|
routing_key: <tmpl_secret>
|
||||||
|
# Read the Pager Duty routing key from a file.
|
||||||
|
# It is mutually exclusive with `routing_key`.
|
||||||
|
routing_key_file: <filepath>
|
||||||
# The PagerDuty integration key (when using PagerDuty integration type `Prometheus`).
|
# The PagerDuty integration key (when using PagerDuty integration type `Prometheus`).
|
||||||
|
# It is mutually exclusive with `service_key_file`.
|
||||||
service_key: <tmpl_secret>
|
service_key: <tmpl_secret>
|
||||||
|
# Read the Pager Duty service key from a file.
|
||||||
|
# It is mutually exclusive with `service_key`.
|
||||||
|
service_key_file: <filepath>
|
||||||
|
|
||||||
# The URL to send API requests to
|
# The URL to send API requests to
|
||||||
[ url: <string> | default = global.pagerduty_url ]
|
[ url: <string> | default = global.pagerduty_url ]
|
||||||
|
|
|
@ -20,6 +20,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/alecthomas/units"
|
"github.com/alecthomas/units"
|
||||||
|
@ -54,7 +55,7 @@ func New(c *config.PagerdutyConfig, t *template.Template, l log.Logger, httpOpts
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
n := &Notifier{conf: c, tmpl: t, logger: l, client: client}
|
n := &Notifier{conf: c, tmpl: t, logger: l, client: client}
|
||||||
if c.ServiceKey != "" {
|
if c.ServiceKey != "" || c.ServiceKeyFile != "" {
|
||||||
n.apiV1 = "https://events.pagerduty.com/generic/2010-04-15/create_event.json"
|
n.apiV1 = "https://events.pagerduty.com/generic/2010-04-15/create_event.json"
|
||||||
// Retrying can solve the issue on 403 (rate limiting) and 5xx response codes.
|
// Retrying can solve the issue on 403 (rate limiting) and 5xx response codes.
|
||||||
// https://v2.developer.pagerduty.com/docs/trigger-events
|
// https://v2.developer.pagerduty.com/docs/trigger-events
|
||||||
|
@ -153,8 +154,17 @@ func (n *Notifier) notifyV1(
|
||||||
level.Debug(n.logger).Log("msg", "Truncated description", "description", description, "key", key)
|
level.Debug(n.logger).Log("msg", "Truncated description", "description", description, "key", key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
serviceKey := string(n.conf.ServiceKey)
|
||||||
|
if serviceKey == "" {
|
||||||
|
content, fileErr := os.ReadFile(n.conf.ServiceKeyFile)
|
||||||
|
if fileErr != nil {
|
||||||
|
return false, errors.Wrap(fileErr, "failed to read service key from file")
|
||||||
|
}
|
||||||
|
serviceKey = strings.TrimSpace(string(content))
|
||||||
|
}
|
||||||
|
|
||||||
msg := &pagerDutyMessage{
|
msg := &pagerDutyMessage{
|
||||||
ServiceKey: tmpl(string(n.conf.ServiceKey)),
|
ServiceKey: tmpl(serviceKey),
|
||||||
EventType: eventType,
|
EventType: eventType,
|
||||||
IncidentKey: key.Hash(),
|
IncidentKey: key.Hash(),
|
||||||
Description: description,
|
Description: description,
|
||||||
|
@ -209,10 +219,19 @@ func (n *Notifier) notifyV2(
|
||||||
level.Debug(n.logger).Log("msg", "Truncated summary", "summary", summary, "key", key)
|
level.Debug(n.logger).Log("msg", "Truncated summary", "summary", summary, "key", key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
routingKey := string(n.conf.RoutingKey)
|
||||||
|
if routingKey == "" {
|
||||||
|
content, fileErr := os.ReadFile(n.conf.RoutingKeyFile)
|
||||||
|
if fileErr != nil {
|
||||||
|
return false, errors.Wrap(fileErr, "failed to read routing key from file")
|
||||||
|
}
|
||||||
|
routingKey = strings.TrimSpace(string(content))
|
||||||
|
}
|
||||||
|
|
||||||
msg := &pagerDutyMessage{
|
msg := &pagerDutyMessage{
|
||||||
Client: tmpl(n.conf.Client),
|
Client: tmpl(n.conf.Client),
|
||||||
ClientURL: tmpl(n.conf.ClientURL),
|
ClientURL: tmpl(n.conf.ClientURL),
|
||||||
RoutingKey: tmpl(string(n.conf.RoutingKey)),
|
RoutingKey: tmpl(routingKey),
|
||||||
EventAction: eventType,
|
EventAction: eventType,
|
||||||
DedupKey: key.Hash(),
|
DedupKey: key.Hash(),
|
||||||
Images: make([]pagerDutyImage, 0, len(n.conf.Images)),
|
Images: make([]pagerDutyImage, 0, len(n.conf.Images)),
|
||||||
|
|
|
@ -22,6 +22,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
@ -111,6 +112,54 @@ func TestPagerDutyRedactedURLV2(t *testing.T) {
|
||||||
test.AssertNotifyLeaksNoSecret(ctx, t, notifier, key)
|
test.AssertNotifyLeaksNoSecret(ctx, t, notifier, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPagerDutyV1ServiceKeyFromFile(t *testing.T) {
|
||||||
|
key := "01234567890123456789012345678901"
|
||||||
|
f, err := os.CreateTemp("", "pagerduty_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.PagerdutyConfig{
|
||||||
|
ServiceKeyFile: f.Name(),
|
||||||
|
HTTPConfig: &commoncfg.HTTPClientConfig{},
|
||||||
|
},
|
||||||
|
test.CreateTmpl(t),
|
||||||
|
log.NewNopLogger(),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
notifier.apiV1 = u.String()
|
||||||
|
|
||||||
|
test.AssertNotifyLeaksNoSecret(ctx, t, notifier, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPagerDutyV2RoutingKeyFromFile(t *testing.T) {
|
||||||
|
key := "01234567890123456789012345678901"
|
||||||
|
f, err := os.CreateTemp("", "pagerduty_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.PagerdutyConfig{
|
||||||
|
URL: &config.URL{URL: u},
|
||||||
|
RoutingKeyFile: f.Name(),
|
||||||
|
HTTPConfig: &commoncfg.HTTPClientConfig{},
|
||||||
|
},
|
||||||
|
test.CreateTmpl(t),
|
||||||
|
log.NewNopLogger(),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
test.AssertNotifyLeaksNoSecret(ctx, t, notifier, key)
|
||||||
|
}
|
||||||
|
|
||||||
func TestPagerDutyTemplating(t *testing.T) {
|
func TestPagerDutyTemplating(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)
|
||||||
|
|
Loading…
Reference in New Issue