From 267368fd1968774d47b9553d113f9438eeeeed0c Mon Sep 17 00:00:00 2001 From: Matthew Jacobson Date: Fri, 10 Jan 2025 17:06:13 -0500 Subject: [PATCH] Add function that returns all default builtin template definitions (#260) --- templates/default_template_test.go | 32 ++++++++++++++++--- templates/template_data.go | 50 ++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 4 deletions(-) diff --git a/templates/default_template_test.go b/templates/default_template_test.go index f6719b72..dc45a67c 100644 --- a/templates/default_template_test.go +++ b/templates/default_template_test.go @@ -3,12 +3,14 @@ package templates import ( "context" "net/url" + "strings" "testing" "time" + "github.com/stretchr/testify/require" + "github.com/prometheus/alertmanager/types" "github.com/prometheus/common/model" - "github.com/stretchr/testify/require" "github.com/grafana/alerting/logging" ) @@ -127,6 +129,19 @@ func TestDefaultTemplateString(t *testing.T) { l := &logging.FakeLogger{} expand, _ := TmplText(context.Background(), tmpl, alerts, l, &tmplErr) + tmplDef, err := DefaultTemplate() + require.NoError(t, err) + + tmplFromDefinition, err := newTemplate() + require.NoError(t, err) + // Parse default template string. + err = tmplFromDefinition.Parse(strings.NewReader(tmplDef.Template)) + require.NoError(t, err) + tmplFromDefinition.ExternalURL = externalURL + + var tmplDefErr error + expandFromDefinition, _ := TmplText(context.Background(), tmplFromDefinition, alerts, l, &tmplDefErr) + cases := []struct { templateString string expected string @@ -305,10 +320,19 @@ Silence: [http://localhost/grafana/alerting/silence/new?alertmanager=grafana&mat for _, c := range cases { t.Run(c.templateString, func(t *testing.T) { - act := expand(c.templateString) - require.NoError(t, tmplErr) - require.Equal(t, c.expected, act) + t.Run("FromContent", func(t *testing.T) { + act := expand(c.templateString) + require.NoError(t, tmplErr) + require.Equal(t, c.expected, act) + }) + + t.Run("DefaultTemplate", func(t *testing.T) { + act := expandFromDefinition(c.templateString) + require.NoError(t, tmplDefErr) + require.Equal(t, c.expected, act) + }) }) } require.NoError(t, tmplErr) + require.NoError(t, tmplDefErr) } diff --git a/templates/template_data.go b/templates/template_data.go index 0e0a6bf4..f9b45262 100644 --- a/templates/template_data.go +++ b/templates/template_data.go @@ -3,10 +3,14 @@ package templates import ( "context" "encoding/json" + "fmt" + tmplhtml "html/template" "net/url" "path" + "slices" "sort" "strings" + tmpltext "text/template" "time" "github.com/go-kit/log" @@ -65,6 +69,52 @@ type ExtendedData struct { ExternalURL string `json:"externalURL"` } +var DefaultTemplateName = "__default__" + +// DefaultTemplate returns a new Template with all default templates parsed. +func DefaultTemplate(options ...template.Option) (TemplateDefinition, error) { + // We cannot simply append the text of each default file together as there can be (and are) duplicate template + // names. Duplicate templates should override when parsed from separate files but will fail to parse if both are in + // the same file. + // So, instead we allow tmpltext to combine the templates and then convert it to a string afterwards. + // The underlying template is not accessible, so we capture it via template.Option. + var newTextTmpl *tmpltext.Template + var captureTemplate template.Option = func(text *tmpltext.Template, _ *tmplhtml.Template) { + newTextTmpl = text + } + + // Call FromContent without any user-provided templates to get the combined default template. + _, err := FromContent(nil, append(options, captureTemplate)...) + if err != nil { + return TemplateDefinition{}, err + } + + var combinedTemplate strings.Builder + tmpls := newTextTmpl.Templates() + // Sort for a consistent order. + slices.SortFunc(tmpls, func(a, b *tmpltext.Template) int { + return strings.Compare(a.Name(), b.Name()) + }) + + // Recreate the "define" blocks for all templates. Would be nice to have a more direct way to do this. + for _, tmpl := range tmpls { + if tmpl.Name() != "" { + def := tmpl.Tree.Root.String() + if tmpl.Name() == "__text_values_list" { + // Temporary fix for https://github.com/golang/go/commit/6fea4094242fe4e7be8bd7ec0b55df9f6df3f025. + // TODO: Can remove with GO v1.24. + def = strings.Replace(def, "$first := false", "$first = false", 1) + } + + combinedTemplate.WriteString(fmt.Sprintf("{{ define \"%s\" }}%s{{ end }}\n\n", tmpl.Name(), def)) + } + } + return TemplateDefinition{ + Name: DefaultTemplateName, + Template: combinedTemplate.String(), + }, nil +} + // FromContent calls Parse on all provided template content and returns the resulting Template. Content equivalent to templates.FromGlobs. func FromContent(tmpls []string, options ...template.Option) (*Template, error) { t, err := newTemplate(options...)