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_test.go

441 lines
13 KiB

package provisioning
import (
"context"
"fmt"
"testing"
"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/setting"
mock "github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
func TestTemplateService(t *testing.T) {
t.Run("service returns templates from config file", func(t *testing.T) {
sut := createTemplateServiceSut()
sut.config.(*MockAMConfigStore).EXPECT().
GetsConfig(models.AlertConfiguration{
AlertmanagerConfiguration: configWithTemplates,
})
result, err := sut.GetTemplates(context.Background(), 1)
require.NoError(t, err)
require.Len(t, result, 1)
})
t.Run("service returns empty map when config file contains no templates", func(t *testing.T) {
sut := createTemplateServiceSut()
sut.config.(*MockAMConfigStore).EXPECT().
GetsConfig(models.AlertConfiguration{
AlertmanagerConfiguration: defaultConfig,
})
result, err := sut.GetTemplates(context.Background(), 1)
require.NoError(t, err)
require.Empty(t, result)
})
t.Run("service propagates errors", func(t *testing.T) {
t.Run("when unable to read config", func(t *testing.T) {
sut := createTemplateServiceSut()
sut.config.(*MockAMConfigStore).EXPECT().
GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything).
Return(fmt.Errorf("failed"))
_, err := sut.GetTemplates(context.Background(), 1)
require.Error(t, err)
})
t.Run("when config is invalid", func(t *testing.T) {
sut := createTemplateServiceSut()
sut.config.(*MockAMConfigStore).EXPECT().
GetsConfig(models.AlertConfiguration{
AlertmanagerConfiguration: brokenConfig,
})
_, err := sut.GetTemplates(context.Background(), 1)
require.ErrorContains(t, err, "failed to deserialize")
})
t.Run("when no AM config in current org", func(t *testing.T) {
sut := createTemplateServiceSut()
sut.config.(*MockAMConfigStore).EXPECT().
GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything).
Return(nil)
_, err := sut.GetTemplates(context.Background(), 1)
require.ErrorContains(t, err, "no alertmanager configuration")
})
})
t.Run("setting templates", func(t *testing.T) {
t.Run("rejects templates that fail validation", func(t *testing.T) {
sut := createTemplateServiceSut()
tmpl := definitions.MessageTemplate{
Name: "",
Template: "",
}
_, err := sut.SetTemplate(context.Background(), 1, tmpl)
require.ErrorIs(t, err, ErrValidation)
})
t.Run("propagates errors", func(t *testing.T) {
t.Run("when unable to read config", func(t *testing.T) {
sut := createTemplateServiceSut()
tmpl := createMessageTemplate()
sut.config.(*MockAMConfigStore).EXPECT().
GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything).
Return(fmt.Errorf("failed"))
_, err := sut.SetTemplate(context.Background(), 1, tmpl)
require.Error(t, err)
})
t.Run("when config is invalid", func(t *testing.T) {
sut := createTemplateServiceSut()
tmpl := createMessageTemplate()
sut.config.(*MockAMConfigStore).EXPECT().
GetsConfig(models.AlertConfiguration{
AlertmanagerConfiguration: brokenConfig,
})
_, err := sut.SetTemplate(context.Background(), 1, tmpl)
require.ErrorContains(t, err, "failed to deserialize")
})
t.Run("when no AM config in current org", func(t *testing.T) {
sut := createTemplateServiceSut()
tmpl := createMessageTemplate()
sut.config.(*MockAMConfigStore).EXPECT().
GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything).
Return(nil)
_, err := sut.SetTemplate(context.Background(), 1, tmpl)
require.ErrorContains(t, err, "no alertmanager configuration")
})
t.Run("when provenance fails to save", func(t *testing.T) {
sut := createTemplateServiceSut()
tmpl := createMessageTemplate()
sut.config.(*MockAMConfigStore).EXPECT().
GetsConfig(models.AlertConfiguration{
AlertmanagerConfiguration: configWithTemplates,
})
sut.config.(*MockAMConfigStore).EXPECT().SaveSucceeds()
sut.prov.(*MockProvisioningStore).EXPECT().
SetProvenance(mock.Anything, mock.Anything, mock.Anything, mock.Anything).
Return(fmt.Errorf("failed to save provenance"))
_, err := sut.SetTemplate(context.Background(), 1, tmpl)
require.ErrorContains(t, err, "failed to save provenance")
})
t.Run("when AM config fails to save", func(t *testing.T) {
sut := createTemplateServiceSut()
tmpl := createMessageTemplate()
sut.config.(*MockAMConfigStore).EXPECT().
GetsConfig(models.AlertConfiguration{
AlertmanagerConfiguration: configWithTemplates,
})
sut.config.(*MockAMConfigStore).EXPECT().
UpdateAlertmanagerConfiguration(mock.Anything, mock.Anything).
Return(fmt.Errorf("failed to save config"))
sut.prov.(*MockProvisioningStore).EXPECT().SaveSucceeds()
_, err := sut.SetTemplate(context.Background(), 1, tmpl)
require.ErrorContains(t, err, "failed to save config")
})
})
t.Run("adds new template to config file on success", func(t *testing.T) {
sut := createTemplateServiceSut()
tmpl := createMessageTemplate()
sut.config.(*MockAMConfigStore).EXPECT().
GetsConfig(models.AlertConfiguration{
AlertmanagerConfiguration: configWithTemplates,
})
sut.config.(*MockAMConfigStore).EXPECT().SaveSucceeds()
sut.prov.(*MockProvisioningStore).EXPECT().SaveSucceeds()
_, err := sut.SetTemplate(context.Background(), 1, tmpl)
require.NoError(t, err)
})
t.Run("succeeds when stitching config file with no templates", func(t *testing.T) {
sut := createTemplateServiceSut()
tmpl := createMessageTemplate()
sut.config.(*MockAMConfigStore).EXPECT().
GetsConfig(models.AlertConfiguration{
AlertmanagerConfiguration: defaultConfig,
})
sut.config.(*MockAMConfigStore).EXPECT().SaveSucceeds()
sut.prov.(*MockProvisioningStore).EXPECT().SaveSucceeds()
_, err := sut.SetTemplate(context.Background(), 1, tmpl)
require.NoError(t, err)
})
t.Run("normalizes template content with no define", func(t *testing.T) {
sut := createTemplateServiceSut()
tmpl := definitions.MessageTemplate{
Name: "name",
Template: "content",
}
sut.config.(*MockAMConfigStore).EXPECT().
GetsConfig(models.AlertConfiguration{
AlertmanagerConfiguration: defaultConfig,
})
sut.config.(*MockAMConfigStore).EXPECT().SaveSucceeds()
sut.prov.(*MockProvisioningStore).EXPECT().SaveSucceeds()
result, _ := sut.SetTemplate(context.Background(), 1, tmpl)
exp := "{{ define \"name\" }}\n content\n{{ end }}"
require.Equal(t, exp, result.Template)
})
t.Run("avoids normalizing template content with define", func(t *testing.T) {
sut := createTemplateServiceSut()
tmpl := definitions.MessageTemplate{
Name: "name",
Template: "{{define \"name\"}}content{{end}}",
}
sut.config.(*MockAMConfigStore).EXPECT().
GetsConfig(models.AlertConfiguration{
AlertmanagerConfiguration: defaultConfig,
})
sut.config.(*MockAMConfigStore).EXPECT().SaveSucceeds()
sut.prov.(*MockProvisioningStore).EXPECT().SaveSucceeds()
result, _ := sut.SetTemplate(context.Background(), 1, tmpl)
require.Equal(t, tmpl.Template, result.Template)
})
t.Run("rejects syntactically invalid template", func(t *testing.T) {
sut := createTemplateServiceSut()
tmpl := definitions.MessageTemplate{
Name: "name",
Template: "{{ .MyField }",
}
sut.config.(*MockAMConfigStore).EXPECT().
GetsConfig(models.AlertConfiguration{
AlertmanagerConfiguration: defaultConfig,
})
sut.config.(*MockAMConfigStore).EXPECT().SaveSucceeds()
sut.prov.(*MockProvisioningStore).EXPECT().SaveSucceeds()
_, err := sut.SetTemplate(context.Background(), 1, tmpl)
require.ErrorIs(t, err, ErrValidation)
})
t.Run("does not reject template with unknown field", func(t *testing.T) {
sut := createTemplateServiceSut()
tmpl := definitions.MessageTemplate{
Name: "name",
Template: "{{ .NotAField }}",
}
sut.config.(*MockAMConfigStore).EXPECT().
GetsConfig(models.AlertConfiguration{
AlertmanagerConfiguration: defaultConfig,
})
sut.config.(*MockAMConfigStore).EXPECT().SaveSucceeds()
sut.prov.(*MockProvisioningStore).EXPECT().SaveSucceeds()
_, err := sut.SetTemplate(context.Background(), 1, tmpl)
require.NoError(t, err)
})
})
t.Run("deleting templates", func(t *testing.T) {
t.Run("propagates errors", func(t *testing.T) {
t.Run("when unable to read config", func(t *testing.T) {
sut := createTemplateServiceSut()
sut.config.(*MockAMConfigStore).EXPECT().
GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything).
Return(fmt.Errorf("failed"))
err := sut.DeleteTemplate(context.Background(), 1, "template")
require.Error(t, err)
})
t.Run("when config is invalid", func(t *testing.T) {
sut := createTemplateServiceSut()
sut.config.(*MockAMConfigStore).EXPECT().
GetsConfig(models.AlertConfiguration{
AlertmanagerConfiguration: brokenConfig,
})
err := sut.DeleteTemplate(context.Background(), 1, "template")
require.ErrorContains(t, err, "failed to deserialize")
})
t.Run("when no AM config in current org", func(t *testing.T) {
sut := createTemplateServiceSut()
sut.config.(*MockAMConfigStore).EXPECT().
GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything).
Return(nil)
err := sut.DeleteTemplate(context.Background(), 1, "template")
require.ErrorContains(t, err, "no alertmanager configuration")
})
t.Run("when provenance fails to save", func(t *testing.T) {
sut := createTemplateServiceSut()
sut.config.(*MockAMConfigStore).EXPECT().
GetsConfig(models.AlertConfiguration{
AlertmanagerConfiguration: configWithTemplates,
})
sut.config.(*MockAMConfigStore).EXPECT().SaveSucceeds()
sut.prov.(*MockProvisioningStore).EXPECT().
DeleteProvenance(mock.Anything, mock.Anything, mock.Anything).
Return(fmt.Errorf("failed to save provenance"))
err := sut.DeleteTemplate(context.Background(), 1, "template")
require.ErrorContains(t, err, "failed to save provenance")
})
t.Run("when AM config fails to save", func(t *testing.T) {
sut := createTemplateServiceSut()
sut.config.(*MockAMConfigStore).EXPECT().
GetsConfig(models.AlertConfiguration{
AlertmanagerConfiguration: configWithTemplates,
})
sut.config.(*MockAMConfigStore).EXPECT().
UpdateAlertmanagerConfiguration(mock.Anything, mock.Anything).
Return(fmt.Errorf("failed to save config"))
sut.prov.(*MockProvisioningStore).EXPECT().SaveSucceeds()
err := sut.DeleteTemplate(context.Background(), 1, "template")
require.ErrorContains(t, err, "failed to save config")
})
})
t.Run("deletes template from config file on success", func(t *testing.T) {
sut := createTemplateServiceSut()
sut.config.(*MockAMConfigStore).EXPECT().
GetsConfig(models.AlertConfiguration{
AlertmanagerConfiguration: configWithTemplates,
})
sut.config.(*MockAMConfigStore).EXPECT().SaveSucceeds()
sut.prov.(*MockProvisioningStore).EXPECT().SaveSucceeds()
err := sut.DeleteTemplate(context.Background(), 1, "a")
require.NoError(t, err)
})
t.Run("does not error when deleting templates that do not exist", func(t *testing.T) {
sut := createTemplateServiceSut()
sut.config.(*MockAMConfigStore).EXPECT().
GetsConfig(models.AlertConfiguration{
AlertmanagerConfiguration: configWithTemplates,
})
sut.config.(*MockAMConfigStore).EXPECT().SaveSucceeds()
sut.prov.(*MockProvisioningStore).EXPECT().SaveSucceeds()
err := sut.DeleteTemplate(context.Background(), 1, "does not exist")
require.NoError(t, err)
})
t.Run("succeeds when deleting from config file with no template section", func(t *testing.T) {
sut := createTemplateServiceSut()
sut.config.(*MockAMConfigStore).EXPECT().
GetsConfig(models.AlertConfiguration{
AlertmanagerConfiguration: defaultConfig,
})
sut.config.(*MockAMConfigStore).EXPECT().SaveSucceeds()
sut.prov.(*MockProvisioningStore).EXPECT().SaveSucceeds()
err := sut.DeleteTemplate(context.Background(), 1, "a")
require.NoError(t, err)
})
})
}
func createTemplateServiceSut() *TemplateService {
return &TemplateService{
config: &MockAMConfigStore{},
prov: &MockProvisioningStore{},
xact: newNopTransactionManager(),
log: log.NewNopLogger(),
}
}
func createMessageTemplate() definitions.MessageTemplate {
return definitions.MessageTemplate{
Name: "test",
Template: "asdf",
}
}
var defaultConfig = setting.GetAlertmanagerDefaultConfiguration()
var configWithTemplates = `
{
"template_files": {
"a": "template"
},
"alertmanager_config": {
"route": {
"receiver": "grafana-default-email"
},
"receivers": [{
"name": "grafana-default-email",
"grafana_managed_receiver_configs": [{
"uid": "",
"name": "email receiver",
"type": "email",
"isDefault": true,
"settings": {
"addresses": "<example@email.com>"
}
}]
}]
}
}
`
var brokenConfig = `
"alertmanager_config": {
"route": {
"receiver": "grafana-default-email"
},
"receivers": [{
"name": "grafana-default-email",
"grafana_managed_receiver_configs": [{
"uid": "abc",
"name": "default-email",
"type": "email",
"isDefault": true,
"settings": {}
}]
}]
}
}`