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:
Oktarian T-B 2022-10-14 13:55:59 +01:00 committed by GitHub
parent d034f116d5
commit 1045dc0f21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 165 additions and 41 deletions

View File

@ -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)
} }

View File

@ -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) {

View File

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

View File

@ -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)),

View File

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