diff --git a/cli/check_config.go b/cli/check_config.go index 00dca985..6b3a66c4 100644 --- a/cli/check_config.go +++ b/cli/check_config.go @@ -84,7 +84,7 @@ func CheckConfig(args []string) error { fmt.Printf(" - %d receivers\n", len(cfg.Receivers)) fmt.Printf(" - %d templates\n", len(cfg.Templates)) if len(cfg.Templates) > 0 { - _, err = template.FromGlobs(cfg.Templates...) + _, err = template.FromGlobs(cfg.Templates) if err != nil { fmt.Printf(" FAILED: %s\n", err) failed++ diff --git a/cli/template_render.go b/cli/template_render.go index 7c5d958b..983115ac 100644 --- a/cli/template_render.go +++ b/cli/template_render.go @@ -107,7 +107,7 @@ func configureTemplateRenderCmd(cc *kingpin.CmdClause) { } func (c *templateRenderCmd) render(ctx context.Context, _ *kingpin.ParseContext) error { - tmpl, err := template.FromGlobs(c.templateFilesGlobs...) + tmpl, err := template.FromGlobs(c.templateFilesGlobs) if err != nil { return err } diff --git a/cmd/alertmanager/main.go b/cmd/alertmanager/main.go index b645aa3e..a1bb96d4 100644 --- a/cmd/alertmanager/main.go +++ b/cmd/alertmanager/main.go @@ -419,7 +419,7 @@ func run() int { configLogger, ) configCoordinator.Subscribe(func(conf *config.Config) error { - tmpl, err = template.FromGlobs(conf.Templates...) + tmpl, err = template.FromGlobs(conf.Templates) if err != nil { return errors.Wrap(err, "failed to parse templates") } diff --git a/notify/email/email_test.go b/notify/email/email_test.go index dbae14ad..a425ba17 100644 --- a/notify/email/email_test.go +++ b/notify/email/email_test.go @@ -183,7 +183,7 @@ func notifyEmailWithContext(ctx context.Context, cfg *config.EmailConfig, server return nil, false, err } - tmpl, err := template.FromGlobs() + tmpl, err := template.FromGlobs([]string{}) if err != nil { return nil, false, err } diff --git a/notify/test/test.go b/notify/test/test.go index 32b55ca5..75729b2a 100644 --- a/notify/test/test.go +++ b/notify/test/test.go @@ -128,7 +128,7 @@ func DefaultRetryCodes() []int { // CreateTmpl returns a ready-to-use template. func CreateTmpl(t *testing.T) *template.Template { - tmpl, err := template.FromGlobs() + tmpl, err := template.FromGlobs([]string{}) require.NoError(t, err) tmpl.ExternalURL, _ = url.Parse("http://am") return tmpl diff --git a/template/template.go b/template/template.go index 3882187f..c7a6639f 100644 --- a/template/template.go +++ b/template/template.go @@ -42,16 +42,24 @@ type Template struct { ExternalURL *url.URL } +// Option is generic modifier of the text and html templates used by a Template. +type Option func(text *tmpltext.Template, html *tmplhtml.Template) + // FromGlobs calls ParseGlob on all path globs provided and returns the -// resulting Template. -func FromGlobs(paths ...string) (*Template, error) { +// resulting Template. Options allows customization of the text and html templates in given order. +// The DefaultFuncs have precedence over any added custom functions. +func FromGlobs(paths []string, options ...Option) (*Template, error) { t := &Template{ text: tmpltext.New("").Option("missingkey=zero"), html: tmplhtml.New("").Option("missingkey=zero"), } - t.text = t.text.Funcs(tmpltext.FuncMap(DefaultFuncs)) - t.html = t.html.Funcs(tmplhtml.FuncMap(DefaultFuncs)) + for _, o := range options { + o(t.text, t.html) + } + + t.text.Funcs(tmpltext.FuncMap(DefaultFuncs)) + t.html.Funcs(tmplhtml.FuncMap(DefaultFuncs)) defaultTemplates := []string{"default.tmpl", "email.tmpl"} diff --git a/template/template_test.go b/template/template_test.go index 9e939c90..884e497b 100644 --- a/template/template_test.go +++ b/template/template_test.go @@ -14,8 +14,10 @@ package template import ( + tmplhtml "html/template" "net/url" "testing" + tmpltext "text/template" "time" "github.com/prometheus/common/model" @@ -277,7 +279,7 @@ func TestData(t *testing.T) { } func TestTemplateExpansion(t *testing.T) { - tmpl, err := FromGlobs() + tmpl, err := FromGlobs([]string{}) require.NoError(t, err) for _, tc := range []struct { @@ -387,3 +389,67 @@ func TestTemplateExpansion(t *testing.T) { }) } } + +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) + }) + } +}