Alerting: Add Create and Update methods to Template service (#91981)

* rename SetTemplate to UpsertTemplate

* Introduce Create\Update methods

* update api endpoint to use GetTemplate
pull/92163/head
Yuri Tseretyan 1 year ago committed by GitHub
parent cab5818bc7
commit d27c3822f2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 15
      pkg/services/ngalert/api/api_provisioning.go
  2. 1
      pkg/services/ngalert/provisioning/errors.go
  3. 103
      pkg/services/ngalert/provisioning/templates.go
  4. 401
      pkg/services/ngalert/provisioning/templates_test.go
  5. 2
      pkg/services/provisioning/alerting/text_templates.go

@ -46,7 +46,7 @@ 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)
SetTemplate(ctx context.Context, orgID int64, tmpl definitions.NotificationTemplate) (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
}
@ -207,17 +207,12 @@ func (srv *ProvisioningSrv) RouteGetTemplates(c *contextmodel.ReqContext) respon
return response.JSON(http.StatusOK, templates)
}
func (srv *ProvisioningSrv) RouteGetTemplate(c *contextmodel.ReqContext, name string) response.Response {
templates, err := srv.templates.GetTemplates(c.Req.Context(), c.SignedInUser.GetOrgID())
func (srv *ProvisioningSrv) RouteGetTemplate(c *contextmodel.ReqContext, nameOrUid string) response.Response {
template, err := srv.templates.GetTemplate(c.Req.Context(), c.SignedInUser.GetOrgID(), nameOrUid)
if err != nil {
return response.ErrOrFallback(http.StatusInternalServerError, "", err)
}
for _, tmpl := range templates {
if tmpl.Name == name {
return response.JSON(http.StatusOK, tmpl)
}
}
return response.Err(provisioning.ErrTemplateNotFound)
return response.JSON(http.StatusOK, template)
}
func (srv *ProvisioningSrv) RoutePutTemplate(c *contextmodel.ReqContext, body definitions.NotificationTemplateContent, name string) response.Response {
@ -227,7 +222,7 @@ func (srv *ProvisioningSrv) RoutePutTemplate(c *contextmodel.ReqContext, body de
Provenance: determineProvenance(c),
ResourceVersion: body.ResourceVersion,
}
modified, err := srv.templates.SetTemplate(c.Req.Context(), c.SignedInUser.GetOrgID(), tmpl)
modified, err := srv.templates.UpsertTemplate(c.Req.Context(), c.SignedInUser.GetOrgID(), tmpl)
if err != nil {
return response.ErrOrFallback(http.StatusInternalServerError, "", err)
}

@ -24,6 +24,7 @@ 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."))
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."))

@ -2,6 +2,7 @@ package provisioning
import (
"context"
"errors"
"fmt"
"hash/fnv"
"unsafe"
@ -9,6 +10,7 @@ import (
"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"
)
@ -89,7 +91,7 @@ func (t *TemplateService) GetTemplate(ctx context.Context, orgID int64, name str
return definitions.NotificationTemplate{}, ErrTemplateNotFound.Errorf("")
}
func (t *TemplateService) SetTemplate(ctx context.Context, orgID int64, tmpl definitions.NotificationTemplate) (definitions.NotificationTemplate, error) {
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)
@ -100,32 +102,99 @@ func (t *TemplateService) SetTemplate(ctx context.Context, orgID int64, tmpl def
return definitions.NotificationTemplate{}, err
}
d, err := t.updateTemplate(ctx, revision, orgID, tmpl)
if err != nil {
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
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{}
}
_, ok := revision.Config.TemplateFiles[tmpl.Name]
if ok {
// 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
}
_, found := revision.Config.TemplateFiles[tmpl.Name]
if found {
return definitions.NotificationTemplate{}, ErrTemplateExists.Errorf("")
}
existing, ok := revision.Config.TemplateFiles[tmpl.Name]
if ok {
err = t.checkOptimisticConcurrency(tmpl.Name, existing, 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 err := t.configStore.Save(ctx, revision, orgID); err != nil {
return err
}
} else if tmpl.ResourceVersion != "" { // if version is set then it's an update operation. Fail because resource does not exist anymore
return t.provenanceStore.SetProvenance(ctx, &tmpl, orgID, models.Provenance(tmpl.Provenance))
})
if err != nil {
return definitions.NotificationTemplate{}, err
}
return definitions.NotificationTemplate{
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{}
}
existingName := tmpl.Name
exisitingContent, found := revision.Config.TemplateFiles[existingName]
if !found {
return definitions.NotificationTemplate{}, ErrTemplateNotFound.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, exisitingContent, 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 {

@ -200,7 +200,7 @@ func TestGetTemplate(t *testing.T) {
})
}
func TestSetTemplate(t *testing.T) {
func TestUpsertTemplate(t *testing.T) {
orgID := int64(1)
templateName := "template1"
currentTemplateContent := "test1"
@ -240,7 +240,7 @@ func TestSetTemplate(t *testing.T) {
ResourceVersion: "",
}
result, err := sut.SetTemplate(context.Background(), orgID, tmpl)
result, err := sut.UpsertTemplate(context.Background(), orgID, tmpl)
require.NoError(t, err)
require.Equal(t, definitions.NotificationTemplate{
@ -281,7 +281,7 @@ func TestSetTemplate(t *testing.T) {
ResourceVersion: calculateTemplateFingerprint("test1"),
}
result, err := sut.SetTemplate(context.Background(), orgID, tmpl)
result, err := sut.UpsertTemplate(context.Background(), orgID, tmpl)
require.NoError(t, err)
assert.Equal(t, definitions.NotificationTemplate{
@ -317,7 +317,7 @@ func TestSetTemplate(t *testing.T) {
ResourceVersion: "",
}
result, err := sut.SetTemplate(context.Background(), orgID, tmpl)
result, err := sut.UpsertTemplate(context.Background(), orgID, tmpl)
require.NoError(t, err)
assert.Equal(t, definitions.NotificationTemplate{
@ -351,7 +351,7 @@ func TestSetTemplate(t *testing.T) {
ResourceVersion: calculateTemplateFingerprint(currentTemplateContent),
}
result, _ := sut.SetTemplate(context.Background(), orgID, tmpl)
result, _ := sut.UpsertTemplate(context.Background(), orgID, tmpl)
expectedContent := fmt.Sprintf("{{ define \"%s\" }}\n content\n{{ end }}", templateName)
require.Equal(t, definitions.NotificationTemplate{
@ -375,7 +375,7 @@ func TestSetTemplate(t *testing.T) {
Name: "name",
Template: "{{ .NotAField }}",
}
_, err := sut.SetTemplate(context.Background(), 1, tmpl)
_, err := sut.UpsertTemplate(context.Background(), 1, tmpl)
require.NoError(t, err)
})
@ -388,7 +388,7 @@ func TestSetTemplate(t *testing.T) {
Name: "",
Template: "",
}
_, err := sut.SetTemplate(context.Background(), orgID, tmpl)
_, err := sut.UpsertTemplate(context.Background(), orgID, tmpl)
require.ErrorIs(t, err, ErrTemplateInvalid)
})
@ -397,7 +397,7 @@ func TestSetTemplate(t *testing.T) {
Name: "",
Template: "{{ .MyField }",
}
_, err := sut.SetTemplate(context.Background(), orgID, tmpl)
_, err := sut.UpsertTemplate(context.Background(), orgID, tmpl)
require.ErrorIs(t, err, ErrTemplateInvalid)
})
@ -426,7 +426,7 @@ func TestSetTemplate(t *testing.T) {
}
template.Provenance = definitions.Provenance(models.ProvenanceNone)
_, err := sut.SetTemplate(context.Background(), orgID, template)
_, err := sut.UpsertTemplate(context.Background(), orgID, template)
require.ErrorIs(t, err, expectedErr)
})
@ -445,7 +445,7 @@ func TestSetTemplate(t *testing.T) {
Provenance: definitions.Provenance(models.ProvenanceNone),
}
_, err := sut.SetTemplate(context.Background(), orgID, template)
_, err := sut.UpsertTemplate(context.Background(), orgID, template)
require.ErrorIs(t, err, ErrVersionConflict)
prov.AssertExpectations(t)
@ -462,7 +462,7 @@ func TestSetTemplate(t *testing.T) {
ResourceVersion: "version",
Provenance: definitions.Provenance(models.ProvenanceNone),
}
_, err := sut.SetTemplate(context.Background(), orgID, template)
_, err := sut.UpsertTemplate(context.Background(), orgID, template)
require.ErrorIs(t, err, ErrTemplateNotFound)
})
t.Run("propagates errors", func(t *testing.T) {
@ -478,7 +478,7 @@ func TestSetTemplate(t *testing.T) {
return nil, expectedErr
}
_, err := sut.SetTemplate(context.Background(), orgID, tmpl)
_, err := sut.UpsertTemplate(context.Background(), orgID, tmpl)
require.ErrorIs(t, err, expectedErr)
})
@ -490,7 +490,7 @@ func TestSetTemplate(t *testing.T) {
expectedErr := errors.New("test")
prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, expectedErr)
_, err := sut.SetTemplate(context.Background(), orgID, tmpl)
_, err := sut.UpsertTemplate(context.Background(), orgID, tmpl)
require.ErrorIs(t, err, expectedErr)
@ -506,7 +506,7 @@ func TestSetTemplate(t *testing.T) {
prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil)
prov.EXPECT().SetProvenance(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(expectedErr)
_, err := sut.SetTemplate(context.Background(), orgID, tmpl)
_, err := sut.UpsertTemplate(context.Background(), orgID, tmpl)
require.ErrorIs(t, err, expectedErr)
prov.AssertExpectations(t)
@ -524,7 +524,378 @@ func TestSetTemplate(t *testing.T) {
prov.EXPECT().SaveSucceeds()
prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil)
_, err := sut.SetTemplate(context.Background(), 1, tmpl)
_, err := sut.UpsertTemplate(context.Background(), 1, tmpl)
require.ErrorIs(t, err, expectedErr)
})
})
}
func TestCreateTemplate(t *testing.T) {
orgID := int64(1)
amConfigToken := util.GenerateShortUID()
tmpl := definitions.NotificationTemplate{
Name: "new-template",
Template: "{{ define \"test\"}} test {{ end }}",
Provenance: definitions.Provenance(models.ProvenanceAPI),
}
revision := func() *legacy_storage.ConfigRevision {
return &legacy_storage.ConfigRevision{
Config: &definitions.PostableUserConfig{},
ConcurrencyToken: amConfigToken,
}
}
t.Run("adds new template to config file", 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
}
store.SaveFn = func(ctx context.Context, revision *legacy_storage.ConfigRevision) error {
assertInTransaction(t, ctx)
return nil
}
prov.EXPECT().SetProvenance(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Run(func(ctx context.Context, o models.Provisionable, org int64, p models.Provenance) {
assertInTransaction(t, ctx)
}).Return(nil)
result, err := sut.CreateTemplate(context.Background(), orgID, tmpl)
require.NoError(t, err)
require.Equal(t, definitions.NotificationTemplate{
Name: tmpl.Name,
Template: tmpl.Template,
Provenance: tmpl.Provenance,
ResourceVersion: calculateTemplateFingerprint(tmpl.Template),
}, result)
require.Len(t, store.Calls, 2)
require.Equal(t, "Save", store.Calls[1].Method)
saved := store.Calls[1].Args[1].(*legacy_storage.ConfigRevision)
assert.Equal(t, amConfigToken, saved.ConcurrencyToken)
assert.Contains(t, saved.Config.TemplateFiles, tmpl.Name)
assert.Equal(t, tmpl.Template, saved.Config.TemplateFiles[tmpl.Name])
prov.AssertCalled(t, "SetProvenance", mock.Anything, mock.MatchedBy(func(t *definitions.NotificationTemplate) bool {
return t.Name == tmpl.Name
}), orgID, models.ProvenanceAPI)
})
t.Run("returns ErrTemplateExists if template exists", 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 &legacy_storage.ConfigRevision{
Config: &definitions.PostableUserConfig{
TemplateFiles: map[string]string{
tmpl.Name: "test",
},
},
ConcurrencyToken: amConfigToken,
}, nil
}
_, err := sut.CreateTemplate(context.Background(), orgID, tmpl)
require.ErrorIs(t, err, ErrTemplateExists)
})
t.Run("rejects templates that fail validation", func(t *testing.T) {
sut, store, prov := createTemplateServiceSut()
t.Run("empty content", func(t *testing.T) {
tmpl := definitions.NotificationTemplate{
Name: "",
Template: "",
}
_, err := sut.CreateTemplate(context.Background(), orgID, tmpl)
require.ErrorIs(t, err, ErrTemplateInvalid)
})
t.Run("invalid content", func(t *testing.T) {
tmpl := definitions.NotificationTemplate{
Name: "",
Template: "{{ .MyField }",
}
_, err := sut.CreateTemplate(context.Background(), orgID, tmpl)
require.ErrorIs(t, err, ErrTemplateInvalid)
})
require.Empty(t, store.Calls)
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()
expectedErr := errors.New("test")
store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) {
return nil, expectedErr
}
_, err := sut.CreateTemplate(context.Background(), orgID, tmpl)
require.ErrorIs(t, err, expectedErr)
})
t.Run("when provenance fails to save", func(t *testing.T) {
sut, store, prov := createTemplateServiceSut()
expectedErr := errors.New("test")
store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) {
return revision(), nil
}
prov.EXPECT().SetProvenance(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(expectedErr)
_, err := sut.CreateTemplate(context.Background(), orgID, tmpl)
require.ErrorIs(t, err, expectedErr)
prov.AssertExpectations(t)
})
t.Run("when AM config fails to save", func(t *testing.T) {
sut, store, prov := createTemplateServiceSut()
store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) {
return revision(), nil
}
expectedErr := errors.New("test")
store.SaveFn = func(ctx context.Context, revision *legacy_storage.ConfigRevision) error {
return expectedErr
}
prov.EXPECT().SaveSucceeds()
prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil)
_, err := sut.CreateTemplate(context.Background(), 1, tmpl)
require.ErrorIs(t, err, expectedErr)
})
})
}
func TestUpdateTemplate(t *testing.T) {
orgID := int64(1)
currentTemplateContent := "test1"
tmpl := definitions.NotificationTemplate{
Name: "template1",
Template: "{{ define \"test\"}} test {{ end }}",
Provenance: definitions.Provenance(models.ProvenanceAPI),
ResourceVersion: "",
}
amConfigToken := util.GenerateShortUID()
revision := func() *legacy_storage.ConfigRevision {
return &legacy_storage.ConfigRevision{
Config: &definitions.PostableUserConfig{
TemplateFiles: map[string]string{
tmpl.Name: currentTemplateContent,
},
},
ConcurrencyToken: amConfigToken,
}
}
t.Run("returns ErrTemplateNotFound if template does not exist", 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 &legacy_storage.ConfigRevision{
Config: &definitions.PostableUserConfig{},
ConcurrencyToken: amConfigToken,
}, nil
}
_, err := sut.UpdateTemplate(context.Background(), orgID, tmpl)
require.ErrorIs(t, err, ErrTemplateNotFound)
require.Len(t, store.Calls, 1)
prov.AssertExpectations(t)
})
t.Run("updates current template", func(t *testing.T) {
t.Run("when version matches", 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.ProvenanceAPI, nil)
prov.EXPECT().SetProvenance(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Run(func(ctx context.Context, o models.Provisionable, org int64, p models.Provenance) {
assertInTransaction(t, ctx)
}).Return(nil)
result, err := sut.UpdateTemplate(context.Background(), orgID, tmpl)
require.NoError(t, err)
assert.Equal(t, definitions.NotificationTemplate{
Name: tmpl.Name,
Template: tmpl.Template,
Provenance: tmpl.Provenance,
ResourceVersion: calculateTemplateFingerprint(tmpl.Template),
}, result)
require.Len(t, store.Calls, 2)
require.Equal(t, "Save", store.Calls[1].Method)
saved := store.Calls[1].Args[1].(*legacy_storage.ConfigRevision)
assert.Equal(t, amConfigToken, saved.ConcurrencyToken)
assert.Contains(t, saved.Config.TemplateFiles, tmpl.Name)
assert.Equal(t, tmpl.Template, saved.Config.TemplateFiles[tmpl.Name])
prov.AssertExpectations(t)
})
t.Run("bypasses optimistic concurrency validation when version is empty", 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.ProvenanceAPI, nil)
prov.EXPECT().SetProvenance(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Run(func(ctx context.Context, o models.Provisionable, org int64, p models.Provenance) {
assertInTransaction(t, ctx)
}).Return(nil)
result, err := sut.UpdateTemplate(context.Background(), orgID, tmpl)
require.NoError(t, err)
assert.Equal(t, definitions.NotificationTemplate{
Name: tmpl.Name,
Template: tmpl.Template,
Provenance: tmpl.Provenance,
ResourceVersion: calculateTemplateFingerprint(tmpl.Template),
}, result)
require.Equal(t, "Save", store.Calls[1].Method)
saved := store.Calls[1].Args[1].(*legacy_storage.ConfigRevision)
assert.Equal(t, amConfigToken, saved.ConcurrencyToken)
assert.Contains(t, saved.Config.TemplateFiles, tmpl.Name)
assert.Equal(t, tmpl.Template, saved.Config.TemplateFiles[tmpl.Name])
})
})
t.Run("rejects templates that fail validation", func(t *testing.T) {
sut, store, prov := createTemplateServiceSut()
t.Run("empty content", func(t *testing.T) {
tmpl := definitions.NotificationTemplate{
Name: "",
Template: "",
}
_, err := sut.UpdateTemplate(context.Background(), orgID, tmpl)
require.ErrorIs(t, err, ErrTemplateInvalid)
})
t.Run("invalid content", func(t *testing.T) {
tmpl := definitions.NotificationTemplate{
Name: "",
Template: "{{ .MyField }",
}
_, err := sut.UpdateTemplate(context.Background(), orgID, tmpl)
require.ErrorIs(t, err, ErrTemplateInvalid)
})
require.Empty(t, store.Calls)
prov.AssertExpectations(t)
})
t.Run("rejects existing templates if provenance is not right", 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.ProvenanceAPI, nil)
expectedErr := errors.New("test")
sut.validator = func(from, to models.Provenance) error {
assert.Equal(t, models.ProvenanceAPI, from)
assert.Equal(t, models.ProvenanceNone, to)
return expectedErr
}
template := definitions.NotificationTemplate{
Name: "template1",
Template: "asdf-new",
}
template.Provenance = definitions.Provenance(models.ProvenanceNone)
_, err := sut.UpdateTemplate(context.Background(), orgID, template)
require.ErrorIs(t, err, expectedErr)
})
t.Run("rejects existing templates if version is not right", 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),
}
_, err := sut.UpdateTemplate(context.Background(), orgID, template)
require.ErrorIs(t, err, ErrVersionConflict)
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()
expectedErr := errors.New("test")
store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) {
return nil, expectedErr
}
_, err := sut.UpdateTemplate(context.Background(), orgID, tmpl)
require.ErrorIs(t, err, expectedErr)
})
t.Run("when reading provenance status fails", func(t *testing.T) {
sut, store, prov := createTemplateServiceSut()
store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
return revision(), nil
}
expectedErr := errors.New("test")
prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, expectedErr)
_, err := sut.UpdateTemplate(context.Background(), orgID, tmpl)
require.ErrorIs(t, err, expectedErr)
prov.AssertExpectations(t)
})
t.Run("when provenance fails to save", func(t *testing.T) {
sut, store, prov := createTemplateServiceSut()
expectedErr := errors.New("test")
store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) {
return revision(), nil
}
prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil)
prov.EXPECT().SetProvenance(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(expectedErr)
_, err := sut.UpdateTemplate(context.Background(), orgID, tmpl)
require.ErrorIs(t, err, expectedErr)
prov.AssertExpectations(t)
})
t.Run("when AM config fails to save", func(t *testing.T) {
sut, store, prov := createTemplateServiceSut()
store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) {
return revision(), nil
}
expectedErr := errors.New("test")
store.SaveFn = func(ctx context.Context, revision *legacy_storage.ConfigRevision) error {
return expectedErr
}
prov.EXPECT().SaveSucceeds()
prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil)
_, err := sut.UpdateTemplate(context.Background(), 1, tmpl)
require.ErrorIs(t, err, expectedErr)
})
})

@ -32,7 +32,7 @@ func (c *defaultTextTemplateProvisioner) Provision(ctx context.Context,
for _, file := range files {
for _, template := range file.Templates {
template.Data.Provenance = definitions.Provenance(models.ProvenanceFile)
_, err := c.templateService.SetTemplate(ctx, template.OrgID, template.Data)
_, err := c.templateService.UpsertTemplate(ctx, template.OrgID, template.Data)
if err != nil {
return err
}

Loading…
Cancel
Save