Alerting: Provisioning GET routes for mute timings (#49044)

* Define GET routes and run codegen

* Wire up forked and non-generated API

* Implement and wire

* Tests, authorization

* Fix linter error
This commit is contained in:
Alexander Weaver 2022-05-17 13:42:48 -05:00 committed by GitHub
parent 7bdf76a694
commit 9af30f6570
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 400 additions and 15 deletions

View File

@ -80,6 +80,7 @@ type API struct {
Policies *provisioning.NotificationPolicyService
ContactPointService *provisioning.ContactPointService
Templates *provisioning.TemplateService
MuteTimings *provisioning.MuteTimingService
}
// RegisterAPIEndpoints registers API handlers
@ -140,6 +141,7 @@ func (api *API) RegisterAPIEndpoints(m *metrics.API) {
policies: api.Policies,
contactPointService: api.ContactPointService,
templates: api.Templates,
muteTimings: api.MuteTimings,
}), m)
}
}

View File

@ -21,6 +21,7 @@ type ProvisioningSrv struct {
policies NotificationPolicyService
contactPointService ContactPointService
templates TemplateService
muteTimings MuteTimingService
}
type ContactPointService interface {
@ -41,6 +42,10 @@ type NotificationPolicyService interface {
UpdatePolicyTree(ctx context.Context, orgID int64, tree apimodels.Route, p alerting_models.Provenance) error
}
type MuteTimingService interface {
GetMuteTimings(ctx context.Context, orgID int64) ([]apimodels.MuteTiming, error)
}
func (srv *ProvisioningSrv) RouteGetPolicyTree(c *models.ReqContext) response.Response {
policies, err := srv.policies.GetPolicyTree(c.Req.Context(), c.OrgId)
if errors.Is(err, store.ErrNoAlertmanagerConfiguration) {
@ -153,3 +158,25 @@ func (srv *ProvisioningSrv) RouteDeleteTemplate(c *models.ReqContext) response.R
}
return response.JSON(http.StatusNoContent, nil)
}
func (srv *ProvisioningSrv) RouteGetMuteTiming(c *models.ReqContext) response.Response {
name := web.Params(c.Req)[":name"]
timings, err := srv.muteTimings.GetMuteTimings(c.Req.Context(), c.OrgId)
if err != nil {
return ErrResp(http.StatusInternalServerError, err, "")
}
for _, timing := range timings {
if name == timing.Name {
return response.JSON(http.StatusOK, timing)
}
}
return response.Empty(http.StatusNotFound)
}
func (srv *ProvisioningSrv) RouteGetMuteTimings(c *models.ReqContext) response.Response {
timings, err := srv.muteTimings.GetMuteTimings(c.Req.Context(), c.OrgId)
if err != nil {
return ErrResp(http.StatusInternalServerError, err, "")
}
return response.JSON(http.StatusOK, timings)
}

View File

@ -181,7 +181,9 @@ func (api *API) authorize(method, path string) web.Handler {
case http.MethodGet + "/api/provisioning/policies",
http.MethodGet + "/api/provisioning/contact-points",
http.MethodGet + "/api/provisioning/templates",
http.MethodGet + "/api/provisioning/templates/{name}":
http.MethodGet + "/api/provisioning/templates/{name}",
http.MethodGet + "/api/provisioning/mute-timings",
http.MethodGet + "/api/provisioning/mute-timings/{name}":
return middleware.ReqSignedIn
case http.MethodPut + "/api/provisioning/policies",

View File

@ -46,7 +46,7 @@ func TestAuthorize(t *testing.T) {
}
paths[p] = methods
}
require.Len(t, paths, 34)
require.Len(t, paths, 36)
ac := acmock.New()
api := &API{AccessControl: ac}

View File

@ -58,3 +58,11 @@ func (f *ForkedProvisioningApi) forkRoutePutTemplate(ctx *models.ReqContext, bod
func (f *ForkedProvisioningApi) forkRouteDeleteTemplate(ctx *models.ReqContext) response.Response {
return f.svc.RouteDeleteTemplate(ctx)
}
func (f *ForkedProvisioningApi) forkRouteGetMuteTiming(ctx *models.ReqContext) response.Response {
return f.svc.RouteGetMuteTiming(ctx)
}
func (f *ForkedProvisioningApi) forkRouteGetMuteTimings(ctx *models.ReqContext) response.Response {
return f.svc.RouteGetMuteTimings(ctx)
}

View File

@ -23,6 +23,8 @@ type ProvisioningApiForkingService interface {
RouteDeleteContactpoints(*models.ReqContext) response.Response
RouteDeleteTemplate(*models.ReqContext) response.Response
RouteGetContactpoints(*models.ReqContext) response.Response
RouteGetMuteTiming(*models.ReqContext) response.Response
RouteGetMuteTimings(*models.ReqContext) response.Response
RouteGetPolicyTree(*models.ReqContext) response.Response
RouteGetTemplate(*models.ReqContext) response.Response
RouteGetTemplates(*models.ReqContext) response.Response
@ -44,6 +46,14 @@ func (f *ForkedProvisioningApi) RouteGetContactpoints(ctx *models.ReqContext) re
return f.forkRouteGetContactpoints(ctx)
}
func (f *ForkedProvisioningApi) RouteGetMuteTiming(ctx *models.ReqContext) response.Response {
return f.forkRouteGetMuteTiming(ctx)
}
func (f *ForkedProvisioningApi) RouteGetMuteTimings(ctx *models.ReqContext) response.Response {
return f.forkRouteGetMuteTimings(ctx)
}
func (f *ForkedProvisioningApi) RouteGetPolicyTree(ctx *models.ReqContext) response.Response {
return f.forkRouteGetPolicyTree(ctx)
}
@ -120,6 +130,26 @@ func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApiForkingServi
m,
),
)
group.Get(
toMacaronPath("/api/provisioning/mute-timings/{name}"),
api.authorize(http.MethodGet, "/api/provisioning/mute-timings/{name}"),
metrics.Instrument(
http.MethodGet,
"/api/provisioning/mute-timings/{name}",
srv.RouteGetMuteTiming,
m,
),
)
group.Get(
toMacaronPath("/api/provisioning/mute-timings"),
api.authorize(http.MethodGet, "/api/provisioning/mute-timings"),
metrics.Instrument(
http.MethodGet,
"/api/provisioning/mute-timings",
srv.RouteGetMuteTimings,
m,
),
)
group.Get(
toMacaronPath("/api/provisioning/policies"),
api.authorize(http.MethodGet, "/api/provisioning/policies"),

View File

@ -0,0 +1,23 @@
package definitions
import prometheus "github.com/prometheus/alertmanager/config"
// swagger:route GET /api/provisioning/mute-timings provisioning RouteGetMuteTimings
//
// Get all the mute timings.
//
// Responses:
// 200: []MuteTiming
// 400: ValidationError
// swagger:route GET /api/provisioning/mute-timings/{name} provisioning RouteGetMuteTiming
//
// Get a mute timing.
//
// Responses:
// 200: MuteTiming
// 400: ValidationError
type MuteTiming struct {
prometheus.MuteTimeInterval
}

View File

@ -3265,6 +3265,7 @@
"$ref": "#/definitions/Duration"
},
"gettableAlert": {
"description": "GettableAlert gettable alert",
"properties": {
"annotations": {
"$ref": "#/definitions/labelSet"
@ -3323,9 +3324,7 @@
"status",
"updatedAt"
],
"type": "object",
"x-go-name": "GettableAlert",
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
"type": "object"
},
"gettableAlerts": {
"items": {
@ -3336,7 +3335,6 @@
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
},
"gettableSilence": {
"description": "GettableSilence gettable silence",
"properties": {
"comment": {
"description": "comment",
@ -3388,7 +3386,9 @@
"status",
"updatedAt"
],
"type": "object"
"type": "object",
"x-go-name": "GettableSilence",
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
},
"gettableSilences": {
"items": {
@ -3525,6 +3525,7 @@
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
},
"postableSilence": {
"description": "PostableSilence postable silence",
"properties": {
"comment": {
"description": "comment",
@ -3564,9 +3565,7 @@
"matchers",
"startsAt"
],
"type": "object",
"x-go-name": "PostableSilence",
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
"type": "object"
},
"receiver": {
"description": "Receiver receiver",
@ -4905,6 +4904,46 @@
]
}
},
"/api/provisioning/mute-timings": {
"get": {
"operationId": "RouteGetMuteTimings",
"responses": {
"200": {
"$ref": "#/responses/MuteTiming"
},
"400": {
"description": "ValidationError",
"schema": {
"$ref": "#/definitions/ValidationError"
}
}
},
"summary": "Get all the mute timings.",
"tags": [
"provisioning"
]
}
},
"/api/provisioning/mute-timings/{name}": {
"get": {
"operationId": "RouteGetMuteTiming",
"responses": {
"200": {
"$ref": "#/responses/MuteTiming"
},
"400": {
"description": "ValidationError",
"schema": {
"$ref": "#/definitions/ValidationError"
}
}
},
"summary": "Get a mute timing.",
"tags": [
"provisioning"
]
}
},
"/api/provisioning/policies": {
"get": {
"operationId": "RouteGetPolicyTree",

View File

@ -1228,6 +1228,46 @@
}
}
},
"/api/provisioning/mute-timings": {
"get": {
"tags": [
"provisioning"
],
"summary": "Get all the mute timings.",
"operationId": "RouteGetMuteTimings",
"responses": {
"200": {
"$ref": "#/responses/MuteTiming"
},
"400": {
"description": "ValidationError",
"schema": {
"$ref": "#/definitions/ValidationError"
}
}
}
}
},
"/api/provisioning/mute-timings/{name}": {
"get": {
"tags": [
"provisioning"
],
"summary": "Get a mute timing.",
"operationId": "RouteGetMuteTiming",
"responses": {
"200": {
"$ref": "#/responses/MuteTiming"
},
"400": {
"description": "ValidationError",
"schema": {
"$ref": "#/definitions/ValidationError"
}
}
}
}
},
"/api/provisioning/policies": {
"get": {
"tags": [
@ -5245,6 +5285,7 @@
"$ref": "#/definitions/Duration"
},
"gettableAlert": {
"description": "GettableAlert gettable alert",
"type": "object",
"required": [
"labels",
@ -5304,8 +5345,6 @@
"x-go-name": "UpdatedAt"
}
},
"x-go-name": "GettableAlert",
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models",
"$ref": "#/definitions/gettableAlert"
},
"gettableAlerts": {
@ -5318,7 +5357,6 @@
"$ref": "#/definitions/gettableAlerts"
},
"gettableSilence": {
"description": "GettableSilence gettable silence",
"type": "object",
"required": [
"comment",
@ -5371,6 +5409,8 @@
"x-go-name": "UpdatedAt"
}
},
"x-go-name": "GettableSilence",
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models",
"$ref": "#/definitions/gettableSilence"
},
"gettableSilences": {
@ -5509,6 +5549,7 @@
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
},
"postableSilence": {
"description": "PostableSilence postable silence",
"type": "object",
"required": [
"comment",
@ -5549,8 +5590,6 @@
"x-go-name": "StartsAt"
}
},
"x-go-name": "PostableSilence",
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models",
"$ref": "#/definitions/postableSilence"
},
"receiver": {

View File

@ -141,6 +141,7 @@ func (ng *AlertNG) init() error {
policyService := provisioning.NewNotificationPolicyService(store, store, store, ng.Log)
contactPointService := provisioning.NewContactPointService(store, ng.SecretsService, store, store, ng.Log)
templateService := provisioning.NewTemplateService(store, store, store, ng.Log)
muteTimingService := provisioning.NewMuteTimingService(store, store, store, ng.Log)
api := api.API{
Cfg: ng.Cfg,
@ -163,6 +164,7 @@ func (ng *AlertNG) init() error {
Policies: policyService,
ContactPointService: contactPointService,
Templates: templateService,
MuteTimings: muteTimingService,
}
api.RegisterAPIEndpoints(ng.Metrics.GetAPIMetrics())

View File

@ -0,0 +1,51 @@
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"
)
type MuteTimingService struct {
config AMConfigStore
prov ProvisioningStore
xact TransactionManager
log log.Logger
}
func NewMuteTimingService(config AMConfigStore, prov ProvisioningStore, xact TransactionManager, log log.Logger) *MuteTimingService {
return &MuteTimingService{
config: config,
prov: prov,
xact: xact,
log: log,
}
}
func (m *MuteTimingService) GetMuteTimings(ctx context.Context, orgID int64) ([]definitions.MuteTiming, error) {
q := models.GetLatestAlertmanagerConfigurationQuery{
OrgID: orgID,
}
err := m.config.GetLatestAlertmanagerConfiguration(ctx, &q)
if err != nil {
return nil, err
}
if q.Result == nil {
return nil, fmt.Errorf("no alertmanager configuration present in this org")
}
cfg, err := DeserializeAlertmanagerConfig([]byte(q.Result.AlertmanagerConfiguration))
if err != nil {
return nil, err
}
result := make([]definitions.MuteTiming, 0, len(cfg.AlertmanagerConfig.MuteTimeIntervals))
for _, interval := range cfg.AlertmanagerConfig.MuteTimeIntervals {
result = append(result, definitions.MuteTiming{MuteTimeInterval: interval})
}
return result, nil
}

View File

@ -0,0 +1,118 @@
package provisioning
import (
"context"
"fmt"
"testing"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/ngalert/models"
mock "github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
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{
AlertmanagerConfiguration: configWithMuteTimings,
})
result, err := sut.GetMuteTimings(context.Background(), 1)
require.NoError(t, err)
require.Len(t, result, 1)
require.Equal(t, "asdf", result[0].Name)
})
t.Run("service returns empty map when config file contains no templates", func(t *testing.T) {
sut := createMuteTimingSvcSut()
sut.config.(*MockAMConfigStore).EXPECT().
getsConfig(models.AlertConfiguration{
AlertmanagerConfiguration: defaultConfig,
})
result, err := sut.GetMuteTimings(context.Background(), 1)
require.NoError(t, err)
require.Empty(t, result)
})
t.Run("service propagates errors", func(t *testing.T) {
t.Run("when unable to read config", func(t *testing.T) {
sut := createMuteTimingSvcSut()
sut.config.(*MockAMConfigStore).EXPECT().
GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything).
Return(fmt.Errorf("failed"))
_, err := sut.GetMuteTimings(context.Background(), 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.GetMuteTimings(context.Background(), 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.GetMuteTimings(context.Background(), 1)
require.ErrorContains(t, err, "no alertmanager configuration")
})
})
}
func createMuteTimingSvcSut() *MuteTimingService {
return &MuteTimingService{
config: &MockAMConfigStore{},
prov: &MockProvisioningStore{},
xact: newNopTransactionManager(),
log: log.NewNopLogger(),
}
}
var configWithMuteTimings = `
{
"template_files": {
"a": "template"
},
"alertmanager_config": {
"route": {
"receiver": "grafana-default-email"
},
"mute_time_intervals": [{
"name": "asdf",
"time_intervals": [{
"times": [],
"weekdays": ["monday"]
}]
}],
"receivers": [{
"name": "grafana-default-email",
"grafana_managed_receiver_configs": [{
"uid": "",
"name": "email receiver",
"type": "email",
"isDefault": true,
"settings": {
"addresses": "<example@email.com>"
}
}]
}]
}
}
`

View File

@ -273,6 +273,50 @@ func TestProvisioning(t *testing.T) {
require.Equal(t, 200, resp.StatusCode)
})
})
t.Run("when provisioning mute timings", func(t *testing.T) {
url := fmt.Sprintf("http://%s/api/provisioning/mute-timings", grafanaListedAddr)
t.Run("un-authenticated GET should 401", func(t *testing.T) {
req := createTestRequest("GET", url, "", "")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, 401, resp.StatusCode)
})
t.Run("viewer GET should succeed", func(t *testing.T) {
req := createTestRequest("GET", url, "viewer", "")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, 200, resp.StatusCode)
})
t.Run("editor GET should succeed", func(t *testing.T) {
req := createTestRequest("GET", url, "editor", "")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, 200, resp.StatusCode)
})
t.Run("admin GET should succeed", func(t *testing.T) {
req := createTestRequest("GET", url, "admin", "")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, 200, resp.StatusCode)
})
})
}
func createTestRequest(method string, url string, user string, body string) *http.Request {