Skip to content

Commit

Permalink
Add support for reading PagerDuty secrets from files (prometheus#3107)
Browse files Browse the repository at this point in the history
* Add support for reading PagerDuty secrets from files

* Update documentation

Signed-off-by: Oktarian Tilney-Bassett <oktariantilneybassett@improbable.io>
Signed-off-by: Yijie Qin <qinyijie@amazon.com>
  • Loading branch information
Oktarian T-B authored and qinxx108 committed Nov 16, 2022
1 parent 34998e0 commit fc49fb6
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 41 deletions.
38 changes: 23 additions & 15 deletions config/notifiers.go
Expand Up @@ -208,20 +208,22 @@ type PagerdutyConfig struct {

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

ServiceKey Secret `yaml:"service_key,omitempty" json:"service_key,omitempty"`
RoutingKey Secret `yaml:"routing_key,omitempty" json:"routing_key,omitempty"`
URL *URL `yaml:"url,omitempty" json:"url,omitempty"`
Client string `yaml:"client,omitempty" json:"client,omitempty"`
ClientURL string `yaml:"client_url,omitempty" json:"client_url,omitempty"`
Description string `yaml:"description,omitempty" json:"description,omitempty"`
Details map[string]string `yaml:"details,omitempty" json:"details,omitempty"`
Images []PagerdutyImage `yaml:"images,omitempty" json:"images,omitempty"`
Links []PagerdutyLink `yaml:"links,omitempty" json:"links,omitempty"`
Source string `yaml:"source,omitempty" json:"source,omitempty"`
Severity string `yaml:"severity,omitempty" json:"severity,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"`
ServiceKey Secret `yaml:"service_key,omitempty" json:"service_key,omitempty"`
ServiceKeyFile string `yaml:"service_key_file,omitempty" json:"service_key_file,omitempty"`
RoutingKey Secret `yaml:"routing_key,omitempty" json:"routing_key,omitempty"`
RoutingKeyFile string `yaml:"routing_key_file,omitempty" json:"routing_key_file,omitempty"`
URL *URL `yaml:"url,omitempty" json:"url,omitempty"`
Client string `yaml:"client,omitempty" json:"client,omitempty"`
ClientURL string `yaml:"client_url,omitempty" json:"client_url,omitempty"`
Description string `yaml:"description,omitempty" json:"description,omitempty"`
Details map[string]string `yaml:"details,omitempty" json:"details,omitempty"`
Images []PagerdutyImage `yaml:"images,omitempty" json:"images,omitempty"`
Links []PagerdutyLink `yaml:"links,omitempty" json:"links,omitempty"`
Source string `yaml:"source,omitempty" json:"source,omitempty"`
Severity string `yaml:"severity,omitempty" json:"severity,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
Expand All @@ -244,9 +246,15 @@ func (c *PagerdutyConfig) UnmarshalYAML(unmarshal func(interface{}) error) error
if err := unmarshal((*plain)(c)); err != nil {
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")
}
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 {
c.Details = make(map[string]string)
}
Expand Down
84 changes: 62 additions & 22 deletions config/notifiers_test.go
Expand Up @@ -59,38 +59,78 @@ headers:
}
}

func TestPagerdutyRoutingKeyIsPresent(t *testing.T) {
in := `
func TestPagerdutyTestRoutingKey(t *testing.T) {
t.Run("error if no routing key or key file", func(t *testing.T) {
in := `
routing_key: ''
`
var cfg PagerdutyConfig
err := yaml.UnmarshalStrict([]byte(in), &cfg)
var cfg PagerdutyConfig
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 {
t.Fatalf("no error returned, expected:\n%v", expected)
}
if err.Error() != expected {
t.Errorf("\nexpected:\n%v\ngot:\n%v", expected, err.Error())
}
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())
}
})

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) {
in := `
func TestPagerdutyServiceKey(t *testing.T) {
t.Run("error if no service key or key file", func(t *testing.T) {
in := `
service_key: ''
`
var cfg PagerdutyConfig
err := yaml.UnmarshalStrict([]byte(in), &cfg)
var cfg PagerdutyConfig
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 {
t.Fatalf("no error returned, expected:\n%v", expected)
}
if err.Error() != expected {
t.Errorf("\nexpected:\n%v\ngot:\n%v", expected, err.Error())
}
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())
}
})

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) {
Expand Down
10 changes: 9 additions & 1 deletion docs/configuration.md
Expand Up @@ -640,11 +640,19 @@ PagerDuty provides [documentation](https://www.pagerduty.com/docs/guides/prometh
# Whether to notify about resolved alerts.
[ 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`).
# It is mutually exclusive with `routing_key_file`.
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`).
# It is mutually exclusive with `service_key_file`.
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
[ url: <string> | default = global.pagerduty_url ]
Expand Down
25 changes: 22 additions & 3 deletions notify/pagerduty/pagerduty.go
Expand Up @@ -20,6 +20,7 @@ import (
"fmt"
"io"
"net/http"
"os"
"strings"

"github.com/alecthomas/units"
Expand Down Expand Up @@ -54,7 +55,7 @@ func New(c *config.PagerdutyConfig, t *template.Template, l log.Logger, httpOpts
return nil, err
}
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"
// Retrying can solve the issue on 403 (rate limiting) and 5xx response codes.
// https://v2.developer.pagerduty.com/docs/trigger-events
Expand Down Expand Up @@ -153,8 +154,17 @@ func (n *Notifier) notifyV1(
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{
ServiceKey: tmpl(string(n.conf.ServiceKey)),
ServiceKey: tmpl(serviceKey),
EventType: eventType,
IncidentKey: key.Hash(),
Description: description,
Expand Down Expand Up @@ -209,10 +219,19 @@ func (n *Notifier) notifyV2(
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{
Client: tmpl(n.conf.Client),
ClientURL: tmpl(n.conf.ClientURL),
RoutingKey: tmpl(string(n.conf.RoutingKey)),
RoutingKey: tmpl(routingKey),
EventAction: eventType,
DedupKey: key.Hash(),
Images: make([]pagerDutyImage, 0, len(n.conf.Images)),
Expand Down
49 changes: 49 additions & 0 deletions notify/pagerduty/pagerduty_test.go
Expand Up @@ -22,6 +22,7 @@ import (
"net/http"
"net/http/httptest"
"net/url"
"os"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -111,6 +112,54 @@ func TestPagerDutyRedactedURLV2(t *testing.T) {
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) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
dec := json.NewDecoder(r.Body)
Expand Down

0 comments on commit fc49fb6

Please sign in to comment.