2018-05-14 12:36:24 +00:00
|
|
|
// Copyright 2018 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.
|
|
|
|
|
2017-11-11 14:12:03 +00:00
|
|
|
package template
|
|
|
|
|
|
|
|
import (
|
2022-12-16 14:13:13 +00:00
|
|
|
tmplhtml "html/template"
|
2019-07-04 13:31:09 +00:00
|
|
|
"net/url"
|
2017-11-15 19:25:23 +00:00
|
|
|
"testing"
|
2022-12-16 14:13:13 +00:00
|
|
|
tmpltext "text/template"
|
2019-07-04 13:31:09 +00:00
|
|
|
"time"
|
2017-11-15 19:25:23 +00:00
|
|
|
|
2017-11-11 14:12:03 +00:00
|
|
|
"github.com/prometheus/common/model"
|
|
|
|
"github.com/stretchr/testify/require"
|
2019-07-04 13:31:09 +00:00
|
|
|
|
|
|
|
"github.com/prometheus/alertmanager/types"
|
2017-11-11 14:12:03 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
func TestPairNames(t *testing.T) {
|
|
|
|
pairs := Pairs{
|
|
|
|
{"name1", "value1"},
|
|
|
|
{"name2", "value2"},
|
|
|
|
{"name3", "value3"},
|
|
|
|
}
|
|
|
|
|
|
|
|
expected := []string{"name1", "name2", "name3"}
|
|
|
|
require.EqualValues(t, expected, pairs.Names())
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestPairValues(t *testing.T) {
|
|
|
|
pairs := Pairs{
|
|
|
|
{"name1", "value1"},
|
|
|
|
{"name2", "value2"},
|
|
|
|
{"name3", "value3"},
|
|
|
|
}
|
|
|
|
|
|
|
|
expected := []string{"value1", "value2", "value3"}
|
|
|
|
require.EqualValues(t, expected, pairs.Values())
|
|
|
|
}
|
|
|
|
|
2017-11-15 19:25:23 +00:00
|
|
|
func TestKVSortedPairs(t *testing.T) {
|
|
|
|
kv := KV{"d": "dVal", "b": "bVal", "c": "cVal"}
|
|
|
|
|
|
|
|
expectedPairs := Pairs{
|
|
|
|
{"b", "bVal"},
|
|
|
|
{"c", "cVal"},
|
|
|
|
{"d", "dVal"},
|
|
|
|
}
|
|
|
|
|
|
|
|
for i, p := range kv.SortedPairs() {
|
|
|
|
require.EqualValues(t, p.Name, expectedPairs[i].Name)
|
|
|
|
require.EqualValues(t, p.Value, expectedPairs[i].Value)
|
|
|
|
}
|
|
|
|
|
|
|
|
// validates alertname always comes first
|
|
|
|
kv = KV{"d": "dVal", "b": "bVal", "c": "cVal", "alertname": "alert", "a": "aVal"}
|
|
|
|
|
|
|
|
expectedPairs = Pairs{
|
|
|
|
{"alertname", "alert"},
|
|
|
|
{"a", "aVal"},
|
|
|
|
{"b", "bVal"},
|
|
|
|
{"c", "cVal"},
|
|
|
|
{"d", "dVal"},
|
|
|
|
}
|
|
|
|
|
|
|
|
for i, p := range kv.SortedPairs() {
|
|
|
|
require.EqualValues(t, p.Name, expectedPairs[i].Name)
|
|
|
|
require.EqualValues(t, p.Value, expectedPairs[i].Value)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-11 14:12:03 +00:00
|
|
|
func TestKVRemove(t *testing.T) {
|
|
|
|
kv := KV{
|
|
|
|
"key1": "val1",
|
|
|
|
"key2": "val2",
|
|
|
|
"key3": "val3",
|
|
|
|
"key4": "val4",
|
|
|
|
}
|
|
|
|
|
|
|
|
kv = kv.Remove([]string{"key2", "key4"})
|
|
|
|
|
|
|
|
expected := []string{"key1", "key3"}
|
|
|
|
require.EqualValues(t, expected, kv.Names())
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestAlertsFiring(t *testing.T) {
|
|
|
|
alerts := Alerts{
|
|
|
|
{Status: string(model.AlertFiring)},
|
|
|
|
{Status: string(model.AlertResolved)},
|
|
|
|
{Status: string(model.AlertFiring)},
|
|
|
|
{Status: string(model.AlertResolved)},
|
|
|
|
{Status: string(model.AlertResolved)},
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, alert := range alerts.Firing() {
|
|
|
|
if alert.Status != string(model.AlertFiring) {
|
|
|
|
t.Errorf("unexpected status %q", alert.Status)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestAlertsResolved(t *testing.T) {
|
|
|
|
alerts := Alerts{
|
|
|
|
{Status: string(model.AlertFiring)},
|
|
|
|
{Status: string(model.AlertResolved)},
|
|
|
|
{Status: string(model.AlertFiring)},
|
|
|
|
{Status: string(model.AlertResolved)},
|
|
|
|
{Status: string(model.AlertResolved)},
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, alert := range alerts.Resolved() {
|
|
|
|
if alert.Status != string(model.AlertResolved) {
|
|
|
|
t.Errorf("unexpected status %q", alert.Status)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-07-04 13:31:09 +00:00
|
|
|
|
|
|
|
func TestData(t *testing.T) {
|
|
|
|
u, err := url.Parse("http://example.com/")
|
|
|
|
require.NoError(t, err)
|
|
|
|
tmpl := &Template{ExternalURL: u}
|
|
|
|
startTime := time.Time{}.Add(1 * time.Second)
|
|
|
|
endTime := time.Time{}.Add(2 * time.Second)
|
|
|
|
|
|
|
|
for _, tc := range []struct {
|
|
|
|
receiver string
|
|
|
|
groupLabels model.LabelSet
|
|
|
|
alerts []*types.Alert
|
|
|
|
|
|
|
|
exp *Data
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
receiver: "webhook",
|
|
|
|
exp: &Data{
|
|
|
|
Receiver: "webhook",
|
|
|
|
Status: "resolved",
|
|
|
|
Alerts: Alerts{},
|
|
|
|
GroupLabels: KV{},
|
|
|
|
CommonLabels: KV{},
|
|
|
|
CommonAnnotations: KV{},
|
|
|
|
ExternalURL: u.String(),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
receiver: "webhook",
|
|
|
|
groupLabels: model.LabelSet{
|
|
|
|
model.LabelName("job"): model.LabelValue("foo"),
|
|
|
|
},
|
|
|
|
alerts: []*types.Alert{
|
|
|
|
{
|
|
|
|
Alert: model.Alert{
|
|
|
|
StartsAt: startTime,
|
|
|
|
Labels: model.LabelSet{
|
|
|
|
model.LabelName("severity"): model.LabelValue("warning"),
|
|
|
|
model.LabelName("job"): model.LabelValue("foo"),
|
|
|
|
},
|
|
|
|
Annotations: model.LabelSet{
|
|
|
|
model.LabelName("description"): model.LabelValue("something happened"),
|
|
|
|
model.LabelName("runbook"): model.LabelValue("foo"),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Alert: model.Alert{
|
|
|
|
StartsAt: startTime,
|
|
|
|
EndsAt: endTime,
|
|
|
|
Labels: model.LabelSet{
|
|
|
|
model.LabelName("severity"): model.LabelValue("critical"),
|
|
|
|
model.LabelName("job"): model.LabelValue("foo"),
|
|
|
|
},
|
|
|
|
Annotations: model.LabelSet{
|
|
|
|
model.LabelName("description"): model.LabelValue("something else happened"),
|
|
|
|
model.LabelName("runbook"): model.LabelValue("foo"),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
exp: &Data{
|
|
|
|
Receiver: "webhook",
|
|
|
|
Status: "firing",
|
|
|
|
Alerts: Alerts{
|
|
|
|
{
|
|
|
|
Status: "firing",
|
|
|
|
Labels: KV{"severity": "warning", "job": "foo"},
|
|
|
|
Annotations: KV{"description": "something happened", "runbook": "foo"},
|
|
|
|
StartsAt: startTime,
|
2019-07-23 07:57:24 +00:00
|
|
|
Fingerprint: "9266ef3da838ad95",
|
2019-07-04 13:31:09 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
Status: "resolved",
|
|
|
|
Labels: KV{"severity": "critical", "job": "foo"},
|
|
|
|
Annotations: KV{"description": "something else happened", "runbook": "foo"},
|
|
|
|
StartsAt: startTime,
|
|
|
|
EndsAt: endTime,
|
2019-07-23 07:57:24 +00:00
|
|
|
Fingerprint: "3b15fd163d36582e",
|
2019-07-04 13:31:09 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
GroupLabels: KV{"job": "foo"},
|
|
|
|
CommonLabels: KV{"job": "foo"},
|
|
|
|
CommonAnnotations: KV{"runbook": "foo"},
|
|
|
|
ExternalURL: u.String(),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
receiver: "webhook",
|
|
|
|
groupLabels: model.LabelSet{},
|
|
|
|
alerts: []*types.Alert{
|
|
|
|
{
|
|
|
|
Alert: model.Alert{
|
|
|
|
StartsAt: startTime,
|
|
|
|
Labels: model.LabelSet{
|
|
|
|
model.LabelName("severity"): model.LabelValue("warning"),
|
|
|
|
model.LabelName("job"): model.LabelValue("foo"),
|
|
|
|
},
|
|
|
|
Annotations: model.LabelSet{
|
|
|
|
model.LabelName("description"): model.LabelValue("something happened"),
|
|
|
|
model.LabelName("runbook"): model.LabelValue("foo"),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Alert: model.Alert{
|
|
|
|
StartsAt: startTime,
|
|
|
|
EndsAt: endTime,
|
|
|
|
Labels: model.LabelSet{
|
|
|
|
model.LabelName("severity"): model.LabelValue("critical"),
|
|
|
|
model.LabelName("job"): model.LabelValue("bar"),
|
|
|
|
},
|
|
|
|
Annotations: model.LabelSet{
|
|
|
|
model.LabelName("description"): model.LabelValue("something else happened"),
|
|
|
|
model.LabelName("runbook"): model.LabelValue("bar"),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
exp: &Data{
|
|
|
|
Receiver: "webhook",
|
|
|
|
Status: "firing",
|
|
|
|
Alerts: Alerts{
|
|
|
|
{
|
|
|
|
Status: "firing",
|
|
|
|
Labels: KV{"severity": "warning", "job": "foo"},
|
|
|
|
Annotations: KV{"description": "something happened", "runbook": "foo"},
|
|
|
|
StartsAt: startTime,
|
2019-07-23 07:57:24 +00:00
|
|
|
Fingerprint: "9266ef3da838ad95",
|
2019-07-04 13:31:09 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
Status: "resolved",
|
|
|
|
Labels: KV{"severity": "critical", "job": "bar"},
|
|
|
|
Annotations: KV{"description": "something else happened", "runbook": "bar"},
|
|
|
|
StartsAt: startTime,
|
|
|
|
EndsAt: endTime,
|
2019-07-23 07:57:24 +00:00
|
|
|
Fingerprint: "c7e68cb08e3e67f9",
|
2019-07-04 13:31:09 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
GroupLabels: KV{},
|
|
|
|
CommonLabels: KV{},
|
|
|
|
CommonAnnotations: KV{},
|
|
|
|
ExternalURL: u.String(),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
} {
|
|
|
|
tc := tc
|
|
|
|
t.Run("", func(t *testing.T) {
|
|
|
|
got := tmpl.Data(tc.receiver, tc.groupLabels, tc.alerts...)
|
|
|
|
require.Equal(t, tc.exp, got)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestTemplateExpansion(t *testing.T) {
|
2022-12-16 14:13:13 +00:00
|
|
|
tmpl, err := FromGlobs([]string{})
|
2019-07-04 13:31:09 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
for _, tc := range []struct {
|
|
|
|
title string
|
|
|
|
in string
|
|
|
|
data interface{}
|
|
|
|
html bool
|
|
|
|
|
|
|
|
exp string
|
|
|
|
fail bool
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
title: "Template without action",
|
|
|
|
in: `abc`,
|
|
|
|
exp: "abc",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
title: "Template with simple action",
|
|
|
|
in: `{{ "abc" }}`,
|
|
|
|
exp: "abc",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
title: "Template with invalid syntax",
|
|
|
|
in: `{{ `,
|
|
|
|
fail: true,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
title: "Template using toUpper",
|
|
|
|
in: `{{ "abc" | toUpper }}`,
|
|
|
|
exp: "ABC",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
title: "Template using toLower",
|
|
|
|
in: `{{ "ABC" | toLower }}`,
|
|
|
|
exp: "abc",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
title: "Template using title",
|
|
|
|
in: `{{ "abc" | title }}`,
|
|
|
|
exp: "Abc",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
title: "Template using positive match",
|
|
|
|
in: `{{ if match "^a" "abc"}}abc{{ end }}`,
|
|
|
|
exp: "abc",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
title: "Template using negative match",
|
|
|
|
in: `{{ if match "abcd" "abc" }}abc{{ end }}`,
|
|
|
|
exp: "",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
title: "Template using join",
|
|
|
|
in: `{{ . | join "," }}`,
|
|
|
|
data: []string{"a", "b", "c"},
|
|
|
|
exp: "a,b,c",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
title: "Text template without HTML escaping",
|
|
|
|
in: `{{ "<b>" }}`,
|
|
|
|
exp: "<b>",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
title: "HTML template with escaping",
|
|
|
|
in: `{{ "<b>" }}`,
|
|
|
|
html: true,
|
|
|
|
exp: "<b>",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
title: "HTML template using safeHTML",
|
|
|
|
in: `{{ "<b>" | safeHtml }}`,
|
|
|
|
html: true,
|
|
|
|
exp: "<b>",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
title: "Template using reReplaceAll",
|
|
|
|
in: `{{ reReplaceAll "ab" "AB" "abcdabcda"}}`,
|
|
|
|
exp: "ABcdABcda",
|
|
|
|
},
|
2019-11-13 09:32:51 +00:00
|
|
|
{
|
|
|
|
title: "Template using stringSlice",
|
|
|
|
in: `{{ with .GroupLabels }}{{ with .Remove (stringSlice "key1" "key3") }}{{ .SortedPairs.Values }}{{ end }}{{ end }}`,
|
|
|
|
data: Data{
|
|
|
|
GroupLabels: KV{
|
|
|
|
"key1": "key1",
|
|
|
|
"key2": "key2",
|
|
|
|
"key3": "key3",
|
|
|
|
"key4": "key4",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
exp: "[key2 key4]",
|
|
|
|
},
|
2019-07-04 13:31:09 +00:00
|
|
|
} {
|
|
|
|
tc := tc
|
|
|
|
t.Run(tc.title, func(t *testing.T) {
|
|
|
|
f := tmpl.ExecuteTextString
|
|
|
|
if tc.html {
|
|
|
|
f = tmpl.ExecuteHTMLString
|
|
|
|
}
|
|
|
|
got, err := f(tc.in, tc.data)
|
|
|
|
if tc.fail {
|
|
|
|
require.NotNil(t, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, tc.exp, got)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2022-12-16 14:13:13 +00:00
|
|
|
|
|
|
|
func TestTemplateExpansionWithOptions(t *testing.T) {
|
|
|
|
testOptionWithAdditionalFuncs := func(funcs FuncMap) Option {
|
|
|
|
return func(text *tmpltext.Template, html *tmplhtml.Template) {
|
|
|
|
text.Funcs(tmpltext.FuncMap(funcs))
|
|
|
|
html.Funcs(tmplhtml.FuncMap(funcs))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, tc := range []struct {
|
|
|
|
options []Option
|
|
|
|
title string
|
|
|
|
in string
|
|
|
|
data interface{}
|
|
|
|
html bool
|
|
|
|
|
|
|
|
exp string
|
|
|
|
fail bool
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
title: "Test custom function",
|
|
|
|
options: []Option{testOptionWithAdditionalFuncs(FuncMap{"printFoo": func() string { return "foo" }})},
|
|
|
|
in: `{{ printFoo }}`,
|
|
|
|
exp: "foo",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
title: "Test Default function with additional function added",
|
|
|
|
options: []Option{testOptionWithAdditionalFuncs(FuncMap{"printFoo": func() string { return "foo" }})},
|
|
|
|
in: `{{ toUpper "test" }}`,
|
|
|
|
exp: "TEST",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
title: "Test custom function is overridden by the DefaultFuncs",
|
|
|
|
options: []Option{testOptionWithAdditionalFuncs(FuncMap{"toUpper": func(s string) string { return "foo" }})},
|
|
|
|
in: `{{ toUpper "test" }}`,
|
|
|
|
exp: "TEST",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
title: "Test later Option overrides the previous",
|
|
|
|
options: []Option{
|
|
|
|
testOptionWithAdditionalFuncs(FuncMap{"printFoo": func() string { return "foo" }}),
|
|
|
|
testOptionWithAdditionalFuncs(FuncMap{"printFoo": func() string { return "bar" }}),
|
|
|
|
},
|
|
|
|
in: `{{ printFoo }}`,
|
|
|
|
exp: "bar",
|
|
|
|
},
|
|
|
|
} {
|
|
|
|
tc := tc
|
|
|
|
t.Run(tc.title, func(t *testing.T) {
|
|
|
|
tmpl, err := FromGlobs([]string{}, tc.options...)
|
|
|
|
require.NoError(t, err)
|
|
|
|
f := tmpl.ExecuteTextString
|
|
|
|
if tc.html {
|
|
|
|
f = tmpl.ExecuteHTMLString
|
|
|
|
}
|
|
|
|
got, err := f(tc.in, tc.data)
|
|
|
|
if tc.fail {
|
|
|
|
require.NotNil(t, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, tc.exp, got)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|