Alerting: Support for imported Templates (#114196)

* refactor template service to contstruct notification template in one place, get provenance before creating and calculate resource version after.
* refactor get by UID and name

* introduce template kind in NotificationTemplate
* introduce includeImported flag and use in the k8s api
* support imported templates
* add kind to template uid
* tests for imported templates
* update API model
* set kind to default templates
This commit is contained in:
Yuri Tseretyan
2025-12-17 20:26:22 +00:00
committed by GitHub
parent 370d5c2dc2
commit db3503fb32
16 changed files with 706 additions and 210 deletions
@@ -5,7 +5,8 @@ import (
"regexp"
"strings"
"github.com/grafana/alerting/templates"
"github.com/grafana/alerting/definition"
"github.com/grafana/alerting/notify"
"go.yaml.in/yaml/v3"
)
@@ -31,11 +32,18 @@ func (t *NotificationTemplate) Validate() error {
content = fmt.Sprintf("{{ define \"%s\" }}\n%s\n{{ end }}", t.Name, content)
}
t.Template = content
def := templates.TemplateDefinition{
Name: t.Name,
Template: t.Template,
Kind: templates.GrafanaKind,
if t.Kind == "" {
t.Kind = definition.GrafanaTemplateKind
}
postable := definition.PostableApiTemplate{
Name: t.Name,
Content: t.Template,
Kind: t.Kind,
}
if err := postable.Validate(); err != nil {
return err
}
def := notify.PostableAPITemplateToTemplateDefinition(postable)
return def.Validate()
}
@@ -462,6 +462,16 @@ func TestValidateNotificationTemplates(t *testing.T) {
},
expContent: `{{ define "Alert Instance Template" }}\nFiring: {{ .Labels.alertname }}\nSilence: {{ .SilenceURL }}\n{{ end }}[what is this?]`,
},
{
name: "unknown template kind",
template: NotificationTemplate{
Name: "Alert Instance Template",
Template: `{{ define "Same name as definition" }}\nFiring: {{ .Labels.alertname }}\nSilence: {{ .SilenceURL }}\n{{ end }}`,
Provenance: "test",
Kind: "unknown",
},
expError: errors.New("unknown template kind: unknown"),
},
}
for _, tt := range tc {
@@ -1,5 +1,7 @@
package definitions
import "github.com/grafana/alerting/definition"
// swagger:route GET /v1/provisioning/templates provisioning stable RouteGetTemplates
//
// Get all notification template groups.
@@ -55,11 +57,12 @@ type RouteDeleteTemplateParam struct {
// swagger:model
type NotificationTemplate struct {
UID string `json:"-" yaml:"-"`
Name string `json:"name"`
Template string `json:"template"`
Provenance Provenance `json:"provenance,omitempty"`
ResourceVersion string `json:"version,omitempty"`
UID string `json:"-" yaml:"-"`
Name string `json:"name"`
Template string `json:"template"`
Provenance Provenance `json:"provenance,omitempty"`
ResourceVersion string `json:"version,omitempty"`
Kind definition.TemplateKind `json:"-" yaml:"-"`
}
// swagger:model
@@ -4,6 +4,7 @@ import (
"fmt"
"github.com/grafana/grafana/pkg/apimachinery/errutil"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/models"
)
@@ -25,6 +26,10 @@ var (
ErrTemplateNotFound = errutil.NotFound("alerting.notifications.templates.notFound")
ErrTemplateInvalid = errutil.BadRequest("alerting.notifications.templates.invalidFormat").MustTemplate("Invalid format of the submitted template", errutil.WithPublic("Template is in invalid format. Correct the payload and try again."))
ErrTemplateExists = errutil.BadRequest("alerting.notifications.templates.nameExists", errutil.WithPublicMessage("Template file with this name already exists. Use a different name or update existing one."))
ErrTemplateOrigin = errutil.BadRequest("alerting.notifications.templates.originInvalid").MustTemplate(
"Template '{{ .Public.Name }}' cannot be {{ .Public.Action }}d because it belongs to an imported configuration.",
errutil.WithPublic("Template '{{ .Public.Name }}' cannot be {{ .Public.Action }}d because it belongs to an imported configuration. Finish the import of the configuration first."),
)
ErrContactPointReferenced = errutil.Conflict("alerting.notifications.contact-points.referenced", errutil.WithPublicMessage("Contact point is currently referenced by a notification policy."))
ErrContactPointUsedInRule = errutil.Conflict("alerting.notifications.contact-points.used-by-rule", errutil.WithPublicMessage("Contact point is currently used in the notification settings of one or many alert rules."))
@@ -129,3 +134,7 @@ func MakeErrContactPointUidExists(uid, name string) error {
},
})
}
func makeErrTemplateOrigin(t definitions.NotificationTemplate, action string) error {
return ErrTemplateOrigin.Build(errutil.TemplateData{Public: map[string]interface{}{"Action": action, "Name": t.Name}})
}
+153 -102
View File
@@ -10,6 +10,8 @@ import (
"sort"
"unsafe"
"github.com/grafana/alerting/definition"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/models"
@@ -23,6 +25,7 @@ type TemplateService struct {
xact TransactionManager
log log.Logger
validator validation.ProvenanceStatusTransitionValidator
includeImported bool
}
func NewTemplateService(config alertmanagerConfigStore, prov ProvisioningStore, xact TransactionManager, log log.Logger) *TemplateService {
@@ -32,6 +35,18 @@ func NewTemplateService(config alertmanagerConfigStore, prov ProvisioningStore,
xact: xact,
validator: validation.ValidateProvenanceRelaxed,
log: log,
includeImported: false,
}
}
func (t *TemplateService) WithIncludeImported() *TemplateService {
return &TemplateService{
configStore: t.configStore,
provenanceStore: t.provenanceStore,
xact: t.xact,
validator: t.validator,
log: t.log,
includeImported: true,
}
}
@@ -41,35 +56,38 @@ func (t *TemplateService) GetTemplates(ctx context.Context, orgID int64) ([]defi
return nil, err
}
if len(revision.Config.TemplateFiles) == 0 {
return nil, nil
}
var templates []definitions.NotificationTemplate
provenances, err := t.provenanceStore.GetProvenances(ctx, orgID, (&definitions.NotificationTemplate{}).ResourceType())
if err != nil {
return nil, err
}
templates := make([]definitions.NotificationTemplate, 0, len(revision.Config.TemplateFiles))
names := slices.Collect(maps.Keys(revision.Config.TemplateFiles))
sort.Strings(names)
for _, name := range names {
content := revision.Config.TemplateFiles[name]
tmpl := definitions.NotificationTemplate{
UID: legacy_storage.NameToUid(name),
Name: name,
Template: content,
ResourceVersion: calculateTemplateFingerprint(content),
if len(revision.Config.TemplateFiles) > 0 {
provenances, err := t.provenanceStore.GetProvenances(ctx, orgID, (&definitions.NotificationTemplate{}).ResourceType())
if err != nil {
return nil, err
}
provenance, ok := provenances[tmpl.ResourceID()]
if !ok {
provenance = models.ProvenanceNone
templates = make([]definitions.NotificationTemplate, 0, len(revision.Config.TemplateFiles))
names := slices.Collect(maps.Keys(revision.Config.TemplateFiles))
sort.Strings(names)
for _, name := range names {
content := revision.Config.TemplateFiles[name]
provenance, ok := provenances[(&definitions.NotificationTemplate{Name: name}).ResourceID()]
if !ok {
provenance = models.ProvenanceNone
}
templates = append(templates, newNotificationTemplate(name, content, provenance, definition.GrafanaTemplateKind))
}
tmpl.Provenance = definitions.Provenance(provenance)
templates = append(templates, tmpl)
}
return templates, nil
var importedTemplates []definitions.NotificationTemplate
if t.includeImported && len(revision.Config.ExtraConfigs) > 0 && len(revision.Config.ExtraConfigs[0].TemplateFiles) > 0 {
imported := revision.Config.ExtraConfigs[0].TemplateFiles
importedTemplates = make([]definitions.NotificationTemplate, 0, len(imported))
names := slices.Collect(maps.Keys(imported))
sort.Strings(names)
for _, name := range names {
content := imported[name]
templates = append(templates, newNotificationTemplate(name, content, models.ProvenanceConvertedPrometheus, definition.MimirTemplateKind))
}
}
return append(templates, importedTemplates...), nil
}
func (t *TemplateService) GetTemplate(ctx context.Context, orgID int64, nameOrUid string) (definitions.NotificationTemplate, error) {
@@ -77,29 +95,21 @@ func (t *TemplateService) GetTemplate(ctx context.Context, orgID int64, nameOrUi
if err != nil {
return definitions.NotificationTemplate{}, err
}
existingName := nameOrUid
existingContent, ok := revision.Config.TemplateFiles[nameOrUid]
if !ok {
existingName, existingContent, ok = getTemplateByUid(revision.Config.TemplateFiles, nameOrUid)
}
if !ok {
return definitions.NotificationTemplate{}, ErrTemplateNotFound.Errorf("")
}
tmpl := definitions.NotificationTemplate{
UID: legacy_storage.NameToUid(existingName),
Name: existingName,
Template: existingContent,
ResourceVersion: calculateTemplateFingerprint(existingContent),
}
provenance, err := t.provenanceStore.GetProvenance(ctx, &tmpl, orgID)
result, found, err := t.getTemplateByName(ctx, revision, orgID, nameOrUid)
if err != nil {
return definitions.NotificationTemplate{}, err
}
tmpl.Provenance = definitions.Provenance(provenance)
return tmpl, nil
if found {
return result, nil
}
result, found, err = t.getTemplateByUID(ctx, revision, orgID, nameOrUid)
if err != nil {
return definitions.NotificationTemplate{}, err
}
if found {
return result, nil
}
return definitions.NotificationTemplate{}, ErrTemplateNotFound.Errorf("")
}
func (t *TemplateService) UpsertTemplate(ctx context.Context, orgID int64, tmpl definitions.NotificationTemplate) (definitions.NotificationTemplate, error) {
@@ -135,6 +145,10 @@ func (t *TemplateService) CreateTemplate(ctx context.Context, orgID int64, tmpl
if err != nil {
return definitions.NotificationTemplate{}, MakeErrTemplateInvalid(err)
}
if tmpl.Kind == definition.MimirTemplateKind {
return definitions.NotificationTemplate{}, MakeErrTemplateInvalid(errors.New("templates of kind 'Mimir' cannot be created"))
}
revision, err := t.configStore.Get(ctx, orgID)
if err != nil {
return definitions.NotificationTemplate{}, err
@@ -143,6 +157,10 @@ func (t *TemplateService) CreateTemplate(ctx context.Context, orgID int64, tmpl
}
func (t *TemplateService) createTemplate(ctx context.Context, revision *legacy_storage.ConfigRevision, orgID int64, tmpl definitions.NotificationTemplate) (definitions.NotificationTemplate, error) {
if tmpl.Kind == definition.MimirTemplateKind {
return definitions.NotificationTemplate{}, MakeErrTemplateInvalid(errors.New("templates of kind 'Mimir' cannot be created"))
}
if revision.Config.TemplateFiles == nil {
revision.Config.TemplateFiles = map[string]string{}
}
@@ -164,13 +182,7 @@ func (t *TemplateService) createTemplate(ctx context.Context, revision *legacy_s
return definitions.NotificationTemplate{}, err
}
return definitions.NotificationTemplate{
UID: legacy_storage.NameToUid(tmpl.Name),
Name: tmpl.Name,
Template: tmpl.Template,
Provenance: tmpl.Provenance,
ResourceVersion: calculateTemplateFingerprint(tmpl.Template),
}, nil
return newNotificationTemplate(tmpl.Name, tmpl.Template, models.Provenance(tmpl.Provenance), tmpl.Kind), nil
}
func (t *TemplateService) UpdateTemplate(ctx context.Context, orgID int64, tmpl definitions.NotificationTemplate) (definitions.NotificationTemplate, error) {
@@ -192,37 +204,39 @@ func (t *TemplateService) updateTemplate(ctx context.Context, revision *legacy_s
}
var found bool
var existingName, existingContent string
var err error
var existing definitions.NotificationTemplate
// if UID is specified, look by UID.
if tmpl.UID != "" {
existingName, existingContent, found = getTemplateByUid(revision.Config.TemplateFiles, tmpl.UID)
// do not fall back to name because we address by UID, and resource can be deleted\renamed
existing, found, err = t.getTemplateByUID(ctx, revision, orgID, tmpl.UID)
} else {
existingName = tmpl.Name
existingContent, found = revision.Config.TemplateFiles[existingName]
existing, found, err = t.getTemplateByName(ctx, revision, orgID, tmpl.Name)
}
if err != nil {
return definitions.NotificationTemplate{}, err
}
if !found {
return definitions.NotificationTemplate{}, ErrTemplateNotFound.Errorf("")
}
if existingName != tmpl.Name { // if template is renamed, check if this name is already taken
if existing.Name != tmpl.Name { // if template is renamed, check if this name is already taken
_, ok := revision.Config.TemplateFiles[tmpl.Name]
if ok {
// return error if template is being renamed to one that already exists
return definitions.NotificationTemplate{}, ErrTemplateExists.Errorf("")
}
}
// check that provenance is not changed in an invalid way
storedProvenance, err := t.provenanceStore.GetProvenance(ctx, &tmpl, orgID)
if err != nil {
return definitions.NotificationTemplate{}, err
if existing.Kind != tmpl.Kind {
return definitions.NotificationTemplate{}, MakeErrTemplateInvalid(errors.New("cannot change template kind"))
}
if err := t.validator(storedProvenance, models.Provenance(tmpl.Provenance)); err != nil {
if existing.Provenance == definitions.Provenance(models.ProvenanceConvertedPrometheus) {
return definitions.NotificationTemplate{}, makeErrTemplateOrigin(existing, "update")
}
if err := t.validator(models.Provenance(existing.Provenance), models.Provenance(tmpl.Provenance)); err != nil {
return definitions.NotificationTemplate{}, err
}
err = t.checkOptimisticConcurrency(tmpl.Name, existingContent, models.Provenance(tmpl.Provenance), tmpl.ResourceVersion, "update")
err = t.checkOptimisticConcurrency(existing.Name, existing.Template, models.Provenance(tmpl.Provenance), tmpl.ResourceVersion, "update")
if err != nil {
return definitions.NotificationTemplate{}, err
}
@@ -230,9 +244,9 @@ func (t *TemplateService) updateTemplate(ctx context.Context, revision *legacy_s
revision.Config.TemplateFiles[tmpl.Name] = tmpl.Template
err = t.xact.InTransaction(ctx, func(ctx context.Context) error {
if existingName != tmpl.Name { // if template by was found by UID and it's name is different, then this is the rename operation. Delete old resources.
delete(revision.Config.TemplateFiles, existingName)
err := t.provenanceStore.DeleteProvenance(ctx, &definitions.NotificationTemplate{Name: existingName}, orgID)
if existing.Name != tmpl.Name { // if template by was found by UID and it's name is different, then this is the rename operation. Delete old resources.
delete(revision.Config.TemplateFiles, existing.Name)
err := t.provenanceStore.DeleteProvenance(ctx, &existing, orgID)
if err != nil {
return err
}
@@ -247,13 +261,8 @@ func (t *TemplateService) updateTemplate(ctx context.Context, revision *legacy_s
return definitions.NotificationTemplate{}, err
}
return definitions.NotificationTemplate{
UID: legacy_storage.NameToUid(tmpl.Name), // if name was changed, this UID will not match the incoming one
Name: tmpl.Name,
Template: tmpl.Template,
Provenance: tmpl.Provenance,
ResourceVersion: calculateTemplateFingerprint(tmpl.Template),
}, nil
// if name was changed, this UID needs to be recalculated
return newNotificationTemplate(tmpl.Name, tmpl.Template, models.Provenance(tmpl.Provenance), tmpl.Kind), nil
}
func (t *TemplateService) DeleteTemplate(ctx context.Context, orgID int64, nameOrUid string, provenance definitions.Provenance, version string) error {
@@ -261,44 +270,39 @@ func (t *TemplateService) DeleteTemplate(ctx context.Context, orgID int64, nameO
if err != nil {
return err
}
if revision.Config.TemplateFiles == nil {
existing, found, err := t.getTemplateByName(ctx, revision, orgID, nameOrUid)
if err != nil {
return err
}
if !found {
existing, found, err = t.getTemplateByUID(ctx, revision, orgID, nameOrUid)
}
if err != nil {
return err
}
if !found {
return nil
}
existingName := nameOrUid
existing, ok := revision.Config.TemplateFiles[nameOrUid]
if !ok {
existingName, existing, ok = getTemplateByUid(revision.Config.TemplateFiles, nameOrUid)
}
if !ok {
return nil
if existing.Provenance == definitions.Provenance(models.ProvenanceConvertedPrometheus) {
return makeErrTemplateOrigin(existing, "delete")
}
err = t.checkOptimisticConcurrency(existingName, existing, models.Provenance(provenance), version, "delete")
err = t.checkOptimisticConcurrency(existing.Name, existing.Template, models.Provenance(provenance), version, "delete")
if err != nil {
return err
}
// check that provenance is not changed in an invalid way
storedProvenance, err := t.provenanceStore.GetProvenance(ctx, &definitions.NotificationTemplate{Name: existingName}, orgID)
if err != nil {
return err
}
if err = t.validator(storedProvenance, models.Provenance(provenance)); err != nil {
if err = t.validator(models.Provenance(existing.Provenance), models.Provenance(provenance)); err != nil {
return err
}
delete(revision.Config.TemplateFiles, existingName)
delete(revision.Config.TemplateFiles, existing.Name)
return t.xact.InTransaction(ctx, func(ctx context.Context) error {
if err := t.configStore.Save(ctx, revision, orgID); err != nil {
return err
}
tgt := definitions.NotificationTemplate{
Name: existingName,
}
return t.provenanceStore.DeleteProvenance(ctx, &tgt, orgID)
return t.provenanceStore.DeleteProvenance(ctx, &existing, orgID)
})
}
@@ -323,11 +327,58 @@ func calculateTemplateFingerprint(t string) string {
return fmt.Sprintf("%016x", sum.Sum64())
}
func getTemplateByUid(templates map[string]string, uid string) (string, string, bool) {
for n, tmpl := range templates {
if legacy_storage.NameToUid(n) == uid {
return n, tmpl, true
}
func newNotificationTemplate(name, content string, provenance models.Provenance, kind definition.TemplateKind) definitions.NotificationTemplate {
tmpl := definitions.NotificationTemplate{
UID: templateUID(kind, name),
Name: name,
Template: content,
Provenance: definitions.Provenance(provenance),
Kind: kind,
}
return "", "", false
tmpl.ResourceVersion = calculateTemplateFingerprint(content)
return tmpl
}
func (t *TemplateService) getTemplateByName(ctx context.Context, revision *legacy_storage.ConfigRevision, orgID int64, name string) (definitions.NotificationTemplate, bool, error) {
existingContent, ok := revision.Config.TemplateFiles[name]
if !ok {
return definitions.NotificationTemplate{}, false, nil
}
provenance, err := t.provenanceStore.GetProvenance(ctx, &definitions.NotificationTemplate{Name: name}, orgID)
if err != nil {
return definitions.NotificationTemplate{}, false, err
}
return newNotificationTemplate(name, existingContent, provenance, definition.GrafanaTemplateKind), true, nil
}
func (t *TemplateService) getTemplateByUID(ctx context.Context, revision *legacy_storage.ConfigRevision, orgID int64, uid string) (definitions.NotificationTemplate, bool, error) {
find := func(templates map[string]string, uid string, kind definition.TemplateKind) (string, string, bool) {
for n, tmpl := range templates {
if templateUID(kind, n) == uid {
return n, tmpl, true
}
}
return "", "", false
}
var provenance models.Provenance
name, content, ok := find(revision.Config.TemplateFiles, uid, definition.GrafanaTemplateKind)
if !ok {
if t.includeImported && len(revision.Config.ExtraConfigs) > 0 {
name, content, ok = find(revision.Config.ExtraConfigs[0].TemplateFiles, uid, definition.MimirTemplateKind)
if ok {
return newNotificationTemplate(name, content, models.ProvenanceConvertedPrometheus, definition.MimirTemplateKind), true, nil
}
}
return definitions.NotificationTemplate{}, false, nil
}
var err error
provenance, err = t.provenanceStore.GetProvenance(ctx, &definitions.NotificationTemplate{Name: name}, orgID)
if err != nil {
return definitions.NotificationTemplate{}, false, err
}
return newNotificationTemplate(name, content, provenance, definition.GrafanaTemplateKind), true, nil
}
func templateUID(kind definition.TemplateKind, name string) string {
return legacy_storage.NameToUid(fmt.Sprintf("%s|%s", string(kind), name))
}
@@ -6,6 +6,7 @@ import (
"fmt"
"testing"
"github.com/grafana/alerting/definition"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
@@ -27,6 +28,15 @@ func TestGetTemplates(t *testing.T) {
"template2": "test2",
"template3": "test3",
},
ExtraConfigs: []definitions.ExtraConfiguration{
{
Identifier: "1234",
TemplateFiles: map[string]string{
"template1": "imported-test1",
"template4": "imported-test4",
},
},
},
},
}
@@ -45,27 +55,24 @@ func TestGetTemplates(t *testing.T) {
require.NoError(t, err)
expected := []definitions.NotificationTemplate{
{
UID: legacy_storage.NameToUid("template1"),
Name: "template1",
Template: "test1",
Provenance: definitions.Provenance(models.ProvenanceAPI),
ResourceVersion: calculateTemplateFingerprint("test1"),
},
{
UID: legacy_storage.NameToUid("template2"),
Name: "template2",
Template: "test2",
Provenance: definitions.Provenance(models.ProvenanceFile),
ResourceVersion: calculateTemplateFingerprint("test2"),
},
{
UID: legacy_storage.NameToUid("template3"),
Name: "template3",
Template: "test3",
Provenance: definitions.Provenance(models.ProvenanceNone),
ResourceVersion: calculateTemplateFingerprint("test3"),
},
newNotificationTemplate(
"template1",
"test1",
models.ProvenanceAPI,
definition.GrafanaTemplateKind,
),
newNotificationTemplate(
"template2",
"test2",
models.ProvenanceFile,
definition.GrafanaTemplateKind,
),
newNotificationTemplate(
"template3",
"test3",
models.ProvenanceNone,
definition.GrafanaTemplateKind,
),
}
require.EqualValues(t, expected, result)
@@ -89,6 +96,60 @@ func TestGetTemplates(t *testing.T) {
prov.AssertExpectations(t)
})
t.Run("returns imported templates if enabled", func(t *testing.T) {
sut, store, prov := createTemplateServiceSut()
sut = sut.WithIncludeImported()
store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
assert.Equal(t, orgID, org)
return revision, nil
}
prov.EXPECT().GetProvenances(mock.Anything, mock.Anything, mock.Anything).Return(map[string]models.Provenance{
"template1": models.ProvenanceAPI,
"template2": models.ProvenanceFile,
}, nil)
result, err := sut.GetTemplates(context.Background(), orgID)
require.NoError(t, err)
expected := []definitions.NotificationTemplate{
newNotificationTemplate(
"template1",
"test1",
models.ProvenanceAPI,
definition.GrafanaTemplateKind,
),
newNotificationTemplate(
"template2",
"test2",
models.ProvenanceFile,
definition.GrafanaTemplateKind,
),
newNotificationTemplate(
"template3",
"test3",
models.ProvenanceNone,
definition.GrafanaTemplateKind,
),
newNotificationTemplate(
"template1",
"imported-test1",
models.ProvenanceConvertedPrometheus,
definition.MimirTemplateKind,
),
newNotificationTemplate(
"template4",
"imported-test4",
models.ProvenanceConvertedPrometheus,
definition.MimirTemplateKind,
),
}
require.EqualValues(t, expected, result)
prov.AssertCalled(t, "GetProvenances", mock.Anything, orgID, (&definitions.NotificationTemplate{}).ResourceType())
prov.AssertExpectations(t)
})
t.Run("propagates errors", func(t *testing.T) {
t.Run("when unable to read config", func(t *testing.T) {
sut, store, prov := createTemplateServiceSut()
@@ -127,15 +188,25 @@ func TestGetTemplate(t *testing.T) {
orgID := int64(1)
templateName := "template1"
templateContent := "test1"
importedTemplateName := "template2"
importedTemplateContent := "imported"
revision := &legacy_storage.ConfigRevision{
Config: &definitions.PostableUserConfig{
TemplateFiles: map[string]string{
templateName: templateContent,
},
ExtraConfigs: []definitions.ExtraConfiguration{
{
Identifier: "1234",
TemplateFiles: map[string]string{
importedTemplateName: importedTemplateContent,
},
},
},
},
}
t.Run("return a template from config file by name", func(t *testing.T) {
t.Run("return a template from config by name", func(t *testing.T) {
sut, store, prov := createTemplateServiceSut()
store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
assert.Equal(t, orgID, org)
@@ -146,13 +217,12 @@ func TestGetTemplate(t *testing.T) {
result, err := sut.GetTemplate(context.Background(), orgID, templateName)
require.NoError(t, err)
expected := definitions.NotificationTemplate{
UID: legacy_storage.NameToUid(templateName),
Name: templateName,
Template: templateContent,
Provenance: definitions.Provenance(models.ProvenanceAPI),
ResourceVersion: calculateTemplateFingerprint(templateContent),
}
expected := newNotificationTemplate(
templateName,
templateContent,
models.ProvenanceAPI,
definition.GrafanaTemplateKind,
)
require.Equal(t, expected, result)
@@ -162,6 +232,62 @@ func TestGetTemplate(t *testing.T) {
prov.AssertExpectations(t)
})
t.Run("imported templates cannot be retrieved by name", func(t *testing.T) {
sut, store, _ := createTemplateServiceSut()
store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
assert.Equal(t, orgID, org)
return revision, nil
}
_, err := sut.GetTemplate(context.Background(), orgID, importedTemplateName)
require.ErrorIs(t, err, ErrTemplateNotFound)
})
t.Run("return a template from config by UID", func(t *testing.T) {
sut, store, prov := createTemplateServiceSut()
store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
assert.Equal(t, orgID, org)
return revision, nil
}
prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil)
result, err := sut.GetTemplate(context.Background(), orgID, templateUID(definition.GrafanaTemplateKind, templateName))
require.NoError(t, err)
expected := newNotificationTemplate(
templateName,
templateContent,
models.ProvenanceNone,
definition.GrafanaTemplateKind,
)
require.Equal(t, expected, result)
})
t.Run("return an imported template from config by UID", func(t *testing.T) {
sut, store, prov := createTemplateServiceSut()
store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
assert.Equal(t, orgID, org)
return revision, nil
}
uid := templateUID(definition.MimirTemplateKind, importedTemplateName)
t.Run("should be not found without flag enabled", func(t *testing.T) {
_, err := sut.GetTemplate(context.Background(), orgID, uid)
require.ErrorIs(t, err, ErrTemplateNotFound)
})
result, err := sut.WithIncludeImported().GetTemplate(context.Background(), orgID, uid)
require.NoError(t, err)
expected := newNotificationTemplate(
importedTemplateName,
importedTemplateContent,
models.ProvenanceConvertedPrometheus,
definition.MimirTemplateKind,
)
require.Equal(t, expected, result)
prov.AssertExpectations(t)
})
t.Run("returns ErrTemplateNotFound when template does not exist", func(t *testing.T) {
sut, store, prov := createTemplateServiceSut()
store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
@@ -242,18 +368,18 @@ func TestUpsertTemplate(t *testing.T) {
Template: "{{ define \"test\"}} test {{ end }}",
Provenance: definitions.Provenance(models.ProvenanceAPI),
ResourceVersion: "",
Kind: definition.GrafanaTemplateKind,
}
result, err := sut.UpsertTemplate(context.Background(), orgID, tmpl)
require.NoError(t, err)
require.Equal(t, definitions.NotificationTemplate{
UID: legacy_storage.NameToUid(tmpl.Name),
Name: tmpl.Name,
Template: tmpl.Template,
Provenance: tmpl.Provenance,
ResourceVersion: calculateTemplateFingerprint(tmpl.Template),
}, result)
require.Equal(t, newNotificationTemplate(
tmpl.Name,
tmpl.Template,
models.Provenance(tmpl.Provenance),
tmpl.Kind,
), result)
require.Len(t, store.Calls, 2)
@@ -284,18 +410,18 @@ func TestUpsertTemplate(t *testing.T) {
Template: "{{ define \"test\"}} test {{ end }}",
Provenance: definitions.Provenance(models.ProvenanceAPI),
ResourceVersion: calculateTemplateFingerprint("test1"),
Kind: definition.GrafanaTemplateKind,
}
result, err := sut.UpsertTemplate(context.Background(), orgID, tmpl)
require.NoError(t, err)
assert.Equal(t, definitions.NotificationTemplate{
UID: legacy_storage.NameToUid(tmpl.Name),
Name: tmpl.Name,
Template: tmpl.Template,
Provenance: tmpl.Provenance,
ResourceVersion: calculateTemplateFingerprint(tmpl.Template),
}, result)
assert.Equal(t, newNotificationTemplate(
tmpl.Name,
tmpl.Template,
models.Provenance(tmpl.Provenance),
tmpl.Kind,
), result)
require.Len(t, store.Calls, 2)
require.Equal(t, "Save", store.Calls[1].Method)
@@ -326,13 +452,12 @@ func TestUpsertTemplate(t *testing.T) {
result, err := sut.UpsertTemplate(context.Background(), orgID, tmpl)
require.NoError(t, err)
assert.Equal(t, definitions.NotificationTemplate{
UID: legacy_storage.NameToUid(tmpl.Name),
Name: tmpl.Name,
Template: tmpl.Template,
Provenance: tmpl.Provenance,
ResourceVersion: calculateTemplateFingerprint(tmpl.Template),
}, result)
assert.Equal(t, newNotificationTemplate(
tmpl.Name,
tmpl.Template,
models.Provenance(tmpl.Provenance),
definition.GrafanaTemplateKind,
), result)
require.Equal(t, "Save", store.Calls[1].Method)
saved := store.Calls[1].Args[1].(*legacy_storage.ConfigRevision)
@@ -356,18 +481,18 @@ func TestUpsertTemplate(t *testing.T) {
Template: "content",
Provenance: definitions.Provenance(models.ProvenanceNone),
ResourceVersion: calculateTemplateFingerprint(currentTemplateContent),
Kind: definition.GrafanaTemplateKind,
}
result, _ := sut.UpsertTemplate(context.Background(), orgID, tmpl)
expectedContent := fmt.Sprintf("{{ define \"%s\" }}\n content\n{{ end }}", templateName)
require.Equal(t, definitions.NotificationTemplate{
UID: legacy_storage.NameToUid(tmpl.Name),
Name: tmpl.Name,
Template: expectedContent,
Provenance: tmpl.Provenance,
ResourceVersion: calculateTemplateFingerprint(expectedContent),
}, result)
require.Equal(t, newNotificationTemplate(
tmpl.Name,
expectedContent,
models.Provenance(tmpl.Provenance),
tmpl.Kind,
), result)
})
t.Run("does not reject template with unknown field", func(t *testing.T) {
@@ -489,6 +614,21 @@ func TestUpsertTemplate(t *testing.T) {
require.ErrorIs(t, err, ErrTemplateNotFound)
})
t.Run("rejects new templates of mimir kind", func(t *testing.T) {
sut, store, _ := createTemplateServiceSut()
store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
return revision(), nil
}
template := definitions.NotificationTemplate{
Name: "template2",
Template: "asdf-new",
Provenance: definitions.Provenance(models.ProvenanceNone),
Kind: definition.MimirTemplateKind,
}
_, err := sut.UpsertTemplate(context.Background(), orgID, template)
require.ErrorIs(t, err, ErrTemplateInvalid)
})
t.Run("propagates errors", func(t *testing.T) {
tmpl := definitions.NotificationTemplate{
Name: templateName,
@@ -562,6 +702,7 @@ func TestCreateTemplate(t *testing.T) {
Name: "new-template",
Template: "{{ define \"test\"}} test {{ end }}",
Provenance: definitions.Provenance(models.ProvenanceAPI),
Kind: definition.GrafanaTemplateKind,
}
revision := func() *legacy_storage.ConfigRevision {
@@ -588,13 +729,12 @@ func TestCreateTemplate(t *testing.T) {
result, err := sut.CreateTemplate(context.Background(), orgID, tmpl)
require.NoError(t, err)
require.Equal(t, definitions.NotificationTemplate{
UID: legacy_storage.NameToUid(tmpl.Name),
Name: tmpl.Name,
Template: tmpl.Template,
Provenance: tmpl.Provenance,
ResourceVersion: calculateTemplateFingerprint(tmpl.Template),
}, result)
require.Equal(t, newNotificationTemplate(
tmpl.Name,
tmpl.Template,
models.Provenance(tmpl.Provenance),
tmpl.Kind,
), result)
require.Len(t, store.Calls, 2)
@@ -649,10 +789,33 @@ func TestCreateTemplate(t *testing.T) {
require.ErrorIs(t, err, ErrTemplateInvalid)
})
t.Run("invalid kind", func(t *testing.T) {
tmpl := definitions.NotificationTemplate{
Name: "new-template",
Template: "{{ define \"test\"}} test {{ end }}",
Kind: "unknown",
}
_, err := sut.CreateTemplate(context.Background(), orgID, tmpl)
require.ErrorIs(t, err, ErrTemplateInvalid)
})
require.Empty(t, store.Calls)
prov.AssertExpectations(t)
})
t.Run("rejects templates with mimir kind", func(t *testing.T) {
sut, _, _ := createTemplateServiceSut()
tmpl := definitions.NotificationTemplate{
Name: "new-template",
Template: "{{ define \"test\"}} test {{ end }}",
Kind: definition.MimirTemplateKind,
}
_, err := sut.CreateTemplate(context.Background(), orgID, tmpl)
require.ErrorIs(t, err, ErrTemplateInvalid)
})
t.Run("propagates errors", func(t *testing.T) {
t.Run("when unable to read config", func(t *testing.T) {
sut, store, _ := createTemplateServiceSut()
@@ -706,6 +869,7 @@ func TestUpdateTemplate(t *testing.T) {
Template: "{{ define \"test\"}} test {{ end }}",
Provenance: definitions.Provenance(models.ProvenanceAPI),
ResourceVersion: "",
Kind: definition.GrafanaTemplateKind,
}
amConfigToken := util.GenerateShortUID()
@@ -771,7 +935,7 @@ func TestUpdateTemplate(t *testing.T) {
},
{
name: "by uid",
templateUid: legacy_storage.NameToUid(tmpl.UID),
templateUid: templateUID(tmpl.Kind, tmpl.Name),
},
}
@@ -791,13 +955,12 @@ func TestUpdateTemplate(t *testing.T) {
result, err := sut.UpdateTemplate(context.Background(), orgID, tmpl)
require.NoError(t, err)
assert.Equal(t, definitions.NotificationTemplate{
UID: legacy_storage.NameToUid(tmpl.Name),
Name: tmpl.Name,
Template: tmpl.Template,
Provenance: tmpl.Provenance,
ResourceVersion: calculateTemplateFingerprint(tmpl.Template),
}, result)
assert.Equal(t, newNotificationTemplate(
tmpl.Name,
tmpl.Template,
models.Provenance(tmpl.Provenance),
tmpl.Kind,
), result)
require.Len(t, store.Calls, 2)
require.Equal(t, "Save", store.Calls[1].Method)
@@ -821,13 +984,12 @@ func TestUpdateTemplate(t *testing.T) {
result, err := sut.UpdateTemplate(context.Background(), orgID, tmpl)
require.NoError(t, err)
assert.Equal(t, definitions.NotificationTemplate{
UID: legacy_storage.NameToUid(tmpl.Name),
Name: tmpl.Name,
Template: tmpl.Template,
Provenance: tmpl.Provenance,
ResourceVersion: calculateTemplateFingerprint(tmpl.Template),
}, result)
assert.Equal(t, newNotificationTemplate(
tmpl.Name,
tmpl.Template,
models.Provenance(tmpl.Provenance),
tmpl.Kind,
), result)
require.Equal(t, "Save", store.Calls[1].Method)
saved := store.Calls[1].Args[1].(*legacy_storage.ConfigRevision)
@@ -853,18 +1015,17 @@ func TestUpdateTemplate(t *testing.T) {
oldName := tmpl.Name
tmpl := tmpl
tmpl.UID = legacy_storage.NameToUid(tmpl.Name) // UID matches the current template
tmpl.Name = "new-template-name" // but name is different
tmpl.UID = templateUID(tmpl.Kind, tmpl.Name) // UID matches the current template
tmpl.Name = "new-template-name" // but name is different
result, err := sut.UpdateTemplate(context.Background(), orgID, tmpl)
require.NoError(t, err)
assert.Equal(t, definitions.NotificationTemplate{
UID: legacy_storage.NameToUid(tmpl.Name),
Name: tmpl.Name,
Template: tmpl.Template,
Provenance: tmpl.Provenance,
ResourceVersion: calculateTemplateFingerprint(tmpl.Template),
}, result)
assert.Equal(t, newNotificationTemplate(
tmpl.Name,
tmpl.Template,
models.Provenance(tmpl.Provenance),
tmpl.Kind,
), result)
require.Len(t, store.Calls, 2)
require.Equal(t, "Save", store.Calls[1].Method)
@@ -882,6 +1043,7 @@ func TestUpdateTemplate(t *testing.T) {
t.Run("rejects rename operation if template with the new name exists", func(t *testing.T) {
sut, store, prov := createTemplateServiceSut()
prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil)
store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
return &legacy_storage.ConfigRevision{
Config: &definitions.PostableUserConfig{
@@ -895,8 +1057,8 @@ func TestUpdateTemplate(t *testing.T) {
}
tmpl := tmpl
tmpl.UID = legacy_storage.NameToUid(tmpl.Name) // UID matches the current template
tmpl.Name = "new-template-name" // but name matches another existing template
tmpl.UID = templateUID(tmpl.Kind, tmpl.Name) // UID matches the current template
tmpl.Name = "new-template-name" // but name matches another existing template
_, err := sut.UpdateTemplate(context.Background(), orgID, tmpl)
require.ErrorIs(t, err, ErrTemplateExists)
@@ -925,6 +1087,16 @@ func TestUpdateTemplate(t *testing.T) {
require.ErrorIs(t, err, ErrTemplateInvalid)
})
t.Run("invalid kind", func(t *testing.T) {
tmpl := definitions.NotificationTemplate{
Name: "",
Template: "",
Kind: "unknown",
}
_, err := sut.UpdateTemplate(context.Background(), orgID, tmpl)
require.ErrorIs(t, err, ErrTemplateInvalid)
})
require.Empty(t, store.Calls)
prov.AssertExpectations(t)
})
@@ -975,6 +1147,27 @@ func TestUpdateTemplate(t *testing.T) {
prov.AssertExpectations(t)
})
t.Run("rejects existing templates if kind changes", func(t *testing.T) {
sut, store, prov := createTemplateServiceSut()
store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
return revision(), nil
}
prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil)
template := definitions.NotificationTemplate{
Name: "template1",
Template: "asdf-new",
ResourceVersion: "bad-version",
Provenance: definitions.Provenance(models.ProvenanceNone),
Kind: definition.MimirTemplateKind,
}
_, err := sut.UpdateTemplate(context.Background(), orgID, template)
require.ErrorIs(t, err, ErrTemplateInvalid)
prov.AssertExpectations(t)
})
t.Run("propagates errors", func(t *testing.T) {
t.Run("when unable to read config", func(t *testing.T) {
sut, store, _ := createTemplateServiceSut()
@@ -1062,7 +1255,7 @@ func TestDeleteTemplate(t *testing.T) {
},
{
name: "by uid",
templateNameOrUid: legacy_storage.NameToUid(templateName),
templateNameOrUid: templateUID(definition.GrafanaTemplateKind, templateName),
},
}
for _, tt := range testCase {
@@ -1125,7 +1318,7 @@ func TestDeleteTemplate(t *testing.T) {
}
t.Run("should look by name before uid", func(t *testing.T) {
expectedToDelete := legacy_storage.NameToUid(templateName)
expectedToDelete := templateUID(definition.GrafanaTemplateKind, templateName)
sut, store, prov := createTemplateServiceSut()
store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) {
return &legacy_storage.ConfigRevision{