mirror of
https://github.com/grafana/grafana.git
synced 2025-02-11 08:05:43 -06:00
Alerting: Provisioning API - Contact points (#47197)
This commit is contained in:
parent
5fb80498b1
commit
388ecb4037
@ -77,6 +77,7 @@ type API struct {
|
||||
SecretsService secrets.Service
|
||||
AccessControl accesscontrol.AccessControl
|
||||
Policies *provisioning.NotificationPolicyService
|
||||
ContactPointService *provisioning.ContactPointService
|
||||
}
|
||||
|
||||
// RegisterAPIEndpoints registers API handlers
|
||||
@ -132,8 +133,9 @@ func (api *API) RegisterAPIEndpoints(m *metrics.API) {
|
||||
|
||||
if api.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagAlertProvisioning) {
|
||||
api.RegisterProvisioningApiEndpoints(NewForkedProvisioningApi(&ProvisioningSrv{
|
||||
log: logger,
|
||||
policies: api.Policies,
|
||||
log: logger,
|
||||
policies: api.Policies,
|
||||
contactPointService: api.ContactPointService,
|
||||
}), m)
|
||||
}
|
||||
}
|
||||
|
@ -9,20 +9,29 @@ import (
|
||||
"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"
|
||||
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"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
type ProvisioningSrv struct {
|
||||
log log.Logger
|
||||
policies NotificationPolicyService
|
||||
log log.Logger
|
||||
policies NotificationPolicyService
|
||||
contactPointService ContactPointService
|
||||
}
|
||||
|
||||
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
|
||||
DeleteContactPoint(ctx context.Context, orgID int64, uid string) error
|
||||
}
|
||||
|
||||
type NotificationPolicyService interface {
|
||||
GetPolicyTree(ctx context.Context, orgID int64) (provisioning.EmbeddedRoutingTree, error)
|
||||
UpdatePolicyTree(ctx context.Context, orgID int64, tree apimodels.Route, p domain.Provenance) error
|
||||
UpdatePolicyTree(ctx context.Context, orgID int64, tree apimodels.Route, p alerting_models.Provenance) error
|
||||
}
|
||||
|
||||
func (srv *ProvisioningSrv) RouteGetPolicyTree(c *models.ReqContext) response.Response {
|
||||
@ -39,7 +48,7 @@ func (srv *ProvisioningSrv) RouteGetPolicyTree(c *models.ReqContext) response.Re
|
||||
|
||||
func (srv *ProvisioningSrv) RoutePostPolicyTree(c *models.ReqContext, tree apimodels.Route) response.Response {
|
||||
// TODO: lift validation out of definitions.Rotue.UnmarshalJSON and friends into a dedicated validator.
|
||||
err := srv.policies.UpdatePolicyTree(c.Req.Context(), c.OrgId, tree, domain.ProvenanceApi)
|
||||
err := srv.policies.UpdatePolicyTree(c.Req.Context(), c.OrgId, tree, alerting_models.ProvenanceAPI)
|
||||
if errors.Is(err, store.ErrNoAlertmanagerConfiguration) {
|
||||
return ErrResp(http.StatusNotFound, err, "")
|
||||
}
|
||||
@ -49,3 +58,37 @@ func (srv *ProvisioningSrv) RoutePostPolicyTree(c *models.ReqContext, tree apimo
|
||||
|
||||
return response.JSON(http.StatusAccepted, util.DynMap{"message": "policies updated"})
|
||||
}
|
||||
|
||||
func (srv *ProvisioningSrv) RouteGetContactPoints(c *models.ReqContext) response.Response {
|
||||
cps, err := srv.contactPointService.GetContactPoints(c.Req.Context(), c.OrgId)
|
||||
if err != nil {
|
||||
return ErrResp(http.StatusInternalServerError, err, "")
|
||||
}
|
||||
return response.JSON(http.StatusOK, cps)
|
||||
}
|
||||
|
||||
func (srv *ProvisioningSrv) RoutePostContactPoint(c *models.ReqContext, cp apimodels.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 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 {
|
||||
err := srv.contactPointService.UpdateContactPoint(c.Req.Context(), c.OrgId, cp, alerting_models.ProvenanceAPI)
|
||||
if err != nil {
|
||||
return ErrResp(http.StatusInternalServerError, err, "")
|
||||
}
|
||||
return response.JSON(http.StatusAccepted, util.DynMap{"message": "contactpoint updated"})
|
||||
}
|
||||
|
||||
func (srv *ProvisioningSrv) RouteDeleteContactPoint(c *models.ReqContext) response.Response {
|
||||
cpID := web.Params(c.Req)[":ID"]
|
||||
err := srv.contactPointService.DeleteContactPoint(c.Req.Context(), c.OrgId, cpID)
|
||||
if err != nil {
|
||||
return ErrResp(http.StatusInternalServerError, err, "")
|
||||
}
|
||||
return response.JSON(http.StatusAccepted, util.DynMap{"message": "contactpoint deleted"})
|
||||
}
|
||||
|
@ -181,10 +181,14 @@ func (api *API) authorize(method, path string) web.Handler {
|
||||
return middleware.ReqOrgAdmin
|
||||
|
||||
// Grafana-only Provisioning Read Paths
|
||||
case http.MethodGet + "/api/provisioning/policies":
|
||||
case http.MethodGet + "/api/provisioning/policies",
|
||||
http.MethodGet + "/api/provisioning/contact-points":
|
||||
return middleware.ReqSignedIn
|
||||
|
||||
case http.MethodPost + "/api/provisioning/policies":
|
||||
case http.MethodPost + "/api/provisioning/policies",
|
||||
http.MethodPost + "/api/provisioning/contact-points",
|
||||
http.MethodPut + "/api/provisioning/contact-points",
|
||||
http.MethodDelete + "/api/provisioning/contact-points/{ID}":
|
||||
return middleware.ReqEditorRole
|
||||
}
|
||||
|
||||
|
@ -47,7 +47,7 @@ func TestAuthorize(t *testing.T) {
|
||||
}
|
||||
paths[p] = methods
|
||||
}
|
||||
require.Len(t, paths, 30)
|
||||
require.Len(t, paths, 32)
|
||||
|
||||
ac := acmock.New()
|
||||
api := &API{AccessControl: ac}
|
||||
|
@ -26,3 +26,19 @@ func (f *ForkedProvisioningApi) forkRouteGetPolicyTree(ctx *models.ReqContext) r
|
||||
func (f *ForkedProvisioningApi) forkRoutePostPolicyTree(ctx *models.ReqContext, route apimodels.Route) response.Response {
|
||||
return f.svc.RoutePostPolicyTree(ctx, route)
|
||||
}
|
||||
|
||||
func (f *ForkedProvisioningApi) forkRouteGetContactpoints(ctx *models.ReqContext) response.Response {
|
||||
return f.svc.RouteGetContactPoints(ctx)
|
||||
}
|
||||
|
||||
func (f *ForkedProvisioningApi) forkRoutePostContactpoints(ctx *models.ReqContext, cp apimodels.EmbeddedContactPoint) response.Response {
|
||||
return f.svc.RoutePostContactPoint(ctx, cp)
|
||||
}
|
||||
|
||||
func (f *ForkedProvisioningApi) forkRoutePutContactpoints(ctx *models.ReqContext, cp apimodels.EmbeddedContactPoint) response.Response {
|
||||
return f.svc.RoutePutContactPoint(ctx, cp)
|
||||
}
|
||||
|
||||
func (f *ForkedProvisioningApi) forkRouteDeleteContactpoints(ctx *models.ReqContext) response.Response {
|
||||
return f.svc.RouteDeleteContactPoint(ctx)
|
||||
}
|
||||
|
@ -20,14 +20,34 @@ import (
|
||||
)
|
||||
|
||||
type ProvisioningApiForkingService interface {
|
||||
RouteDeleteContactpoints(*models.ReqContext) response.Response
|
||||
RouteGetContactpoints(*models.ReqContext) response.Response
|
||||
RouteGetPolicyTree(*models.ReqContext) response.Response
|
||||
RoutePostContactpoints(*models.ReqContext) response.Response
|
||||
RoutePostPolicyTree(*models.ReqContext) response.Response
|
||||
RoutePutContactpoints(*models.ReqContext) response.Response
|
||||
}
|
||||
|
||||
func (f *ForkedProvisioningApi) RouteDeleteContactpoints(ctx *models.ReqContext) response.Response {
|
||||
return f.forkRouteDeleteContactpoints(ctx)
|
||||
}
|
||||
|
||||
func (f *ForkedProvisioningApi) RouteGetContactpoints(ctx *models.ReqContext) response.Response {
|
||||
return f.forkRouteGetContactpoints(ctx)
|
||||
}
|
||||
|
||||
func (f *ForkedProvisioningApi) RouteGetPolicyTree(ctx *models.ReqContext) response.Response {
|
||||
return f.forkRouteGetPolicyTree(ctx)
|
||||
}
|
||||
|
||||
func (f *ForkedProvisioningApi) RoutePostContactpoints(ctx *models.ReqContext) response.Response {
|
||||
conf := apimodels.EmbeddedContactPoint{}
|
||||
if err := web.Bind(ctx.Req, &conf); err != nil {
|
||||
return response.Error(http.StatusBadRequest, "bad request data", err)
|
||||
}
|
||||
return f.forkRoutePostContactpoints(ctx, conf)
|
||||
}
|
||||
|
||||
func (f *ForkedProvisioningApi) RoutePostPolicyTree(ctx *models.ReqContext) response.Response {
|
||||
conf := apimodels.Route{}
|
||||
if err := web.Bind(ctx.Req, &conf); err != nil {
|
||||
@ -36,8 +56,36 @@ func (f *ForkedProvisioningApi) RoutePostPolicyTree(ctx *models.ReqContext) resp
|
||||
return f.forkRoutePostPolicyTree(ctx, conf)
|
||||
}
|
||||
|
||||
func (f *ForkedProvisioningApi) RoutePutContactpoints(ctx *models.ReqContext) response.Response {
|
||||
conf := apimodels.EmbeddedContactPoint{}
|
||||
if err := web.Bind(ctx.Req, &conf); err != nil {
|
||||
return response.Error(http.StatusBadRequest, "bad request data", err)
|
||||
}
|
||||
return f.forkRoutePutContactpoints(ctx, conf)
|
||||
}
|
||||
|
||||
func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApiForkingService, m *metrics.API) {
|
||||
api.RouteRegister.Group("", func(group routing.RouteRegister) {
|
||||
group.Delete(
|
||||
toMacaronPath("/api/provisioning/contact-points/{ID}"),
|
||||
api.authorize(http.MethodDelete, "/api/provisioning/contact-points/{ID}"),
|
||||
metrics.Instrument(
|
||||
http.MethodDelete,
|
||||
"/api/provisioning/contact-points/{ID}",
|
||||
srv.RouteDeleteContactpoints,
|
||||
m,
|
||||
),
|
||||
)
|
||||
group.Get(
|
||||
toMacaronPath("/api/provisioning/contact-points"),
|
||||
api.authorize(http.MethodGet, "/api/provisioning/contact-points"),
|
||||
metrics.Instrument(
|
||||
http.MethodGet,
|
||||
"/api/provisioning/contact-points",
|
||||
srv.RouteGetContactpoints,
|
||||
m,
|
||||
),
|
||||
)
|
||||
group.Get(
|
||||
toMacaronPath("/api/provisioning/policies"),
|
||||
api.authorize(http.MethodGet, "/api/provisioning/policies"),
|
||||
@ -48,6 +96,16 @@ func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApiForkingServi
|
||||
m,
|
||||
),
|
||||
)
|
||||
group.Post(
|
||||
toMacaronPath("/api/provisioning/contact-points"),
|
||||
api.authorize(http.MethodPost, "/api/provisioning/contact-points"),
|
||||
metrics.Instrument(
|
||||
http.MethodPost,
|
||||
"/api/provisioning/contact-points",
|
||||
srv.RoutePostContactpoints,
|
||||
m,
|
||||
),
|
||||
)
|
||||
group.Post(
|
||||
toMacaronPath("/api/provisioning/policies"),
|
||||
api.authorize(http.MethodPost, "/api/provisioning/policies"),
|
||||
@ -58,5 +116,15 @@ func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApiForkingServi
|
||||
m,
|
||||
),
|
||||
)
|
||||
group.Put(
|
||||
toMacaronPath("/api/provisioning/contact-points"),
|
||||
api.authorize(http.MethodPut, "/api/provisioning/contact-points"),
|
||||
metrics.Instrument(
|
||||
http.MethodPut,
|
||||
"/api/provisioning/contact-points",
|
||||
srv.RoutePutContactpoints,
|
||||
m,
|
||||
),
|
||||
)
|
||||
}, middleware.ReqSignedIn)
|
||||
}
|
||||
|
@ -0,0 +1,157 @@
|
||||
package definitions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/notifier/channels"
|
||||
)
|
||||
|
||||
// swagger:route GET /api/provisioning/contact-points provisioning RouteGetContactpoints
|
||||
//
|
||||
// Get all the contact points.
|
||||
//
|
||||
// Responses:
|
||||
// 200: Route
|
||||
// 400: ValidationError
|
||||
|
||||
// swagger:route POST /api/provisioning/contact-points provisioning RoutePostContactpoints
|
||||
//
|
||||
// Create a contact point.
|
||||
//
|
||||
// Consumes:
|
||||
// - application/json
|
||||
//
|
||||
// Responses:
|
||||
// 202: Accepted
|
||||
// 400: ValidationError
|
||||
|
||||
// swagger:route PUT /api/provisioning/contact-points provisioning RoutePutContactpoints
|
||||
//
|
||||
// Update an existing contact point.
|
||||
//
|
||||
// Consumes:
|
||||
// - application/json
|
||||
//
|
||||
// Responses:
|
||||
// 202: Accepted
|
||||
// 400: ValidationError
|
||||
|
||||
// swagger:route DELETE /api/provisioning/contact-points/{ID} provisioning RouteDeleteContactpoints
|
||||
//
|
||||
// Delete a contact point.
|
||||
//
|
||||
// Consumes:
|
||||
// - application/json
|
||||
//
|
||||
// Responses:
|
||||
// 202: Accepted
|
||||
// 400: ValidationError
|
||||
|
||||
// swagger:parameters RoutePostContactpoints RoutePutContactpoints
|
||||
type ContactPointPayload struct {
|
||||
// in:body
|
||||
Body EmbeddedContactPoint
|
||||
}
|
||||
|
||||
// EmbeddedContactPoint is the contact point type that is used
|
||||
// by grafanas embedded alertmanager implementation.
|
||||
type EmbeddedContactPoint struct {
|
||||
// UID is the unique identifier of the contact point. This will be
|
||||
// automatically set be the Grafana.
|
||||
UID string `json:"uid"`
|
||||
// Name is used as grouping key in the UI. Contact points with the
|
||||
// same name will be grouped in the UI.
|
||||
Name string `json:"name" binding:"required"`
|
||||
Type string `json:"type" binding:"required"`
|
||||
Settings *simplejson.Json `json:"settings" binding:"required"`
|
||||
DisableResolveMessage bool `json:"disableResolveMessage"`
|
||||
Provenance string `json:"provanance"`
|
||||
}
|
||||
|
||||
const RedactedValue = "[REDACTED]"
|
||||
|
||||
func (e *EmbeddedContactPoint) Valid(decryptFunc channels.GetDecryptedValueFn) error {
|
||||
if e.Type == "" {
|
||||
return fmt.Errorf("type should not be an empty string")
|
||||
}
|
||||
if e.Settings == nil {
|
||||
return fmt.Errorf("settings should not be empty")
|
||||
}
|
||||
factory, exists := channels.Factory(e.Type)
|
||||
if !exists {
|
||||
return fmt.Errorf("unknown type '%s'", e.Type)
|
||||
}
|
||||
cfg, _ := channels.NewFactoryConfig(&channels.NotificationChannelConfig{
|
||||
Settings: e.Settings,
|
||||
Type: e.Type,
|
||||
}, nil, decryptFunc, nil)
|
||||
if _, err := factory(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *EmbeddedContactPoint) SecretKeys() ([]string, error) {
|
||||
switch e.Type {
|
||||
case "alertmanager":
|
||||
return []string{"basicAuthPassword"}, nil
|
||||
case "dingding":
|
||||
return []string{}, nil
|
||||
case "discord":
|
||||
return []string{}, nil
|
||||
case "email":
|
||||
return []string{}, nil
|
||||
case "googlechat":
|
||||
return []string{}, nil
|
||||
case "kafka":
|
||||
return []string{}, nil
|
||||
case "line":
|
||||
return []string{"token"}, nil
|
||||
case "opsgenie":
|
||||
return []string{"apiKey"}, nil
|
||||
case "pagerduty":
|
||||
return []string{"integrationKey"}, nil
|
||||
case "pushover":
|
||||
return []string{"userKey", "apiToken"}, nil
|
||||
case "sensugo":
|
||||
return []string{"apiKey"}, nil
|
||||
case "slack":
|
||||
return []string{"url", "token"}, nil
|
||||
case "teams":
|
||||
return []string{}, nil
|
||||
case "telegram":
|
||||
return []string{"bottoken"}, nil
|
||||
case "threema":
|
||||
return []string{"api_secret"}, nil
|
||||
case "victorops":
|
||||
return []string{}, nil
|
||||
case "webhook":
|
||||
return []string{}, nil
|
||||
case "wecom":
|
||||
return []string{"url"}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("no secrets configured for type '%s'", e.Type)
|
||||
}
|
||||
|
||||
func (e *EmbeddedContactPoint) ExtractSecrets() (map[string]string, error) {
|
||||
secrets := map[string]string{}
|
||||
secretKeys, err := e.SecretKeys()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, secretKey := range secretKeys {
|
||||
secretValue := e.Settings.Get(secretKey).MustString()
|
||||
e.Settings.Del(secretKey)
|
||||
secrets[secretKey] = secretValue
|
||||
}
|
||||
return secrets, nil
|
||||
}
|
||||
|
||||
func (e *EmbeddedContactPoint) ResourceID() string {
|
||||
return e.UID
|
||||
}
|
||||
|
||||
func (e *EmbeddedContactPoint) ResourceType() string {
|
||||
return "contactPoint"
|
||||
}
|
@ -558,6 +558,36 @@
|
||||
"type": "object",
|
||||
"x-go-package": "github.com/prometheus/alertmanager/config"
|
||||
},
|
||||
"EmbeddedContactPoint": {
|
||||
"description": "EmbeddedContactPoint is the contact point type that is used\nby grafanas embedded alertmanager implementation.",
|
||||
"properties": {
|
||||
"disableResolveMessage": {
|
||||
"type": "boolean",
|
||||
"x-go-name": "DisableResolveMessage"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"x-go-name": "Name"
|
||||
},
|
||||
"provanance": {
|
||||
"type": "string",
|
||||
"x-go-name": "Provenance"
|
||||
},
|
||||
"settings": {
|
||||
"$ref": "#/definitions/Json"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"x-go-name": "Type"
|
||||
},
|
||||
"uid": {
|
||||
"type": "string",
|
||||
"x-go-name": "UID"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||
},
|
||||
"ErrorType": {
|
||||
"title": "ErrorType models the different API error types.",
|
||||
"type": "string",
|
||||
@ -3034,6 +3064,7 @@
|
||||
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
|
||||
},
|
||||
"alertGroup": {
|
||||
"description": "AlertGroup alert group",
|
||||
"properties": {
|
||||
"alerts": {
|
||||
"description": "alerts",
|
||||
@ -3055,17 +3086,14 @@
|
||||
"labels",
|
||||
"receiver"
|
||||
],
|
||||
"type": "object",
|
||||
"x-go-name": "AlertGroup",
|
||||
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
|
||||
"type": "object"
|
||||
},
|
||||
"alertGroups": {
|
||||
"description": "AlertGroups alert groups",
|
||||
"items": {
|
||||
"$ref": "#/definitions/alertGroup"
|
||||
},
|
||||
"type": "array",
|
||||
"x-go-name": "AlertGroups",
|
||||
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
|
||||
"type": "array"
|
||||
},
|
||||
"alertStatus": {
|
||||
"description": "AlertStatus alert status",
|
||||
@ -3255,6 +3283,7 @@
|
||||
"type": "array"
|
||||
},
|
||||
"gettableSilence": {
|
||||
"description": "GettableSilence gettable silence",
|
||||
"properties": {
|
||||
"comment": {
|
||||
"description": "comment",
|
||||
@ -3306,17 +3335,14 @@
|
||||
"status",
|
||||
"updatedAt"
|
||||
],
|
||||
"type": "object",
|
||||
"x-go-name": "GettableSilence",
|
||||
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
|
||||
"type": "object"
|
||||
},
|
||||
"gettableSilences": {
|
||||
"description": "GettableSilences gettable silences",
|
||||
"items": {
|
||||
"$ref": "#/definitions/gettableSilence"
|
||||
},
|
||||
"type": "array",
|
||||
"x-go-name": "GettableSilences",
|
||||
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
|
||||
"type": "array"
|
||||
},
|
||||
"labelSet": {
|
||||
"additionalProperties": {
|
||||
@ -3445,7 +3471,6 @@
|
||||
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
|
||||
},
|
||||
"postableSilence": {
|
||||
"description": "PostableSilence postable silence",
|
||||
"properties": {
|
||||
"comment": {
|
||||
"description": "comment",
|
||||
@ -3485,7 +3510,9 @@
|
||||
"matchers",
|
||||
"startsAt"
|
||||
],
|
||||
"type": "object"
|
||||
"type": "object",
|
||||
"x-go-name": "PostableSilence",
|
||||
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
|
||||
},
|
||||
"receiver": {
|
||||
"properties": {
|
||||
@ -4733,6 +4760,112 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/provisioning/contact-points": {
|
||||
"get": {
|
||||
"operationId": "RouteGetContactpoints",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Route",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/Route"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "ValidationError",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
"summary": "Get all the contact points.",
|
||||
"tags": [
|
||||
"provisioning"
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"operationId": "RoutePostContactpoints",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "body",
|
||||
"name": "Body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/EmbeddedContactPoint"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"202": {
|
||||
"$ref": "#/responses/Accepted"
|
||||
},
|
||||
"400": {
|
||||
"description": "ValidationError",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
"summary": "Create a contact point.",
|
||||
"tags": [
|
||||
"provisioning"
|
||||
]
|
||||
},
|
||||
"put": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"operationId": "RoutePutContactpoints",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "body",
|
||||
"name": "Body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/EmbeddedContactPoint"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"202": {
|
||||
"$ref": "#/responses/Accepted"
|
||||
},
|
||||
"400": {
|
||||
"description": "ValidationError",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
"summary": "Update an existing contact point.",
|
||||
"tags": [
|
||||
"provisioning"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/provisioning/contact-points/{ID}": {
|
||||
"delete": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"operationId": "RouteDeleteContactpoints",
|
||||
"responses": {
|
||||
"202": {
|
||||
"$ref": "#/responses/Accepted"
|
||||
},
|
||||
"400": {
|
||||
"description": "ValidationError",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
"summary": "Delete a contact point.",
|
||||
"tags": [
|
||||
"provisioning"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/provisioning/policies": {
|
||||
"get": {
|
||||
"operationId": "RouteGetPolicyTree",
|
||||
|
@ -1136,6 +1136,112 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/provisioning/contact-points": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"provisioning"
|
||||
],
|
||||
"summary": "Get all the contact points.",
|
||||
"operationId": "RouteGetContactpoints",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Route",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/Route"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "ValidationError",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"provisioning"
|
||||
],
|
||||
"summary": "Update an existing contact point.",
|
||||
"operationId": "RoutePutContactpoints",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Body",
|
||||
"in": "body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/EmbeddedContactPoint"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"202": {
|
||||
"$ref": "#/responses/Accepted"
|
||||
},
|
||||
"400": {
|
||||
"description": "ValidationError",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"provisioning"
|
||||
],
|
||||
"summary": "Create a contact point.",
|
||||
"operationId": "RoutePostContactpoints",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Body",
|
||||
"in": "body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/EmbeddedContactPoint"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"202": {
|
||||
"$ref": "#/responses/Accepted"
|
||||
},
|
||||
"400": {
|
||||
"description": "ValidationError",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/provisioning/contact-points/{ID}": {
|
||||
"delete": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"provisioning"
|
||||
],
|
||||
"summary": "Delete a contact point.",
|
||||
"operationId": "RouteDeleteContactpoints",
|
||||
"responses": {
|
||||
"202": {
|
||||
"$ref": "#/responses/Accepted"
|
||||
},
|
||||
"400": {
|
||||
"description": "ValidationError",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/provisioning/policies": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@ -2369,6 +2475,36 @@
|
||||
},
|
||||
"x-go-package": "github.com/prometheus/alertmanager/config"
|
||||
},
|
||||
"EmbeddedContactPoint": {
|
||||
"description": "EmbeddedContactPoint is the contact point type that is used\nby grafanas embedded alertmanager implementation.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"disableResolveMessage": {
|
||||
"type": "boolean",
|
||||
"x-go-name": "DisableResolveMessage"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"x-go-name": "Name"
|
||||
},
|
||||
"provanance": {
|
||||
"type": "string",
|
||||
"x-go-name": "Provenance"
|
||||
},
|
||||
"settings": {
|
||||
"$ref": "#/definitions/Json"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"x-go-name": "Type"
|
||||
},
|
||||
"uid": {
|
||||
"type": "string",
|
||||
"x-go-name": "UID"
|
||||
}
|
||||
},
|
||||
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||
},
|
||||
"ErrorType": {
|
||||
"type": "string",
|
||||
"title": "ErrorType models the different API error types.",
|
||||
@ -4848,6 +4984,7 @@
|
||||
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
|
||||
},
|
||||
"alertGroup": {
|
||||
"description": "AlertGroup alert group",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"alerts",
|
||||
@ -4870,17 +5007,14 @@
|
||||
"$ref": "#/definitions/receiver"
|
||||
}
|
||||
},
|
||||
"x-go-name": "AlertGroup",
|
||||
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models",
|
||||
"$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": {
|
||||
@ -5073,6 +5207,7 @@
|
||||
"$ref": "#/definitions/gettableAlerts"
|
||||
},
|
||||
"gettableSilence": {
|
||||
"description": "GettableSilence gettable silence",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"comment",
|
||||
@ -5125,17 +5260,14 @@
|
||||
"x-go-name": "UpdatedAt"
|
||||
}
|
||||
},
|
||||
"x-go-name": "GettableSilence",
|
||||
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models",
|
||||
"$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": {
|
||||
@ -5265,7 +5397,6 @@
|
||||
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
|
||||
},
|
||||
"postableSilence": {
|
||||
"description": "PostableSilence postable silence",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"comment",
|
||||
@ -5306,6 +5437,8 @@
|
||||
"x-go-name": "StartsAt"
|
||||
}
|
||||
},
|
||||
"x-go-name": "PostableSilence",
|
||||
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models",
|
||||
"$ref": "#/definitions/postableSilence"
|
||||
},
|
||||
"receiver": {
|
||||
|
@ -3,8 +3,10 @@ package models
|
||||
type Provenance string
|
||||
|
||||
const (
|
||||
// ProvenanceNone reflects the provenance when no provenance is stored
|
||||
// for the requested object in the database.
|
||||
ProvenanceNone Provenance = ""
|
||||
ProvenanceApi Provenance = "api"
|
||||
ProvenanceAPI Provenance = "api"
|
||||
ProvenanceFile Provenance = "file"
|
||||
)
|
||||
|
||||
|
@ -139,6 +139,7 @@ func (ng *AlertNG) init() error {
|
||||
|
||||
// Provisioning
|
||||
policyService := provisioning.NewNotificationPolicyService(store, store, store, ng.Log)
|
||||
contactPointService := provisioning.NewContactPointService(store, ng.SecretsService, store, store, ng.Log)
|
||||
|
||||
api := api.API{
|
||||
Cfg: ng.Cfg,
|
||||
@ -158,6 +159,7 @@ func (ng *AlertNG) init() error {
|
||||
StateManager: ng.stateManager,
|
||||
AccessControl: ng.accesscontrol,
|
||||
Policies: policyService,
|
||||
ContactPointService: contactPointService,
|
||||
}
|
||||
api.RegisterAPIEndpoints(ng.Metrics.GetAPIMetrics())
|
||||
|
||||
|
402
pkg/services/ngalert/provisioning/contactpoints.go
Normal file
402
pkg/services/ngalert/provisioning/contactpoints.go
Normal file
@ -0,0 +1,402 @@
|
||||
package provisioning
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
type ContactPointService struct {
|
||||
amStore AMConfigStore
|
||||
encryptionService secrets.Service
|
||||
provenanceStore ProvisioningStore
|
||||
xact TransactionManager
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func NewContactPointService(store store.AlertingStore, encryptionService secrets.Service,
|
||||
provenanceStore ProvisioningStore, xact TransactionManager, log log.Logger) *ContactPointService {
|
||||
return &ContactPointService{
|
||||
amStore: store,
|
||||
encryptionService: encryptionService,
|
||||
provenanceStore: provenanceStore,
|
||||
xact: xact,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
func (ecp *ContactPointService) GetContactPoints(ctx context.Context, orgID int64) ([]apimodels.EmbeddedContactPoint, error) {
|
||||
cfg, _, err := ecp.getCurrentConfig(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
provenances, err := ecp.provenanceStore.GetProvenances(ctx, orgID, "contactPoint")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
contactPoints := []apimodels.EmbeddedContactPoint{}
|
||||
for _, contactPoint := range cfg.GetGrafanaReceiverMap() {
|
||||
embeddedContactPoint := apimodels.EmbeddedContactPoint{
|
||||
UID: contactPoint.UID,
|
||||
Type: contactPoint.Type,
|
||||
Name: contactPoint.Name,
|
||||
DisableResolveMessage: contactPoint.DisableResolveMessage,
|
||||
Settings: contactPoint.Settings,
|
||||
}
|
||||
if val, exists := provenances[embeddedContactPoint.UID]; exists && val != "" {
|
||||
embeddedContactPoint.Provenance = string(val)
|
||||
}
|
||||
for k, v := range contactPoint.SecureSettings {
|
||||
decryptedValue, err := ecp.decryptValue(v)
|
||||
if err != nil {
|
||||
ecp.log.Warn("decrypting value failed", "err", err.Error())
|
||||
continue
|
||||
}
|
||||
if decryptedValue == "" {
|
||||
continue
|
||||
}
|
||||
embeddedContactPoint.Settings.Set(k, apimodels.RedactedValue)
|
||||
}
|
||||
contactPoints = append(contactPoints, embeddedContactPoint)
|
||||
}
|
||||
sort.SliceStable(contactPoints, func(i, j int) bool {
|
||||
return contactPoints[i].Name < contactPoints[j].Name
|
||||
})
|
||||
return contactPoints, nil
|
||||
}
|
||||
|
||||
// internal only
|
||||
func (ecp *ContactPointService) getContactPointDecrypted(ctx context.Context, orgID int64, uid string) (apimodels.EmbeddedContactPoint, error) {
|
||||
cfg, _, err := ecp.getCurrentConfig(ctx, orgID)
|
||||
if err != nil {
|
||||
return apimodels.EmbeddedContactPoint{}, err
|
||||
}
|
||||
for _, receiver := range cfg.GetGrafanaReceiverMap() {
|
||||
if receiver.UID != uid {
|
||||
continue
|
||||
}
|
||||
embeddedContactPoint := apimodels.EmbeddedContactPoint{
|
||||
UID: receiver.UID,
|
||||
Type: receiver.Type,
|
||||
Name: receiver.Name,
|
||||
DisableResolveMessage: receiver.DisableResolveMessage,
|
||||
Settings: receiver.Settings,
|
||||
}
|
||||
for k, v := range receiver.SecureSettings {
|
||||
decryptedValue, err := ecp.decryptValue(v)
|
||||
if err != nil {
|
||||
ecp.log.Warn("decrypting value failed", "err", err.Error())
|
||||
continue
|
||||
}
|
||||
if decryptedValue == "" {
|
||||
continue
|
||||
}
|
||||
embeddedContactPoint.Settings.Set(k, decryptedValue)
|
||||
}
|
||||
return embeddedContactPoint, nil
|
||||
}
|
||||
return apimodels.EmbeddedContactPoint{}, fmt.Errorf("contact point with uid '%s' not found", uid)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
cfg, fetchedHash, err := ecp.getCurrentConfig(ctx, orgID)
|
||||
if err != nil {
|
||||
return apimodels.EmbeddedContactPoint{}, err
|
||||
}
|
||||
|
||||
extractedSecrets, err := contactPoint.ExtractSecrets()
|
||||
if err != nil {
|
||||
return apimodels.EmbeddedContactPoint{}, err
|
||||
}
|
||||
|
||||
for k, v := range extractedSecrets {
|
||||
encryptedValue, err := ecp.encryptValue(v)
|
||||
if err != nil {
|
||||
return apimodels.EmbeddedContactPoint{}, err
|
||||
}
|
||||
extractedSecrets[k] = encryptedValue
|
||||
}
|
||||
|
||||
contactPoint.UID = util.GenerateShortUID()
|
||||
grafanaReceiver := &apimodels.PostableGrafanaReceiver{
|
||||
UID: contactPoint.UID,
|
||||
Name: contactPoint.Name,
|
||||
Type: contactPoint.Type,
|
||||
DisableResolveMessage: contactPoint.DisableResolveMessage,
|
||||
Settings: contactPoint.Settings,
|
||||
SecureSettings: extractedSecrets,
|
||||
}
|
||||
|
||||
receiverFound := false
|
||||
for _, receiver := range cfg.AlertmanagerConfig.Receivers {
|
||||
if receiver.Name == contactPoint.Name {
|
||||
receiver.PostableGrafanaReceivers.GrafanaManagedReceivers = append(receiver.PostableGrafanaReceivers.GrafanaManagedReceivers, grafanaReceiver)
|
||||
receiverFound = true
|
||||
}
|
||||
}
|
||||
|
||||
if !receiverFound {
|
||||
cfg.AlertmanagerConfig.Receivers = append(cfg.AlertmanagerConfig.Receivers, &apimodels.PostableApiReceiver{
|
||||
Receiver: config.Receiver{
|
||||
Name: grafanaReceiver.Name,
|
||||
},
|
||||
PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{
|
||||
GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{grafanaReceiver},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
data, err := json.Marshal(cfg)
|
||||
if err != nil {
|
||||
return apimodels.EmbeddedContactPoint{}, err
|
||||
}
|
||||
|
||||
err = ecp.xact.InTransaction(ctx, func(ctx context.Context) error {
|
||||
err = ecp.amStore.UpdateAlertmanagerConfiguration(ctx, &models.SaveAlertmanagerConfigurationCmd{
|
||||
AlertmanagerConfiguration: string(data),
|
||||
FetchedConfigurationHash: fetchedHash,
|
||||
ConfigurationVersion: "v1",
|
||||
Default: false,
|
||||
OrgID: orgID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
adapter := provenanceOrgAdapter{
|
||||
inner: &contactPoint,
|
||||
orgID: orgID,
|
||||
}
|
||||
err = ecp.provenanceStore.SetProvenance(ctx, adapter, provenance)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
contactPoint.Provenance = string(provenance)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return apimodels.EmbeddedContactPoint{}, err
|
||||
}
|
||||
for k := range extractedSecrets {
|
||||
contactPoint.Settings.Set(k, apimodels.RedactedValue)
|
||||
}
|
||||
return contactPoint, nil
|
||||
}
|
||||
|
||||
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
|
||||
rawContactPoint, err := ecp.getContactPointDecrypted(ctx, orgID, contactPoint.UID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
secretKeys, err := contactPoint.SecretKeys()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, secretKey := range secretKeys {
|
||||
secretValue := contactPoint.Settings.Get(secretKey).MustString()
|
||||
if secretValue == apimodels.RedactedValue {
|
||||
contactPoint.Settings.Set(secretKey, rawContactPoint.Settings.Get(secretKey).MustString())
|
||||
}
|
||||
}
|
||||
// validate merged values
|
||||
if err := contactPoint.Valid(ecp.encryptionService.GetDecryptedValue); err != nil {
|
||||
return err
|
||||
}
|
||||
// check that provenance is not changed in a invalid way
|
||||
storedProvenance, err := ecp.provenanceStore.GetProvenance(ctx, provenanceOrgAdapter{
|
||||
inner: &contactPoint,
|
||||
orgID: orgID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if storedProvenance != provenance && storedProvenance != models.ProvenanceNone {
|
||||
return fmt.Errorf("cannot changed provenance from '%s' to '%s'", storedProvenance, provenance)
|
||||
}
|
||||
// transform to internal model
|
||||
extractedSecrets, err := contactPoint.ExtractSecrets()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for k, v := range extractedSecrets {
|
||||
encryptedValue, err := ecp.encryptValue(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
extractedSecrets[k] = encryptedValue
|
||||
}
|
||||
mergedReceiver := &apimodels.PostableGrafanaReceiver{
|
||||
UID: contactPoint.UID,
|
||||
Name: contactPoint.Name,
|
||||
Type: contactPoint.Type,
|
||||
DisableResolveMessage: contactPoint.DisableResolveMessage,
|
||||
Settings: contactPoint.Settings,
|
||||
SecureSettings: extractedSecrets,
|
||||
}
|
||||
// save to store
|
||||
cfg, fetchedHash, err := ecp.getCurrentConfig(ctx, orgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, receiver := range cfg.AlertmanagerConfig.Receivers {
|
||||
if receiver.Name == contactPoint.Name {
|
||||
receiverNotFound := true
|
||||
for i, grafanaReceiver := range receiver.GrafanaManagedReceivers {
|
||||
if grafanaReceiver.UID == mergedReceiver.UID {
|
||||
receiverNotFound = false
|
||||
receiver.GrafanaManagedReceivers[i] = mergedReceiver
|
||||
break
|
||||
}
|
||||
}
|
||||
if receiverNotFound {
|
||||
return fmt.Errorf("contact point with uid '%s' not found", mergedReceiver.UID)
|
||||
}
|
||||
}
|
||||
}
|
||||
data, err := json.Marshal(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ecp.xact.InTransaction(ctx, func(ctx context.Context) error {
|
||||
err = ecp.amStore.UpdateAlertmanagerConfiguration(ctx, &models.SaveAlertmanagerConfigurationCmd{
|
||||
AlertmanagerConfiguration: string(data),
|
||||
FetchedConfigurationHash: fetchedHash,
|
||||
ConfigurationVersion: "v1",
|
||||
Default: false,
|
||||
OrgID: orgID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
adapter := provenanceOrgAdapter{
|
||||
inner: &contactPoint,
|
||||
orgID: orgID,
|
||||
}
|
||||
err = ecp.provenanceStore.SetProvenance(ctx, adapter, provenance)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
contactPoint.Provenance = string(provenance)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (ecp *ContactPointService) DeleteContactPoint(ctx context.Context, orgID int64, uid string) error {
|
||||
cfg, fetchedHash, err := ecp.getCurrentConfig(ctx, orgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Indicates if the full contact point is removed or just one of the
|
||||
// configurations, as a contactpoint can consist of any number of
|
||||
// configurations.
|
||||
fullRemoval := false
|
||||
// Name of the contact point that will be removed, might be used if a
|
||||
// full removal is done to check if it's referenced in any route.
|
||||
name := ""
|
||||
for i, receiver := range cfg.AlertmanagerConfig.Receivers {
|
||||
for j, grafanaReceiver := range receiver.GrafanaManagedReceivers {
|
||||
if grafanaReceiver.UID == uid {
|
||||
name = grafanaReceiver.Name
|
||||
receiver.GrafanaManagedReceivers = append(receiver.GrafanaManagedReceivers[:j], receiver.GrafanaManagedReceivers[j+1:]...)
|
||||
// if this was the last receiver we removed, we remove the whole receiver
|
||||
if len(receiver.GrafanaManagedReceivers) == 0 {
|
||||
fullRemoval = true
|
||||
cfg.AlertmanagerConfig.Receivers = append(cfg.AlertmanagerConfig.Receivers[:i], cfg.AlertmanagerConfig.Receivers[i+1:]...)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if fullRemoval && isContactPointInUse(name, []*apimodels.Route{cfg.AlertmanagerConfig.Route}) {
|
||||
return fmt.Errorf("contact point '%s' is currently used by a notification policy", name)
|
||||
}
|
||||
data, err := json.Marshal(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ecp.xact.InTransaction(ctx, func(ctx context.Context) error {
|
||||
err := ecp.provenanceStore.DeleteProvenance(ctx, provenanceOrgAdapter{
|
||||
inner: &apimodels.EmbeddedContactPoint{
|
||||
UID: uid,
|
||||
},
|
||||
orgID: orgID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ecp.amStore.UpdateAlertmanagerConfiguration(ctx, &models.SaveAlertmanagerConfigurationCmd{
|
||||
AlertmanagerConfiguration: string(data),
|
||||
FetchedConfigurationHash: fetchedHash,
|
||||
ConfigurationVersion: "v1",
|
||||
Default: false,
|
||||
OrgID: orgID,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (ecp *ContactPointService) getCurrentConfig(ctx context.Context, orgID int64) (*apimodels.PostableUserConfig, string, error) {
|
||||
query := &models.GetLatestAlertmanagerConfigurationQuery{
|
||||
OrgID: orgID,
|
||||
}
|
||||
err := ecp.amStore.GetLatestAlertmanagerConfiguration(ctx, query)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
cfg, err := DeserializeAlertmanagerConfig([]byte(query.Result.AlertmanagerConfiguration))
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return cfg, query.Result.ConfigurationHash, nil
|
||||
}
|
||||
|
||||
func isContactPointInUse(name string, routes []*apimodels.Route) bool {
|
||||
if len(routes) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, route := range routes {
|
||||
if route.Receiver == name {
|
||||
return true
|
||||
}
|
||||
if isContactPointInUse(name, route.Routes) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (ecp *ContactPointService) decryptValue(value string) (string, error) {
|
||||
decodeValue, err := base64.StdEncoding.DecodeString(value)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
decryptedValue, err := ecp.encryptionService.Decrypt(context.Background(), decodeValue)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(decryptedValue), nil
|
||||
}
|
||||
|
||||
func (ecp *ContactPointService) encryptValue(value string) (string, error) {
|
||||
encryptedData, err := ecp.encryptionService.Encrypt(context.Background(), []byte(value), secrets.WithoutScope())
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to encrypt secure settings: %w", err)
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(encryptedData), nil
|
||||
}
|
195
pkg/services/ngalert/provisioning/contactpoints_test.go
Normal file
195
pkg/services/ngalert/provisioning/contactpoints_test.go
Normal file
@ -0,0 +1,195 @@
|
||||
package provisioning
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/grafana/grafana/pkg/services/secrets"
|
||||
"github.com/grafana/grafana/pkg/services/secrets/database"
|
||||
"github.com/grafana/grafana/pkg/services/secrets/manager"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestContactPointService(t *testing.T) {
|
||||
sqlStore := sqlstore.InitTestDB(t)
|
||||
secretsService := manager.SetupTestService(t, database.ProvideSecretsStore(sqlStore))
|
||||
t.Run("service gets contact points from AM config", func(t *testing.T) {
|
||||
sut := createContactPointServiceSut(secretsService)
|
||||
|
||||
cps, err := sut.GetContactPoints(context.Background(), 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, cps, 1)
|
||||
require.Equal(t, "email receiver", cps[0].Name)
|
||||
})
|
||||
|
||||
t.Run("service stitches contact point into org's AM config", func(t *testing.T) {
|
||||
sut := createContactPointServiceSut(secretsService)
|
||||
newCp := createTestContactPoint()
|
||||
|
||||
_, err := sut.CreateContactPoint(context.Background(), 1, newCp, models.ProvenanceAPI)
|
||||
require.NoError(t, err)
|
||||
|
||||
cps, err := sut.GetContactPoints(context.Background(), 1)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, cps, 2)
|
||||
require.Equal(t, "test-contact-point", cps[1].Name)
|
||||
require.Equal(t, "slack", cps[1].Type)
|
||||
})
|
||||
|
||||
t.Run("default provenance of contact points is none", func(t *testing.T) {
|
||||
sut := createContactPointServiceSut(secretsService)
|
||||
|
||||
cps, err := sut.GetContactPoints(context.Background(), 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, models.ProvenanceNone, models.Provenance(cps[0].Provenance))
|
||||
})
|
||||
|
||||
t.Run("it's possible to update provenance from none to API", func(t *testing.T) {
|
||||
sut := createContactPointServiceSut(secretsService)
|
||||
newCp := createTestContactPoint()
|
||||
|
||||
newCp, err := sut.CreateContactPoint(context.Background(), 1, newCp, models.ProvenanceNone)
|
||||
require.NoError(t, err)
|
||||
|
||||
cps, err := sut.GetContactPoints(context.Background(), 1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, newCp.UID, cps[1].UID)
|
||||
require.Equal(t, models.ProvenanceNone, models.Provenance(cps[1].Provenance))
|
||||
|
||||
err = sut.UpdateContactPoint(context.Background(), 1, newCp, models.ProvenanceAPI)
|
||||
require.NoError(t, err)
|
||||
|
||||
cps, err = sut.GetContactPoints(context.Background(), 1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, newCp.UID, cps[1].UID)
|
||||
require.Equal(t, models.ProvenanceAPI, models.Provenance(cps[1].Provenance))
|
||||
})
|
||||
|
||||
t.Run("it's possible to update provenance from none to File", func(t *testing.T) {
|
||||
sut := createContactPointServiceSut(secretsService)
|
||||
newCp := createTestContactPoint()
|
||||
|
||||
newCp, err := sut.CreateContactPoint(context.Background(), 1, newCp, models.ProvenanceNone)
|
||||
require.NoError(t, err)
|
||||
|
||||
cps, err := sut.GetContactPoints(context.Background(), 1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, newCp.UID, cps[1].UID)
|
||||
require.Equal(t, models.ProvenanceNone, models.Provenance(cps[1].Provenance))
|
||||
|
||||
err = sut.UpdateContactPoint(context.Background(), 1, newCp, models.ProvenanceFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
cps, err = sut.GetContactPoints(context.Background(), 1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, newCp.UID, cps[1].UID)
|
||||
require.Equal(t, models.ProvenanceFile, models.Provenance(cps[1].Provenance))
|
||||
})
|
||||
|
||||
t.Run("it's not possible to update provenance from File to API", func(t *testing.T) {
|
||||
sut := createContactPointServiceSut(secretsService)
|
||||
newCp := createTestContactPoint()
|
||||
|
||||
newCp, err := sut.CreateContactPoint(context.Background(), 1, newCp, models.ProvenanceFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
cps, err := sut.GetContactPoints(context.Background(), 1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, newCp.UID, cps[1].UID)
|
||||
require.Equal(t, models.ProvenanceFile, models.Provenance(cps[1].Provenance))
|
||||
|
||||
err = sut.UpdateContactPoint(context.Background(), 1, newCp, models.ProvenanceAPI)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("it's not possible to update provenance from API to File", func(t *testing.T) {
|
||||
sut := createContactPointServiceSut(secretsService)
|
||||
newCp := createTestContactPoint()
|
||||
|
||||
newCp, err := sut.CreateContactPoint(context.Background(), 1, newCp, models.ProvenanceAPI)
|
||||
require.NoError(t, err)
|
||||
|
||||
cps, err := sut.GetContactPoints(context.Background(), 1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, newCp.UID, cps[1].UID)
|
||||
require.Equal(t, models.ProvenanceAPI, models.Provenance(cps[1].Provenance))
|
||||
|
||||
err = sut.UpdateContactPoint(context.Background(), 1, newCp, models.ProvenanceFile)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("service respects concurrency token when updating", func(t *testing.T) {
|
||||
sut := createContactPointServiceSut(secretsService)
|
||||
newCp := createTestContactPoint()
|
||||
q := models.GetLatestAlertmanagerConfigurationQuery{
|
||||
OrgID: 1,
|
||||
}
|
||||
err := sut.amStore.GetLatestAlertmanagerConfiguration(context.Background(), &q)
|
||||
require.NoError(t, err)
|
||||
expectedConcurrencyToken := q.Result.ConfigurationHash
|
||||
|
||||
_, err = sut.CreateContactPoint(context.Background(), 1, newCp, models.ProvenanceAPI)
|
||||
require.NoError(t, err)
|
||||
|
||||
fake := sut.amStore.(*fakeAMConfigStore)
|
||||
intercepted := fake.lastSaveCommand
|
||||
require.Equal(t, expectedConcurrencyToken, intercepted.FetchedConfigurationHash)
|
||||
})
|
||||
}
|
||||
|
||||
func TestContactPointInUse(t *testing.T) {
|
||||
result := isContactPointInUse("test", []*definitions.Route{
|
||||
{
|
||||
Receiver: "not-test",
|
||||
Routes: []*definitions.Route{
|
||||
{
|
||||
Receiver: "not-test",
|
||||
},
|
||||
{
|
||||
Receiver: "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.True(t, result)
|
||||
result = isContactPointInUse("test", []*definitions.Route{
|
||||
{
|
||||
Receiver: "not-test",
|
||||
Routes: []*definitions.Route{
|
||||
{
|
||||
Receiver: "not-test",
|
||||
},
|
||||
{
|
||||
Receiver: "not-test",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.False(t, result)
|
||||
}
|
||||
|
||||
func createContactPointServiceSut(secretService secrets.Service) *ContactPointService {
|
||||
return &ContactPointService{
|
||||
amStore: newFakeAMConfigStore(),
|
||||
provenanceStore: newFakeProvisioningStore(),
|
||||
xact: newNopTransactionManager(),
|
||||
encryptionService: secretService,
|
||||
log: log.NewNopLogger(),
|
||||
}
|
||||
}
|
||||
|
||||
func createTestContactPoint() definitions.EmbeddedContactPoint {
|
||||
settings, _ := simplejson.NewJson([]byte(`{"recipient":"value_recipient","token":"value_token"}`))
|
||||
return definitions.EmbeddedContactPoint{
|
||||
Name: "test-contact-point",
|
||||
Type: "slack",
|
||||
Settings: settings,
|
||||
}
|
||||
}
|
@ -45,12 +45,12 @@ func TestNotificationPolicyService(t *testing.T) {
|
||||
sut := createNotificationPolicyServiceSut()
|
||||
newRoute := createTestRoutingTree()
|
||||
|
||||
err := sut.UpdatePolicyTree(context.Background(), 1, newRoute, models.ProvenanceApi)
|
||||
err := sut.UpdatePolicyTree(context.Background(), 1, newRoute, models.ProvenanceAPI)
|
||||
require.NoError(t, err)
|
||||
|
||||
updated, err := sut.GetPolicyTree(context.Background(), 1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, models.ProvenanceApi, updated.Provenance)
|
||||
require.Equal(t, models.ProvenanceAPI, updated.Provenance)
|
||||
})
|
||||
|
||||
t.Run("service respects concurrency token when updating", func(t *testing.T) {
|
||||
@ -63,7 +63,7 @@ func TestNotificationPolicyService(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
expectedConcurrencyToken := q.Result.ConfigurationHash
|
||||
|
||||
err = sut.UpdatePolicyTree(context.Background(), 1, newRoute, models.ProvenanceApi)
|
||||
err = sut.UpdatePolicyTree(context.Background(), 1, newRoute, models.ProvenanceAPI)
|
||||
require.NoError(t, err)
|
||||
|
||||
fake := sut.GetAMConfigStore().(*fakeAMConfigStore)
|
||||
|
@ -15,7 +15,9 @@ type AMConfigStore interface {
|
||||
// ProvisioningStore is a store of provisioning data for arbitrary objects.
|
||||
type ProvisioningStore interface {
|
||||
GetProvenance(ctx context.Context, o models.Provisionable) (models.Provenance, error)
|
||||
GetProvenances(ctx context.Context, orgID int64, resourceType string) (map[string]models.Provenance, error)
|
||||
SetProvenance(ctx context.Context, o models.Provisionable, p models.Provenance) error
|
||||
DeleteProvenance(ctx context.Context, o models.Provisionable) error
|
||||
}
|
||||
|
||||
// TransactionManager represents the ability to issue and close transactions through contexts.
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
)
|
||||
@ -99,19 +100,39 @@ func newFakeProvisioningStore() *fakeProvisioningStore {
|
||||
|
||||
func (f *fakeProvisioningStore) GetProvenance(ctx context.Context, o models.Provisionable) (models.Provenance, error) {
|
||||
if val, ok := f.records[o.ResourceOrgID()]; ok {
|
||||
if prov, ok := val[o.ResourceID()]; ok {
|
||||
if prov, ok := val[o.ResourceID()+o.ResourceType()]; ok {
|
||||
return prov, nil
|
||||
}
|
||||
}
|
||||
return models.ProvenanceNone, nil
|
||||
}
|
||||
|
||||
func (f *fakeProvisioningStore) GetProvenances(ctx context.Context, orgID int64, resourceType string) (map[string]models.Provenance, error) {
|
||||
results := make(map[string]models.Provenance)
|
||||
if val, ok := f.records[orgID]; ok {
|
||||
for k, v := range val {
|
||||
if strings.HasSuffix(k, resourceType) {
|
||||
results[strings.TrimSuffix(k, resourceType)] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (f *fakeProvisioningStore) SetProvenance(ctx context.Context, o models.Provisionable, p models.Provenance) error {
|
||||
orgID := o.ResourceOrgID()
|
||||
if _, ok := f.records[orgID]; !ok {
|
||||
f.records[orgID] = map[string]models.Provenance{}
|
||||
}
|
||||
f.records[orgID][o.ResourceID()] = p
|
||||
_ = f.DeleteProvenance(ctx, o) // delete old entries first
|
||||
f.records[orgID][o.ResourceID()+o.ResourceType()] = p
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeProvisioningStore) DeleteProvenance(ctx context.Context, o models.Provisionable) error {
|
||||
if val, ok := f.records[o.ResourceOrgID()]; ok {
|
||||
delete(val, o.ResourceID()+o.ResourceType())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -45,6 +45,23 @@ func (st DBstore) GetProvenance(ctx context.Context, o models.Provisionable) (mo
|
||||
return provenance, nil
|
||||
}
|
||||
|
||||
// GetProvenance gets the provenance status for a provisionable object.
|
||||
func (st DBstore) GetProvenances(ctx context.Context, orgID int64, resourceType string) (map[string]models.Provenance, error) {
|
||||
resultMap := make(map[string]models.Provenance)
|
||||
err := st.SQLStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||
filter := "record_type = ? AND org_id = ?"
|
||||
rawData, err := sess.Table(provenanceRecord{}).Where(filter, resourceType, orgID).Desc("id").Cols("record_key", "provenance").QueryString()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to query for existing provenance status: %w", err)
|
||||
}
|
||||
for _, data := range rawData {
|
||||
resultMap[data["record_key"]] = models.Provenance(data["provenance"])
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return resultMap, err
|
||||
}
|
||||
|
||||
// SetProvenance changes the provenance status for a provisionable object.
|
||||
func (st DBstore) SetProvenance(ctx context.Context, o models.Provisionable, p models.Provenance) error {
|
||||
recordType := o.ResourceType()
|
||||
@ -76,3 +93,15 @@ func (st DBstore) SetProvenance(ctx context.Context, o models.Provisionable, p m
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteProvenance deletes the provenance record from the table
|
||||
func (st DBstore) DeleteProvenance(ctx context.Context, o models.Provisionable) error {
|
||||
return st.SQLStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||
_, err := sess.Delete(provenanceRecord{
|
||||
RecordKey: o.ResourceID(),
|
||||
RecordType: o.ResourceType(),
|
||||
OrgID: o.ResourceOrgID(),
|
||||
})
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
@ -73,16 +73,58 @@ func TestProvisioningStore(t *testing.T) {
|
||||
err = store.SetProvenance(context.Background(), &ruleOrg3, models.ProvenanceFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = store.SetProvenance(context.Background(), &ruleOrg2, models.ProvenanceApi)
|
||||
err = store.SetProvenance(context.Background(), &ruleOrg2, models.ProvenanceAPI)
|
||||
require.NoError(t, err)
|
||||
|
||||
p, err := store.GetProvenance(context.Background(), &ruleOrg2)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, models.ProvenanceApi, p)
|
||||
require.Equal(t, models.ProvenanceAPI, p)
|
||||
p, err = store.GetProvenance(context.Background(), &ruleOrg3)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, models.ProvenanceFile, p)
|
||||
})
|
||||
|
||||
t.Run("Store should return all provenances by type", func(t *testing.T) {
|
||||
const orgID = 123
|
||||
ruleOrg1 := models.AlertRule{
|
||||
UID: "789",
|
||||
OrgID: orgID,
|
||||
}
|
||||
ruleOrg2 := models.AlertRule{
|
||||
UID: "790",
|
||||
OrgID: orgID,
|
||||
}
|
||||
err := store.SetProvenance(context.Background(), &ruleOrg1, models.ProvenanceFile)
|
||||
require.NoError(t, err)
|
||||
err = store.SetProvenance(context.Background(), &ruleOrg2, models.ProvenanceAPI)
|
||||
require.NoError(t, err)
|
||||
|
||||
p, err := store.GetProvenances(context.Background(), orgID, ruleOrg1.ResourceType())
|
||||
require.NoError(t, err)
|
||||
require.Len(t, p, 2)
|
||||
require.Equal(t, models.ProvenanceFile, p[ruleOrg1.UID])
|
||||
require.Equal(t, models.ProvenanceAPI, p[ruleOrg2.UID])
|
||||
})
|
||||
|
||||
t.Run("Store should delete provenance correctly", func(t *testing.T) {
|
||||
const orgID = 1234
|
||||
ruleOrg := models.AlertRule{
|
||||
UID: "7834539",
|
||||
OrgID: orgID,
|
||||
}
|
||||
err := store.SetProvenance(context.Background(), &ruleOrg, models.ProvenanceFile)
|
||||
require.NoError(t, err)
|
||||
p, err := store.GetProvenance(context.Background(), &ruleOrg)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, models.ProvenanceFile, p)
|
||||
|
||||
err = store.DeleteProvenance(context.Background(), &ruleOrg)
|
||||
require.NoError(t, err)
|
||||
|
||||
p, err = store.GetProvenance(context.Background(), &ruleOrg)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, models.ProvenanceNone, p)
|
||||
})
|
||||
}
|
||||
|
||||
func createProvisioningStoreSut(_ *ngalert.AlertNG, db *store.DBstore) provisioning.ProvisioningStore {
|
||||
|
@ -126,6 +126,98 @@ func TestProvisioning(t *testing.T) {
|
||||
require.Equal(t, 202, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("admin POST should succeed", func(t *testing.T) {
|
||||
req := createTestRequest("POST", url, "admin", body)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, resp.Body.Close())
|
||||
|
||||
require.Equal(t, 202, resp.StatusCode)
|
||||
})
|
||||
})
|
||||
t.Run("when provisioning contactpoints", func(t *testing.T) {
|
||||
url := fmt.Sprintf("http://%s/api/provisioning/contact-points", grafanaListedAddr)
|
||||
body := `
|
||||
{
|
||||
"name": "my-contact-point",
|
||||
"type": "slack",
|
||||
"settings": {
|
||||
"recipient": "value_recipient",
|
||||
"token": "value_token"
|
||||
}
|
||||
}`
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
t.Run("un-authenticated POST should 401", func(t *testing.T) {
|
||||
req := createTestRequest("POST", url, "", body)
|
||||
|
||||
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 POST should 403", func(t *testing.T) {
|
||||
req := createTestRequest("POST", url, "viewer", body)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, resp.Body.Close())
|
||||
|
||||
require.Equal(t, 403, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("editor POST should succeed", func(t *testing.T) {
|
||||
req := createTestRequest("POST", url, "editor", body)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, resp.Body.Close())
|
||||
|
||||
require.Equal(t, 202, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("admin POST should succeed", func(t *testing.T) {
|
||||
req := createTestRequest("POST", url, "admin", body)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user