From fc49fb627cf255172c03e3b65aea209de2e8efba Mon Sep 17 00:00:00 2001 From: Oktarian T-B Date: Fri, 14 Oct 2022 13:55:59 +0100 Subject: [PATCH] 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 Signed-off-by: Yijie Qin --- config/notifiers.go | 38 ++++++++------ config/notifiers_test.go | 84 ++++++++++++++++++++++-------- docs/configuration.md | 10 +++- notify/pagerduty/pagerduty.go | 25 +++++++-- notify/pagerduty/pagerduty_test.go | 49 +++++++++++++++++ 5 files changed, 165 insertions(+), 41 deletions(-) diff --git a/config/notifiers.go b/config/notifiers.go index da561730af..84a3dad9e0 100644 --- a/config/notifiers.go +++ b/config/notifiers.go @@ -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 @@ -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) } diff --git a/config/notifiers_test.go b/config/notifiers_test.go index 0d38623163..871c2f8f7a 100644 --- a/config/notifiers_test.go +++ b/config/notifiers_test.go @@ -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) { diff --git a/docs/configuration.md b/docs/configuration.md index c76f33a1aa..cc33bdebd1 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -640,11 +640,19 @@ PagerDuty provides [documentation](https://www.pagerduty.com/docs/guides/prometh # Whether to notify about resolved alerts. [ send_resolved: | 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: +# Read the Pager Duty routing key from a file. +# It is mutually exclusive with `routing_key`. +routing_key_file: # The PagerDuty integration key (when using PagerDuty integration type `Prometheus`). +# It is mutually exclusive with `service_key_file`. service_key: +# Read the Pager Duty service key from a file. +# It is mutually exclusive with `service_key`. +service_key_file: # The URL to send API requests to [ url: | default = global.pagerduty_url ] diff --git a/notify/pagerduty/pagerduty.go b/notify/pagerduty/pagerduty.go index 32cd6b418f..fad2c6369a 100644 --- a/notify/pagerduty/pagerduty.go +++ b/notify/pagerduty/pagerduty.go @@ -20,6 +20,7 @@ import ( "fmt" "io" "net/http" + "os" "strings" "github.com/alecthomas/units" @@ -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 @@ -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, @@ -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)), diff --git a/notify/pagerduty/pagerduty_test.go b/notify/pagerduty/pagerduty_test.go index bb84327203..be4e83dc04 100644 --- a/notify/pagerduty/pagerduty_test.go +++ b/notify/pagerduty/pagerduty_test.go @@ -22,6 +22,7 @@ import ( "net/http" "net/http/httptest" "net/url" + "os" "strings" "testing" "time" @@ -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)