The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/services/ngalert/provisioning/templates.go

333 lines
11 KiB

package provisioning
import (
"context"
"errors"
"fmt"
"hash/fnv"
"maps"
"slices"
"sort"
"unsafe"
"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"
"github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage"
"github.com/grafana/grafana/pkg/services/ngalert/provisioning/validation"
)
type TemplateService struct {
configStore alertmanagerConfigStore
provenanceStore ProvisioningStore
xact TransactionManager
log log.Logger
validator validation.ProvenanceStatusTransitionValidator
}
func NewTemplateService(config alertmanagerConfigStore, prov ProvisioningStore, xact TransactionManager, log log.Logger) *TemplateService {
return &TemplateService{
configStore: config,
provenanceStore: prov,
xact: xact,
validator: validation.ValidateProvenanceRelaxed,
log: log,
}
}
func (t *TemplateService) GetTemplates(ctx context.Context, orgID int64) ([]definitions.NotificationTemplate, error) {
revision, err := t.configStore.Get(ctx, orgID)
if err != nil {
return nil, err
}
if len(revision.Config.TemplateFiles) == 0 {
return nil, nil
}
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),
}
provenance, ok := provenances[tmpl.ResourceID()]
if !ok {
provenance = models.ProvenanceNone
}
tmpl.Provenance = definitions.Provenance(provenance)
templates = append(templates, tmpl)
}
return templates, nil
}
func (t *TemplateService) GetTemplate(ctx context.Context, orgID int64, nameOrUid string) (definitions.NotificationTemplate, error) {
revision, err := t.configStore.Get(ctx, orgID)
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)
if err != nil {
return definitions.NotificationTemplate{}, err
}
tmpl.Provenance = definitions.Provenance(provenance)
return tmpl, nil
}
func (t *TemplateService) UpsertTemplate(ctx context.Context, orgID int64, tmpl definitions.NotificationTemplate) (definitions.NotificationTemplate, error) {
err := tmpl.Validate()
if err != nil {
return definitions.NotificationTemplate{}, MakeErrTemplateInvalid(err)
}
revision, err := t.configStore.Get(ctx, orgID)
if err != nil {
return definitions.NotificationTemplate{}, err
}
d, err := t.updateTemplate(ctx, revision, orgID, tmpl)
if err != nil {
if !errors.Is(err, ErrTemplateNotFound) {
return d, err
}
// If template was not found, this is assumed to be a create operation except for two cases:
// - If a ResourceVersion is provided: we should assume that this was meant to be a conditional update operation.
// - If UID is provided: custom UID for templates is not currently supported, so this was meant to be an update
// operation without a ResourceVersion.
if tmpl.ResourceVersion != "" || tmpl.UID != "" {
return definitions.NotificationTemplate{}, ErrTemplateNotFound.Errorf("")
}
return t.createTemplate(ctx, revision, orgID, tmpl)
}
return d, err
}
func (t *TemplateService) CreateTemplate(ctx context.Context, orgID int64, tmpl definitions.NotificationTemplate) (definitions.NotificationTemplate, error) {
err := tmpl.Validate()
if err != nil {
return definitions.NotificationTemplate{}, MakeErrTemplateInvalid(err)
}
revision, err := t.configStore.Get(ctx, orgID)
if err != nil {
return definitions.NotificationTemplate{}, err
}
return t.createTemplate(ctx, revision, orgID, tmpl)
}
func (t *TemplateService) createTemplate(ctx context.Context, revision *legacy_storage.ConfigRevision, orgID int64, tmpl definitions.NotificationTemplate) (definitions.NotificationTemplate, error) {
if revision.Config.TemplateFiles == nil {
revision.Config.TemplateFiles = map[string]string{}
}
_, found := revision.Config.TemplateFiles[tmpl.Name]
if found {
return definitions.NotificationTemplate{}, ErrTemplateExists.Errorf("")
}
revision.Config.TemplateFiles[tmpl.Name] = tmpl.Template
err := t.xact.InTransaction(ctx, func(ctx context.Context) error {
if err := t.configStore.Save(ctx, revision, orgID); err != nil {
return err
}
return t.provenanceStore.SetProvenance(ctx, &tmpl, orgID, models.Provenance(tmpl.Provenance))
})
if err != nil {
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
}
func (t *TemplateService) UpdateTemplate(ctx context.Context, orgID int64, tmpl definitions.NotificationTemplate) (definitions.NotificationTemplate, error) {
err := tmpl.Validate()
if err != nil {
return definitions.NotificationTemplate{}, MakeErrTemplateInvalid(err)
}
revision, err := t.configStore.Get(ctx, orgID)
if err != nil {
return definitions.NotificationTemplate{}, err
}
return t.updateTemplate(ctx, revision, orgID, tmpl)
}
func (t *TemplateService) updateTemplate(ctx context.Context, revision *legacy_storage.ConfigRevision, orgID int64, tmpl definitions.NotificationTemplate) (definitions.NotificationTemplate, error) {
if revision.Config.TemplateFiles == nil {
revision.Config.TemplateFiles = map[string]string{}
}
var found bool
var existingName, existingContent string
// 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
} else {
existingName = tmpl.Name
existingContent, found = revision.Config.TemplateFiles[existingName]
}
if !found {
return definitions.NotificationTemplate{}, ErrTemplateNotFound.Errorf("")
}
if existingName != 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 err := t.validator(storedProvenance, models.Provenance(tmpl.Provenance)); err != nil {
return definitions.NotificationTemplate{}, err
}
err = t.checkOptimisticConcurrency(tmpl.Name, existingContent, models.Provenance(tmpl.Provenance), tmpl.ResourceVersion, "update")
if err != nil {
return definitions.NotificationTemplate{}, err
}
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 err != nil {
return err
}
}
if err := t.configStore.Save(ctx, revision, orgID); err != nil {
return err
}
return t.provenanceStore.SetProvenance(ctx, &tmpl, orgID, models.Provenance(tmpl.Provenance))
})
if err != nil {
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
}
func (t *TemplateService) DeleteTemplate(ctx context.Context, orgID int64, nameOrUid string, provenance definitions.Provenance, version string) error {
revision, err := t.configStore.Get(ctx, orgID)
if err != nil {
return err
}
if revision.Config.TemplateFiles == nil {
return nil
}
existingName := nameOrUid
existing, ok := revision.Config.TemplateFiles[nameOrUid]
if !ok {
existingName, existing, ok = getTemplateByUid(revision.Config.TemplateFiles, nameOrUid)
}
if !ok {
return nil
}
err = t.checkOptimisticConcurrency(existingName, existing, 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 {
return err
}
delete(revision.Config.TemplateFiles, existingName)
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)
})
}
func (t *TemplateService) checkOptimisticConcurrency(name, currentContent string, provenance models.Provenance, desiredVersion string, action string) error {
if desiredVersion == "" {
if provenance != models.ProvenanceFile {
// if version is not specified and it's not a file provisioning, emit a log message to reflect that optimistic concurrency is disabled for this request
t.log.Debug("ignoring optimistic concurrency check because version was not provided", "template", name, "operation", action)
}
return nil
}
currentVersion := calculateTemplateFingerprint(currentContent)
if currentVersion != desiredVersion {
return ErrVersionConflict.Errorf("provided version %s of template %s does not match current version %s", desiredVersion, name, currentVersion)
}
return nil
}
func calculateTemplateFingerprint(t string) string {
sum := fnv.New64()
_, _ = sum.Write(unsafe.Slice(unsafe.StringData(t), len(t))) //nolint:gosec
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
}
}
return "", "", false
}