mirror of
https://github.com/prometheus/alertmanager
synced 2025-01-16 02:54:37 +00:00
59a96579cc
PagerDuty Event API v2 [1] requires images to have an `src` property, and links to have an `href` property. This commit filters out images and links that don't satisfy those conditions, to avoid getting an HTTP 400 error in response. This also adds flexibilty when using templates to configure images and links, as it's now possible to omit images or links by letting the template return an empty string for the `src` or `href` property, respectively. [1]: https://developer.pagerduty.com/docs/events-api-v2/trigger-events/#context-properties Signed-off-by: Benoît Knecht <bknecht@protonmail.ch>
451 lines
11 KiB
Go
451 lines
11 KiB
Go
// Copyright 2019 Prometheus Team
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package pagerduty
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/go-kit/kit/log"
|
|
commoncfg "github.com/prometheus/common/config"
|
|
"github.com/prometheus/common/model"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/prometheus/alertmanager/config"
|
|
"github.com/prometheus/alertmanager/notify"
|
|
"github.com/prometheus/alertmanager/notify/test"
|
|
"github.com/prometheus/alertmanager/types"
|
|
)
|
|
|
|
func TestPagerDutyRetryV1(t *testing.T) {
|
|
notifier, err := New(
|
|
&config.PagerdutyConfig{
|
|
ServiceKey: config.Secret("01234567890123456789012345678901"),
|
|
HTTPConfig: &commoncfg.HTTPClientConfig{},
|
|
},
|
|
test.CreateTmpl(t),
|
|
log.NewNopLogger(),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
retryCodes := append(test.DefaultRetryCodes(), http.StatusForbidden)
|
|
for statusCode, expected := range test.RetryTests(retryCodes) {
|
|
actual, _ := notifier.retrier.Check(statusCode, nil)
|
|
require.Equal(t, expected, actual, fmt.Sprintf("retryv1 - error on status %d", statusCode))
|
|
}
|
|
}
|
|
|
|
func TestPagerDutyRetryV2(t *testing.T) {
|
|
notifier, err := New(
|
|
&config.PagerdutyConfig{
|
|
RoutingKey: config.Secret("01234567890123456789012345678901"),
|
|
HTTPConfig: &commoncfg.HTTPClientConfig{},
|
|
},
|
|
test.CreateTmpl(t),
|
|
log.NewNopLogger(),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
retryCodes := append(test.DefaultRetryCodes(), http.StatusTooManyRequests)
|
|
for statusCode, expected := range test.RetryTests(retryCodes) {
|
|
actual, _ := notifier.retrier.Check(statusCode, nil)
|
|
require.Equal(t, expected, actual, fmt.Sprintf("retryv2 - error on status %d", statusCode))
|
|
}
|
|
}
|
|
|
|
func TestPagerDutyRedactedURLV1(t *testing.T) {
|
|
ctx, u, fn := test.GetContextWithCancelingURL()
|
|
defer fn()
|
|
|
|
key := "01234567890123456789012345678901"
|
|
notifier, err := New(
|
|
&config.PagerdutyConfig{
|
|
ServiceKey: config.Secret(key),
|
|
HTTPConfig: &commoncfg.HTTPClientConfig{},
|
|
},
|
|
test.CreateTmpl(t),
|
|
log.NewNopLogger(),
|
|
)
|
|
require.NoError(t, err)
|
|
notifier.apiV1 = u.String()
|
|
|
|
test.AssertNotifyLeaksNoSecret(t, ctx, notifier, key)
|
|
}
|
|
|
|
func TestPagerDutyRedactedURLV2(t *testing.T) {
|
|
ctx, u, fn := test.GetContextWithCancelingURL()
|
|
defer fn()
|
|
|
|
key := "01234567890123456789012345678901"
|
|
notifier, err := New(
|
|
&config.PagerdutyConfig{
|
|
URL: &config.URL{URL: u},
|
|
RoutingKey: config.Secret(key),
|
|
HTTPConfig: &commoncfg.HTTPClientConfig{},
|
|
},
|
|
test.CreateTmpl(t),
|
|
log.NewNopLogger(),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
test.AssertNotifyLeaksNoSecret(t, ctx, notifier, key)
|
|
}
|
|
|
|
func TestPagerDutyTemplating(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
dec := json.NewDecoder(r.Body)
|
|
out := make(map[string]interface{})
|
|
err := dec.Decode(&out)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
u, _ := url.Parse(srv.URL)
|
|
|
|
for _, tc := range []struct {
|
|
title string
|
|
cfg *config.PagerdutyConfig
|
|
|
|
retry bool
|
|
errMsg string
|
|
}{
|
|
{
|
|
title: "full-blown message",
|
|
cfg: &config.PagerdutyConfig{
|
|
RoutingKey: config.Secret("01234567890123456789012345678901"),
|
|
Images: []config.PagerdutyImage{
|
|
{
|
|
Src: "{{ .Status }}",
|
|
Alt: "{{ .Status }}",
|
|
Href: "{{ .Status }}",
|
|
},
|
|
},
|
|
Links: []config.PagerdutyLink{
|
|
{
|
|
Href: "{{ .Status }}",
|
|
Text: "{{ .Status }}",
|
|
},
|
|
},
|
|
Details: map[string]string{
|
|
"firing": `{{ template "pagerduty.default.instances" .Alerts.Firing }}`,
|
|
"resolved": `{{ template "pagerduty.default.instances" .Alerts.Resolved }}`,
|
|
"num_firing": `{{ .Alerts.Firing | len }}`,
|
|
"num_resolved": `{{ .Alerts.Resolved | len }}`,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
title: "details with templating errors",
|
|
cfg: &config.PagerdutyConfig{
|
|
RoutingKey: config.Secret("01234567890123456789012345678901"),
|
|
Details: map[string]string{
|
|
"firing": `{{ template "pagerduty.default.instances" .Alerts.Firing`,
|
|
"resolved": `{{ template "pagerduty.default.instances" .Alerts.Resolved }}`,
|
|
"num_firing": `{{ .Alerts.Firing | len }}`,
|
|
"num_resolved": `{{ .Alerts.Resolved | len }}`,
|
|
},
|
|
},
|
|
errMsg: "failed to template",
|
|
},
|
|
{
|
|
title: "v2 message with templating errors",
|
|
cfg: &config.PagerdutyConfig{
|
|
RoutingKey: config.Secret("01234567890123456789012345678901"),
|
|
Severity: "{{ ",
|
|
},
|
|
errMsg: "failed to template",
|
|
},
|
|
{
|
|
title: "v1 message with templating errors",
|
|
cfg: &config.PagerdutyConfig{
|
|
ServiceKey: config.Secret("01234567890123456789012345678901"),
|
|
Client: "{{ ",
|
|
},
|
|
errMsg: "failed to template",
|
|
},
|
|
{
|
|
title: "routing key cannot be empty",
|
|
cfg: &config.PagerdutyConfig{
|
|
RoutingKey: config.Secret(`{{ "" }}`),
|
|
},
|
|
errMsg: "routing key cannot be empty",
|
|
},
|
|
{
|
|
title: "service_key cannot be empty",
|
|
cfg: &config.PagerdutyConfig{
|
|
ServiceKey: config.Secret(`{{ "" }}`),
|
|
},
|
|
errMsg: "service key cannot be empty",
|
|
},
|
|
} {
|
|
t.Run(tc.title, func(t *testing.T) {
|
|
tc.cfg.URL = &config.URL{URL: u}
|
|
tc.cfg.HTTPConfig = &commoncfg.HTTPClientConfig{}
|
|
pd, err := New(tc.cfg, test.CreateTmpl(t), log.NewNopLogger())
|
|
require.NoError(t, err)
|
|
if pd.apiV1 != "" {
|
|
pd.apiV1 = u.String()
|
|
}
|
|
|
|
ctx := context.Background()
|
|
ctx = notify.WithGroupKey(ctx, "1")
|
|
|
|
ok, err := pd.Notify(ctx, []*types.Alert{
|
|
&types.Alert{
|
|
Alert: model.Alert{
|
|
Labels: model.LabelSet{
|
|
"lbl1": "val1",
|
|
},
|
|
StartsAt: time.Now(),
|
|
EndsAt: time.Now().Add(time.Hour),
|
|
},
|
|
},
|
|
}...)
|
|
if tc.errMsg == "" {
|
|
require.NoError(t, err)
|
|
} else {
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), tc.errMsg)
|
|
}
|
|
require.Equal(t, tc.retry, ok)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestErrDetails(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
status int
|
|
body io.Reader
|
|
|
|
exp string
|
|
}{
|
|
{
|
|
status: http.StatusBadRequest,
|
|
body: bytes.NewBuffer([]byte(
|
|
`{"status":"invalid event","message":"Event object is invalid","errors":["Length of 'routing_key' is incorrect (should be 32 characters)"]}`,
|
|
)),
|
|
|
|
exp: "Length of 'routing_key' is incorrect",
|
|
},
|
|
{
|
|
status: http.StatusBadRequest,
|
|
body: bytes.NewBuffer([]byte(`{"status"}`)),
|
|
|
|
exp: "",
|
|
},
|
|
{
|
|
status: http.StatusBadRequest,
|
|
|
|
exp: "",
|
|
},
|
|
{
|
|
status: http.StatusTooManyRequests,
|
|
|
|
exp: "",
|
|
},
|
|
} {
|
|
tc := tc
|
|
t.Run("", func(t *testing.T) {
|
|
err := errDetails(tc.status, tc.body)
|
|
require.Contains(t, err, tc.exp)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEventSizeEnforcement(t *testing.T) {
|
|
bigDetails := map[string]string{
|
|
"firing": strings.Repeat("a", 513000),
|
|
}
|
|
|
|
// V1 Messages
|
|
msgV1 := &pagerDutyMessage{
|
|
ServiceKey: "01234567890123456789012345678901",
|
|
EventType: "trigger",
|
|
Details: bigDetails,
|
|
}
|
|
|
|
notifierV1, err := New(
|
|
&config.PagerdutyConfig{
|
|
ServiceKey: config.Secret("01234567890123456789012345678901"),
|
|
HTTPConfig: &commoncfg.HTTPClientConfig{},
|
|
},
|
|
test.CreateTmpl(t),
|
|
log.NewNopLogger(),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
encodedV1, err := notifierV1.encodeMessage(msgV1)
|
|
require.NoError(t, err)
|
|
require.Contains(t, encodedV1.String(), `"details":{"error":"Custom details have been removed because the original event exceeds the maximum size of 512KB"}`)
|
|
|
|
// V2 Messages
|
|
msgV2 := &pagerDutyMessage{
|
|
RoutingKey: "01234567890123456789012345678901",
|
|
EventAction: "trigger",
|
|
Payload: &pagerDutyPayload{
|
|
CustomDetails: bigDetails,
|
|
},
|
|
}
|
|
|
|
notifierV2, err := New(
|
|
&config.PagerdutyConfig{
|
|
RoutingKey: config.Secret("01234567890123456789012345678901"),
|
|
HTTPConfig: &commoncfg.HTTPClientConfig{},
|
|
},
|
|
test.CreateTmpl(t),
|
|
log.NewNopLogger(),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
encodedV2, err := notifierV2.encodeMessage(msgV2)
|
|
require.NoError(t, err)
|
|
require.Contains(t, encodedV2.String(), `"custom_details":{"error":"Custom details have been removed because the original event exceeds the maximum size of 512KB"}`)
|
|
}
|
|
|
|
func TestPagerDutyEmptySrcHref(t *testing.T) {
|
|
type pagerDutyEvent struct {
|
|
RoutingKey string `json:"routing_key"`
|
|
EventAction string `json:"event_action"`
|
|
DedupKey string `json:"dedup_key"`
|
|
Payload pagerDutyPayload `json:"payload"`
|
|
Images []pagerDutyImage
|
|
Links []pagerDutyLink
|
|
}
|
|
|
|
images := []config.PagerdutyImage{
|
|
{
|
|
Src: "",
|
|
Alt: "Empty src",
|
|
Href: "https://example.com/",
|
|
},
|
|
{
|
|
Src: "https://example.com/cat.jpg",
|
|
Alt: "Empty href",
|
|
Href: "",
|
|
},
|
|
{
|
|
Src: "https://example.com/cat.jpg",
|
|
Alt: "",
|
|
Href: "https://example.com/",
|
|
},
|
|
}
|
|
|
|
links := []config.PagerdutyLink{
|
|
{
|
|
Href: "",
|
|
Text: "Empty href",
|
|
},
|
|
{
|
|
Href: "https://example.com/",
|
|
Text: "",
|
|
},
|
|
}
|
|
|
|
expectedImages := make([]pagerDutyImage, 0, len(images))
|
|
for _, image := range images {
|
|
if image.Src == "" {
|
|
continue
|
|
}
|
|
expectedImages = append(expectedImages, pagerDutyImage{
|
|
Src: image.Src,
|
|
Alt: image.Alt,
|
|
Href: image.Href,
|
|
})
|
|
}
|
|
|
|
expectedLinks := make([]pagerDutyLink, 0, len(links))
|
|
for _, link := range links {
|
|
if link.Href == "" {
|
|
continue
|
|
}
|
|
expectedLinks = append(expectedLinks, pagerDutyLink{
|
|
HRef: link.Href,
|
|
Text: link.Text,
|
|
})
|
|
}
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(
|
|
func(w http.ResponseWriter, r *http.Request) {
|
|
decoder := json.NewDecoder(r.Body)
|
|
var event pagerDutyEvent
|
|
if err := decoder.Decode(&event); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
if event.RoutingKey == "" || event.EventAction == "" {
|
|
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
for _, image := range event.Images {
|
|
if image.Src == "" {
|
|
http.Error(w, "Event object is invalid: 'image src' is missing or blank", http.StatusBadRequest)
|
|
return
|
|
}
|
|
}
|
|
|
|
for _, link := range event.Links {
|
|
if link.HRef == "" {
|
|
http.Error(w, "Event object is invalid: 'link href' is missing or blank", http.StatusBadRequest)
|
|
return
|
|
}
|
|
}
|
|
|
|
require.Equal(t, expectedImages, event.Images)
|
|
require.Equal(t, expectedLinks, event.Links)
|
|
},
|
|
))
|
|
defer server.Close()
|
|
|
|
url, err := url.Parse(server.URL)
|
|
require.NoError(t, err)
|
|
|
|
pagerDutyConfig := config.PagerdutyConfig{
|
|
HTTPConfig: &commoncfg.HTTPClientConfig{},
|
|
RoutingKey: config.Secret("01234567890123456789012345678901"),
|
|
URL: &config.URL{URL: url},
|
|
Images: images,
|
|
Links: links,
|
|
}
|
|
|
|
pagerDuty, err := New(&pagerDutyConfig, test.CreateTmpl(t), log.NewNopLogger())
|
|
require.NoError(t, err)
|
|
|
|
ctx := context.Background()
|
|
ctx = notify.WithGroupKey(ctx, "1")
|
|
|
|
_, err = pagerDuty.Notify(ctx, []*types.Alert{
|
|
{
|
|
Alert: model.Alert{
|
|
Labels: model.LabelSet{
|
|
"lbl1": "val1",
|
|
},
|
|
StartsAt: time.Now(),
|
|
EndsAt: time.Now().Add(time.Hour),
|
|
},
|
|
},
|
|
}...)
|
|
require.NoError(t, err)
|
|
}
|