mirror of
https://github.com/grafana/grafana.git
synced 2025-08-01 13:52:10 +08:00
334 lines
11 KiB
Go
334 lines
11 KiB
Go
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
|
|
}
|