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
@@ -1,6 +1,9 @@
package v0alpha1
TemplateKind: *"grafana" | "mimir"
TemplateGroupSpec: {
title: string
content: string
kind: TemplateKind
}
@@ -2,13 +2,24 @@
package v0alpha1
// +k8s:openapi-gen=true
type TemplateGroupTemplateKind string
const (
TemplateGroupTemplateKindGrafana TemplateGroupTemplateKind = "grafana"
TemplateGroupTemplateKindMimir TemplateGroupTemplateKind = "mimir"
)
// +k8s:openapi-gen=true
type TemplateGroupSpec struct {
Title string `json:"title"`
Content string `json:"content"`
Title string `json:"title"`
Content string `json:"content"`
Kind TemplateGroupTemplateKind `json:"kind"`
}
// NewTemplateGroupSpec creates a new TemplateGroupSpec object.
func NewTemplateGroupSpec() *TemplateGroupSpec {
return &TemplateGroupSpec{}
return &TemplateGroupSpec{
Kind: TemplateGroupTemplateKindGrafana,
}
}
@@ -26,7 +26,7 @@ var (
rawSchemaRoutingTreev0alpha1 = []byte(`{"Matcher":{"additionalProperties":false,"properties":{"label":{"type":"string"},"type":{"enum":["=","!=","=~","!~"],"type":"string"},"value":{"type":"string"}},"required":["type","label","value"],"type":"object"},"Route":{"additionalProperties":false,"properties":{"active_time_intervals":{"items":{"type":"string"},"type":"array"},"continue":{"type":"boolean"},"group_by":{"items":{"type":"string"},"type":"array"},"group_interval":{"type":"string"},"group_wait":{"type":"string"},"matchers":{"items":{"$ref":"#/components/schemas/Matcher"},"type":"array"},"mute_time_intervals":{"items":{"type":"string"},"type":"array"},"receiver":{"type":"string"},"repeat_interval":{"type":"string"},"routes":{"items":{"$ref":"#/components/schemas/Route"},"type":"array"}},"required":["continue"],"type":"object"},"RouteDefaults":{"additionalProperties":false,"properties":{"group_by":{"items":{"type":"string"},"type":"array"},"group_interval":{"type":"string"},"group_wait":{"type":"string"},"receiver":{"type":"string"},"repeat_interval":{"type":"string"}},"required":["receiver"],"type":"object"},"RoutingTree":{"properties":{"spec":{"$ref":"#/components/schemas/spec"}},"required":["spec"]},"spec":{"additionalProperties":false,"properties":{"defaults":{"$ref":"#/components/schemas/RouteDefaults"},"routes":{"items":{"$ref":"#/components/schemas/Route"},"type":"array"}},"required":["defaults","routes"],"type":"object"}}`)
versionSchemaRoutingTreev0alpha1 app.VersionSchema
_ = json.Unmarshal(rawSchemaRoutingTreev0alpha1, &versionSchemaRoutingTreev0alpha1)
rawSchemaTemplateGroupv0alpha1 = []byte(`{"TemplateGroup":{"properties":{"spec":{"$ref":"#/components/schemas/spec"}},"required":["spec"]},"spec":{"additionalProperties":false,"properties":{"content":{"type":"string"},"title":{"type":"string"}},"required":["title","content"],"type":"object"}}`)
rawSchemaTemplateGroupv0alpha1 = []byte(`{"TemplateGroup":{"properties":{"spec":{"$ref":"#/components/schemas/spec"}},"required":["spec"]},"TemplateKind":{"enum":["grafana","mimir"],"type":"string"},"spec":{"additionalProperties":false,"properties":{"content":{"type":"string"},"kind":{"$ref":"#/components/schemas/TemplateKind","default":"grafana"},"title":{"type":"string"}},"required":["title","content","kind"],"type":"object"}}`)
versionSchemaTemplateGroupv0alpha1 app.VersionSchema
_ = json.Unmarshal(rawSchemaTemplateGroupv0alpha1, &versionSchemaTemplateGroupv0alpha1)
rawSchemaTimeIntervalv0alpha1 = []byte(`{"Interval":{"additionalProperties":false,"properties":{"days_of_month":{"items":{"type":"string"},"type":"array"},"location":{"type":"string"},"months":{"items":{"type":"string"},"type":"array"},"times":{"items":{"$ref":"#/components/schemas/TimeRange"},"type":"array"},"weekdays":{"items":{"type":"string"},"type":"array"},"years":{"items":{"type":"string"},"type":"array"}},"type":"object"},"TimeInterval":{"properties":{"spec":{"$ref":"#/components/schemas/spec"}},"required":["spec"]},"TimeRange":{"additionalProperties":false,"properties":{"end_time":{"type":"string"},"start_time":{"type":"string"}},"required":["start_time","end_time"],"type":"object"},"spec":{"additionalProperties":false,"properties":{"name":{"type":"string"},"time_intervals":{"items":{"$ref":"#/components/schemas/Interval"},"type":"array"}},"required":["name","time_intervals"],"type":"object"}}`)
@@ -20,6 +20,7 @@ import (
"github.com/grafana/grafana/pkg/registry/apps/alerting/notifications/timeinterval"
"github.com/grafana/grafana/pkg/services/apiserver/appinstaller"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/ngalert"
ac "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
@@ -97,7 +98,12 @@ func (a AlertingNotificationsAppInstaller) GetLegacyStorage(gvr schema.GroupVers
} else if gvr == timeinterval.ResourceInfo.GroupVersionResource() {
return timeinterval.NewStorage(api.MuteTimings, namespacer)
} else if gvr == templategroup.ResourceInfo.GroupVersionResource() {
return templategroup.NewStorage(api.Templates, namespacer)
srv := api.Templates
//nolint:staticcheck // not yet migrated to OpenFeature
if a.ng.FeatureToggles.IsEnabledGlobally(featuremgmt.FlagAlertingImportAlertmanagerAPI) {
srv = srv.WithIncludeImported()
}
return templategroup.NewStorage(srv, namespacer)
} else if gvr == routingtree.ResourceInfo.GroupVersionResource() {
return routingtree.NewStorage(api.Policies, namespacer)
}
@@ -1,6 +1,7 @@
package templategroup
import (
"github.com/grafana/alerting/definition"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/types"
@@ -36,6 +37,7 @@ func convertToK8sResource(orgID int64, template definitions.NotificationTemplate
Spec: model.TemplateGroupSpec{
Title: template.Name,
Content: template.Template,
Kind: model.TemplateGroupTemplateKind(template.Kind),
},
}
result.SetProvenanceStatus(string(template.Provenance))
@@ -50,5 +52,6 @@ func convertToDomainModel(template *model.TemplateGroup) definitions.Notificatio
Template: template.Spec.Content,
ResourceVersion: template.ResourceVersion,
Provenance: definitions.Provenance(ngmodels.ProvenanceNone),
Kind: definition.TemplateKind(template.Spec.Kind),
}
}
@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"github.com/grafana/alerting/definition"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -211,6 +212,7 @@ func (s *legacyStorage) defaultTemplate() (definitions.NotificationTemplate, err
UID: defaultTemplate.Name,
Provenance: definitions.Provenance("system"),
Template: defaultTemplate.Template,
Kind: definition.GrafanaTemplateKind,
}
return dto, nil
@@ -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{
@@ -0,0 +1,129 @@
package templateGroup
import (
"context"
"embed"
"path"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.yaml.in/yaml/v3"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1"
"github.com/grafana/grafana/pkg/services/featuremgmt"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/tests/api/alerting"
"github.com/grafana/grafana/pkg/tests/apis"
"github.com/grafana/grafana/pkg/tests/apis/alerting/notifications/common"
"github.com/grafana/grafana/pkg/tests/testinfra"
"github.com/grafana/grafana/pkg/util/testutil"
)
//go:embed test-data/*.*
var testData embed.FS
func TestIntegrationImportedTemplates(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
EnableFeatureToggles: []string{
featuremgmt.FlagAlertingImportAlertmanagerAPI,
},
})
client := common.NewTemplateGroupClient(t, helper.Org1.Admin)
cliCfg := helper.Org1.Admin.NewRestConfig()
alertingApi := alerting.NewAlertingLegacyAPIClient(helper.GetEnv().Server.HTTPServer.Listener.Addr().String(), cliCfg.Username, cliCfg.Password)
configYaml, err := testData.ReadFile(path.Join("test-data", "imported.yaml"))
require.NoError(t, err)
identifier := "test-create-get-config"
mergeMatchers := "_imported=true"
headers := map[string]string{
"Content-Type": "application/yaml",
"X-Grafana-Alerting-Config-Identifier": identifier,
"X-Grafana-Alerting-Merge-Matchers": mergeMatchers,
}
var amConfig apimodels.AlertmanagerUserConfig
require.NoError(t, yaml.Unmarshal(configYaml, &amConfig))
response := alertingApi.ConvertPrometheusPostAlertmanagerConfig(t, amConfig, headers)
require.Equal(t, "success", response.Status)
templates, err := client.List(context.Background(), metav1.ListOptions{})
require.NoError(t, err)
require.Len(t, templates.Items, 3)
require.Equal(t, v0alpha1.DefaultTemplateTitle, templates.Items[0].Spec.Title)
require.Equal(t, "imported", templates.Items[1].Spec.Title)
require.Equal(t, "template", templates.Items[2].Spec.Title)
t.Run("should be correct kind", func(t *testing.T) {
assert.Equal(t,
v0alpha1.TemplateGroupSpec{
Title: "imported",
Content: amConfig.TemplateFiles["imported"],
Kind: v0alpha1.TemplateGroupTemplateKindMimir,
}, templates.Items[1].Spec)
assert.Equal(t,
v0alpha1.TemplateGroupSpec{
Title: "template",
Content: amConfig.TemplateFiles["template"],
Kind: v0alpha1.TemplateGroupTemplateKindMimir,
}, templates.Items[2].Spec)
})
t.Run("should be provisioned", func(t *testing.T) {
for _, tpl := range templates.Items[1:] {
assert.EqualValues(t, models.ProvenanceConvertedPrometheus, tpl.GetProvenanceStatus())
}
})
t.Run("should not be able to update", func(t *testing.T) {
tpl := templates.Items[1]
tpl.Spec.Content = "new content"
_, err := client.Update(context.Background(), &tpl, metav1.UpdateOptions{})
require.Truef(t, errors.IsBadRequest(err), "expected bad request but got %s", err)
})
t.Run("should not be able to delete", func(t *testing.T) {
err := client.Delete(context.Background(), templates.Items[1].Name, metav1.DeleteOptions{})
require.Truef(t, errors.IsBadRequest(err), "expected bad request but got %s", err)
})
t.Run("should not conflict with Grafana kind", func(t *testing.T) {
tpl := v0alpha1.TemplateGroup{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
},
Spec: templates.Items[1].Spec,
}
tpl.Spec.Kind = v0alpha1.TemplateGroupTemplateKindGrafana
created, err := client.Create(context.Background(), &tpl, metav1.CreateOptions{})
require.NoError(t, err)
assert.NotEqual(t, templates.Items[1].Name, created.Name)
})
t.Run("sort by kind and then name", func(t *testing.T) {
templates, err := client.List(context.Background(), metav1.ListOptions{})
require.NoError(t, err)
require.Len(t, templates.Items, 4)
assert.Equal(t, v0alpha1.DefaultTemplateTitle, templates.Items[0].Spec.Title)
assert.Equal(t, "imported", templates.Items[1].Spec.Title)
assert.Equal(t, v0alpha1.TemplateGroupTemplateKindGrafana, templates.Items[1].Spec.Kind)
assert.Equal(t, "imported", templates.Items[2].Spec.Title)
assert.Equal(t, v0alpha1.TemplateGroupTemplateKindMimir, templates.Items[2].Spec.Kind)
assert.Equal(t, "template", templates.Items[3].Spec.Title)
})
}
@@ -54,6 +54,7 @@ func TestIntegrationResourceIdentifier(t *testing.T) {
Spec: v0alpha1.TemplateGroupSpec{
Title: "templateGroup",
Content: `{{ define "test" }} test {{ end }}`,
Kind: v0alpha1.TemplateGroupTemplateKindGrafana,
},
}
@@ -112,6 +113,7 @@ func TestIntegrationResourceIdentifier(t *testing.T) {
require.Equal(t, v0alpha1.TemplateGroupSpec{
Title: v0alpha1.DefaultTemplateTitle,
Content: defaultDefn.Template,
Kind: v0alpha1.TemplateGroupTemplateKindGrafana,
}, actual.Spec)
defaultTemplateGroup = actual
})
@@ -226,6 +228,7 @@ func TestIntegrationAccessControl(t *testing.T) {
Spec: v0alpha1.TemplateGroupSpec{
Title: fmt.Sprintf("template-group-1-%s", tc.user.Identity.GetLogin()),
Content: `{{ define "test" }} test {{ end }}`,
Kind: v0alpha1.TemplateGroupTemplateKindGrafana,
},
}
expected.SetProvenanceStatus("")
@@ -385,6 +388,7 @@ func TestIntegrationProvisioning(t *testing.T) {
Spec: v0alpha1.TemplateGroupSpec{
Title: "template-group-1",
Content: `{{ define "test" }} test {{ end }}`,
Kind: v0alpha1.TemplateGroupTemplateKindGrafana,
},
}, v1.CreateOptions{})
require.NoError(t, err)
@@ -428,6 +432,7 @@ func TestIntegrationOptimisticConcurrency(t *testing.T) {
Spec: v0alpha1.TemplateGroupSpec{
Title: "template-group-1",
Content: `{{ define "test" }} test {{ end }}`,
Kind: v0alpha1.TemplateGroupTemplateKindGrafana,
},
}
@@ -510,6 +515,7 @@ func TestIntegrationPatch(t *testing.T) {
Spec: v0alpha1.TemplateGroupSpec{
Title: "template-group",
Content: `{{ define "test" }} test {{ end }}`,
Kind: v0alpha1.TemplateGroupTemplateKindGrafana,
},
}
@@ -568,6 +574,7 @@ func TestIntegrationListSelector(t *testing.T) {
Spec: v0alpha1.TemplateGroupSpec{
Title: "test1",
Content: `{{ define "test1" }} test {{ end }}`,
Kind: v0alpha1.TemplateGroupTemplateKindGrafana,
},
}
template1, err := adminClient.Create(ctx, template1, v1.CreateOptions{})
@@ -580,6 +587,7 @@ func TestIntegrationListSelector(t *testing.T) {
Spec: v0alpha1.TemplateGroupSpec{
Title: "test2",
Content: `{{ define "test2" }} test {{ end }}`,
Kind: v0alpha1.TemplateGroupTemplateKindGrafana,
},
}
template2, err = adminClient.Create(ctx, template2, v1.CreateOptions{})
@@ -655,3 +663,37 @@ func TestIntegrationListSelector(t *testing.T) {
require.NotEqualf(t, templates.DefaultTemplateName, list.Items[1].Name, "Expected non-default template but got %s", list.Items[1].Name)
})
}
func TestIntegrationKinds(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
ctx := context.Background()
helper := getTestHelper(t)
client := common.NewTemplateGroupClient(t, helper.Org1.Admin)
newTemplate := &v0alpha1.TemplateGroup{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
},
Spec: v0alpha1.TemplateGroupSpec{
Title: "templateGroup",
Content: `{{ define "test" }} test {{ end }}`,
Kind: v0alpha1.TemplateGroupTemplateKindMimir,
},
}
t.Run("should not let create Mimir template", func(t *testing.T) {
_, err := client.Create(ctx, newTemplate, v1.CreateOptions{})
require.Truef(t, errors.IsBadRequest(err), "expected bad request but got %s", err)
})
t.Run("should not let change kind", func(t *testing.T) {
newTemplate.Spec.Kind = v0alpha1.TemplateGroupTemplateKindGrafana
created, err := client.Create(ctx, newTemplate, v1.CreateOptions{})
require.NoError(t, err)
created.Spec.Kind = v0alpha1.TemplateGroupTemplateKindMimir
_, err = client.Update(ctx, created, v1.UpdateOptions{})
require.Truef(t, errors.IsBadRequest(err), "expected bad request but got %s", err)
})
}
@@ -0,0 +1,15 @@
alertmanager_config: |
route:
receiver: sinkhole
group_by:
- alertname
receivers:
- name: sinkhole
template_files:
imported: |
{{ define "imported" }}
{{ end }}
template: |
{{ define "template" }}
{{ template "imported" . }}
{{ end }}
@@ -3783,18 +3783,29 @@
"type": "object",
"required": [
"title",
"content"
"content",
"kind"
],
"properties": {
"content": {
"type": "string"
},
"kind": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroupTemplateKind"
},
"title": {
"type": "string"
}
},
"additionalProperties": false
},
"com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroupTemplateKind": {
"type": "string",
"enum": [
"grafana",
"mimir"
]
},
"com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval": {
"type": "object",
"required": [