Skip to content

Commit

Permalink
Alerting: Support template UID in template service (grafana#92164)
Browse files Browse the repository at this point in the history
* add uid to template and populate it
* update delete method to support both uid and name
* update UpdateTemplate to support search by UID and fallback to name + support renaming of the template
* update upsert to exit if template not found and uid is specified
* update Get method to address by name or uid

---------

Co-authored-by: Matthew Jacobson <[email protected]>
  • Loading branch information
yuri-tceretian and JacobsonMT authored Aug 26, 2024
1 parent 354aee9 commit 4755eb5
Show file tree
Hide file tree
Showing 4 changed files with 364 additions and 128 deletions.
8 changes: 4 additions & 4 deletions pkg/services/ngalert/api/api_provisioning.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ type ContactPointService interface {

type TemplateService interface {
GetTemplates(ctx context.Context, orgID int64) ([]definitions.NotificationTemplate, error)
GetTemplate(ctx context.Context, orgID int64, name string) (definitions.NotificationTemplate, error)
GetTemplate(ctx context.Context, orgID int64, nameOrUid string) (definitions.NotificationTemplate, error)
UpsertTemplate(ctx context.Context, orgID int64, tmpl definitions.NotificationTemplate) (definitions.NotificationTemplate, error)
DeleteTemplate(ctx context.Context, orgID int64, name string, provenance definitions.Provenance, version string) error
DeleteTemplate(ctx context.Context, orgID int64, nameOrUid string, provenance definitions.Provenance, version string) error
}

type NotificationPolicyService interface {
Expand Down Expand Up @@ -229,9 +229,9 @@ func (srv *ProvisioningSrv) RoutePutTemplate(c *contextmodel.ReqContext, body de
return response.JSON(http.StatusAccepted, modified)
}

func (srv *ProvisioningSrv) RouteDeleteTemplate(c *contextmodel.ReqContext, name string) response.Response {
func (srv *ProvisioningSrv) RouteDeleteTemplate(c *contextmodel.ReqContext, nameOrUid string) response.Response {
version := c.Query("version")
err := srv.templates.DeleteTemplate(c.Req.Context(), c.SignedInUser.GetOrgID(), name, determineProvenance(c), version)
err := srv.templates.DeleteTemplate(c.Req.Context(), c.SignedInUser.GetOrgID(), nameOrUid, determineProvenance(c), version)
if err != nil {
return response.ErrOrFallback(http.StatusInternalServerError, "", err)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ 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"`
Expand Down
102 changes: 75 additions & 27 deletions pkg/services/ngalert/provisioning/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ func (t *TemplateService) GetTemplates(ctx context.Context, orgID int64) ([]defi
templates := make([]definitions.NotificationTemplate, 0, len(revision.Config.TemplateFiles))
for name, tmpl := range revision.Config.TemplateFiles {
tmpl := definitions.NotificationTemplate{
UID: legacy_storage.NameToUid(name),
Name: name,
Template: tmpl,
ResourceVersion: calculateTemplateFingerprint(tmpl),
Expand All @@ -65,30 +66,34 @@ func (t *TemplateService) GetTemplates(ctx context.Context, orgID int64) ([]defi
return templates, nil
}

func (t *TemplateService) GetTemplate(ctx context.Context, orgID int64, name string) (definitions.NotificationTemplate, error) {
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
}

for tmplName, tmpl := range revision.Config.TemplateFiles {
if tmplName != name {
continue
}
tmpl := definitions.NotificationTemplate{
Name: name,
Template: tmpl,
ResourceVersion: calculateTemplateFingerprint(tmpl),
}
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("")
}

provenance, err := t.provenanceStore.GetProvenance(ctx, &tmpl, orgID)
if err != nil {
return definitions.NotificationTemplate{}, err
}
tmpl.Provenance = definitions.Provenance(provenance)
return tmpl, nil
tmpl := definitions.NotificationTemplate{
UID: legacy_storage.NameToUid(existingName),
Name: existingName,
Template: existingContent,
ResourceVersion: calculateTemplateFingerprint(existingContent),
}
return definitions.NotificationTemplate{}, ErrTemplateNotFound.Errorf("")

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) {
Expand All @@ -107,7 +112,11 @@ func (t *TemplateService) UpsertTemplate(ctx context.Context, orgID int64, tmpl
if !errors.Is(err, ErrTemplateNotFound) {
return d, err
}
if tmpl.ResourceVersion != "" { // if version is set then it's an update operation. Fail because resource does not exist anymore
// 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)
Expand Down Expand Up @@ -150,6 +159,7 @@ func (t *TemplateService) createTemplate(ctx context.Context, revision *legacy_s
}

return definitions.NotificationTemplate{
UID: legacy_storage.NameToUid(tmpl.Name),
Name: tmpl.Name,
Template: tmpl.Template,
Provenance: tmpl.Provenance,
Expand All @@ -175,12 +185,28 @@ func (t *TemplateService) updateTemplate(ctx context.Context, revision *legacy_s
revision.Config.TemplateFiles = map[string]string{}
}

existingName := tmpl.Name
exisitingContent, found := revision.Config.TemplateFiles[existingName]
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 {
Expand All @@ -190,14 +216,22 @@ func (t *TemplateService) updateTemplate(ctx context.Context, revision *legacy_s
return definitions.NotificationTemplate{}, err
}

err = t.checkOptimisticConcurrency(tmpl.Name, exisitingContent, models.Provenance(tmpl.Provenance), tmpl.ResourceVersion, "update")
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
}
Expand All @@ -208,14 +242,15 @@ func (t *TemplateService) updateTemplate(ctx context.Context, revision *legacy_s
}

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, name string, provenance definitions.Provenance, version string) error {
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
Expand All @@ -225,33 +260,37 @@ func (t *TemplateService) DeleteTemplate(ctx context.Context, orgID int64, name
return nil
}

existing, ok := revision.Config.TemplateFiles[name]
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(name, existing, models.Provenance(provenance), version, "delete")
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: name}, orgID)
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, name)
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: name,
Name: existingName,
}
return t.provenanceStore.DeleteProvenance(ctx, &tgt, orgID)
})
Expand All @@ -277,3 +316,12 @@ func calculateTemplateFingerprint(t string) string {
_, _ = 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
}
Loading

0 comments on commit 4755eb5

Please sign in to comment.