mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Endpoints for provisioning mute timings (#49635)
* Add validator for mute timing and make it provisionable * Add tests to ensure prometheus validators are running and errors are propagated * Internal API for manipulating mute timings * Define and generate API layer * Wire up generated code * Implement API handlers * Tests for golang layer * Fix reference bug * Fix linter and auth tests * Resolve semantic errors and regenerate * Remove pointless comment * Extract out provisioning path param keys, simplify * Expected number of paths
This commit is contained in:
parent
097583e952
commit
909ebcf979
@ -16,6 +16,9 @@ import (
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
const namePathParam = ":name"
|
||||
const idPathParam = ":ID"
|
||||
|
||||
type ProvisioningSrv struct {
|
||||
log log.Logger
|
||||
policies NotificationPolicyService
|
||||
@ -43,7 +46,10 @@ type NotificationPolicyService interface {
|
||||
}
|
||||
|
||||
type MuteTimingService interface {
|
||||
GetMuteTimings(ctx context.Context, orgID int64) ([]apimodels.MuteTiming, error)
|
||||
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)
|
||||
DeleteMuteTiming(ctx context.Context, name string, orgID int64) error
|
||||
}
|
||||
|
||||
func (srv *ProvisioningSrv) RouteGetPolicyTree(c *models.ReqContext) response.Response {
|
||||
@ -91,7 +97,7 @@ func (srv *ProvisioningSrv) RoutePostContactPoint(c *models.ReqContext, cp apimo
|
||||
}
|
||||
|
||||
func (srv *ProvisioningSrv) RoutePutContactPoint(c *models.ReqContext, cp apimodels.EmbeddedContactPoint) response.Response {
|
||||
id := web.Params(c.Req)[":ID"]
|
||||
id := pathParam(c, idPathParam)
|
||||
cp.UID = id
|
||||
err := srv.contactPointService.UpdateContactPoint(c.Req.Context(), c.OrgId, cp, alerting_models.ProvenanceAPI)
|
||||
if err != nil {
|
||||
@ -101,7 +107,7 @@ func (srv *ProvisioningSrv) RoutePutContactPoint(c *models.ReqContext, cp apimod
|
||||
}
|
||||
|
||||
func (srv *ProvisioningSrv) RouteDeleteContactPoint(c *models.ReqContext) response.Response {
|
||||
cpID := web.Params(c.Req)[":ID"]
|
||||
cpID := pathParam(c, idPathParam)
|
||||
err := srv.contactPointService.DeleteContactPoint(c.Req.Context(), c.OrgId, cpID)
|
||||
if err != nil {
|
||||
return ErrResp(http.StatusInternalServerError, err, "")
|
||||
@ -122,19 +128,19 @@ func (srv *ProvisioningSrv) RouteGetTemplates(c *models.ReqContext) response.Res
|
||||
}
|
||||
|
||||
func (srv *ProvisioningSrv) RouteGetTemplate(c *models.ReqContext) response.Response {
|
||||
id := web.Params(c.Req)[":name"]
|
||||
name := pathParam(c, namePathParam)
|
||||
templates, err := srv.templates.GetTemplates(c.Req.Context(), c.OrgId)
|
||||
if err != nil {
|
||||
return ErrResp(http.StatusInternalServerError, err, "")
|
||||
}
|
||||
if tmpl, ok := templates[id]; ok {
|
||||
return response.JSON(http.StatusOK, apimodels.MessageTemplate{Name: id, Template: tmpl})
|
||||
if tmpl, ok := templates[name]; ok {
|
||||
return response.JSON(http.StatusOK, apimodels.MessageTemplate{Name: name, Template: tmpl})
|
||||
}
|
||||
return response.Empty(http.StatusNotFound)
|
||||
}
|
||||
|
||||
func (srv *ProvisioningSrv) RoutePutTemplate(c *models.ReqContext, body apimodels.MessageTemplateContent) response.Response {
|
||||
name := web.Params(c.Req)[":name"]
|
||||
name := pathParam(c, namePathParam)
|
||||
tmpl := apimodels.MessageTemplate{
|
||||
Name: name,
|
||||
Template: body.Template,
|
||||
@ -151,7 +157,7 @@ func (srv *ProvisioningSrv) RoutePutTemplate(c *models.ReqContext, body apimodel
|
||||
}
|
||||
|
||||
func (srv *ProvisioningSrv) RouteDeleteTemplate(c *models.ReqContext) response.Response {
|
||||
name := web.Params(c.Req)[":name"]
|
||||
name := pathParam(c, namePathParam)
|
||||
err := srv.templates.DeleteTemplate(c.Req.Context(), c.OrgId, name)
|
||||
if err != nil {
|
||||
return ErrResp(http.StatusInternalServerError, err, "")
|
||||
@ -160,7 +166,7 @@ func (srv *ProvisioningSrv) RouteDeleteTemplate(c *models.ReqContext) response.R
|
||||
}
|
||||
|
||||
func (srv *ProvisioningSrv) RouteGetMuteTiming(c *models.ReqContext) response.Response {
|
||||
name := web.Params(c.Req)[":name"]
|
||||
name := pathParam(c, namePathParam)
|
||||
timings, err := srv.muteTimings.GetMuteTimings(c.Req.Context(), c.OrgId)
|
||||
if err != nil {
|
||||
return ErrResp(http.StatusInternalServerError, err, "")
|
||||
@ -180,3 +186,43 @@ 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 {
|
||||
created, err := srv.muteTimings.CreateMuteTiming(c.Req.Context(), mt, c.OrgId)
|
||||
if err != nil {
|
||||
if errors.Is(err, provisioning.ErrValidation) {
|
||||
return ErrResp(http.StatusBadRequest, err, "")
|
||||
}
|
||||
return ErrResp(http.StatusInternalServerError, err, "")
|
||||
}
|
||||
return response.JSON(http.StatusCreated, created)
|
||||
}
|
||||
|
||||
func (srv *ProvisioningSrv) RoutePutMuteTiming(c *models.ReqContext, mt apimodels.MuteTimeInterval) response.Response {
|
||||
name := pathParam(c, namePathParam)
|
||||
mt.Name = name
|
||||
updated, err := srv.muteTimings.UpdateMuteTiming(c.Req.Context(), mt, c.OrgId)
|
||||
if err != nil {
|
||||
if errors.Is(err, provisioning.ErrValidation) {
|
||||
return ErrResp(http.StatusBadRequest, err, "")
|
||||
}
|
||||
return ErrResp(http.StatusInternalServerError, err, "")
|
||||
}
|
||||
if updated == nil {
|
||||
return response.Empty(http.StatusNotFound)
|
||||
}
|
||||
return response.JSON(http.StatusAccepted, updated)
|
||||
}
|
||||
|
||||
func (srv *ProvisioningSrv) RouteDeleteMuteTiming(c *models.ReqContext) response.Response {
|
||||
name := pathParam(c, namePathParam)
|
||||
err := srv.muteTimings.DeleteMuteTiming(c.Req.Context(), name, c.OrgId)
|
||||
if err != nil {
|
||||
return ErrResp(http.StatusInternalServerError, err, "")
|
||||
}
|
||||
return response.JSON(http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
func pathParam(c *models.ReqContext, param string) string {
|
||||
return web.Params(c.Req)[param]
|
||||
}
|
||||
|
@ -192,7 +192,10 @@ func (api *API) authorize(method, path string) web.Handler {
|
||||
http.MethodPut + "/api/provisioning/contact-points/{ID}",
|
||||
http.MethodDelete + "/api/provisioning/contact-points/{ID}",
|
||||
http.MethodPut + "/api/provisioning/templates/{name}",
|
||||
http.MethodDelete + "/api/provisioning/templates/{name}":
|
||||
http.MethodDelete + "/api/provisioning/templates/{name}",
|
||||
http.MethodPost + "/api/provisioning/mute-timings",
|
||||
http.MethodPut + "/api/provisioning/mute-timings/{name}",
|
||||
http.MethodDelete + "/api/provisioning/mute-timings/{name}":
|
||||
return middleware.ReqEditorRole
|
||||
}
|
||||
|
||||
|
@ -66,3 +66,15 @@ func (f *ForkedProvisioningApi) forkRouteGetMuteTiming(ctx *models.ReqContext) r
|
||||
func (f *ForkedProvisioningApi) forkRouteGetMuteTimings(ctx *models.ReqContext) response.Response {
|
||||
return f.svc.RouteGetMuteTimings(ctx)
|
||||
}
|
||||
|
||||
func (f *ForkedProvisioningApi) forkRoutePostMuteTiming(ctx *models.ReqContext, mt apimodels.MuteTimeInterval) response.Response {
|
||||
return f.svc.RoutePostMuteTiming(ctx, mt)
|
||||
}
|
||||
|
||||
func (f *ForkedProvisioningApi) forkRoutePutMuteTiming(ctx *models.ReqContext, mt apimodels.MuteTimeInterval) response.Response {
|
||||
return f.svc.RoutePutMuteTiming(ctx, mt)
|
||||
}
|
||||
|
||||
func (f *ForkedProvisioningApi) forkRouteDeleteMuteTiming(ctx *models.ReqContext) response.Response {
|
||||
return f.svc.RouteDeleteMuteTiming(ctx)
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import (
|
||||
|
||||
type ProvisioningApiForkingService interface {
|
||||
RouteDeleteContactpoints(*models.ReqContext) response.Response
|
||||
RouteDeleteMuteTiming(*models.ReqContext) response.Response
|
||||
RouteDeleteTemplate(*models.ReqContext) response.Response
|
||||
RouteGetContactpoints(*models.ReqContext) response.Response
|
||||
RouteGetMuteTiming(*models.ReqContext) response.Response
|
||||
@ -29,7 +30,9 @@ type ProvisioningApiForkingService interface {
|
||||
RouteGetTemplate(*models.ReqContext) response.Response
|
||||
RouteGetTemplates(*models.ReqContext) response.Response
|
||||
RoutePostContactpoints(*models.ReqContext) response.Response
|
||||
RoutePostMuteTiming(*models.ReqContext) response.Response
|
||||
RoutePutContactpoint(*models.ReqContext) response.Response
|
||||
RoutePutMuteTiming(*models.ReqContext) response.Response
|
||||
RoutePutPolicyTree(*models.ReqContext) response.Response
|
||||
RoutePutTemplate(*models.ReqContext) response.Response
|
||||
}
|
||||
@ -38,6 +41,10 @@ func (f *ForkedProvisioningApi) RouteDeleteContactpoints(ctx *models.ReqContext)
|
||||
return f.forkRouteDeleteContactpoints(ctx)
|
||||
}
|
||||
|
||||
func (f *ForkedProvisioningApi) RouteDeleteMuteTiming(ctx *models.ReqContext) response.Response {
|
||||
return f.forkRouteDeleteMuteTiming(ctx)
|
||||
}
|
||||
|
||||
func (f *ForkedProvisioningApi) RouteDeleteTemplate(ctx *models.ReqContext) response.Response {
|
||||
return f.forkRouteDeleteTemplate(ctx)
|
||||
}
|
||||
@ -74,6 +81,14 @@ func (f *ForkedProvisioningApi) RoutePostContactpoints(ctx *models.ReqContext) r
|
||||
return f.forkRoutePostContactpoints(ctx, conf)
|
||||
}
|
||||
|
||||
func (f *ForkedProvisioningApi) RoutePostMuteTiming(ctx *models.ReqContext) response.Response {
|
||||
conf := apimodels.MuteTimeInterval{}
|
||||
if err := web.Bind(ctx.Req, &conf); err != nil {
|
||||
return response.Error(http.StatusBadRequest, "bad request data", err)
|
||||
}
|
||||
return f.forkRoutePostMuteTiming(ctx, conf)
|
||||
}
|
||||
|
||||
func (f *ForkedProvisioningApi) RoutePutContactpoint(ctx *models.ReqContext) response.Response {
|
||||
conf := apimodels.EmbeddedContactPoint{}
|
||||
if err := web.Bind(ctx.Req, &conf); err != nil {
|
||||
@ -82,6 +97,14 @@ func (f *ForkedProvisioningApi) RoutePutContactpoint(ctx *models.ReqContext) res
|
||||
return f.forkRoutePutContactpoint(ctx, conf)
|
||||
}
|
||||
|
||||
func (f *ForkedProvisioningApi) RoutePutMuteTiming(ctx *models.ReqContext) response.Response {
|
||||
conf := apimodels.MuteTimeInterval{}
|
||||
if err := web.Bind(ctx.Req, &conf); err != nil {
|
||||
return response.Error(http.StatusBadRequest, "bad request data", err)
|
||||
}
|
||||
return f.forkRoutePutMuteTiming(ctx, conf)
|
||||
}
|
||||
|
||||
func (f *ForkedProvisioningApi) RoutePutPolicyTree(ctx *models.ReqContext) response.Response {
|
||||
conf := apimodels.Route{}
|
||||
if err := web.Bind(ctx.Req, &conf); err != nil {
|
||||
@ -110,6 +133,16 @@ func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApiForkingServi
|
||||
m,
|
||||
),
|
||||
)
|
||||
group.Delete(
|
||||
toMacaronPath("/api/provisioning/mute-timings/{name}"),
|
||||
api.authorize(http.MethodDelete, "/api/provisioning/mute-timings/{name}"),
|
||||
metrics.Instrument(
|
||||
http.MethodDelete,
|
||||
"/api/provisioning/mute-timings/{name}",
|
||||
srv.RouteDeleteMuteTiming,
|
||||
m,
|
||||
),
|
||||
)
|
||||
group.Delete(
|
||||
toMacaronPath("/api/provisioning/templates/{name}"),
|
||||
api.authorize(http.MethodDelete, "/api/provisioning/templates/{name}"),
|
||||
@ -190,6 +223,16 @@ func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApiForkingServi
|
||||
m,
|
||||
),
|
||||
)
|
||||
group.Post(
|
||||
toMacaronPath("/api/provisioning/mute-timings"),
|
||||
api.authorize(http.MethodPost, "/api/provisioning/mute-timings"),
|
||||
metrics.Instrument(
|
||||
http.MethodPost,
|
||||
"/api/provisioning/mute-timings",
|
||||
srv.RoutePostMuteTiming,
|
||||
m,
|
||||
),
|
||||
)
|
||||
group.Put(
|
||||
toMacaronPath("/api/provisioning/contact-points/{ID}"),
|
||||
api.authorize(http.MethodPut, "/api/provisioning/contact-points/{ID}"),
|
||||
@ -200,6 +243,16 @@ func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApiForkingServi
|
||||
m,
|
||||
),
|
||||
)
|
||||
group.Put(
|
||||
toMacaronPath("/api/provisioning/mute-timings/{name}"),
|
||||
api.authorize(http.MethodPut, "/api/provisioning/mute-timings/{name}"),
|
||||
metrics.Instrument(
|
||||
http.MethodPut,
|
||||
"/api/provisioning/mute-timings/{name}",
|
||||
srv.RoutePutMuteTiming,
|
||||
m,
|
||||
),
|
||||
)
|
||||
group.Put(
|
||||
toMacaronPath("/api/provisioning/policies"),
|
||||
api.authorize(http.MethodPut, "/api/provisioning/policies"),
|
||||
|
@ -1439,26 +1439,9 @@
|
||||
"type": "object",
|
||||
"x-go-package": "github.com/prometheus/alertmanager/config"
|
||||
},
|
||||
"MuteTiming": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"x-go-name": "Name"
|
||||
},
|
||||
"time_intervals": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/TimeInterval"
|
||||
},
|
||||
"type": "array",
|
||||
"x-go-name": "TimeIntervals"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||
},
|
||||
"MuteTimings": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/MuteTiming"
|
||||
"$ref": "#/definitions/MuteTimeInterval"
|
||||
},
|
||||
"type": "array",
|
||||
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||
@ -3392,7 +3375,6 @@
|
||||
"type": "array"
|
||||
},
|
||||
"gettableSilence": {
|
||||
"description": "GettableSilence gettable silence",
|
||||
"properties": {
|
||||
"comment": {
|
||||
"description": "comment",
|
||||
@ -3444,7 +3426,9 @@
|
||||
"status",
|
||||
"updatedAt"
|
||||
],
|
||||
"type": "object"
|
||||
"type": "object",
|
||||
"x-go-name": "GettableSilence",
|
||||
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
|
||||
},
|
||||
"gettableSilences": {
|
||||
"items": {
|
||||
@ -3581,6 +3565,7 @@
|
||||
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
|
||||
},
|
||||
"postableSilence": {
|
||||
"description": "PostableSilence postable silence",
|
||||
"properties": {
|
||||
"comment": {
|
||||
"description": "comment",
|
||||
@ -3620,11 +3605,10 @@
|
||||
"matchers",
|
||||
"startsAt"
|
||||
],
|
||||
"type": "object",
|
||||
"x-go-name": "PostableSilence",
|
||||
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
|
||||
"type": "object"
|
||||
},
|
||||
"receiver": {
|
||||
"description": "Receiver receiver",
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "name",
|
||||
@ -3635,9 +3619,7 @@
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"type": "object",
|
||||
"x-go-name": "Receiver",
|
||||
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
|
||||
"type": "object"
|
||||
},
|
||||
"silence": {
|
||||
"description": "Silence silence",
|
||||
|
@ -850,6 +850,20 @@ func checkTimeInterval(r *Route, timeIntervals map[string]struct{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// swagger:model
|
||||
type MuteTimeInterval struct {
|
||||
config.MuteTimeInterval
|
||||
Provenance models.Provenance `json:"provenance,omitempty"`
|
||||
}
|
||||
|
||||
func (mt *MuteTimeInterval) ResourceType() string {
|
||||
return "muteTimeInterval"
|
||||
}
|
||||
|
||||
func (mt *MuteTimeInterval) ResourceID() string {
|
||||
return mt.MuteTimeInterval.Name
|
||||
}
|
||||
|
||||
type PostableApiAlertingConfig struct {
|
||||
Config `yaml:",inline"`
|
||||
|
||||
|
@ -2,9 +2,13 @@ package definitions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/common/model"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Validate normalizes a possibly nested Route r, and returns errors if r is invalid.
|
||||
@ -52,6 +56,37 @@ func (r *Route) validateChild() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *MessageTemplate) Validate() error {
|
||||
if t.Name == "" {
|
||||
return fmt.Errorf("template must have a name")
|
||||
}
|
||||
if t.Template == "" {
|
||||
return fmt.Errorf("template must have content")
|
||||
}
|
||||
|
||||
_, err := template.New("").Parse(t.Template)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid template: %w", err)
|
||||
}
|
||||
|
||||
content := strings.TrimSpace(t.Template)
|
||||
found, err := regexp.MatchString(`\{\{\s*define`, content)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to match regex: %w", err)
|
||||
}
|
||||
if !found {
|
||||
lines := strings.Split(content, "\n")
|
||||
for i, s := range lines {
|
||||
lines[i] = " " + s
|
||||
}
|
||||
content = strings.Join(lines, "\n")
|
||||
content = fmt.Sprintf("{{ define \"%s\" }}\n%s\n{{ end }}", t.Name, content)
|
||||
}
|
||||
t.Template = content
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate normalizes a Route r, and returns errors if r is an invalid root route. Root routes must satisfy a few additional conditions.
|
||||
func (r *Route) Validate() error {
|
||||
if len(r.Receiver) == 0 {
|
||||
@ -65,3 +100,14 @@ func (r *Route) Validate() error {
|
||||
}
|
||||
return r.validateChild()
|
||||
}
|
||||
|
||||
func (mt *MuteTimeInterval) Validate() error {
|
||||
s, err := yaml.Marshal(mt.MuteTimeInterval)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = yaml.Unmarshal(s, &(mt.MuteTimeInterval)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -3,6 +3,8 @@ package definitions
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/prometheus/alertmanager/config"
|
||||
"github.com/prometheus/alertmanager/timeinterval"
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@ -255,3 +257,111 @@ func TestValidateRoutes(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidateMuteTimeInterval(t *testing.T) {
|
||||
type testCase struct {
|
||||
desc string
|
||||
mti MuteTimeInterval
|
||||
expMsg string
|
||||
}
|
||||
|
||||
t.Run("valid interval", func(t *testing.T) {
|
||||
cases := []testCase{
|
||||
{
|
||||
desc: "nil intervals",
|
||||
mti: MuteTimeInterval{
|
||||
MuteTimeInterval: config.MuteTimeInterval{
|
||||
Name: "interval",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "empty intervals",
|
||||
mti: MuteTimeInterval{
|
||||
MuteTimeInterval: config.MuteTimeInterval{
|
||||
Name: "interval",
|
||||
TimeIntervals: []timeinterval.TimeInterval{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "blank interval",
|
||||
mti: MuteTimeInterval{
|
||||
MuteTimeInterval: config.MuteTimeInterval{
|
||||
Name: "interval",
|
||||
TimeIntervals: []timeinterval.TimeInterval{
|
||||
{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "simple",
|
||||
mti: MuteTimeInterval{
|
||||
MuteTimeInterval: config.MuteTimeInterval{
|
||||
Name: "interval",
|
||||
TimeIntervals: []timeinterval.TimeInterval{
|
||||
{
|
||||
Weekdays: []timeinterval.WeekdayRange{
|
||||
{
|
||||
InclusiveRange: timeinterval.InclusiveRange{
|
||||
Begin: 1,
|
||||
End: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.desc, func(t *testing.T) {
|
||||
err := c.mti.Validate()
|
||||
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid interval", func(t *testing.T) {
|
||||
cases := []testCase{
|
||||
{
|
||||
desc: "empty",
|
||||
mti: MuteTimeInterval{},
|
||||
expMsg: "missing name",
|
||||
},
|
||||
{
|
||||
desc: "empty",
|
||||
mti: MuteTimeInterval{
|
||||
MuteTimeInterval: config.MuteTimeInterval{
|
||||
Name: "interval",
|
||||
TimeIntervals: []timeinterval.TimeInterval{
|
||||
{
|
||||
Weekdays: []timeinterval.WeekdayRange{
|
||||
{
|
||||
InclusiveRange: timeinterval.InclusiveRange{
|
||||
Begin: -1,
|
||||
End: 7,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expMsg: "unable to convert -1 into weekday",
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.desc, func(t *testing.T) {
|
||||
err := c.mti.Validate()
|
||||
|
||||
require.ErrorContains(t, err, c.expMsg)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -1,7 +1,5 @@
|
||||
package definitions
|
||||
|
||||
import prometheus "github.com/prometheus/alertmanager/config"
|
||||
|
||||
// swagger:route GET /api/provisioning/mute-timings provisioning RouteGetMuteTimings
|
||||
//
|
||||
// Get all the mute timings.
|
||||
@ -15,20 +13,52 @@ import prometheus "github.com/prometheus/alertmanager/config"
|
||||
// Get a mute timing.
|
||||
//
|
||||
// Responses:
|
||||
// 200: MuteTiming
|
||||
// 200: MuteTimeInterval
|
||||
// 400: ValidationError
|
||||
|
||||
// swagger:model
|
||||
type MuteTiming struct {
|
||||
prometheus.MuteTimeInterval
|
||||
}
|
||||
// swagger:route POST /api/provisioning/mute-timings provisioning RoutePostMuteTiming
|
||||
//
|
||||
// Create a new mute timing.
|
||||
//
|
||||
// Consumes:
|
||||
// - application/json
|
||||
//
|
||||
// Responses:
|
||||
// 201: MuteTimeInterval
|
||||
// 400: ValidationError
|
||||
|
||||
// swagger:route PUT /api/provisioning/mute-timings/{name} provisioning RoutePutMuteTiming
|
||||
//
|
||||
// Replace an existing mute timing.
|
||||
//
|
||||
// Consumes:
|
||||
// - application/json
|
||||
//
|
||||
// Responses:
|
||||
// 200: MuteTimeInterval
|
||||
// 400: ValidationError
|
||||
|
||||
// swagger:route DELETE /api/provisioning/mute-timings/{name} provisioning RouteDeleteMuteTiming
|
||||
//
|
||||
// Delete a mute timing.
|
||||
//
|
||||
// Responses:
|
||||
// 204: Ack
|
||||
|
||||
// swagger:route
|
||||
|
||||
// swagger:model
|
||||
type MuteTimings []MuteTiming
|
||||
type MuteTimings []MuteTimeInterval
|
||||
|
||||
// swagger:parameters RouteGetTemplate RouteGetMuteTiming
|
||||
// swagger:parameters RouteGetTemplate RouteGetMuteTiming RoutePutMuteTiming RouteDeleteMuteTiming
|
||||
type RouteGetMuteTimingParam struct {
|
||||
// Template Name
|
||||
// in:path
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// swagger:parameters RoutePostMuteTiming RoutePutMuteTiming
|
||||
type MuteTimingPayload struct {
|
||||
// in:body
|
||||
Body MuteTimeInterval
|
||||
}
|
||||
|
@ -1,11 +1,6 @@
|
||||
package definitions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
)
|
||||
|
||||
@ -77,34 +72,3 @@ func (t *MessageTemplate) ResourceType() string {
|
||||
func (t *MessageTemplate) ResourceID() string {
|
||||
return t.Name
|
||||
}
|
||||
|
||||
func (t *MessageTemplate) Validate() error {
|
||||
if t.Name == "" {
|
||||
return fmt.Errorf("template must have a name")
|
||||
}
|
||||
if t.Template == "" {
|
||||
return fmt.Errorf("template must have content")
|
||||
}
|
||||
|
||||
_, err := template.New("").Parse(t.Template)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid template: %w", err)
|
||||
}
|
||||
|
||||
content := strings.TrimSpace(t.Template)
|
||||
found, err := regexp.MatchString(`\{\{\s*define`, content)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to match regex: %w", err)
|
||||
}
|
||||
if !found {
|
||||
lines := strings.Split(content, "\n")
|
||||
for i, s := range lines {
|
||||
lines[i] = " " + s
|
||||
}
|
||||
content = strings.Join(lines, "\n")
|
||||
content = fmt.Sprintf("{{ define \"%s\" }}\n%s\n{{ end }}", t.Name, content)
|
||||
}
|
||||
t.Template = content
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -1439,26 +1439,9 @@
|
||||
"type": "object",
|
||||
"x-go-package": "github.com/prometheus/alertmanager/config"
|
||||
},
|
||||
"MuteTiming": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"x-go-name": "Name"
|
||||
},
|
||||
"time_intervals": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/TimeInterval"
|
||||
},
|
||||
"type": "array",
|
||||
"x-go-name": "TimeIntervals"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||
},
|
||||
"MuteTimings": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/MuteTiming"
|
||||
"$ref": "#/definitions/MuteTimeInterval"
|
||||
},
|
||||
"type": "array",
|
||||
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||
@ -3196,11 +3179,12 @@
|
||||
"type": "object"
|
||||
},
|
||||
"alertGroups": {
|
||||
"description": "AlertGroups alert groups",
|
||||
"items": {
|
||||
"$ref": "#/definitions/alertGroup"
|
||||
},
|
||||
"type": "array"
|
||||
"type": "array",
|
||||
"x-go-name": "AlertGroups",
|
||||
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
|
||||
},
|
||||
"alertStatus": {
|
||||
"description": "AlertStatus alert status",
|
||||
@ -3445,11 +3429,12 @@
|
||||
"type": "object"
|
||||
},
|
||||
"gettableSilences": {
|
||||
"description": "GettableSilences gettable silences",
|
||||
"items": {
|
||||
"$ref": "#/definitions/gettableSilence"
|
||||
},
|
||||
"type": "array"
|
||||
"type": "array",
|
||||
"x-go-name": "GettableSilences",
|
||||
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
|
||||
},
|
||||
"labelSet": {
|
||||
"additionalProperties": {
|
||||
@ -5005,9 +4990,67 @@
|
||||
"tags": [
|
||||
"provisioning"
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"operationId": "RoutePostMuteTiming",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "body",
|
||||
"name": "Body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/MuteTimeInterval"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "MuteTimeInterval",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/MuteTimeInterval"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "ValidationError",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
"summary": "Create a new mute timing.",
|
||||
"tags": [
|
||||
"provisioning"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/provisioning/mute-timings/{name}": {
|
||||
"delete": {
|
||||
"operationId": "RouteDeleteMuteTiming",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Template Name",
|
||||
"in": "path",
|
||||
"name": "name",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"x-go-name": "Name"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "Ack",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/Ack"
|
||||
}
|
||||
}
|
||||
},
|
||||
"summary": "Delete a mute timing.",
|
||||
"tags": [
|
||||
"provisioning"
|
||||
]
|
||||
},
|
||||
"get": {
|
||||
"operationId": "RouteGetMuteTiming",
|
||||
"parameters": [
|
||||
@ -5022,9 +5065,9 @@
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "MuteTiming",
|
||||
"description": "MuteTimeInterval",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/MuteTiming"
|
||||
"$ref": "#/definitions/MuteTimeInterval"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
@ -5038,6 +5081,47 @@
|
||||
"tags": [
|
||||
"provisioning"
|
||||
]
|
||||
},
|
||||
"put": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"operationId": "RoutePutMuteTiming",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Template Name",
|
||||
"in": "path",
|
||||
"name": "name",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"x-go-name": "Name"
|
||||
},
|
||||
{
|
||||
"in": "body",
|
||||
"name": "Body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/MuteTimeInterval"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "MuteTimeInterval",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/MuteTimeInterval"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "ValidationError",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
"summary": "Replace an existing mute timing.",
|
||||
"tags": [
|
||||
"provisioning"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/provisioning/policies": {
|
||||
|
@ -1274,6 +1274,39 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"provisioning"
|
||||
],
|
||||
"summary": "Create a new mute timing.",
|
||||
"operationId": "RoutePostMuteTiming",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Body",
|
||||
"in": "body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/MuteTimeInterval"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "MuteTimeInterval",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/MuteTimeInterval"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "ValidationError",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/provisioning/mute-timings/{name}": {
|
||||
@ -1295,9 +1328,9 @@
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "MuteTiming",
|
||||
"description": "MuteTimeInterval",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/MuteTiming"
|
||||
"$ref": "#/definitions/MuteTimeInterval"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
@ -1307,6 +1340,72 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"provisioning"
|
||||
],
|
||||
"summary": "Replace an existing mute timing.",
|
||||
"operationId": "RoutePutMuteTiming",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"x-go-name": "Name",
|
||||
"description": "Template Name",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "Body",
|
||||
"in": "body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/MuteTimeInterval"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "MuteTimeInterval",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/MuteTimeInterval"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "ValidationError",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"provisioning"
|
||||
],
|
||||
"summary": "Delete a mute timing.",
|
||||
"operationId": "RouteDeleteMuteTiming",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"x-go-name": "Name",
|
||||
"description": "Template Name",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "Ack",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/Ack"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/provisioning/policies": {
|
||||
@ -3544,27 +3643,10 @@
|
||||
},
|
||||
"x-go-package": "github.com/prometheus/alertmanager/config"
|
||||
},
|
||||
"MuteTiming": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"x-go-name": "Name"
|
||||
},
|
||||
"time_intervals": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/TimeInterval"
|
||||
},
|
||||
"x-go-name": "TimeIntervals"
|
||||
}
|
||||
},
|
||||
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||
},
|
||||
"MuteTimings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/MuteTiming"
|
||||
"$ref": "#/definitions/MuteTimeInterval"
|
||||
},
|
||||
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||
},
|
||||
@ -5302,11 +5384,12 @@
|
||||
"$ref": "#/definitions/alertGroup"
|
||||
},
|
||||
"alertGroups": {
|
||||
"description": "AlertGroups alert groups",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/alertGroup"
|
||||
},
|
||||
"x-go-name": "AlertGroups",
|
||||
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models",
|
||||
"$ref": "#/definitions/alertGroups"
|
||||
},
|
||||
"alertStatus": {
|
||||
@ -5555,11 +5638,12 @@
|
||||
"$ref": "#/definitions/gettableSilence"
|
||||
},
|
||||
"gettableSilences": {
|
||||
"description": "GettableSilences gettable silences",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/gettableSilence"
|
||||
},
|
||||
"x-go-name": "GettableSilences",
|
||||
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models",
|
||||
"$ref": "#/definitions/gettableSilences"
|
||||
},
|
||||
"labelSet": {
|
||||
|
@ -2,9 +2,12 @@ package provisioning
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"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/prometheus/alertmanager/config"
|
||||
)
|
||||
|
||||
type MuteTimingService struct {
|
||||
@ -23,19 +26,167 @@ func NewMuteTimingService(config AMConfigStore, prov ProvisioningStore, xact Tra
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MuteTimingService) GetMuteTimings(ctx context.Context, orgID int64) ([]definitions.MuteTiming, error) {
|
||||
rev, err := getLastConfiguration(ctx, orgID, m.config)
|
||||
// GetMuteTimings returns a slice of all mute timings within the specified org.
|
||||
func (svc *MuteTimingService) GetMuteTimings(ctx context.Context, orgID int64) ([]definitions.MuteTimeInterval, error) {
|
||||
rev, err := getLastConfiguration(ctx, orgID, svc.config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if rev.cfg.AlertmanagerConfig.MuteTimeIntervals == nil {
|
||||
return []definitions.MuteTiming{}, nil
|
||||
return []definitions.MuteTimeInterval{}, nil
|
||||
}
|
||||
|
||||
result := make([]definitions.MuteTiming, 0, len(rev.cfg.AlertmanagerConfig.MuteTimeIntervals))
|
||||
result := make([]definitions.MuteTimeInterval, 0, len(rev.cfg.AlertmanagerConfig.MuteTimeIntervals))
|
||||
for _, interval := range rev.cfg.AlertmanagerConfig.MuteTimeIntervals {
|
||||
result = append(result, definitions.MuteTiming{MuteTimeInterval: interval})
|
||||
result = append(result, definitions.MuteTimeInterval{MuteTimeInterval: interval})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// CreateMuteTiming adds a new mute timing within the specified org. The created mute timing is returned.
|
||||
func (svc *MuteTimingService) CreateMuteTiming(ctx context.Context, mt definitions.MuteTimeInterval, orgID int64) (*definitions.MuteTimeInterval, error) {
|
||||
if err := mt.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrValidation, err.Error())
|
||||
}
|
||||
|
||||
revision, err := getLastConfiguration(ctx, orgID, svc.config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if revision.cfg.AlertmanagerConfig.MuteTimeIntervals == nil {
|
||||
revision.cfg.AlertmanagerConfig.MuteTimeIntervals = []config.MuteTimeInterval{}
|
||||
}
|
||||
for _, existing := range revision.cfg.AlertmanagerConfig.MuteTimeIntervals {
|
||||
if mt.Name == existing.Name {
|
||||
return nil, fmt.Errorf("%w: %s", ErrValidation, "a mute timing with this name already exists")
|
||||
}
|
||||
}
|
||||
revision.cfg.AlertmanagerConfig.MuteTimeIntervals = append(revision.cfg.AlertmanagerConfig.MuteTimeIntervals, mt.MuteTimeInterval)
|
||||
|
||||
serialized, err := serializeAlertmanagerConfig(*revision.cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmd := models.SaveAlertmanagerConfigurationCmd{
|
||||
AlertmanagerConfiguration: string(serialized),
|
||||
ConfigurationVersion: revision.version,
|
||||
FetchedConfigurationHash: revision.concurrencyToken,
|
||||
Default: false,
|
||||
OrgID: orgID,
|
||||
}
|
||||
err = svc.xact.InTransaction(ctx, func(ctx context.Context) error {
|
||||
err = svc.config.UpdateAlertmanagerConfiguration(ctx, &cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = svc.prov.SetProvenance(ctx, &mt, orgID, mt.Provenance)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &mt, nil
|
||||
}
|
||||
|
||||
// UpdateMuteTiming replaces an existing mute timing within the specified org. The replaced mute timing is returned. If the mute timing does not exist, nil is returned and no action is taken.
|
||||
func (svc *MuteTimingService) UpdateMuteTiming(ctx context.Context, mt definitions.MuteTimeInterval, orgID int64) (*definitions.MuteTimeInterval, error) {
|
||||
if err := mt.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrValidation, err.Error())
|
||||
}
|
||||
|
||||
revision, err := getLastConfiguration(ctx, orgID, svc.config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if revision.cfg.AlertmanagerConfig.MuteTimeIntervals == nil {
|
||||
return nil, nil
|
||||
}
|
||||
updated := false
|
||||
for i, existing := range revision.cfg.AlertmanagerConfig.MuteTimeIntervals {
|
||||
if mt.Name == existing.Name {
|
||||
revision.cfg.AlertmanagerConfig.MuteTimeIntervals[i] = mt.MuteTimeInterval
|
||||
updated = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !updated {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
serialized, err := serializeAlertmanagerConfig(*revision.cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmd := models.SaveAlertmanagerConfigurationCmd{
|
||||
AlertmanagerConfiguration: string(serialized),
|
||||
ConfigurationVersion: revision.version,
|
||||
FetchedConfigurationHash: revision.concurrencyToken,
|
||||
Default: false,
|
||||
OrgID: orgID,
|
||||
}
|
||||
err = svc.xact.InTransaction(ctx, func(ctx context.Context) error {
|
||||
err = svc.config.UpdateAlertmanagerConfiguration(ctx, &cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = svc.prov.SetProvenance(ctx, &mt, orgID, mt.Provenance)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &mt, err
|
||||
}
|
||||
|
||||
// DeleteMuteTiming deletes the mute timing with the given name in the given org. If the mute timing does not exist, no error is returned.
|
||||
func (svc *MuteTimingService) DeleteMuteTiming(ctx context.Context, name string, orgID int64) error {
|
||||
revision, err := getLastConfiguration(ctx, orgID, svc.config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if revision.cfg.AlertmanagerConfig.MuteTimeIntervals == nil {
|
||||
return nil
|
||||
}
|
||||
for i, existing := range revision.cfg.AlertmanagerConfig.MuteTimeIntervals {
|
||||
if name == existing.Name {
|
||||
intervals := revision.cfg.AlertmanagerConfig.MuteTimeIntervals
|
||||
revision.cfg.AlertmanagerConfig.MuteTimeIntervals = append(intervals[:i], intervals[i+1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
serialized, err := serializeAlertmanagerConfig(*revision.cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd := models.SaveAlertmanagerConfigurationCmd{
|
||||
AlertmanagerConfiguration: string(serialized),
|
||||
ConfigurationVersion: revision.version,
|
||||
FetchedConfigurationHash: revision.concurrencyToken,
|
||||
Default: false,
|
||||
OrgID: orgID,
|
||||
}
|
||||
return svc.xact.InTransaction(ctx, func(ctx context.Context) error {
|
||||
err = svc.config.UpdateAlertmanagerConfiguration(ctx, &cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
target := definitions.MuteTimeInterval{MuteTimeInterval: config.MuteTimeInterval{Name: name}}
|
||||
err := svc.prov.DeleteProvenance(ctx, &target, orgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
@ -6,7 +6,9 @@ import (
|
||||
"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/prometheus/alertmanager/config"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@ -74,6 +76,288 @@ func TestMuteTimingService(t *testing.T) {
|
||||
require.ErrorContains(t, err, "no alertmanager configuration")
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("creating mute timings", func(t *testing.T) {
|
||||
t.Run("rejects mute timings that fail validation", func(t *testing.T) {
|
||||
sut := createMuteTimingSvcSut()
|
||||
timing := definitions.MuteTimeInterval{
|
||||
MuteTimeInterval: config.MuteTimeInterval{
|
||||
Name: "",
|
||||
},
|
||||
}
|
||||
|
||||
_, err := sut.CreateMuteTiming(context.Background(), timing, 1)
|
||||
|
||||
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 := createMuteTimingSvcSut()
|
||||
timing := createMuteTiming()
|
||||
sut.config.(*MockAMConfigStore).EXPECT().
|
||||
GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything).
|
||||
Return(fmt.Errorf("failed"))
|
||||
|
||||
_, err := sut.CreateMuteTiming(context.Background(), timing, 1)
|
||||
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("when config is invalid", func(t *testing.T) {
|
||||
sut := createMuteTimingSvcSut()
|
||||
timing := createMuteTiming()
|
||||
sut.config.(*MockAMConfigStore).EXPECT().
|
||||
getsConfig(models.AlertConfiguration{
|
||||
AlertmanagerConfiguration: brokenConfig,
|
||||
})
|
||||
|
||||
_, err := sut.CreateMuteTiming(context.Background(), timing, 1)
|
||||
|
||||
require.ErrorContains(t, err, "failed to deserialize")
|
||||
})
|
||||
|
||||
t.Run("when no AM config in current org", func(t *testing.T) {
|
||||
sut := createMuteTimingSvcSut()
|
||||
timing := createMuteTiming()
|
||||
sut.config.(*MockAMConfigStore).EXPECT().
|
||||
GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything).
|
||||
Return(nil)
|
||||
|
||||
_, err := sut.CreateMuteTiming(context.Background(), timing, 1)
|
||||
|
||||
require.ErrorContains(t, err, "no alertmanager configuration")
|
||||
})
|
||||
|
||||
t.Run("when provenance fails to save", func(t *testing.T) {
|
||||
sut := createMuteTimingSvcSut()
|
||||
timing := createMuteTiming()
|
||||
sut.config.(*MockAMConfigStore).EXPECT().
|
||||
getsConfig(models.AlertConfiguration{
|
||||
AlertmanagerConfiguration: configWithMuteTimings,
|
||||
})
|
||||
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.CreateMuteTiming(context.Background(), timing, 1)
|
||||
|
||||
require.ErrorContains(t, err, "failed to save provenance")
|
||||
})
|
||||
|
||||
t.Run("when AM config fails to save", func(t *testing.T) {
|
||||
sut := createMuteTimingSvcSut()
|
||||
timing := createMuteTiming()
|
||||
sut.config.(*MockAMConfigStore).EXPECT().
|
||||
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()
|
||||
|
||||
_, err := sut.CreateMuteTiming(context.Background(), timing, 1)
|
||||
|
||||
require.ErrorContains(t, err, "failed to save config")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("updating mute timings", func(t *testing.T) {
|
||||
t.Run("rejects mute timings that fail validation", func(t *testing.T) {
|
||||
sut := createMuteTimingSvcSut()
|
||||
timing := definitions.MuteTimeInterval{
|
||||
MuteTimeInterval: config.MuteTimeInterval{
|
||||
Name: "",
|
||||
},
|
||||
}
|
||||
|
||||
_, err := sut.UpdateMuteTiming(context.Background(), timing, 1)
|
||||
|
||||
require.ErrorIs(t, err, ErrValidation)
|
||||
})
|
||||
|
||||
t.Run("returns nil if timing does not exist", func(t *testing.T) {
|
||||
sut := createMuteTimingSvcSut()
|
||||
timing := createMuteTiming()
|
||||
timing.Name = "does not exist"
|
||||
sut.config.(*MockAMConfigStore).EXPECT().
|
||||
getsConfig(models.AlertConfiguration{
|
||||
AlertmanagerConfiguration: configWithMuteTimings,
|
||||
})
|
||||
sut.config.(*MockAMConfigStore).EXPECT().saveSucceeds()
|
||||
sut.prov.(*MockProvisioningStore).EXPECT().saveSucceeds()
|
||||
|
||||
updated, err := sut.UpdateMuteTiming(context.Background(), timing, 1)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, updated)
|
||||
})
|
||||
|
||||
t.Run("propagates errors", func(t *testing.T) {
|
||||
t.Run("when unable to read config", func(t *testing.T) {
|
||||
sut := createMuteTimingSvcSut()
|
||||
timing := createMuteTiming()
|
||||
timing.Name = "asdf"
|
||||
sut.config.(*MockAMConfigStore).EXPECT().
|
||||
GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything).
|
||||
Return(fmt.Errorf("failed"))
|
||||
|
||||
_, err := sut.UpdateMuteTiming(context.Background(), timing, 1)
|
||||
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("when config is invalid", func(t *testing.T) {
|
||||
sut := createMuteTimingSvcSut()
|
||||
timing := createMuteTiming()
|
||||
timing.Name = "asdf"
|
||||
sut.config.(*MockAMConfigStore).EXPECT().
|
||||
getsConfig(models.AlertConfiguration{
|
||||
AlertmanagerConfiguration: brokenConfig,
|
||||
})
|
||||
|
||||
_, err := sut.UpdateMuteTiming(context.Background(), timing, 1)
|
||||
|
||||
require.ErrorContains(t, err, "failed to deserialize")
|
||||
})
|
||||
|
||||
t.Run("when no AM config in current org", func(t *testing.T) {
|
||||
sut := createMuteTimingSvcSut()
|
||||
timing := createMuteTiming()
|
||||
timing.Name = "asdf"
|
||||
sut.config.(*MockAMConfigStore).EXPECT().
|
||||
GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything).
|
||||
Return(nil)
|
||||
|
||||
_, err := sut.UpdateMuteTiming(context.Background(), timing, 1)
|
||||
|
||||
require.ErrorContains(t, err, "no alertmanager configuration")
|
||||
})
|
||||
|
||||
t.Run("when provenance fails to save", func(t *testing.T) {
|
||||
sut := createMuteTimingSvcSut()
|
||||
timing := createMuteTiming()
|
||||
timing.Name = "asdf"
|
||||
sut.config.(*MockAMConfigStore).EXPECT().
|
||||
getsConfig(models.AlertConfiguration{
|
||||
AlertmanagerConfiguration: configWithMuteTimings,
|
||||
})
|
||||
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.UpdateMuteTiming(context.Background(), timing, 1)
|
||||
|
||||
require.ErrorContains(t, err, "failed to save provenance")
|
||||
})
|
||||
|
||||
t.Run("when AM config fails to save", func(t *testing.T) {
|
||||
sut := createMuteTimingSvcSut()
|
||||
timing := createMuteTiming()
|
||||
timing.Name = "asdf"
|
||||
sut.config.(*MockAMConfigStore).EXPECT().
|
||||
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()
|
||||
|
||||
_, err := sut.UpdateMuteTiming(context.Background(), timing, 1)
|
||||
|
||||
require.ErrorContains(t, err, "failed to save config")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("deleting mute timings", func(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{
|
||||
AlertmanagerConfiguration: configWithMuteTimings,
|
||||
})
|
||||
sut.config.(*MockAMConfigStore).EXPECT().saveSucceeds()
|
||||
sut.prov.(*MockProvisioningStore).EXPECT().saveSucceeds()
|
||||
|
||||
err := sut.DeleteMuteTiming(context.Background(), "does not exist", 1)
|
||||
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("propagates errors", func(t *testing.T) {
|
||||
t.Run("when unable to read config", func(t *testing.T) {
|
||||
sut := createMuteTimingSvcSut()
|
||||
sut.config.(*MockAMConfigStore).EXPECT().
|
||||
GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything).
|
||||
Return(fmt.Errorf("failed"))
|
||||
|
||||
err := sut.DeleteMuteTiming(context.Background(), "asdf", 1)
|
||||
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("when config is invalid", func(t *testing.T) {
|
||||
sut := createMuteTimingSvcSut()
|
||||
sut.config.(*MockAMConfigStore).EXPECT().
|
||||
getsConfig(models.AlertConfiguration{
|
||||
AlertmanagerConfiguration: brokenConfig,
|
||||
})
|
||||
|
||||
err := sut.DeleteMuteTiming(context.Background(), "asdf", 1)
|
||||
|
||||
require.ErrorContains(t, err, "failed to deserialize")
|
||||
})
|
||||
|
||||
t.Run("when no AM config in current org", func(t *testing.T) {
|
||||
sut := createMuteTimingSvcSut()
|
||||
sut.config.(*MockAMConfigStore).EXPECT().
|
||||
GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything).
|
||||
Return(nil)
|
||||
|
||||
err := sut.DeleteMuteTiming(context.Background(), "asdf", 1)
|
||||
|
||||
require.ErrorContains(t, err, "no alertmanager configuration")
|
||||
})
|
||||
|
||||
t.Run("when provenance fails to save", func(t *testing.T) {
|
||||
sut := createMuteTimingSvcSut()
|
||||
sut.config.(*MockAMConfigStore).EXPECT().
|
||||
getsConfig(models.AlertConfiguration{
|
||||
AlertmanagerConfiguration: configWithMuteTimings,
|
||||
})
|
||||
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.DeleteMuteTiming(context.Background(), "asdf", 1)
|
||||
|
||||
require.ErrorContains(t, err, "failed to save provenance")
|
||||
})
|
||||
|
||||
t.Run("when AM config fails to save", func(t *testing.T) {
|
||||
sut := createMuteTimingSvcSut()
|
||||
sut.config.(*MockAMConfigStore).EXPECT().
|
||||
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()
|
||||
|
||||
err := sut.DeleteMuteTiming(context.Background(), "asdf", 1)
|
||||
|
||||
require.ErrorContains(t, err, "failed to save config")
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func createMuteTimingSvcSut() *MuteTimingService {
|
||||
@ -85,6 +369,14 @@ func createMuteTimingSvcSut() *MuteTimingService {
|
||||
}
|
||||
}
|
||||
|
||||
func createMuteTiming() definitions.MuteTimeInterval {
|
||||
return definitions.MuteTimeInterval{
|
||||
MuteTimeInterval: config.MuteTimeInterval{
|
||||
Name: "interval",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var configWithMuteTimings = `
|
||||
{
|
||||
"template_files": {
|
||||
|
Loading…
Reference in New Issue
Block a user