Skip to content

Commit

Permalink
Accept template contents as strings instead of reading from disk (#161)
Browse files Browse the repository at this point in the history
Makes use of the new upstream Template.New() to stop passing user-defined templates to the Grafana Alertmanager by persisting them to disk.
  • Loading branch information
JacobsonMT authored Mar 4, 2024
1 parent af130e9 commit e81931a
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 112 deletions.
35 changes: 16 additions & 19 deletions notify/grafana_alertmanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"errors"
"fmt"
"net/url"
"path/filepath"
"sync"
"time"

Expand Down Expand Up @@ -96,10 +95,9 @@ type GrafanaAlertmanager struct {
// buildReceiverIntegrationsFunc builds the integrations for a receiver based on its APIReceiver configuration and the current parsed template.
buildReceiverIntegrationsFunc func(next *APIReceiver, tmpl *templates.Template) ([]*Integration, error)
externalURL string
workingDirectory string

// templates contains the filenames (not full paths) of the persisted templates that were used to construct the current parsed template.
templates []string
// templates contains the template name -> template contents for each user-defined template.
templates []templates.TemplateDefinition
}

// State represents any of the two 'states' of the alertmanager. Notification log or Silences.
Expand Down Expand Up @@ -145,14 +143,13 @@ type Configuration interface {
BuildReceiverIntegrationsFunc() func(next *APIReceiver, tmpl *templates.Template) ([]*Integration, error)

RoutingTree() *Route
Templates() []string
Templates() []templates.TemplateDefinition

Hash() [16]byte
Raw() []byte
}

type GrafanaAlertmanagerConfig struct {
WorkingDirectory string
ExternalURL string
AlertStoreCallback mem.AlertStoreCallback
PeerTimeout time.Duration
Expand Down Expand Up @@ -187,7 +184,6 @@ func NewGrafanaAlertmanager(tenantKey string, tenantID int64, config *GrafanaAle
Metrics: m,
tenantID: tenantID,
externalURL: config.ExternalURL,
workingDirectory: config.WorkingDirectory,
}

if err := config.Validate(); err != nil {
Expand Down Expand Up @@ -302,10 +298,6 @@ func (am *GrafanaAlertmanager) ExternalURL() string {
return am.externalURL
}

func (am *GrafanaAlertmanager) WorkingDirectory() string {
return am.workingDirectory
}

// ConfigHash returns the hash of the current running configuration.
// It is not safe to call without a lock.
func (am *GrafanaAlertmanager) ConfigHash() [16]byte {
Expand All @@ -324,9 +316,9 @@ func (am *GrafanaAlertmanager) WithLock(fn func()) {
fn()
}

// TemplateFromPaths returns a set of *Templates based on the paths given.
func (am *GrafanaAlertmanager) TemplateFromPaths(paths []string, options ...template.Option) (*templates.Template, error) {
tmpl, err := templates.FromGlobs(paths, options...)
// TemplateFromContent returns a *Template based on defaults and the provided template contents.
func (am *GrafanaAlertmanager) TemplateFromContent(tmpls []string, options ...template.Option) (*templates.Template, error) {
tmpl, err := templates.FromContent(tmpls, options...)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -354,13 +346,18 @@ func (am *GrafanaAlertmanager) buildTimeIntervals(timeIntervals []config.TimeInt
func (am *GrafanaAlertmanager) ApplyConfig(cfg Configuration) (err error) {
am.templates = cfg.Templates()

// Create the parsed template using the template paths.
paths := make([]string, 0)
for _, name := range am.templates {
paths = append(paths, filepath.Join(am.workingDirectory, name))
seen := make(map[string]struct{})
tmpls := make([]string, 0, len(am.templates))
for _, tc := range am.templates {
if _, ok := seen[tc.Name]; ok {
level.Warn(am.logger).Log("msg", "template with same name is defined multiple times, skipping...", "template_name", tc.Name)
continue
}
tmpls = append(tmpls, tc.Template)
seen[tc.Name] = struct{}{}
}

tmpl, err := am.TemplateFromPaths(paths)
tmpl, err := am.TemplateFromContent(tmpls)
if err != nil {
return err
}
Expand Down
32 changes: 16 additions & 16 deletions notify/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"bytes"
"context"
tmplhtml "html/template"
"path/filepath"
tmpltext "text/template"

"github.com/grafana/alerting/templates"
Expand Down Expand Up @@ -74,30 +73,31 @@ func (am *GrafanaAlertmanager) TestTemplate(ctx context.Context, c TestTemplates
}, nil
}

// Recreate the current template without the definition blocks that are being tested. This is so that any blocks that were removed don't get defined.
paths := make([]string, 0)
for _, name := range am.templates {
if name == c.Name {
// Skip the existing template of the same name as we're going to parse the one for testing instead.
// Recreate the current template replacing the definition blocks that are being tested. This is so that any blocks that were removed don't get defined.
var found bool
templateContents := make([]string, 0, len(am.templates)+1)
for _, td := range am.templates {
if td.Name == c.Name {
// Template already exists, test with the new definition replacing the old one.
templateContents = append(templateContents, c.Template)
found = true
continue
}
paths = append(paths, filepath.Join(am.workingDirectory, name))
templateContents = append(templateContents, td.Template)
}

// Parse current templates.
if !found {
// Template is a new one, add it to the list.
templateContents = append(templateContents, c.Template)
}

// Capture the underlying text template so we can use ExecuteTemplate.
var newTextTmpl *tmpltext.Template
var captureTemplate template.Option = func(text *tmpltext.Template, _ *tmplhtml.Template) {
newTextTmpl = text
}
newTmpl, err := am.TemplateFromPaths(paths, captureTemplate)
if err != nil {
return nil, err
}

// Parse test template.
_, err = newTextTmpl.New(c.Name).Parse(c.Template)
newTmpl, err := am.TemplateFromContent(templateContents, captureTemplate)
if err != nil {
// This shouldn't happen since we already parsed the template above.
return nil, err
}

Expand Down
67 changes: 23 additions & 44 deletions notify/templates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ package notify
import (
"context"
"errors"
"os"
"path/filepath"
"testing"
"text/template"

"github.com/grafana/alerting/templates"

"github.com/go-openapi/strfmt"
amv2 "github.com/prometheus/alertmanager/api/v2/models"
"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -102,7 +102,7 @@ func TestTemplateSimple(t *testing.T) {
Kind: ExecutionError,
Error: template.ExecError{
Name: "slack.title",
Err: errors.New(`template: slack.title:1:38: executing "slack.title" at <{{template "missing" .}}>: template "missing" not defined`),
Err: errors.New(`template: :1:38: executing "slack.title" at <{{template "missing" .}}>: template "missing" not defined`),
},
}},
},
Expand Down Expand Up @@ -154,7 +154,7 @@ func TestTemplateSimple(t *testing.T) {
Kind: ExecutionError,
Error: template.ExecError{
Name: "other",
Err: errors.New(`template: slack.title:1:91: executing "other" at <{{template "missing" .}}>: template "missing" not defined`),
Err: errors.New(`template: :1:91: executing "other" at <{{template "missing" .}}>: template "missing" not defined`),
},
}},
},
Expand Down Expand Up @@ -275,17 +275,10 @@ func TestTemplateSpecialCases(t *testing.T) {

func TestTemplateWithExistingTemplates(t *testing.T) {
am, _ := setupAMTest(t)
tmpDir, err := os.MkdirTemp("", "test-templates")
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, os.RemoveAll(tmpDir))
})

am.workingDirectory = tmpDir

tests := []struct {
name string
existingTemplates map[string]string
existingTemplates []templates.TemplateDefinition
input TestTemplatesConfigBodyParams
expected TestTemplatesResults
}{{
Expand All @@ -310,9 +303,10 @@ func TestTemplateWithExistingTemplates(t *testing.T) {
Name: "slack.title",
Template: `{{ define "slack.title" }}{{ template "existing" . }}{{ end }}`,
},
existingTemplates: map[string]string{
"existing": `{{ define "existing" }}Some existing template{{ end }}`,
},
existingTemplates: []templates.TemplateDefinition{{
Name: "existing",
Template: `{{ define "existing" }}Some existing template{{ end }}`,
}},
expected: TestTemplatesResults{
Results: []TestTemplatesResult{{
Name: "slack.title",
Expand All @@ -327,9 +321,10 @@ func TestTemplateWithExistingTemplates(t *testing.T) {
Name: "slack.title",
Template: `{{ define "slack.title" }}New template{{ end }}`,
},
existingTemplates: map[string]string{
"slack.title": `{{ define "slack.title" }}Some existing template{{ end }}`,
},
existingTemplates: []templates.TemplateDefinition{{
Name: "slack.title",
Template: `{{ define "slack.title" }}Some existing template{{ end }}`,
}},
expected: TestTemplatesResults{
Results: []TestTemplatesResult{{
Name: "slack.title",
Expand All @@ -344,17 +339,18 @@ func TestTemplateWithExistingTemplates(t *testing.T) {
Name: "slack.title",
Template: `{{ define "slack.title" }}{{ template "slack.alternate_title" . }}{{ end }}`,
},
existingTemplates: map[string]string{
"slack.title": `{{ define "slack.title" }}Some existing template{{ end }}{{ define "slack.alternate_title" }}Some existing alternate template{{ end }}`,
},
existingTemplates: []templates.TemplateDefinition{{
Name: "slack.title",
Template: `{{ define "slack.title" }}Some existing template{{ end }}{{ define "slack.alternate_title" }}Some existing alternate template{{ end }}`,
}},
expected: TestTemplatesResults{
Results: nil,
Errors: []TestTemplatesErrorResult{{
Name: "slack.title",
Kind: ExecutionError,
Error: template.ExecError{
Name: "slack.title",
Err: errors.New(`template: slack.title:1:38: executing "slack.title" at <{{template "slack.alternate_title" .}}>: template "slack.alternate_title" not defined`),
Err: errors.New(`template: :1:38: executing "slack.title" at <{{template "slack.alternate_title" .}}>: template "slack.alternate_title" not defined`),
},
}},
},
Expand All @@ -365,9 +361,10 @@ func TestTemplateWithExistingTemplates(t *testing.T) {
Name: "slack.title",
Template: `{{ define "slack.title" }}{{ template "slack.alternate_title" . }}{{ end }}{{ define "slack.alternate_title" }}Some new alternate template{{ end }}`,
},
existingTemplates: map[string]string{
"slack.title": `{{ define "slack.title" }}Some existing template{{ end }}{{ define "slack.alternate_title" }}Some existing alternate template{{ end }}`,
},
existingTemplates: []templates.TemplateDefinition{{
Name: "slack.title",
Template: `{{ define "slack.title" }}Some existing template{{ end }}{{ define "slack.alternate_title" }}Some existing alternate template{{ end }}`,
}},
expected: TestTemplatesResults{
Results: []TestTemplatesResult{{
Name: "slack.title",
Expand All @@ -381,10 +378,7 @@ func TestTemplateWithExistingTemplates(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if len(test.existingTemplates) > 0 {
for name, tmpl := range test.existingTemplates {
createTemplate(t, tmpDir, name, tmpl)
am.templates = append(am.templates, name)
}
am.templates = test.existingTemplates
}
res, err := am.TestTemplate(context.Background(), test.input)
require.NoError(t, err)
Expand Down Expand Up @@ -436,18 +430,3 @@ CommonAnnotations: {{ range .CommonAnnotations.SortedPairs }}{{ .Name }}={{ .Val
})
}
}

func createTemplate(t *testing.T, tmpDir string, name string, tmpl string) {
f, err := os.Create(filepath.Join(tmpDir, name))
require.NoError(t, err)
defer func(f *os.File) {
_ = f.Close()
}(f)

t.Cleanup(func() {
require.NoError(t, os.RemoveAll(f.Name()))
})

_, err = f.WriteString(tmpl)
require.NoError(t, err)
}
18 changes: 1 addition & 17 deletions templates/default_template.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
package templates

import (
"os"
"testing"

"github.com/prometheus/alertmanager/template"
"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -102,21 +100,7 @@ Labels:
`

func ForTests(t *testing.T) *Template {
f, err := os.CreateTemp("/tmp", "template")
tmpl, err := FromContent([]string{TemplateForTestsString})
require.NoError(t, err)
defer func(f *os.File) {
_ = f.Close()
}(f)

t.Cleanup(func() {
require.NoError(t, os.RemoveAll(f.Name()))
})

_, err = f.WriteString(TemplateForTestsString)
require.NoError(t, err)

tmpl, err := template.FromGlobs([]string{f.Name()})
require.NoError(t, err)

return tmpl
}
16 changes: 1 addition & 15 deletions templates/default_template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package templates
import (
"context"
"net/url"
"os"
"testing"
"time"

Expand Down Expand Up @@ -117,20 +116,7 @@ func TestDefaultTemplateString(t *testing.T) {
},
}

f, err := os.CreateTemp("/tmp", "template")
require.NoError(t, err)
defer func(f *os.File) {
_ = f.Close()
}(f)

t.Cleanup(func() {
require.NoError(t, os.RemoveAll(f.Name()))
})

_, err = f.WriteString(DefaultTemplateString)
require.NoError(t, err)

tmpl, err := FromGlobs([]string{f.Name()})
tmpl, err := FromContent(nil)
require.NoError(t, err)

externalURL, err := url.Parse("http://localhost/grafana")
Expand Down
Loading

0 comments on commit e81931a

Please sign in to comment.