Alerting: take datasources as external alertmanagers into consideration (#52534)

This commit is contained in:
Jean-Philippe Quéméner
2022-07-20 16:50:49 +02:00
committed by GitHub
parent 5c4aa4a7ac
commit 50ae42130b
12 changed files with 332 additions and 24 deletions

View File

@@ -62,6 +62,7 @@ type AlertingStore interface {
type API struct {
Cfg *setting.Cfg
DatasourceCache datasources.CacheService
DatasourceService datasources.DataSourceService
RouteRegister routing.RouteRegister
ExpressionService *expr.Service
QuotaService quota.Service
@@ -130,6 +131,7 @@ func (api *API) RegisterAPIEndpoints(m *metrics.API) {
}), m)
api.RegisterConfigurationApiEndpoints(NewConfiguration(
&ConfigSrv{
datasourceService: api.DatasourceService,
store: api.AdminConfigStore,
log: logger,
alertmanagerProvider: api.AlertsRouter,

View File

@@ -1,12 +1,15 @@
package api
import (
"context"
"errors"
"fmt"
"net/http"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/datasources"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/store"
@@ -16,6 +19,7 @@ import (
)
type ConfigSrv struct {
datasourceService datasources.DataSourceService
alertmanagerProvider ExternalAlertmanagerProvider
store store.AdminConfigurationStore
log log.Logger
@@ -68,11 +72,17 @@ func (srv ConfigSrv) RoutePostNGalertConfig(c *models.ReqContext, body apimodels
sendAlertsTo, err := ngmodels.StringToAlertmanagersChoice(string(body.AlertmanagersChoice))
if err != nil {
return response.Error(400, "Invalid alertmanager choice specified", nil)
return response.Error(400, "Invalid alertmanager choice specified", err)
}
if sendAlertsTo == ngmodels.ExternalAlertmanagers && len(body.Alertmanagers) == 0 {
return response.Error(400, "At least one Alertmanager must be provided to choose this option", nil)
externalAlertmanagers, err := srv.externalAlertmanagers(c.Req.Context(), c.OrgId)
if err != nil {
return response.Error(500, "Couldn't fetch the external Alertmanagers from datasources", err)
}
if sendAlertsTo == ngmodels.ExternalAlertmanagers &&
len(body.Alertmanagers)+len(externalAlertmanagers) < 1 {
return response.Error(400, "At least one Alertmanager must be provided or configured as a datasource that handles alerts to choose this option", nil)
}
cfg := &ngmodels.AdminConfiguration{
@@ -110,3 +120,25 @@ func (srv ConfigSrv) RouteDeleteNGalertConfig(c *models.ReqContext) response.Res
return response.JSON(http.StatusOK, util.DynMap{"message": "admin configuration deleted"})
}
// externalAlertmanagers returns the URL of any external alertmanager that is
// configured as datasource. The URL does not contain any auth.
func (srv ConfigSrv) externalAlertmanagers(ctx context.Context, orgID int64) ([]string, error) {
var alertmanagers []string
query := &datasources.GetDataSourcesByTypeQuery{
OrgId: orgID,
Type: datasources.DS_ALERTMANAGER,
}
err := srv.datasourceService.GetDataSourcesByType(ctx, query)
if err != nil {
return nil, fmt.Errorf("failed to fetch datasources for org: %w", err)
}
for _, ds := range query.Result {
if ds.JsonData.Get(apimodels.HandleGrafanaManagedAlerts).MustBool(false) {
// we don't need to build the exact URL as we only need
// to know if any is set
alertmanagers = append(alertmanagers, ds.Uid)
}
}
return alertmanagers, nil
}

View File

@@ -0,0 +1,117 @@
package api
import (
"encoding/json"
"net/http"
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/datasources"
fakeDatasources "github.com/grafana/grafana/pkg/services/datasources/fakes"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/store"
"github.com/stretchr/testify/require"
)
func TestExternalAlertmanagerChoice(t *testing.T) {
tests := []struct {
name string
alertmanagerChoice definitions.AlertmanagersChoice
alertmanagers []string
datasources []*datasources.DataSource
statusCode int
message string
}{
{
name: "setting the choice to external by passing a plain url should succeed",
alertmanagerChoice: definitions.ExternalAlertmanagers,
alertmanagers: []string{"http://localhost:9000"},
datasources: []*datasources.DataSource{},
statusCode: http.StatusCreated,
message: "admin configuration updated",
},
{
name: "setting the choice to external by having a enabled external am datasource should succeed",
alertmanagerChoice: definitions.ExternalAlertmanagers,
alertmanagers: []string{},
datasources: []*datasources.DataSource{
{
OrgId: 1,
Type: datasources.DS_ALERTMANAGER,
Url: "http://localhost:9000",
JsonData: simplejson.NewFromAny(map[string]interface{}{
definitions.HandleGrafanaManagedAlerts: true,
}),
},
},
statusCode: http.StatusCreated,
message: "admin configuration updated",
},
{
name: "setting the choice to external by having a disabled external am datasource should fail",
alertmanagerChoice: definitions.ExternalAlertmanagers,
alertmanagers: []string{},
datasources: []*datasources.DataSource{
{
OrgId: 1,
Type: datasources.DS_ALERTMANAGER,
Url: "http://localhost:9000",
JsonData: simplejson.NewFromAny(map[string]interface{}{}),
},
},
statusCode: http.StatusBadRequest,
message: "At least one Alertmanager must be provided or configured as a datasource that handles alerts to choose this option",
},
{
name: "setting the choice to external and having no am configured should fail",
alertmanagerChoice: definitions.ExternalAlertmanagers,
alertmanagers: []string{},
datasources: []*datasources.DataSource{},
statusCode: http.StatusBadRequest,
message: "At least one Alertmanager must be provided or configured as a datasource that handles alerts to choose this option",
},
{
name: "setting the choice to all and having no external am configured should succeed",
alertmanagerChoice: definitions.AllAlertmanagers,
alertmanagers: []string{},
datasources: []*datasources.DataSource{},
statusCode: http.StatusCreated,
message: "admin configuration updated",
},
{
name: "setting the choice to internal should always succeed",
alertmanagerChoice: definitions.InternalAlertmanager,
alertmanagers: []string{},
datasources: []*datasources.DataSource{},
statusCode: http.StatusCreated,
message: "admin configuration updated",
},
}
ctx := createRequestCtxInOrg(1)
ctx.OrgRole = models.ROLE_ADMIN
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
sut := createAPIAdminSut(t, test.datasources)
resp := sut.RoutePostNGalertConfig(ctx, definitions.PostableNGalertConfig{
Alertmanagers: test.alertmanagers,
AlertmanagersChoice: test.alertmanagerChoice,
})
var res map[string]interface{}
err := json.Unmarshal(resp.Body(), &res)
require.NoError(t, err)
require.Equal(t, test.message, res["message"])
require.Equal(t, test.statusCode, resp.Status())
})
}
}
func createAPIAdminSut(t *testing.T,
datasources []*datasources.DataSource) ConfigSrv {
return ConfigSrv{
datasourceService: &fakeDatasources.FakeDataSourceService{
DataSources: datasources,
},
store: store.NewFakeAdminConfigStore(t),
}
}

View File

@@ -58,9 +58,10 @@ type NGalertConfig struct {
type AlertmanagersChoice string
const (
AllAlertmanagers AlertmanagersChoice = "all"
InternalAlertmanager AlertmanagersChoice = "internal"
ExternalAlertmanagers AlertmanagersChoice = "external"
AllAlertmanagers AlertmanagersChoice = "all"
InternalAlertmanager AlertmanagersChoice = "internal"
ExternalAlertmanagers AlertmanagersChoice = "external"
HandleGrafanaManagedAlerts = "handleGrafanaManagedAlerts"
)
// swagger:model