mirror of https://github.com/grafana/grafana
Alerting: Move alertmanager api silence code to separate files (#86947)
* Move alertmanager api silence code to separate files unchanged * Replace with silence model instead interface --------- Co-authored-by: Matt Jacobson <matthew.jacobson@grafana.com>pull/86952/head
parent
0f98bd3b7b
commit
dff7cb9afb
@ -0,0 +1,112 @@ |
||||
package api |
||||
|
||||
import ( |
||||
"errors" |
||||
"fmt" |
||||
"net/http" |
||||
|
||||
"github.com/go-openapi/strfmt" |
||||
|
||||
alertingNotify "github.com/grafana/alerting/notify" |
||||
"github.com/grafana/grafana/pkg/api/response" |
||||
"github.com/grafana/grafana/pkg/services/accesscontrol" |
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" |
||||
authz "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol" |
||||
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" |
||||
"github.com/grafana/grafana/pkg/services/ngalert/notifier" |
||||
"github.com/grafana/grafana/pkg/util" |
||||
) |
||||
|
||||
func (srv AlertmanagerSrv) RouteGetSilence(c *contextmodel.ReqContext, silenceID string) response.Response { |
||||
am, errResp := srv.AlertmanagerFor(c.SignedInUser.GetOrgID()) |
||||
if errResp != nil { |
||||
return errResp |
||||
} |
||||
|
||||
gettableSilence, err := am.GetSilence(c.Req.Context(), silenceID) |
||||
if err != nil { |
||||
if errors.Is(err, alertingNotify.ErrSilenceNotFound) { |
||||
return ErrResp(http.StatusNotFound, err, "") |
||||
} |
||||
// any other error here should be an unexpected failure and thus an internal error
|
||||
return ErrResp(http.StatusInternalServerError, err, "") |
||||
} |
||||
return response.JSON(http.StatusOK, gettableSilence) |
||||
} |
||||
|
||||
func (srv AlertmanagerSrv) RouteGetSilences(c *contextmodel.ReqContext) response.Response { |
||||
am, errResp := srv.AlertmanagerFor(c.SignedInUser.GetOrgID()) |
||||
if errResp != nil { |
||||
return errResp |
||||
} |
||||
|
||||
gettableSilences, err := am.ListSilences(c.Req.Context(), c.QueryStrings("filter")) |
||||
if err != nil { |
||||
if errors.Is(err, alertingNotify.ErrListSilencesBadPayload) { |
||||
return ErrResp(http.StatusBadRequest, err, "") |
||||
} |
||||
// any other error here should be an unexpected failure and thus an internal error
|
||||
return ErrResp(http.StatusInternalServerError, err, "") |
||||
} |
||||
return response.JSON(http.StatusOK, gettableSilences) |
||||
} |
||||
|
||||
func (srv AlertmanagerSrv) RouteCreateSilence(c *contextmodel.ReqContext, postableSilence apimodels.PostableSilence) response.Response { |
||||
err := postableSilence.Validate(strfmt.Default) |
||||
if err != nil { |
||||
srv.log.Error("Silence failed validation", "error", err) |
||||
return ErrResp(http.StatusBadRequest, err, "silence failed validation") |
||||
} |
||||
|
||||
action := accesscontrol.ActionAlertingInstanceUpdate |
||||
if postableSilence.ID == "" { |
||||
action = accesscontrol.ActionAlertingInstanceCreate |
||||
} |
||||
evaluator := accesscontrol.EvalPermission(action) |
||||
if !accesscontrol.HasAccess(srv.ac, c)(evaluator) { |
||||
errAction := "update" |
||||
if postableSilence.ID == "" { |
||||
errAction = "create" |
||||
} |
||||
return response.Err(authz.NewAuthorizationErrorWithPermissions(fmt.Sprintf("%s silences", errAction), evaluator)) |
||||
} |
||||
|
||||
silenceID, err := srv.mam.CreateSilence(c.Req.Context(), c.SignedInUser.GetOrgID(), &postableSilence) |
||||
if err != nil { |
||||
if errors.Is(err, notifier.ErrNoAlertmanagerForOrg) { |
||||
return ErrResp(http.StatusNotFound, err, "") |
||||
} |
||||
if errors.Is(err, notifier.ErrAlertmanagerNotReady) { |
||||
return ErrResp(http.StatusConflict, err, "") |
||||
} |
||||
|
||||
if errors.Is(err, alertingNotify.ErrSilenceNotFound) { |
||||
return ErrResp(http.StatusNotFound, err, "") |
||||
} |
||||
|
||||
if errors.Is(err, alertingNotify.ErrCreateSilenceBadPayload) { |
||||
return ErrResp(http.StatusBadRequest, err, "") |
||||
} |
||||
|
||||
return ErrResp(http.StatusInternalServerError, err, "failed to create silence") |
||||
} |
||||
return response.JSON(http.StatusAccepted, apimodels.PostSilencesOKBody{ |
||||
SilenceID: silenceID, |
||||
}) |
||||
} |
||||
|
||||
func (srv AlertmanagerSrv) RouteDeleteSilence(c *contextmodel.ReqContext, silenceID string) response.Response { |
||||
if err := srv.mam.DeleteSilence(c.Req.Context(), c.SignedInUser.GetOrgID(), silenceID); err != nil { |
||||
if errors.Is(err, notifier.ErrNoAlertmanagerForOrg) { |
||||
return ErrResp(http.StatusNotFound, err, "") |
||||
} |
||||
if errors.Is(err, notifier.ErrAlertmanagerNotReady) { |
||||
return ErrResp(http.StatusConflict, err, "") |
||||
} |
||||
if errors.Is(err, alertingNotify.ErrSilenceNotFound) { |
||||
return ErrResp(http.StatusNotFound, err, "") |
||||
} |
||||
return ErrResp(http.StatusInternalServerError, err, "") |
||||
} |
||||
return response.JSON(http.StatusOK, util.DynMap{"message": "silence deleted"}) |
||||
} |
||||
@ -0,0 +1,194 @@ |
||||
package api |
||||
|
||||
import ( |
||||
"context" |
||||
"math/rand" |
||||
"net/http" |
||||
"testing" |
||||
"time" |
||||
|
||||
"github.com/go-openapi/strfmt" |
||||
amv2 "github.com/prometheus/alertmanager/api/v2/models" |
||||
"github.com/stretchr/testify/require" |
||||
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol" |
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" |
||||
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" |
||||
"github.com/grafana/grafana/pkg/services/org" |
||||
"github.com/grafana/grafana/pkg/services/user" |
||||
"github.com/grafana/grafana/pkg/util" |
||||
"github.com/grafana/grafana/pkg/web" |
||||
) |
||||
|
||||
func TestSilenceCreate(t *testing.T) { |
||||
makeSilence := func(comment string, createdBy string, |
||||
startsAt, endsAt strfmt.DateTime, matchers amv2.Matchers) amv2.Silence { |
||||
return amv2.Silence{ |
||||
Comment: &comment, |
||||
CreatedBy: &createdBy, |
||||
StartsAt: &startsAt, |
||||
EndsAt: &endsAt, |
||||
Matchers: matchers, |
||||
} |
||||
} |
||||
|
||||
now := time.Now() |
||||
dt := func(t time.Time) strfmt.DateTime { return strfmt.DateTime(t) } |
||||
tru := true |
||||
testString := "testName" |
||||
matchers := amv2.Matchers{&amv2.Matcher{Name: &testString, IsEqual: &tru, IsRegex: &tru, Value: &testString}} |
||||
|
||||
cases := []struct { |
||||
name string |
||||
silence amv2.Silence |
||||
status int |
||||
}{ |
||||
{"Valid Silence", |
||||
makeSilence("", "tests", dt(now), dt(now.Add(1*time.Second)), matchers), |
||||
http.StatusAccepted, |
||||
}, |
||||
{"No Comment Silence", |
||||
func() amv2.Silence { |
||||
s := makeSilence("", "tests", dt(now), dt(now.Add(1*time.Second)), matchers) |
||||
s.Comment = nil |
||||
return s |
||||
}(), |
||||
http.StatusBadRequest, |
||||
}, |
||||
} |
||||
|
||||
for _, cas := range cases { |
||||
t.Run(cas.name, func(t *testing.T) { |
||||
rc := contextmodel.ReqContext{ |
||||
Context: &web.Context{ |
||||
Req: &http.Request{}, |
||||
}, |
||||
SignedInUser: &user.SignedInUser{ |
||||
OrgRole: org.RoleEditor, |
||||
OrgID: 1, |
||||
Permissions: map[int64]map[string][]string{ |
||||
1: {accesscontrol.ActionAlertingInstanceCreate: {}}, |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
srv := createSut(t) |
||||
|
||||
resp := srv.RouteCreateSilence(&rc, amv2.PostableSilence{ |
||||
ID: "", |
||||
Silence: cas.silence, |
||||
}) |
||||
require.Equal(t, cas.status, resp.Status()) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestRouteCreateSilence(t *testing.T) { |
||||
tesCases := []struct { |
||||
name string |
||||
silence func() apimodels.PostableSilence |
||||
permissions map[int64]map[string][]string |
||||
expectedStatus int |
||||
}{ |
||||
{ |
||||
name: "new silence, role-based access control is enabled, not authorized", |
||||
silence: silenceGen(withEmptyID), |
||||
permissions: map[int64]map[string][]string{ |
||||
1: {}, |
||||
}, |
||||
expectedStatus: http.StatusForbidden, |
||||
}, |
||||
{ |
||||
name: "new silence, role-based access control is enabled, authorized", |
||||
silence: silenceGen(withEmptyID), |
||||
permissions: map[int64]map[string][]string{ |
||||
1: {accesscontrol.ActionAlertingInstanceCreate: {}}, |
||||
}, |
||||
expectedStatus: http.StatusAccepted, |
||||
}, |
||||
{ |
||||
name: "update silence, role-based access control is enabled, not authorized", |
||||
silence: silenceGen(), |
||||
permissions: map[int64]map[string][]string{ |
||||
1: {accesscontrol.ActionAlertingInstanceCreate: {}}, |
||||
}, |
||||
expectedStatus: http.StatusForbidden, |
||||
}, |
||||
{ |
||||
name: "update silence, role-based access control is enabled, authorized", |
||||
silence: silenceGen(), |
||||
permissions: map[int64]map[string][]string{ |
||||
1: {accesscontrol.ActionAlertingInstanceUpdate: {}}, |
||||
}, |
||||
expectedStatus: http.StatusAccepted, |
||||
}, |
||||
} |
||||
|
||||
for _, tesCase := range tesCases { |
||||
t.Run(tesCase.name, func(t *testing.T) { |
||||
sut := createSut(t) |
||||
|
||||
rc := contextmodel.ReqContext{ |
||||
Context: &web.Context{ |
||||
Req: &http.Request{}, |
||||
}, |
||||
SignedInUser: &user.SignedInUser{ |
||||
Permissions: tesCase.permissions, |
||||
OrgID: 1, |
||||
}, |
||||
} |
||||
|
||||
silence := tesCase.silence() |
||||
|
||||
if silence.ID != "" { |
||||
alertmanagerFor, err := sut.mam.AlertmanagerFor(1) |
||||
require.NoError(t, err) |
||||
silence.ID = "" |
||||
newID, err := alertmanagerFor.CreateSilence(context.Background(), &silence) |
||||
require.NoError(t, err) |
||||
silence.ID = newID |
||||
} |
||||
|
||||
response := sut.RouteCreateSilence(&rc, silence) |
||||
require.Equal(t, tesCase.expectedStatus, response.Status()) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func silenceGen(mutatorFuncs ...func(*apimodels.PostableSilence)) func() apimodels.PostableSilence { |
||||
return func() apimodels.PostableSilence { |
||||
testString := util.GenerateShortUID() |
||||
isEqual := rand.Int()%2 == 0 |
||||
isRegex := rand.Int()%2 == 0 |
||||
value := util.GenerateShortUID() |
||||
if isRegex { |
||||
value = ".*" + util.GenerateShortUID() |
||||
} |
||||
|
||||
matchers := amv2.Matchers{&amv2.Matcher{Name: &testString, IsEqual: &isEqual, IsRegex: &isRegex, Value: &value}} |
||||
comment := util.GenerateShortUID() |
||||
starts := strfmt.DateTime(timeNow().Add(-time.Duration(rand.Int63n(9)+1) * time.Second)) |
||||
ends := strfmt.DateTime(timeNow().Add(time.Duration(rand.Int63n(9)+1) * time.Second)) |
||||
createdBy := "User-" + util.GenerateShortUID() |
||||
s := apimodels.PostableSilence{ |
||||
ID: util.GenerateShortUID(), |
||||
Silence: amv2.Silence{ |
||||
Comment: &comment, |
||||
CreatedBy: &createdBy, |
||||
EndsAt: &ends, |
||||
Matchers: matchers, |
||||
StartsAt: &starts, |
||||
}, |
||||
} |
||||
|
||||
for _, mutator := range mutatorFuncs { |
||||
mutator(&s) |
||||
} |
||||
|
||||
return s |
||||
} |
||||
} |
||||
|
||||
func withEmptyID(silence *apimodels.PostableSilence) { |
||||
silence.ID = "" |
||||
} |
||||
@ -0,0 +1,11 @@ |
||||
package models |
||||
|
||||
// Silence is the model-layer representation of an alertmanager silence.
|
||||
type Silence struct { // TODO implement using matchers
|
||||
ID *string |
||||
RuleUID *string |
||||
} |
||||
|
||||
func (s *Silence) GetRuleUID() *string { |
||||
return s.RuleUID |
||||
} |
||||
Loading…
Reference in new issue