diff --git a/pkg/services/ngalert/api/api_provisioning.go b/pkg/services/ngalert/api/api_provisioning.go index 83c40b57ff8..df29ebd480b 100644 --- a/pkg/services/ngalert/api/api_provisioning.go +++ b/pkg/services/ngalert/api/api_provisioning.go @@ -8,7 +8,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" - apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" alerting_models "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/provisioning" "github.com/grafana/grafana/pkg/services/ngalert/store" @@ -33,27 +33,27 @@ type ProvisioningSrv struct { } type ContactPointService interface { - GetContactPoints(ctx context.Context, orgID int64) ([]apimodels.EmbeddedContactPoint, error) - CreateContactPoint(ctx context.Context, orgID int64, contactPoint apimodels.EmbeddedContactPoint, p alerting_models.Provenance) (apimodels.EmbeddedContactPoint, error) - UpdateContactPoint(ctx context.Context, orgID int64, contactPoint apimodels.EmbeddedContactPoint, p alerting_models.Provenance) error + GetContactPoints(ctx context.Context, orgID int64) ([]definitions.EmbeddedContactPoint, error) + CreateContactPoint(ctx context.Context, orgID int64, contactPoint definitions.EmbeddedContactPoint, p alerting_models.Provenance) (definitions.EmbeddedContactPoint, error) + UpdateContactPoint(ctx context.Context, orgID int64, contactPoint definitions.EmbeddedContactPoint, p alerting_models.Provenance) error DeleteContactPoint(ctx context.Context, orgID int64, uid string) error } type TemplateService interface { GetTemplates(ctx context.Context, orgID int64) (map[string]string, error) - SetTemplate(ctx context.Context, orgID int64, tmpl apimodels.MessageTemplate) (apimodels.MessageTemplate, error) + SetTemplate(ctx context.Context, orgID int64, tmpl definitions.MessageTemplate) (definitions.MessageTemplate, error) DeleteTemplate(ctx context.Context, orgID int64, name string) error } type NotificationPolicyService interface { - GetPolicyTree(ctx context.Context, orgID int64) (apimodels.Route, error) - UpdatePolicyTree(ctx context.Context, orgID int64, tree apimodels.Route, p alerting_models.Provenance) error + GetPolicyTree(ctx context.Context, orgID int64) (definitions.Route, error) + UpdatePolicyTree(ctx context.Context, orgID int64, tree definitions.Route, p alerting_models.Provenance) error } type MuteTimingService interface { - GetMuteTimings(ctx context.Context, orgID int64) ([]apimodels.MuteTimeInterval, error) - CreateMuteTiming(ctx context.Context, mt apimodels.MuteTimeInterval, orgID int64) (*apimodels.MuteTimeInterval, error) - UpdateMuteTiming(ctx context.Context, mt apimodels.MuteTimeInterval, orgID int64) (*apimodels.MuteTimeInterval, error) + GetMuteTimings(ctx context.Context, orgID int64) ([]definitions.MuteTimeInterval, error) + CreateMuteTiming(ctx context.Context, mt definitions.MuteTimeInterval, orgID int64) (*definitions.MuteTimeInterval, error) + UpdateMuteTiming(ctx context.Context, mt definitions.MuteTimeInterval, orgID int64) (*definitions.MuteTimeInterval, error) DeleteMuteTiming(ctx context.Context, name string, orgID int64) error } @@ -77,7 +77,7 @@ func (srv *ProvisioningSrv) RouteGetPolicyTree(c *models.ReqContext) response.Re return response.JSON(http.StatusOK, policies) } -func (srv *ProvisioningSrv) RoutePutPolicyTree(c *models.ReqContext, tree apimodels.Route) response.Response { +func (srv *ProvisioningSrv) RoutePutPolicyTree(c *models.ReqContext, tree definitions.Route) response.Response { err := srv.policies.UpdatePolicyTree(c.Req.Context(), c.OrgId, tree, alerting_models.ProvenanceAPI) if errors.Is(err, store.ErrNoAlertmanagerConfiguration) { return ErrResp(http.StatusNotFound, err, "") @@ -100,18 +100,24 @@ func (srv *ProvisioningSrv) RouteGetContactPoints(c *models.ReqContext) response return response.JSON(http.StatusOK, cps) } -func (srv *ProvisioningSrv) RoutePostContactPoint(c *models.ReqContext, cp apimodels.EmbeddedContactPoint) response.Response { +func (srv *ProvisioningSrv) RoutePostContactPoint(c *models.ReqContext, cp definitions.EmbeddedContactPoint) response.Response { // TODO: provenance is hardcoded for now, change it later to make it more flexible contactPoint, err := srv.contactPointService.CreateContactPoint(c.Req.Context(), c.OrgId, cp, alerting_models.ProvenanceAPI) + if errors.Is(err, provisioning.ErrValidation) { + return ErrResp(http.StatusBadRequest, err, "") + } if err != nil { return ErrResp(http.StatusInternalServerError, err, "") } return response.JSON(http.StatusAccepted, contactPoint) } -func (srv *ProvisioningSrv) RoutePutContactPoint(c *models.ReqContext, cp apimodels.EmbeddedContactPoint) response.Response { +func (srv *ProvisioningSrv) RoutePutContactPoint(c *models.ReqContext, cp definitions.EmbeddedContactPoint) response.Response { cp.UID = pathParam(c, uidPathParam) err := srv.contactPointService.UpdateContactPoint(c.Req.Context(), c.OrgId, cp, alerting_models.ProvenanceAPI) + if errors.Is(err, provisioning.ErrValidation) { + return ErrResp(http.StatusBadRequest, err, "") + } if err != nil { return ErrResp(http.StatusInternalServerError, err, "") } @@ -132,9 +138,9 @@ func (srv *ProvisioningSrv) RouteGetTemplates(c *models.ReqContext) response.Res if err != nil { return ErrResp(http.StatusInternalServerError, err, "") } - result := make([]apimodels.MessageTemplate, 0, len(templates)) + result := make([]definitions.MessageTemplate, 0, len(templates)) for k, v := range templates { - result = append(result, apimodels.MessageTemplate{Name: k, Template: v}) + result = append(result, definitions.MessageTemplate{Name: k, Template: v}) } return response.JSON(http.StatusOK, result) } @@ -146,14 +152,14 @@ func (srv *ProvisioningSrv) RouteGetTemplate(c *models.ReqContext) response.Resp return ErrResp(http.StatusInternalServerError, err, "") } if tmpl, ok := templates[name]; ok { - return response.JSON(http.StatusOK, apimodels.MessageTemplate{Name: name, Template: tmpl}) + return response.JSON(http.StatusOK, definitions.MessageTemplate{Name: name, Template: tmpl}) } return response.Empty(http.StatusNotFound) } -func (srv *ProvisioningSrv) RoutePutTemplate(c *models.ReqContext, body apimodels.MessageTemplateContent) response.Response { +func (srv *ProvisioningSrv) RoutePutTemplate(c *models.ReqContext, body definitions.MessageTemplateContent) response.Response { name := pathParam(c, namePathParam) - tmpl := apimodels.MessageTemplate{ + tmpl := definitions.MessageTemplate{ Name: name, Template: body.Template, Provenance: alerting_models.ProvenanceAPI, @@ -199,7 +205,7 @@ func (srv *ProvisioningSrv) RouteGetMuteTimings(c *models.ReqContext) response.R return response.JSON(http.StatusOK, timings) } -func (srv *ProvisioningSrv) RoutePostMuteTiming(c *models.ReqContext, mt apimodels.MuteTimeInterval) response.Response { +func (srv *ProvisioningSrv) RoutePostMuteTiming(c *models.ReqContext, mt definitions.MuteTimeInterval) response.Response { created, err := srv.muteTimings.CreateMuteTiming(c.Req.Context(), mt, c.OrgId) if err != nil { if errors.Is(err, provisioning.ErrValidation) { @@ -210,7 +216,7 @@ func (srv *ProvisioningSrv) RoutePostMuteTiming(c *models.ReqContext, mt apimode return response.JSON(http.StatusCreated, created) } -func (srv *ProvisioningSrv) RoutePutMuteTiming(c *models.ReqContext, mt apimodels.MuteTimeInterval) response.Response { +func (srv *ProvisioningSrv) RoutePutMuteTiming(c *models.ReqContext, mt definitions.MuteTimeInterval) response.Response { name := pathParam(c, namePathParam) mt.Name = name updated, err := srv.muteTimings.UpdateMuteTiming(c.Req.Context(), mt, c.OrgId) @@ -241,11 +247,14 @@ func (srv *ProvisioningSrv) RouteRouteGetAlertRule(c *models.ReqContext) respons if err != nil { return ErrResp(http.StatusInternalServerError, err, "") } - return response.JSON(http.StatusOK, apimodels.NewAlertRule(rule, provenace)) + return response.JSON(http.StatusOK, definitions.NewAlertRule(rule, provenace)) } -func (srv *ProvisioningSrv) RoutePostAlertRule(c *models.ReqContext, ar apimodels.AlertRule) response.Response { +func (srv *ProvisioningSrv) RoutePostAlertRule(c *models.ReqContext, ar definitions.AlertRule) response.Response { createdAlertRule, err := srv.alertRules.CreateAlertRule(c.Req.Context(), ar.UpstreamModel(), alerting_models.ProvenanceAPI) + if errors.Is(err, alerting_models.ErrAlertRuleFailedValidation) { + return ErrResp(http.StatusBadRequest, err, "") + } if err != nil { return ErrResp(http.StatusInternalServerError, err, "") } @@ -255,8 +264,14 @@ func (srv *ProvisioningSrv) RoutePostAlertRule(c *models.ReqContext, ar apimodel return response.JSON(http.StatusCreated, ar) } -func (srv *ProvisioningSrv) RoutePutAlertRule(c *models.ReqContext, ar apimodels.AlertRule) response.Response { +func (srv *ProvisioningSrv) RoutePutAlertRule(c *models.ReqContext, ar definitions.AlertRule) response.Response { updatedAlertRule, err := srv.alertRules.UpdateAlertRule(c.Req.Context(), ar.UpstreamModel(), alerting_models.ProvenanceAPI) + if errors.Is(err, alerting_models.ErrAlertRuleNotFound) { + return response.Empty(http.StatusNotFound) + } + if errors.Is(err, alerting_models.ErrAlertRuleFailedValidation) { + return ErrResp(http.StatusBadRequest, err, "") + } if err != nil { return ErrResp(http.StatusInternalServerError, err, "") } @@ -273,7 +288,7 @@ func (srv *ProvisioningSrv) RouteDeleteAlertRule(c *models.ReqContext) response. return response.JSON(http.StatusNoContent, "") } -func (srv *ProvisioningSrv) RoutePutAlertRuleGroup(c *models.ReqContext, ag apimodels.AlertRuleGroup) response.Response { +func (srv *ProvisioningSrv) RoutePutAlertRuleGroup(c *models.ReqContext, ag definitions.AlertRuleGroup) response.Response { rulegroup := pathParam(c, groupPathParam) folderUID := pathParam(c, folderUIDPathParam) err := srv.alertRules.UpdateRuleGroup(c.Req.Context(), c.OrgId, folderUID, rulegroup, ag.Interval) diff --git a/pkg/services/ngalert/api/api_provisioning_test.go b/pkg/services/ngalert/api/api_provisioning_test.go index a72b92b371b..814fe633918 100644 --- a/pkg/services/ngalert/api/api_provisioning_test.go +++ b/pkg/services/ngalert/api/api_provisioning_test.go @@ -2,147 +2,308 @@ package api import ( "context" + "encoding/json" "fmt" "net/http" "testing" + "time" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" - apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" - domain "github.com/grafana/grafana/pkg/services/ngalert/models" + gfcore "github.com/grafana/grafana/pkg/models" + "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/provisioning" "github.com/grafana/grafana/pkg/services/ngalert/store" + secrets "github.com/grafana/grafana/pkg/services/secrets/fakes" + "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/web" + prometheus "github.com/prometheus/alertmanager/config" + "github.com/prometheus/alertmanager/timeinterval" "github.com/stretchr/testify/require" ) func TestProvisioningApi(t *testing.T) { - t.Run("successful GET policies returns 200", func(t *testing.T) { - sut := createProvisioningSrvSut() - rc := createTestRequestCtx() + t.Run("policies", func(t *testing.T) { + t.Run("successful GET returns 200", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + + response := sut.RouteGetPolicyTree(&rc) + + require.Equal(t, 200, response.Status()) + }) + + t.Run("successful PUT returns 202", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + tree := definitions.Route{} + + response := sut.RoutePutPolicyTree(&rc, tree) + + require.Equal(t, 202, response.Status()) + }) + + t.Run("when new policy tree is invalid", func(t *testing.T) { + t.Run("PUT returns 400", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + sut.policies = &fakeRejectingNotificationPolicyService{} + rc := createTestRequestCtx() + tree := definitions.Route{} + + response := sut.RoutePutPolicyTree(&rc, tree) + + require.Equal(t, 400, response.Status()) + expBody := `{"error":"invalid object specification: invalid policy tree","message":"invalid object specification: invalid policy tree"}` + require.Equal(t, expBody, string(response.Body())) + }) + }) + + t.Run("when org has no AM config", func(t *testing.T) { + t.Run("GET returns 404", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + rc.SignedInUser.OrgId = 2 + + response := sut.RouteGetPolicyTree(&rc) + + require.Equal(t, 404, response.Status()) + }) - response := sut.RouteGetPolicyTree(&rc) + t.Run("POST returns 404", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + rc.SignedInUser.OrgId = 2 - require.Equal(t, 200, response.Status()) + response := sut.RouteGetPolicyTree(&rc) + + require.Equal(t, 404, response.Status()) + }) + }) + + t.Run("when an unspecified error occurrs", func(t *testing.T) { + t.Run("GET returns 500", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + sut.policies = &fakeFailingNotificationPolicyService{} + rc := createTestRequestCtx() + + response := sut.RouteGetPolicyTree(&rc) + + require.Equal(t, 500, response.Status()) + require.NotEmpty(t, response.Body()) + require.Contains(t, string(response.Body()), "something went wrong") + }) + + t.Run("PUT returns 500", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + sut.policies = &fakeFailingNotificationPolicyService{} + rc := createTestRequestCtx() + tree := definitions.Route{} + + response := sut.RoutePutPolicyTree(&rc, tree) + + require.Equal(t, 500, response.Status()) + require.NotEmpty(t, response.Body()) + require.Contains(t, string(response.Body()), "something went wrong") + }) + }) }) - t.Run("successful PUT policies returns 202", func(t *testing.T) { - sut := createProvisioningSrvSut() - rc := createTestRequestCtx() - tree := apimodels.Route{} + t.Run("contact points", func(t *testing.T) { + t.Run("are invalid", func(t *testing.T) { + t.Run("POST returns 400", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + cp := createInvalidContactPoint() - response := sut.RoutePutPolicyTree(&rc, tree) + response := sut.RoutePostContactPoint(&rc, cp) - require.Equal(t, 202, response.Status()) + require.Equal(t, 400, response.Status()) + require.NotEmpty(t, response.Body()) + require.Contains(t, string(response.Body()), "recipient must be specified") + }) + + t.Run("PUT returns 400", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + cp := createInvalidContactPoint() + + response := sut.RoutePutContactPoint(&rc, cp) + + require.Equal(t, 400, response.Status()) + require.NotEmpty(t, response.Body()) + require.Contains(t, string(response.Body()), "recipient must be specified") + }) + }) }) - t.Run("when new policy tree is invalid", func(t *testing.T) { - t.Run("PUT policies returns 400", func(t *testing.T) { - sut := createProvisioningSrvSut() - sut.policies = &fakeRejectingNotificationPolicyService{} - rc := createTestRequestCtx() - tree := apimodels.Route{} + t.Run("templates", func(t *testing.T) { + t.Run("are invalid", func(t *testing.T) { + t.Run("PUT returns 400", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + withURLParams(rc, namePathParam, "test") + tmpl := definitions.MessageTemplateContent{Template: ""} - response := sut.RoutePutPolicyTree(&rc, tree) + response := sut.RoutePutTemplate(&rc, tmpl) - require.Equal(t, 400, response.Status()) - expBody := `{"error":"invalid object specification: invalid policy tree","message":"invalid object specification: invalid policy tree"}` - require.Equal(t, expBody, string(response.Body())) + require.Equal(t, 400, response.Status()) + require.NotEmpty(t, response.Body()) + require.Contains(t, string(response.Body()), "template must have content") + }) }) }) - t.Run("when org has no AM config", func(t *testing.T) { - t.Run("GET policies returns 404", func(t *testing.T) { - sut := createProvisioningSrvSut() - rc := createTestRequestCtx() - rc.SignedInUser.OrgId = 2 + t.Run("mute timings", func(t *testing.T) { + t.Run("are invalid", func(t *testing.T) { + t.Run("POST returns 400", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + mti := createInvalidMuteTiming() - response := sut.RouteGetPolicyTree(&rc) + response := sut.RoutePostMuteTiming(&rc, mti) - require.Equal(t, 404, response.Status()) + require.Equal(t, 400, response.Status()) + require.NotEmpty(t, response.Body()) + require.Contains(t, string(response.Body()), "invalid") + }) + + t.Run("PUT returns 400", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + withURLParams(rc, namePathParam, "interval") + mti := createInvalidMuteTiming() + + response := sut.RoutePutMuteTiming(&rc, mti) + + require.Equal(t, 400, response.Status()) + require.NotEmpty(t, response.Body()) + require.Contains(t, string(response.Body()), "invalid") + }) }) - t.Run("POST policies returns 404", func(t *testing.T) { - sut := createProvisioningSrvSut() + t.Run("are missing, PUT returns 404", func(t *testing.T) { + sut := createProvisioningSrvSut(t) rc := createTestRequestCtx() - rc.SignedInUser.OrgId = 2 + withURLParams(rc, namePathParam, "does not exist") + mti := definitions.MuteTimeInterval{} - response := sut.RouteGetPolicyTree(&rc) + response := sut.RoutePutMuteTiming(&rc, mti) require.Equal(t, 404, response.Status()) }) }) - t.Run("when an unspecified error occurrs", func(t *testing.T) { - t.Run("GET policies returns 500", func(t *testing.T) { - sut := createProvisioningSrvSut() - sut.policies = &fakeFailingNotificationPolicyService{} - rc := createTestRequestCtx() + t.Run("alert rules", func(t *testing.T) { + t.Run("are invalid", func(t *testing.T) { + t.Run("POST returns 400", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + rule := createInvalidAlertRule() - response := sut.RouteGetPolicyTree(&rc) + response := sut.RoutePostAlertRule(&rc, rule) + + require.Equal(t, 400, response.Status()) + require.NotEmpty(t, response.Body()) + require.Contains(t, string(response.Body()), "invalid alert rule") + }) + + t.Run("PUT returns 400", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule", 1)) + rule := createInvalidAlertRule() + + response := sut.RoutePutAlertRule(&rc, rule) - require.Equal(t, 500, response.Status()) - require.NotEmpty(t, response.Body()) - require.Contains(t, string(response.Body()), "something went wrong") + require.Equal(t, 400, response.Status()) + require.NotEmpty(t, response.Body()) + require.Contains(t, string(response.Body()), "invalid alert rule") + }) }) - t.Run("PUT policies returns 500", func(t *testing.T) { - sut := createProvisioningSrvSut() - sut.policies = &fakeFailingNotificationPolicyService{} + t.Run("are missing, PUT returns 404", func(t *testing.T) { + sut := createProvisioningSrvSut(t) rc := createTestRequestCtx() - tree := apimodels.Route{} + rule := createTestAlertRule("rule", 1) - response := sut.RoutePutPolicyTree(&rc, tree) + response := sut.RoutePutAlertRule(&rc, rule) - require.Equal(t, 500, response.Status()) - require.NotEmpty(t, response.Body()) - require.Contains(t, string(response.Body()), "something went wrong") + require.Equal(t, 404, response.Status()) }) }) } -func createProvisioningSrvSut() ProvisioningSrv { +func createProvisioningSrvSut(t *testing.T) ProvisioningSrv { + t.Helper() + secrets := secrets.NewFakeSecretsService() + log := log.NewNopLogger() + configs := &provisioning.MockAMConfigStore{} + configs.EXPECT(). + GetsConfig(models.AlertConfiguration{ + AlertmanagerConfiguration: testConfig, + }) + sqlStore := sqlstore.InitTestDB(t) + store := store.DBstore{ + SQLStore: sqlStore, + BaseInterval: time.Second * 10, + } + xact := &provisioning.NopTransactionManager{} + prov := &provisioning.MockProvisioningStore{} + prov.EXPECT().SaveSucceeds() + prov.EXPECT().GetReturns(models.ProvenanceNone) + return ProvisioningSrv{ - log: log.NewNopLogger(), - policies: newFakeNotificationPolicyService(), + log: log, + policies: newFakeNotificationPolicyService(), + contactPointService: provisioning.NewContactPointService(configs, secrets, prov, xact, log), + templates: provisioning.NewTemplateService(configs, prov, xact, log), + muteTimings: provisioning.NewMuteTimingService(configs, prov, xact, log), + alertRules: provisioning.NewAlertRuleService(store, prov, xact, 60, 10, log), } } -func createTestRequestCtx() models.ReqContext { - return models.ReqContext{ +func createTestRequestCtx() gfcore.ReqContext { + return gfcore.ReqContext{ Context: &web.Context{ Req: &http.Request{}, }, - SignedInUser: &models.SignedInUser{ + SignedInUser: &gfcore.SignedInUser{ OrgId: 1, }, } } +func withURLParams(rc gfcore.ReqContext, key, value string) { + params := web.Params(rc.Req) + params[key] = value + rc.Req = web.SetURLParams(rc.Req, params) +} + type fakeNotificationPolicyService struct { - tree apimodels.Route - prov domain.Provenance + tree definitions.Route + prov models.Provenance } func newFakeNotificationPolicyService() *fakeNotificationPolicyService { return &fakeNotificationPolicyService{ - tree: apimodels.Route{ + tree: definitions.Route{ Receiver: "some-receiver", }, - prov: domain.ProvenanceNone, + prov: models.ProvenanceNone, } } -func (f *fakeNotificationPolicyService) GetPolicyTree(ctx context.Context, orgID int64) (apimodels.Route, error) { +func (f *fakeNotificationPolicyService) GetPolicyTree(ctx context.Context, orgID int64) (definitions.Route, error) { if orgID != 1 { - return apimodels.Route{}, store.ErrNoAlertmanagerConfiguration + return definitions.Route{}, store.ErrNoAlertmanagerConfiguration } result := f.tree result.Provenance = f.prov return result, nil } -func (f *fakeNotificationPolicyService) UpdatePolicyTree(ctx context.Context, orgID int64, tree apimodels.Route, p domain.Provenance) error { +func (f *fakeNotificationPolicyService) UpdatePolicyTree(ctx context.Context, orgID int64, tree definitions.Route, p models.Provenance) error { if orgID != 1 { return store.ErrNoAlertmanagerConfiguration } @@ -153,20 +314,112 @@ func (f *fakeNotificationPolicyService) UpdatePolicyTree(ctx context.Context, or type fakeFailingNotificationPolicyService struct{} -func (f *fakeFailingNotificationPolicyService) GetPolicyTree(ctx context.Context, orgID int64) (apimodels.Route, error) { - return apimodels.Route{}, fmt.Errorf("something went wrong") +func (f *fakeFailingNotificationPolicyService) GetPolicyTree(ctx context.Context, orgID int64) (definitions.Route, error) { + return definitions.Route{}, fmt.Errorf("something went wrong") } -func (f *fakeFailingNotificationPolicyService) UpdatePolicyTree(ctx context.Context, orgID int64, tree apimodels.Route, p domain.Provenance) error { +func (f *fakeFailingNotificationPolicyService) UpdatePolicyTree(ctx context.Context, orgID int64, tree definitions.Route, p models.Provenance) error { return fmt.Errorf("something went wrong") } type fakeRejectingNotificationPolicyService struct{} -func (f *fakeRejectingNotificationPolicyService) GetPolicyTree(ctx context.Context, orgID int64) (apimodels.Route, error) { - return apimodels.Route{}, nil +func (f *fakeRejectingNotificationPolicyService) GetPolicyTree(ctx context.Context, orgID int64) (definitions.Route, error) { + return definitions.Route{}, nil } -func (f *fakeRejectingNotificationPolicyService) UpdatePolicyTree(ctx context.Context, orgID int64, tree apimodels.Route, p domain.Provenance) error { +func (f *fakeRejectingNotificationPolicyService) UpdatePolicyTree(ctx context.Context, orgID int64, tree definitions.Route, p models.Provenance) error { return fmt.Errorf("%w: invalid policy tree", provisioning.ErrValidation) } + +func createInvalidContactPoint() definitions.EmbeddedContactPoint { + settings, _ := simplejson.NewJson([]byte(`{}`)) + return definitions.EmbeddedContactPoint{ + Name: "test-contact-point", + Type: "slack", + Settings: settings, + } +} + +func createInvalidMuteTiming() definitions.MuteTimeInterval { + return definitions.MuteTimeInterval{ + MuteTimeInterval: prometheus.MuteTimeInterval{ + Name: "interval", + TimeIntervals: []timeinterval.TimeInterval{ + { + Weekdays: []timeinterval.WeekdayRange{ + { + InclusiveRange: timeinterval.InclusiveRange{ + Begin: -1, + End: 7, + }, + }, + }, + }, + }, + }, + } +} + +func createInvalidAlertRule() definitions.AlertRule { + return definitions.AlertRule{} +} + +func createTestAlertRule(title string, orgID int64) definitions.AlertRule { + return definitions.AlertRule{ + OrgID: orgID, + Title: title, + Condition: "A", + Data: []models.AlertQuery{ + { + RefID: "A", + Model: json.RawMessage("{}"), + RelativeTimeRange: models.RelativeTimeRange{ + From: models.Duration(60), + To: models.Duration(0), + }, + }, + }, + RuleGroup: "my-cool-group", + For: time.Second * 60, + NoDataState: models.OK, + ExecErrState: models.OkErrState, + } +} + +func insertRule(t *testing.T, srv ProvisioningSrv, rule definitions.AlertRule) { + t.Helper() + + rc := createTestRequestCtx() + resp := srv.RoutePostAlertRule(&rc, rule) + require.Equal(t, 201, resp.Status()) +} + +var testConfig = ` +{ + "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": "" + } + }] + }], + "mute_time_intervals": [{ + "name": "interval", + "time_intervals": [] + }] + } +} +` diff --git a/pkg/services/ngalert/provisioning/alert_rules.go b/pkg/services/ngalert/provisioning/alert_rules.go index b71bb17d25b..7de30f4824c 100644 --- a/pkg/services/ngalert/provisioning/alert_rules.go +++ b/pkg/services/ngalert/provisioning/alert_rules.go @@ -15,13 +15,13 @@ import ( type AlertRuleService struct { defaultIntervalSeconds int64 baseIntervalSeconds int64 - ruleStore store.RuleStore + ruleStore RuleStore provenanceStore ProvisioningStore xact TransactionManager log log.Logger } -func NewAlertRuleService(ruleStore store.RuleStore, +func NewAlertRuleService(ruleStore RuleStore, provenanceStore ProvisioningStore, xact TransactionManager, defaultIntervalSeconds int64, diff --git a/pkg/services/ngalert/provisioning/contactpoints.go b/pkg/services/ngalert/provisioning/contactpoints.go index 8c8142b8064..1023948171d 100644 --- a/pkg/services/ngalert/provisioning/contactpoints.go +++ b/pkg/services/ngalert/provisioning/contactpoints.go @@ -10,7 +10,6 @@ import ( "github.com/grafana/grafana/pkg/infra/log" apimodels "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/store" "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/util" "github.com/prometheus/alertmanager/config" @@ -24,7 +23,7 @@ type ContactPointService struct { log log.Logger } -func NewContactPointService(store store.AlertingStore, encryptionService secrets.Service, +func NewContactPointService(store AMConfigStore, encryptionService secrets.Service, provenanceStore ProvisioningStore, xact TransactionManager, log log.Logger) *ContactPointService { return &ContactPointService{ amStore: store, @@ -111,7 +110,7 @@ func (ecp *ContactPointService) getContactPointDecrypted(ctx context.Context, or func (ecp *ContactPointService) CreateContactPoint(ctx context.Context, orgID int64, contactPoint apimodels.EmbeddedContactPoint, provenance models.Provenance) (apimodels.EmbeddedContactPoint, error) { if err := contactPoint.Valid(ecp.encryptionService.GetDecryptedValue); err != nil { - return apimodels.EmbeddedContactPoint{}, fmt.Errorf("contact point is not valid: %w", err) + return apimodels.EmbeddedContactPoint{}, fmt.Errorf("%w: %s", ErrValidation, err.Error()) } revision, err := getLastConfiguration(ctx, orgID, ecp.amStore) @@ -197,13 +196,16 @@ func (ecp *ContactPointService) CreateContactPoint(ctx context.Context, orgID in func (ecp *ContactPointService) UpdateContactPoint(ctx context.Context, orgID int64, contactPoint apimodels.EmbeddedContactPoint, provenance models.Provenance) error { // set all redacted values with the latest known value from the store + if contactPoint.Settings == nil { + return fmt.Errorf("%w: %s", ErrValidation, "settings should not be empty") + } rawContactPoint, err := ecp.getContactPointDecrypted(ctx, orgID, contactPoint.UID) if err != nil { return err } secretKeys, err := contactPoint.SecretKeys() if err != nil { - return err + return fmt.Errorf("%w: %s", ErrValidation, err.Error()) } for _, secretKey := range secretKeys { secretValue := contactPoint.Settings.Get(secretKey).MustString() @@ -211,10 +213,12 @@ func (ecp *ContactPointService) UpdateContactPoint(ctx context.Context, orgID in contactPoint.Settings.Set(secretKey, rawContactPoint.Settings.Get(secretKey).MustString()) } } + // validate merged values if err := contactPoint.Valid(ecp.encryptionService.GetDecryptedValue); err != nil { - return err + return fmt.Errorf("%w: %s", ErrValidation, err.Error()) } + // check that provenance is not changed in a invalid way storedProvenance, err := ecp.provenanceStore.GetProvenance(ctx, &contactPoint, orgID) if err != nil { diff --git a/pkg/services/ngalert/provisioning/contactpoints_test.go b/pkg/services/ngalert/provisioning/contactpoints_test.go index 7377f1093b6..f3b2527e0c0 100644 --- a/pkg/services/ngalert/provisioning/contactpoints_test.go +++ b/pkg/services/ngalert/provisioning/contactpoints_test.go @@ -57,6 +57,52 @@ func TestContactPointService(t *testing.T) { require.Equal(t, customUID, cps[1].UID) }) + t.Run("create rejects contact points that fail validation", func(t *testing.T) { + sut := createContactPointServiceSut(secretsService) + newCp := createTestContactPoint() + newCp.Type = "" + + _, err := sut.CreateContactPoint(context.Background(), 1, newCp, models.ProvenanceAPI) + + require.ErrorIs(t, err, ErrValidation) + }) + + t.Run("update rejects contact points with no settings", func(t *testing.T) { + sut := createContactPointServiceSut(secretsService) + newCp := createTestContactPoint() + newCp, err := sut.CreateContactPoint(context.Background(), 1, newCp, models.ProvenanceAPI) + require.NoError(t, err) + newCp.Settings = nil + + err = sut.UpdateContactPoint(context.Background(), 1, newCp, models.ProvenanceAPI) + + require.ErrorIs(t, err, ErrValidation) + }) + + t.Run("update rejects contact points with no type", func(t *testing.T) { + sut := createContactPointServiceSut(secretsService) + newCp := createTestContactPoint() + newCp, err := sut.CreateContactPoint(context.Background(), 1, newCp, models.ProvenanceAPI) + require.NoError(t, err) + newCp.Type = "" + + err = sut.UpdateContactPoint(context.Background(), 1, newCp, models.ProvenanceAPI) + + require.ErrorIs(t, err, ErrValidation) + }) + + t.Run("update rejects contact points which fail validation after merging", func(t *testing.T) { + sut := createContactPointServiceSut(secretsService) + newCp := createTestContactPoint() + newCp, err := sut.CreateContactPoint(context.Background(), 1, newCp, models.ProvenanceAPI) + require.NoError(t, err) + newCp.Settings, _ = simplejson.NewJson([]byte(`{}`)) + + err = sut.UpdateContactPoint(context.Background(), 1, newCp, models.ProvenanceAPI) + + require.ErrorIs(t, err, ErrValidation) + }) + t.Run("default provenance of contact points is none", func(t *testing.T) { sut := createContactPointServiceSut(secretsService) diff --git a/pkg/services/ngalert/provisioning/mute_timings_test.go b/pkg/services/ngalert/provisioning/mute_timings_test.go index 4085f713d7a..84845a9ba53 100644 --- a/pkg/services/ngalert/provisioning/mute_timings_test.go +++ b/pkg/services/ngalert/provisioning/mute_timings_test.go @@ -17,7 +17,7 @@ func TestMuteTimingService(t *testing.T) { t.Run("service returns timings from config file", func(t *testing.T) { sut := createMuteTimingSvcSut() sut.config.(*MockAMConfigStore).EXPECT(). - getsConfig(models.AlertConfiguration{ + GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: configWithMuteTimings, }) @@ -31,7 +31,7 @@ func TestMuteTimingService(t *testing.T) { t.Run("service returns empty list when config file contains no mute timings", func(t *testing.T) { sut := createMuteTimingSvcSut() sut.config.(*MockAMConfigStore).EXPECT(). - getsConfig(models.AlertConfiguration{ + GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: defaultConfig, }) @@ -56,7 +56,7 @@ func TestMuteTimingService(t *testing.T) { t.Run("when config is invalid", func(t *testing.T) { sut := createMuteTimingSvcSut() sut.config.(*MockAMConfigStore).EXPECT(). - getsConfig(models.AlertConfiguration{ + GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: brokenConfig, }) @@ -108,7 +108,7 @@ func TestMuteTimingService(t *testing.T) { sut := createMuteTimingSvcSut() timing := createMuteTiming() sut.config.(*MockAMConfigStore).EXPECT(). - getsConfig(models.AlertConfiguration{ + GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: brokenConfig, }) @@ -133,10 +133,10 @@ func TestMuteTimingService(t *testing.T) { sut := createMuteTimingSvcSut() timing := createMuteTiming() sut.config.(*MockAMConfigStore).EXPECT(). - getsConfig(models.AlertConfiguration{ + GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: configWithMuteTimings, }) - sut.config.(*MockAMConfigStore).EXPECT().saveSucceeds() + 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")) @@ -150,13 +150,13 @@ func TestMuteTimingService(t *testing.T) { sut := createMuteTimingSvcSut() timing := createMuteTiming() sut.config.(*MockAMConfigStore).EXPECT(). - getsConfig(models.AlertConfiguration{ + GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: configWithMuteTimings, }) sut.config.(*MockAMConfigStore).EXPECT(). UpdateAlertmanagerConfiguration(mock.Anything, mock.Anything). Return(fmt.Errorf("failed to save config")) - sut.prov.(*MockProvisioningStore).EXPECT().saveSucceeds() + sut.prov.(*MockProvisioningStore).EXPECT().SaveSucceeds() _, err := sut.CreateMuteTiming(context.Background(), timing, 1) @@ -184,11 +184,11 @@ func TestMuteTimingService(t *testing.T) { timing := createMuteTiming() timing.Name = "does not exist" sut.config.(*MockAMConfigStore).EXPECT(). - getsConfig(models.AlertConfiguration{ + GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: configWithMuteTimings, }) - sut.config.(*MockAMConfigStore).EXPECT().saveSucceeds() - sut.prov.(*MockProvisioningStore).EXPECT().saveSucceeds() + sut.config.(*MockAMConfigStore).EXPECT().SaveSucceeds() + sut.prov.(*MockProvisioningStore).EXPECT().SaveSucceeds() updated, err := sut.UpdateMuteTiming(context.Background(), timing, 1) @@ -215,7 +215,7 @@ func TestMuteTimingService(t *testing.T) { timing := createMuteTiming() timing.Name = "asdf" sut.config.(*MockAMConfigStore).EXPECT(). - getsConfig(models.AlertConfiguration{ + GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: brokenConfig, }) @@ -242,10 +242,10 @@ func TestMuteTimingService(t *testing.T) { timing := createMuteTiming() timing.Name = "asdf" sut.config.(*MockAMConfigStore).EXPECT(). - getsConfig(models.AlertConfiguration{ + GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: configWithMuteTimings, }) - sut.config.(*MockAMConfigStore).EXPECT().saveSucceeds() + 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")) @@ -260,13 +260,13 @@ func TestMuteTimingService(t *testing.T) { timing := createMuteTiming() timing.Name = "asdf" sut.config.(*MockAMConfigStore).EXPECT(). - getsConfig(models.AlertConfiguration{ + GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: configWithMuteTimings, }) sut.config.(*MockAMConfigStore).EXPECT(). UpdateAlertmanagerConfiguration(mock.Anything, mock.Anything). Return(fmt.Errorf("failed to save config")) - sut.prov.(*MockProvisioningStore).EXPECT().saveSucceeds() + sut.prov.(*MockProvisioningStore).EXPECT().SaveSucceeds() _, err := sut.UpdateMuteTiming(context.Background(), timing, 1) @@ -279,11 +279,11 @@ func TestMuteTimingService(t *testing.T) { t.Run("returns nil if timing does not exist", func(t *testing.T) { sut := createMuteTimingSvcSut() sut.config.(*MockAMConfigStore).EXPECT(). - getsConfig(models.AlertConfiguration{ + GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: configWithMuteTimings, }) - sut.config.(*MockAMConfigStore).EXPECT().saveSucceeds() - sut.prov.(*MockProvisioningStore).EXPECT().saveSucceeds() + sut.config.(*MockAMConfigStore).EXPECT().SaveSucceeds() + sut.prov.(*MockProvisioningStore).EXPECT().SaveSucceeds() err := sut.DeleteMuteTiming(context.Background(), "does not exist", 1) @@ -305,7 +305,7 @@ func TestMuteTimingService(t *testing.T) { t.Run("when config is invalid", func(t *testing.T) { sut := createMuteTimingSvcSut() sut.config.(*MockAMConfigStore).EXPECT(). - getsConfig(models.AlertConfiguration{ + GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: brokenConfig, }) @@ -328,10 +328,10 @@ func TestMuteTimingService(t *testing.T) { t.Run("when provenance fails to save", func(t *testing.T) { sut := createMuteTimingSvcSut() sut.config.(*MockAMConfigStore).EXPECT(). - getsConfig(models.AlertConfiguration{ + GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: configWithMuteTimings, }) - sut.config.(*MockAMConfigStore).EXPECT().saveSucceeds() + sut.config.(*MockAMConfigStore).EXPECT().SaveSucceeds() sut.prov.(*MockProvisioningStore).EXPECT(). DeleteProvenance(mock.Anything, mock.Anything, mock.Anything). Return(fmt.Errorf("failed to save provenance")) @@ -344,13 +344,13 @@ func TestMuteTimingService(t *testing.T) { t.Run("when AM config fails to save", func(t *testing.T) { sut := createMuteTimingSvcSut() sut.config.(*MockAMConfigStore).EXPECT(). - getsConfig(models.AlertConfiguration{ + GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: configWithMuteTimings, }) sut.config.(*MockAMConfigStore).EXPECT(). UpdateAlertmanagerConfiguration(mock.Anything, mock.Anything). Return(fmt.Errorf("failed to save config")) - sut.prov.(*MockProvisioningStore).EXPECT().saveSucceeds() + sut.prov.(*MockProvisioningStore).EXPECT().SaveSucceeds() err := sut.DeleteMuteTiming(context.Background(), "asdf", 1) @@ -360,7 +360,7 @@ func TestMuteTimingService(t *testing.T) { t.Run("when mute timing is used in route", func(t *testing.T) { sut := createMuteTimingSvcSut() sut.config.(*MockAMConfigStore).EXPECT(). - getsConfig(models.AlertConfiguration{ + GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: configWithMuteTimingsInRoute, }) diff --git a/pkg/services/ngalert/provisioning/persist.go b/pkg/services/ngalert/provisioning/persist.go index d2cce408883..c50204fa504 100644 --- a/pkg/services/ngalert/provisioning/persist.go +++ b/pkg/services/ngalert/provisioning/persist.go @@ -4,6 +4,7 @@ import ( "context" "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/ngalert/store" ) // AMStore is a store of Alertmanager configurations. @@ -26,3 +27,14 @@ type ProvisioningStore interface { type TransactionManager interface { InTransaction(ctx context.Context, work func(ctx context.Context) error) error } + +// RuleStore represents the ability to persist and query alert rules. +type RuleStore interface { + GetAlertRuleByUID(ctx context.Context, query *models.GetAlertRuleByUIDQuery) error + ListAlertRules(ctx context.Context, query *models.ListAlertRulesQuery) error + GetRuleGroupInterval(ctx context.Context, orgID int64, namespaceUID string, ruleGroup string) (int64, error) + InsertAlertRules(ctx context.Context, rule []models.AlertRule) (map[string]int64, error) + UpdateRuleGroup(ctx context.Context, orgID int64, namespaceUID string, ruleGroup string, interval int64) error + UpdateAlertRules(ctx context.Context, rule []store.UpdateRule) error + DeleteAlertRulesByUID(ctx context.Context, orgID int64, ruleUID ...string) error +} diff --git a/pkg/services/ngalert/provisioning/templates_test.go b/pkg/services/ngalert/provisioning/templates_test.go index 2155b83d390..4b9f8cde37a 100644 --- a/pkg/services/ngalert/provisioning/templates_test.go +++ b/pkg/services/ngalert/provisioning/templates_test.go @@ -17,7 +17,7 @@ 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{ + GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: configWithTemplates, }) @@ -30,7 +30,7 @@ func TestTemplateService(t *testing.T) { 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{ + GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: defaultConfig, }) @@ -55,7 +55,7 @@ func TestTemplateService(t *testing.T) { t.Run("when config is invalid", func(t *testing.T) { sut := createTemplateServiceSut() sut.config.(*MockAMConfigStore).EXPECT(). - getsConfig(models.AlertConfiguration{ + GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: brokenConfig, }) @@ -106,7 +106,7 @@ func TestTemplateService(t *testing.T) { sut := createTemplateServiceSut() tmpl := createMessageTemplate() sut.config.(*MockAMConfigStore).EXPECT(). - getsConfig(models.AlertConfiguration{ + GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: brokenConfig, }) @@ -131,10 +131,10 @@ func TestTemplateService(t *testing.T) { sut := createTemplateServiceSut() tmpl := createMessageTemplate() sut.config.(*MockAMConfigStore).EXPECT(). - getsConfig(models.AlertConfiguration{ + GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: configWithTemplates, }) - sut.config.(*MockAMConfigStore).EXPECT().saveSucceeds() + 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")) @@ -148,13 +148,13 @@ func TestTemplateService(t *testing.T) { sut := createTemplateServiceSut() tmpl := createMessageTemplate() sut.config.(*MockAMConfigStore).EXPECT(). - getsConfig(models.AlertConfiguration{ + 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() + sut.prov.(*MockProvisioningStore).EXPECT().SaveSucceeds() _, err := sut.SetTemplate(context.Background(), 1, tmpl) @@ -166,11 +166,11 @@ func TestTemplateService(t *testing.T) { sut := createTemplateServiceSut() tmpl := createMessageTemplate() sut.config.(*MockAMConfigStore).EXPECT(). - getsConfig(models.AlertConfiguration{ + GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: configWithTemplates, }) - sut.config.(*MockAMConfigStore).EXPECT().saveSucceeds() - sut.prov.(*MockProvisioningStore).EXPECT().saveSucceeds() + sut.config.(*MockAMConfigStore).EXPECT().SaveSucceeds() + sut.prov.(*MockProvisioningStore).EXPECT().SaveSucceeds() _, err := sut.SetTemplate(context.Background(), 1, tmpl) @@ -181,11 +181,11 @@ func TestTemplateService(t *testing.T) { sut := createTemplateServiceSut() tmpl := createMessageTemplate() sut.config.(*MockAMConfigStore).EXPECT(). - getsConfig(models.AlertConfiguration{ + GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: defaultConfig, }) - sut.config.(*MockAMConfigStore).EXPECT().saveSucceeds() - sut.prov.(*MockProvisioningStore).EXPECT().saveSucceeds() + sut.config.(*MockAMConfigStore).EXPECT().SaveSucceeds() + sut.prov.(*MockProvisioningStore).EXPECT().SaveSucceeds() _, err := sut.SetTemplate(context.Background(), 1, tmpl) @@ -199,11 +199,11 @@ func TestTemplateService(t *testing.T) { Template: "content", } sut.config.(*MockAMConfigStore).EXPECT(). - getsConfig(models.AlertConfiguration{ + GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: defaultConfig, }) - sut.config.(*MockAMConfigStore).EXPECT().saveSucceeds() - sut.prov.(*MockProvisioningStore).EXPECT().saveSucceeds() + sut.config.(*MockAMConfigStore).EXPECT().SaveSucceeds() + sut.prov.(*MockProvisioningStore).EXPECT().SaveSucceeds() result, _ := sut.SetTemplate(context.Background(), 1, tmpl) @@ -218,11 +218,11 @@ func TestTemplateService(t *testing.T) { Template: "{{define \"name\"}}content{{end}}", } sut.config.(*MockAMConfigStore).EXPECT(). - getsConfig(models.AlertConfiguration{ + GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: defaultConfig, }) - sut.config.(*MockAMConfigStore).EXPECT().saveSucceeds() - sut.prov.(*MockProvisioningStore).EXPECT().saveSucceeds() + sut.config.(*MockAMConfigStore).EXPECT().SaveSucceeds() + sut.prov.(*MockProvisioningStore).EXPECT().SaveSucceeds() result, _ := sut.SetTemplate(context.Background(), 1, tmpl) @@ -236,11 +236,11 @@ func TestTemplateService(t *testing.T) { Template: "{{ .MyField }", } sut.config.(*MockAMConfigStore).EXPECT(). - getsConfig(models.AlertConfiguration{ + GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: defaultConfig, }) - sut.config.(*MockAMConfigStore).EXPECT().saveSucceeds() - sut.prov.(*MockProvisioningStore).EXPECT().saveSucceeds() + sut.config.(*MockAMConfigStore).EXPECT().SaveSucceeds() + sut.prov.(*MockProvisioningStore).EXPECT().SaveSucceeds() _, err := sut.SetTemplate(context.Background(), 1, tmpl) @@ -254,11 +254,11 @@ func TestTemplateService(t *testing.T) { Template: "{{ .NotAField }}", } sut.config.(*MockAMConfigStore).EXPECT(). - getsConfig(models.AlertConfiguration{ + GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: defaultConfig, }) - sut.config.(*MockAMConfigStore).EXPECT().saveSucceeds() - sut.prov.(*MockProvisioningStore).EXPECT().saveSucceeds() + sut.config.(*MockAMConfigStore).EXPECT().SaveSucceeds() + sut.prov.(*MockProvisioningStore).EXPECT().SaveSucceeds() _, err := sut.SetTemplate(context.Background(), 1, tmpl) @@ -282,7 +282,7 @@ func TestTemplateService(t *testing.T) { t.Run("when config is invalid", func(t *testing.T) { sut := createTemplateServiceSut() sut.config.(*MockAMConfigStore).EXPECT(). - getsConfig(models.AlertConfiguration{ + GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: brokenConfig, }) @@ -305,10 +305,10 @@ func TestTemplateService(t *testing.T) { t.Run("when provenance fails to save", func(t *testing.T) { sut := createTemplateServiceSut() sut.config.(*MockAMConfigStore).EXPECT(). - getsConfig(models.AlertConfiguration{ + GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: configWithTemplates, }) - sut.config.(*MockAMConfigStore).EXPECT().saveSucceeds() + sut.config.(*MockAMConfigStore).EXPECT().SaveSucceeds() sut.prov.(*MockProvisioningStore).EXPECT(). DeleteProvenance(mock.Anything, mock.Anything, mock.Anything). Return(fmt.Errorf("failed to save provenance")) @@ -321,13 +321,13 @@ func TestTemplateService(t *testing.T) { t.Run("when AM config fails to save", func(t *testing.T) { sut := createTemplateServiceSut() sut.config.(*MockAMConfigStore).EXPECT(). - getsConfig(models.AlertConfiguration{ + 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() + sut.prov.(*MockProvisioningStore).EXPECT().SaveSucceeds() err := sut.DeleteTemplate(context.Background(), 1, "template") @@ -338,11 +338,11 @@ func TestTemplateService(t *testing.T) { t.Run("deletes template from config file on success", func(t *testing.T) { sut := createTemplateServiceSut() sut.config.(*MockAMConfigStore).EXPECT(). - getsConfig(models.AlertConfiguration{ + GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: configWithTemplates, }) - sut.config.(*MockAMConfigStore).EXPECT().saveSucceeds() - sut.prov.(*MockProvisioningStore).EXPECT().saveSucceeds() + sut.config.(*MockAMConfigStore).EXPECT().SaveSucceeds() + sut.prov.(*MockProvisioningStore).EXPECT().SaveSucceeds() err := sut.DeleteTemplate(context.Background(), 1, "a") @@ -352,11 +352,11 @@ func TestTemplateService(t *testing.T) { 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{ + GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: configWithTemplates, }) - sut.config.(*MockAMConfigStore).EXPECT().saveSucceeds() - sut.prov.(*MockProvisioningStore).EXPECT().saveSucceeds() + sut.config.(*MockAMConfigStore).EXPECT().SaveSucceeds() + sut.prov.(*MockProvisioningStore).EXPECT().SaveSucceeds() err := sut.DeleteTemplate(context.Background(), 1, "does not exist") @@ -366,11 +366,11 @@ func TestTemplateService(t *testing.T) { 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{ + GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: defaultConfig, }) - sut.config.(*MockAMConfigStore).EXPECT().saveSucceeds() - sut.prov.(*MockProvisioningStore).EXPECT().saveSucceeds() + sut.config.(*MockAMConfigStore).EXPECT().SaveSucceeds() + sut.prov.(*MockProvisioningStore).EXPECT().SaveSucceeds() err := sut.DeleteTemplate(context.Background(), 1, "a") @@ -395,26 +395,6 @@ func createMessageTemplate() definitions.MessageTemplate { } } -func (m *MockAMConfigStore_Expecter) getsConfig(ac models.AlertConfiguration) *MockAMConfigStore_Expecter { - m.GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything). - Run(func(ctx context.Context, q *models.GetLatestAlertmanagerConfigurationQuery) { - q.Result = &ac - }). - Return(nil) - return m -} - -func (m *MockAMConfigStore_Expecter) saveSucceeds() *MockAMConfigStore_Expecter { - m.UpdateAlertmanagerConfiguration(mock.Anything, mock.Anything).Return(nil) - return m -} - -func (m *MockProvisioningStore_Expecter) saveSucceeds() *MockProvisioningStore_Expecter { - m.SetProvenance(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) - m.DeleteProvenance(mock.Anything, mock.Anything, mock.Anything).Return(nil) - return m -} - var defaultConfig = setting.GetAlertmanagerDefaultConfiguration() var configWithTemplates = ` diff --git a/pkg/services/ngalert/provisioning/testing.go b/pkg/services/ngalert/provisioning/testing.go index ea8df1c666b..d303cfb1f25 100644 --- a/pkg/services/ngalert/provisioning/testing.go +++ b/pkg/services/ngalert/provisioning/testing.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/grafana/grafana/pkg/services/ngalert/models" + mock "github.com/stretchr/testify/mock" ) const defaultAlertmanagerConfigJSON = ` @@ -135,12 +136,37 @@ func (f *fakeProvisioningStore) DeleteProvenance(ctx context.Context, o models.P return nil } -type nopTransactionManager struct{} +type NopTransactionManager struct{} -func newNopTransactionManager() *nopTransactionManager { - return &nopTransactionManager{} +func newNopTransactionManager() *NopTransactionManager { + return &NopTransactionManager{} } -func (n *nopTransactionManager) InTransaction(ctx context.Context, work func(ctx context.Context) error) error { +func (n *NopTransactionManager) InTransaction(ctx context.Context, work func(ctx context.Context) error) error { return work(ctx) } + +func (m *MockAMConfigStore_Expecter) GetsConfig(ac models.AlertConfiguration) *MockAMConfigStore_Expecter { + m.GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything). + Run(func(ctx context.Context, q *models.GetLatestAlertmanagerConfigurationQuery) { + q.Result = &ac + }). + Return(nil) + return m +} + +func (m *MockAMConfigStore_Expecter) SaveSucceeds() *MockAMConfigStore_Expecter { + m.UpdateAlertmanagerConfiguration(mock.Anything, mock.Anything).Return(nil) + return m +} + +func (m *MockProvisioningStore_Expecter) GetReturns(p models.Provenance) *MockProvisioningStore_Expecter { + m.GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(p, nil) + return m +} + +func (m *MockProvisioningStore_Expecter) SaveSucceeds() *MockProvisioningStore_Expecter { + m.SetProvenance(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) + m.DeleteProvenance(mock.Anything, mock.Anything, mock.Anything).Return(nil) + return m +}