Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for reading PagerDuty secrets from files #3107

Merged
merged 4 commits into from Oct 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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`).
OktarianTB marked this conversation as resolved.
Show resolved Hide resolved
# 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`).
OktarianTB marked this conversation as resolved.
Show resolved Hide resolved
# 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