Skip to content

Commit e81931a

Browse files
authored
Accept template contents as strings instead of reading from disk (#161)
Makes use of the new upstream Template.New() to stop passing user-defined templates to the Grafana Alertmanager by persisting them to disk.
1 parent af130e9 commit e81931a

File tree

6 files changed

+103
-112
lines changed

6 files changed

+103
-112
lines changed

notify/grafana_alertmanager.go

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"errors"
66
"fmt"
77
"net/url"
8-
"path/filepath"
98
"sync"
109
"time"
1110

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

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

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

147145
RoutingTree() *Route
148-
Templates() []string
146+
Templates() []templates.TemplateDefinition
149147

150148
Hash() [16]byte
151149
Raw() []byte
152150
}
153151

154152
type GrafanaAlertmanagerConfig struct {
155-
WorkingDirectory string
156153
ExternalURL string
157154
AlertStoreCallback mem.AlertStoreCallback
158155
PeerTimeout time.Duration
@@ -187,7 +184,6 @@ func NewGrafanaAlertmanager(tenantKey string, tenantID int64, config *GrafanaAle
187184
Metrics: m,
188185
tenantID: tenantID,
189186
externalURL: config.ExternalURL,
190-
workingDirectory: config.WorkingDirectory,
191187
}
192188

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

305-
func (am *GrafanaAlertmanager) WorkingDirectory() string {
306-
return am.workingDirectory
307-
}
308-
309301
// ConfigHash returns the hash of the current running configuration.
310302
// It is not safe to call without a lock.
311303
func (am *GrafanaAlertmanager) ConfigHash() [16]byte {
@@ -324,9 +316,9 @@ func (am *GrafanaAlertmanager) WithLock(fn func()) {
324316
fn()
325317
}
326318

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

357-
// Create the parsed template using the template paths.
358-
paths := make([]string, 0)
359-
for _, name := range am.templates {
360-
paths = append(paths, filepath.Join(am.workingDirectory, name))
349+
seen := make(map[string]struct{})
350+
tmpls := make([]string, 0, len(am.templates))
351+
for _, tc := range am.templates {
352+
if _, ok := seen[tc.Name]; ok {
353+
level.Warn(am.logger).Log("msg", "template with same name is defined multiple times, skipping...", "template_name", tc.Name)
354+
continue
355+
}
356+
tmpls = append(tmpls, tc.Template)
357+
seen[tc.Name] = struct{}{}
361358
}
362359

363-
tmpl, err := am.TemplateFromPaths(paths)
360+
tmpl, err := am.TemplateFromContent(tmpls)
364361
if err != nil {
365362
return err
366363
}

notify/templates.go

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"bytes"
55
"context"
66
tmplhtml "html/template"
7-
"path/filepath"
87
tmpltext "text/template"
98

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

77-
// 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.
78-
paths := make([]string, 0)
79-
for _, name := range am.templates {
80-
if name == c.Name {
81-
// Skip the existing template of the same name as we're going to parse the one for testing instead.
76+
// 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.
77+
var found bool
78+
templateContents := make([]string, 0, len(am.templates)+1)
79+
for _, td := range am.templates {
80+
if td.Name == c.Name {
81+
// Template already exists, test with the new definition replacing the old one.
82+
templateContents = append(templateContents, c.Template)
83+
found = true
8284
continue
8385
}
84-
paths = append(paths, filepath.Join(am.workingDirectory, name))
86+
templateContents = append(templateContents, td.Template)
8587
}
8688

87-
// Parse current templates.
89+
if !found {
90+
// Template is a new one, add it to the list.
91+
templateContents = append(templateContents, c.Template)
92+
}
93+
94+
// Capture the underlying text template so we can use ExecuteTemplate.
8895
var newTextTmpl *tmpltext.Template
8996
var captureTemplate template.Option = func(text *tmpltext.Template, _ *tmplhtml.Template) {
9097
newTextTmpl = text
9198
}
92-
newTmpl, err := am.TemplateFromPaths(paths, captureTemplate)
93-
if err != nil {
94-
return nil, err
95-
}
96-
97-
// Parse test template.
98-
_, err = newTextTmpl.New(c.Name).Parse(c.Template)
99+
newTmpl, err := am.TemplateFromContent(templateContents, captureTemplate)
99100
if err != nil {
100-
// This shouldn't happen since we already parsed the template above.
101101
return nil, err
102102
}
103103

notify/templates_test.go

Lines changed: 23 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ package notify
33
import (
44
"context"
55
"errors"
6-
"os"
7-
"path/filepath"
86
"testing"
97
"text/template"
108

9+
"github.com/grafana/alerting/templates"
10+
1111
"github.com/go-openapi/strfmt"
1212
amv2 "github.com/prometheus/alertmanager/api/v2/models"
1313
"github.com/stretchr/testify/assert"
@@ -102,7 +102,7 @@ func TestTemplateSimple(t *testing.T) {
102102
Kind: ExecutionError,
103103
Error: template.ExecError{
104104
Name: "slack.title",
105-
Err: errors.New(`template: slack.title:1:38: executing "slack.title" at <{{template "missing" .}}>: template "missing" not defined`),
105+
Err: errors.New(`template: :1:38: executing "slack.title" at <{{template "missing" .}}>: template "missing" not defined`),
106106
},
107107
}},
108108
},
@@ -154,7 +154,7 @@ func TestTemplateSimple(t *testing.T) {
154154
Kind: ExecutionError,
155155
Error: template.ExecError{
156156
Name: "other",
157-
Err: errors.New(`template: slack.title:1:91: executing "other" at <{{template "missing" .}}>: template "missing" not defined`),
157+
Err: errors.New(`template: :1:91: executing "other" at <{{template "missing" .}}>: template "missing" not defined`),
158158
},
159159
}},
160160
},
@@ -275,17 +275,10 @@ func TestTemplateSpecialCases(t *testing.T) {
275275

276276
func TestTemplateWithExistingTemplates(t *testing.T) {
277277
am, _ := setupAMTest(t)
278-
tmpDir, err := os.MkdirTemp("", "test-templates")
279-
require.NoError(t, err)
280-
t.Cleanup(func() {
281-
require.NoError(t, os.RemoveAll(tmpDir))
282-
})
283-
284-
am.workingDirectory = tmpDir
285278

286279
tests := []struct {
287280
name string
288-
existingTemplates map[string]string
281+
existingTemplates []templates.TemplateDefinition
289282
input TestTemplatesConfigBodyParams
290283
expected TestTemplatesResults
291284
}{{
@@ -310,9 +303,10 @@ func TestTemplateWithExistingTemplates(t *testing.T) {
310303
Name: "slack.title",
311304
Template: `{{ define "slack.title" }}{{ template "existing" . }}{{ end }}`,
312305
},
313-
existingTemplates: map[string]string{
314-
"existing": `{{ define "existing" }}Some existing template{{ end }}`,
315-
},
306+
existingTemplates: []templates.TemplateDefinition{{
307+
Name: "existing",
308+
Template: `{{ define "existing" }}Some existing template{{ end }}`,
309+
}},
316310
expected: TestTemplatesResults{
317311
Results: []TestTemplatesResult{{
318312
Name: "slack.title",
@@ -327,9 +321,10 @@ func TestTemplateWithExistingTemplates(t *testing.T) {
327321
Name: "slack.title",
328322
Template: `{{ define "slack.title" }}New template{{ end }}`,
329323
},
330-
existingTemplates: map[string]string{
331-
"slack.title": `{{ define "slack.title" }}Some existing template{{ end }}`,
332-
},
324+
existingTemplates: []templates.TemplateDefinition{{
325+
Name: "slack.title",
326+
Template: `{{ define "slack.title" }}Some existing template{{ end }}`,
327+
}},
333328
expected: TestTemplatesResults{
334329
Results: []TestTemplatesResult{{
335330
Name: "slack.title",
@@ -344,17 +339,18 @@ func TestTemplateWithExistingTemplates(t *testing.T) {
344339
Name: "slack.title",
345340
Template: `{{ define "slack.title" }}{{ template "slack.alternate_title" . }}{{ end }}`,
346341
},
347-
existingTemplates: map[string]string{
348-
"slack.title": `{{ define "slack.title" }}Some existing template{{ end }}{{ define "slack.alternate_title" }}Some existing alternate template{{ end }}`,
349-
},
342+
existingTemplates: []templates.TemplateDefinition{{
343+
Name: "slack.title",
344+
Template: `{{ define "slack.title" }}Some existing template{{ end }}{{ define "slack.alternate_title" }}Some existing alternate template{{ end }}`,
345+
}},
350346
expected: TestTemplatesResults{
351347
Results: nil,
352348
Errors: []TestTemplatesErrorResult{{
353349
Name: "slack.title",
354350
Kind: ExecutionError,
355351
Error: template.ExecError{
356352
Name: "slack.title",
357-
Err: errors.New(`template: slack.title:1:38: executing "slack.title" at <{{template "slack.alternate_title" .}}>: template "slack.alternate_title" not defined`),
353+
Err: errors.New(`template: :1:38: executing "slack.title" at <{{template "slack.alternate_title" .}}>: template "slack.alternate_title" not defined`),
358354
},
359355
}},
360356
},
@@ -365,9 +361,10 @@ func TestTemplateWithExistingTemplates(t *testing.T) {
365361
Name: "slack.title",
366362
Template: `{{ define "slack.title" }}{{ template "slack.alternate_title" . }}{{ end }}{{ define "slack.alternate_title" }}Some new alternate template{{ end }}`,
367363
},
368-
existingTemplates: map[string]string{
369-
"slack.title": `{{ define "slack.title" }}Some existing template{{ end }}{{ define "slack.alternate_title" }}Some existing alternate template{{ end }}`,
370-
},
364+
existingTemplates: []templates.TemplateDefinition{{
365+
Name: "slack.title",
366+
Template: `{{ define "slack.title" }}Some existing template{{ end }}{{ define "slack.alternate_title" }}Some existing alternate template{{ end }}`,
367+
}},
371368
expected: TestTemplatesResults{
372369
Results: []TestTemplatesResult{{
373370
Name: "slack.title",
@@ -381,10 +378,7 @@ func TestTemplateWithExistingTemplates(t *testing.T) {
381378
for _, test := range tests {
382379
t.Run(test.name, func(t *testing.T) {
383380
if len(test.existingTemplates) > 0 {
384-
for name, tmpl := range test.existingTemplates {
385-
createTemplate(t, tmpDir, name, tmpl)
386-
am.templates = append(am.templates, name)
387-
}
381+
am.templates = test.existingTemplates
388382
}
389383
res, err := am.TestTemplate(context.Background(), test.input)
390384
require.NoError(t, err)
@@ -436,18 +430,3 @@ CommonAnnotations: {{ range .CommonAnnotations.SortedPairs }}{{ .Name }}={{ .Val
436430
})
437431
}
438432
}
439-
440-
func createTemplate(t *testing.T, tmpDir string, name string, tmpl string) {
441-
f, err := os.Create(filepath.Join(tmpDir, name))
442-
require.NoError(t, err)
443-
defer func(f *os.File) {
444-
_ = f.Close()
445-
}(f)
446-
447-
t.Cleanup(func() {
448-
require.NoError(t, os.RemoveAll(f.Name()))
449-
})
450-
451-
_, err = f.WriteString(tmpl)
452-
require.NoError(t, err)
453-
}

templates/default_template.go

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
package templates
22

33
import (
4-
"os"
54
"testing"
65

7-
"github.com/prometheus/alertmanager/template"
86
"github.com/stretchr/testify/require"
97
)
108

@@ -102,21 +100,7 @@ Labels:
102100
`
103101

104102
func ForTests(t *testing.T) *Template {
105-
f, err := os.CreateTemp("/tmp", "template")
103+
tmpl, err := FromContent([]string{TemplateForTestsString})
106104
require.NoError(t, err)
107-
defer func(f *os.File) {
108-
_ = f.Close()
109-
}(f)
110-
111-
t.Cleanup(func() {
112-
require.NoError(t, os.RemoveAll(f.Name()))
113-
})
114-
115-
_, err = f.WriteString(TemplateForTestsString)
116-
require.NoError(t, err)
117-
118-
tmpl, err := template.FromGlobs([]string{f.Name()})
119-
require.NoError(t, err)
120-
121105
return tmpl
122106
}

templates/default_template_test.go

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package templates
33
import (
44
"context"
55
"net/url"
6-
"os"
76
"testing"
87
"time"
98

@@ -117,20 +116,7 @@ func TestDefaultTemplateString(t *testing.T) {
117116
},
118117
}
119118

120-
f, err := os.CreateTemp("/tmp", "template")
121-
require.NoError(t, err)
122-
defer func(f *os.File) {
123-
_ = f.Close()
124-
}(f)
125-
126-
t.Cleanup(func() {
127-
require.NoError(t, os.RemoveAll(f.Name()))
128-
})
129-
130-
_, err = f.WriteString(DefaultTemplateString)
131-
require.NoError(t, err)
132-
133-
tmpl, err := FromGlobs([]string{f.Name()})
119+
tmpl, err := FromContent(nil)
134120
require.NoError(t, err)
135121

136122
externalURL, err := url.Parse("http://localhost/grafana")

0 commit comments

Comments
 (0)