Compare commits
1 Commits
njvrzm/err
...
alexander-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f57b784bd2 |
@@ -1110,6 +1110,10 @@ export interface FeatureToggles {
|
||||
*/
|
||||
alertingTriage?: boolean;
|
||||
/**
|
||||
* When manageAlerts=false, disable data source API for alert rules
|
||||
*/
|
||||
alertingDisableDSAPIWithManageAlerts?: boolean;
|
||||
/**
|
||||
* Enables the Graphite data source full backend mode
|
||||
* @default false
|
||||
*/
|
||||
|
||||
@@ -1833,6 +1833,13 @@ var (
|
||||
HideFromDocs: true,
|
||||
Expression: "false",
|
||||
},
|
||||
{
|
||||
Name: "alertingDisableDSAPIWithManageAlerts",
|
||||
Description: "When manageAlerts=false, disable data source API for alert rules",
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: grafanaAlertingSquad,
|
||||
HideFromDocs: true,
|
||||
},
|
||||
{
|
||||
Name: "graphiteBackendMode",
|
||||
Description: "Enables the Graphite data source full backend mode",
|
||||
|
||||
1
pkg/services/featuremgmt/toggles_gen.csv
generated
1
pkg/services/featuremgmt/toggles_gen.csv
generated
@@ -250,6 +250,7 @@ newClickhouseConfigPageDesign,privatePreview,@grafana/partner-datasources,false,
|
||||
teamFolders,experimental,@grafana/grafana-search-navigate-organise,false,false,false
|
||||
interactiveLearning,preview,@grafana/pathfinder,false,false,false
|
||||
alertingTriage,experimental,@grafana/alerting-squad,false,false,false
|
||||
alertingDisableDSAPIWithManageAlerts,experimental,@grafana/alerting-squad,false,false,false
|
||||
graphiteBackendMode,privatePreview,@grafana/partner-datasources,false,false,false
|
||||
azureResourcePickerUpdates,GA,@grafana/partner-datasources,false,false,true
|
||||
prometheusTypeMigration,experimental,@grafana/partner-datasources,false,true,false
|
||||
|
||||
|
4
pkg/services/featuremgmt/toggles_gen.go
generated
4
pkg/services/featuremgmt/toggles_gen.go
generated
@@ -718,6 +718,10 @@ const (
|
||||
// Enables the alerting triage feature
|
||||
FlagAlertingTriage = "alertingTriage"
|
||||
|
||||
// FlagAlertingDisableDSAPIWithManageAlerts
|
||||
// When manageAlerts=false, disable data source API for alert rules
|
||||
FlagAlertingDisableDSAPIWithManageAlerts = "alertingDisableDSAPIWithManageAlerts"
|
||||
|
||||
// FlagGraphiteBackendMode
|
||||
// Enables the Graphite data source full backend mode
|
||||
FlagGraphiteBackendMode = "graphiteBackendMode"
|
||||
|
||||
13
pkg/services/featuremgmt/toggles_gen.json
generated
13
pkg/services/featuremgmt/toggles_gen.json
generated
@@ -195,6 +195,19 @@
|
||||
"codeowner": "@grafana/alerting-squad"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "alertingDisableDSAPIWithManageAlerts",
|
||||
"resourceVersion": "1766143488044",
|
||||
"creationTimestamp": "2025-12-19T11:24:48Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "When manageAlerts=false, disable data source API for alert rules",
|
||||
"stage": "experimental",
|
||||
"codeowner": "@grafana/alerting-squad",
|
||||
"hideFromDocs": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "alertingDisableSendAlertsExternal",
|
||||
|
||||
@@ -137,7 +137,7 @@ func (api *API) RegisterAPIEndpoints(m *metrics.API) {
|
||||
// Register endpoints for proxying to Cortex Ruler-compatible backends.
|
||||
api.RegisterRulerApiEndpoints(NewForkingRuler(
|
||||
api.DatasourceCache,
|
||||
NewLotexRuler(proxy, logger),
|
||||
NewLotexRuler(proxy, logger, api.FeatureManager, api.Cfg.DefaultDatasourceManageAlertsUIToggle),
|
||||
&RulerSrv{
|
||||
conditionValidator: api.ConditionValidator,
|
||||
QuotaService: api.QuotaService,
|
||||
|
||||
@@ -2,6 +2,7 @@ package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -10,9 +11,11 @@ import (
|
||||
"go.yaml.in/yaml/v3"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/errutil"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
@@ -37,29 +40,48 @@ var subtypeToPrefix = map[string]string{
|
||||
Mimir: mimirPrefix,
|
||||
}
|
||||
|
||||
var (
|
||||
errManageAlertsDisabled = errutil.Forbidden(
|
||||
"alerting.manageAlertsDisabled",
|
||||
errutil.WithPublicMessage("Alert management is disabled for this datasource"),
|
||||
).Errorf("manage alerts is disabled")
|
||||
)
|
||||
|
||||
// The requester is primarily used for testing purposes, allowing us to inject a different implementation of withReq.
|
||||
type requester interface {
|
||||
withReq(ctx *contextmodel.ReqContext, method string, u *url.URL, body io.Reader, extractor func(*response.NormalResponse) (any, error), headers map[string]string) response.Response
|
||||
}
|
||||
|
||||
type LotexRuler struct {
|
||||
log log.Logger
|
||||
log log.Logger
|
||||
features featuremgmt.FeatureToggles
|
||||
defaultManageAlertsEnabled bool
|
||||
*AlertingProxy
|
||||
requester requester
|
||||
}
|
||||
|
||||
func NewLotexRuler(proxy *AlertingProxy, log log.Logger) *LotexRuler {
|
||||
func NewLotexRuler(proxy *AlertingProxy, log log.Logger, features featuremgmt.FeatureToggles, defaultManageAlertsEnabled bool) *LotexRuler {
|
||||
return &LotexRuler{
|
||||
log: log,
|
||||
AlertingProxy: proxy,
|
||||
requester: proxy,
|
||||
log: log,
|
||||
features: features,
|
||||
defaultManageAlertsEnabled: defaultManageAlertsEnabled,
|
||||
AlertingProxy: proxy,
|
||||
requester: proxy,
|
||||
}
|
||||
}
|
||||
|
||||
// handleValidateError returns appropriate error response: 403 for manageAlerts, 500 for others
|
||||
func handleValidateError(err error) response.Response {
|
||||
if errors.Is(err, errManageAlertsDisabled) {
|
||||
return errorToResponse(err)
|
||||
}
|
||||
return ErrResp(500, err, "")
|
||||
}
|
||||
|
||||
func (r *LotexRuler) RouteDeleteNamespaceRulesConfig(ctx *contextmodel.ReqContext, namespace string) response.Response {
|
||||
legacyRulerPrefix, err := r.validateAndGetPrefix(ctx)
|
||||
if err != nil {
|
||||
return ErrResp(500, err, "")
|
||||
return handleValidateError(err)
|
||||
}
|
||||
|
||||
finalNamespace, err := getRulesNamespaceParam(ctx, namespace)
|
||||
@@ -83,7 +105,7 @@ func (r *LotexRuler) RouteDeleteNamespaceRulesConfig(ctx *contextmodel.ReqContex
|
||||
func (r *LotexRuler) RouteDeleteRuleGroupConfig(ctx *contextmodel.ReqContext, namespace string, group string) response.Response {
|
||||
legacyRulerPrefix, err := r.validateAndGetPrefix(ctx)
|
||||
if err != nil {
|
||||
return ErrResp(500, err, "")
|
||||
return handleValidateError(err)
|
||||
}
|
||||
|
||||
finalNamespace, err := getRulesNamespaceParam(ctx, namespace)
|
||||
@@ -117,7 +139,7 @@ func (r *LotexRuler) RouteDeleteRuleGroupConfig(ctx *contextmodel.ReqContext, na
|
||||
func (r *LotexRuler) RouteGetNamespaceRulesConfig(ctx *contextmodel.ReqContext, namespace string) response.Response {
|
||||
legacyRulerPrefix, err := r.validateAndGetPrefix(ctx)
|
||||
if err != nil {
|
||||
return ErrResp(500, err, "")
|
||||
return handleValidateError(err)
|
||||
}
|
||||
|
||||
finalNamespace, err := getRulesNamespaceParam(ctx, namespace)
|
||||
@@ -145,7 +167,7 @@ func (r *LotexRuler) RouteGetNamespaceRulesConfig(ctx *contextmodel.ReqContext,
|
||||
func (r *LotexRuler) RouteGetRulegGroupConfig(ctx *contextmodel.ReqContext, namespace string, group string) response.Response {
|
||||
legacyRulerPrefix, err := r.validateAndGetPrefix(ctx)
|
||||
if err != nil {
|
||||
return ErrResp(500, err, "")
|
||||
return handleValidateError(err)
|
||||
}
|
||||
|
||||
finalNamespace, err := getRulesNamespaceParam(ctx, namespace)
|
||||
@@ -179,7 +201,7 @@ func (r *LotexRuler) RouteGetRulegGroupConfig(ctx *contextmodel.ReqContext, name
|
||||
func (r *LotexRuler) RouteGetRulesConfig(ctx *contextmodel.ReqContext) response.Response {
|
||||
legacyRulerPrefix, err := r.validateAndGetPrefix(ctx)
|
||||
if err != nil {
|
||||
return ErrResp(500, err, "")
|
||||
return handleValidateError(err)
|
||||
}
|
||||
|
||||
return r.requester.withReq(
|
||||
@@ -198,7 +220,7 @@ func (r *LotexRuler) RouteGetRulesConfig(ctx *contextmodel.ReqContext) response.
|
||||
func (r *LotexRuler) RoutePostNameRulesConfig(ctx *contextmodel.ReqContext, conf apimodels.PostableRuleGroupConfig, ns string) response.Response {
|
||||
legacyRulerPrefix, err := r.validateAndGetPrefix(ctx)
|
||||
if err != nil {
|
||||
return ErrResp(500, err, "")
|
||||
return handleValidateError(err)
|
||||
}
|
||||
yml, err := yaml.Marshal(conf)
|
||||
if err != nil {
|
||||
@@ -225,6 +247,21 @@ func (r *LotexRuler) validateAndGetPrefix(ctx *contextmodel.ReqContext) (string,
|
||||
return "", err
|
||||
}
|
||||
|
||||
// nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if r.features.IsEnabledGlobally(featuremgmt.FlagAlertingDisableDSAPIWithManageAlerts) {
|
||||
// Check if manageAlerts is disabled for this datasource
|
||||
// Use server default if not explicitly set in datasource jsonData
|
||||
manageAlerts := r.defaultManageAlertsEnabled
|
||||
if ds.JsonData != nil {
|
||||
if manageAlertsVal, ok := ds.JsonData.CheckGet("manageAlerts"); ok {
|
||||
manageAlerts = manageAlertsVal.MustBool(r.defaultManageAlertsEnabled)
|
||||
}
|
||||
}
|
||||
if !manageAlerts {
|
||||
return "", errManageAlertsDisabled
|
||||
}
|
||||
}
|
||||
|
||||
// Validate URL
|
||||
if ds.URL == "" {
|
||||
return "", fmt.Errorf("URL for this data source is empty")
|
||||
|
||||
@@ -13,10 +13,12 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/datasourceproxy"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
@@ -117,13 +119,75 @@ func TestLotexRuler_ValidateAndGetPrefix(t *testing.T) {
|
||||
datasourceCache: fakeCacheService{datasource: &datasources.DataSource{URL: "http://azp.com", Type: datasources.DS_AZURE_PROMETHEUS}},
|
||||
expected: "/config/v1/rules",
|
||||
},
|
||||
{
|
||||
name: "with manageAlerts explicitly set to false (feature toggle enabled)",
|
||||
namedParams: map[string]string{":DatasourceUID": "d164"},
|
||||
datasourceCache: fakeCacheService{datasource: createDatasourceWithManageAlerts("http://loki.com", datasources.DS_LOKI, false)},
|
||||
err: errManageAlertsDisabled,
|
||||
},
|
||||
{
|
||||
name: "with manageAlerts explicitly set to true (feature toggle enabled)",
|
||||
namedParams: map[string]string{":DatasourceUID": "d164"},
|
||||
datasourceCache: fakeCacheService{datasource: createDatasourceWithManageAlerts("http://loki.com", datasources.DS_LOKI, true)},
|
||||
expected: "/api/prom/rules",
|
||||
},
|
||||
{
|
||||
name: "with manageAlerts undefined (feature toggle enabled)",
|
||||
namedParams: map[string]string{":DatasourceUID": "d164"},
|
||||
datasourceCache: fakeCacheService{datasource: &datasources.DataSource{URL: "http://loki.com", Type: datasources.DS_LOKI, JsonData: simplejson.New()}},
|
||||
expected: "/api/prom/rules",
|
||||
},
|
||||
{
|
||||
name: "with JsonData nil (feature toggle enabled)",
|
||||
namedParams: map[string]string{":DatasourceUID": "d164"},
|
||||
datasourceCache: fakeCacheService{datasource: &datasources.DataSource{URL: "http://loki.com", Type: datasources.DS_LOKI, JsonData: nil}},
|
||||
expected: "/api/prom/rules",
|
||||
},
|
||||
}
|
||||
|
||||
features := featuremgmt.WithFeatures(featuremgmt.FlagAlertingDisableDSAPIWithManageAlerts)
|
||||
|
||||
t.Run("with server default manage alerts = false", func(t *testing.T) {
|
||||
proxy := &AlertingProxy{DataProxy: &datasourceproxy.DataSourceProxyService{DataSourceCache: fakeCacheService{datasource: &datasources.DataSource{URL: "http://loki.com", Type: datasources.DS_LOKI}}}}
|
||||
ruler := &LotexRuler{AlertingProxy: proxy, log: log.NewNopLogger(), features: features, defaultManageAlertsEnabled: false}
|
||||
httpReq, err := http.NewRequest(http.MethodGet, "http://grafanacloud.com", nil)
|
||||
require.NoError(t, err)
|
||||
ctx := &contextmodel.ReqContext{Context: &web.Context{Req: web.SetURLParams(httpReq, map[string]string{":DatasourceUID": "d164"})}}
|
||||
|
||||
prefix, err := ruler.validateAndGetPrefix(ctx)
|
||||
require.Empty(t, prefix)
|
||||
require.ErrorIs(t, err, errManageAlertsDisabled)
|
||||
})
|
||||
|
||||
t.Run("with server default false but datasource manageAlerts true should allow", func(t *testing.T) {
|
||||
proxy := &AlertingProxy{DataProxy: &datasourceproxy.DataSourceProxyService{DataSourceCache: fakeCacheService{datasource: createDatasourceWithManageAlerts("http://loki.com", datasources.DS_LOKI, true)}}}
|
||||
ruler := &LotexRuler{AlertingProxy: proxy, log: log.NewNopLogger(), features: features, defaultManageAlertsEnabled: false}
|
||||
httpReq, err := http.NewRequest(http.MethodGet, "http://grafanacloud.com", nil)
|
||||
require.NoError(t, err)
|
||||
ctx := &contextmodel.ReqContext{Context: &web.Context{Req: web.SetURLParams(httpReq, map[string]string{":DatasourceUID": "d164"})}}
|
||||
|
||||
prefix, err := ruler.validateAndGetPrefix(ctx)
|
||||
require.Equal(t, "/api/prom/rules", prefix)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("with feature toggle disabled, manageAlerts false should allow", func(t *testing.T) {
|
||||
proxy := &AlertingProxy{DataProxy: &datasourceproxy.DataSourceProxyService{DataSourceCache: fakeCacheService{datasource: createDatasourceWithManageAlerts("http://loki.com", datasources.DS_LOKI, false)}}}
|
||||
ruler := &LotexRuler{AlertingProxy: proxy, log: log.NewNopLogger(), features: featuremgmt.WithFeatures(), defaultManageAlertsEnabled: true}
|
||||
httpReq, err := http.NewRequest(http.MethodGet, "http://grafanacloud.com", nil)
|
||||
require.NoError(t, err)
|
||||
ctx := &contextmodel.ReqContext{Context: &web.Context{Req: web.SetURLParams(httpReq, map[string]string{":DatasourceUID": "d164"})}}
|
||||
|
||||
prefix, err := ruler.validateAndGetPrefix(ctx)
|
||||
require.Equal(t, "/api/prom/rules", prefix)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
for _, tt := range tc {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Setup Proxy.
|
||||
proxy := &AlertingProxy{DataProxy: &datasourceproxy.DataSourceProxyService{DataSourceCache: tt.datasourceCache}}
|
||||
ruler := &LotexRuler{AlertingProxy: proxy, log: log.NewNopLogger()}
|
||||
ruler := &LotexRuler{AlertingProxy: proxy, log: log.NewNopLogger(), features: features, defaultManageAlertsEnabled: true}
|
||||
|
||||
// Setup request context.
|
||||
httpReq, err := http.NewRequest(http.MethodGet, "http://grafanacloud.com"+tt.urlParams, nil)
|
||||
@@ -160,6 +224,16 @@ func (f fakeCacheService) GetDatasourceByUID(ctx context.Context, datasourceUID
|
||||
return f.datasource, nil
|
||||
}
|
||||
|
||||
func createDatasourceWithManageAlerts(url, dsType string, manageAlerts bool) *datasources.DataSource {
|
||||
data := simplejson.New()
|
||||
data.Set("manageAlerts", manageAlerts)
|
||||
return &datasources.DataSource{
|
||||
URL: url,
|
||||
Type: dsType,
|
||||
JsonData: data,
|
||||
}
|
||||
}
|
||||
|
||||
func TestLotexRuler_RouteDeleteNamespaceRulesConfig(t *testing.T) {
|
||||
tc := []struct {
|
||||
name string
|
||||
@@ -207,7 +281,7 @@ func TestLotexRuler_RouteDeleteNamespaceRulesConfig(t *testing.T) {
|
||||
|
||||
// Setup Proxy.
|
||||
proxy := &AlertingProxy{DataProxy: &datasourceproxy.DataSourceProxyService{DataSourceCache: fakeCacheService{datasource: tt.datasource}}}
|
||||
ruler := &LotexRuler{AlertingProxy: proxy, log: log.NewNopLogger(), requester: &requestMock}
|
||||
ruler := &LotexRuler{AlertingProxy: proxy, log: log.NewNopLogger(), features: featuremgmt.WithFeatures(), defaultManageAlertsEnabled: true, requester: &requestMock}
|
||||
|
||||
// Setup request context.
|
||||
httpReq, err := http.NewRequest(http.MethodGet, tt.datasource.URL+tt.urlParams, nil)
|
||||
@@ -269,7 +343,7 @@ func TestLotexRuler_RouteDeleteRuleGroupConfig(t *testing.T) {
|
||||
|
||||
// Setup Proxy.
|
||||
proxy := &AlertingProxy{DataProxy: &datasourceproxy.DataSourceProxyService{DataSourceCache: fakeCacheService{datasource: tt.datasource}}}
|
||||
ruler := &LotexRuler{AlertingProxy: proxy, log: log.NewNopLogger(), requester: &requestMock}
|
||||
ruler := &LotexRuler{AlertingProxy: proxy, log: log.NewNopLogger(), features: featuremgmt.WithFeatures(), defaultManageAlertsEnabled: true, requester: &requestMock}
|
||||
|
||||
// Setup request context.
|
||||
httpReq, err := http.NewRequest(http.MethodGet, tt.datasource.URL+tt.urlParams, nil)
|
||||
@@ -329,7 +403,7 @@ func TestLotexRuler_RouteGetNamespaceRulesConfig(t *testing.T) {
|
||||
|
||||
// Setup Proxy.
|
||||
proxy := &AlertingProxy{DataProxy: &datasourceproxy.DataSourceProxyService{DataSourceCache: fakeCacheService{datasource: tt.datasource}}}
|
||||
ruler := &LotexRuler{AlertingProxy: proxy, log: log.NewNopLogger(), requester: &requestMock}
|
||||
ruler := &LotexRuler{AlertingProxy: proxy, log: log.NewNopLogger(), features: featuremgmt.WithFeatures(), defaultManageAlertsEnabled: true, requester: &requestMock}
|
||||
|
||||
// Setup request context.
|
||||
httpReq, err := http.NewRequest(http.MethodGet, tt.datasource.URL+tt.urlParams, nil)
|
||||
@@ -391,7 +465,7 @@ func TestLotexRuler_RouteGetRulegGroupConfig(t *testing.T) {
|
||||
|
||||
// Setup Proxy.
|
||||
proxy := &AlertingProxy{DataProxy: &datasourceproxy.DataSourceProxyService{DataSourceCache: fakeCacheService{datasource: tt.datasource}}}
|
||||
ruler := &LotexRuler{AlertingProxy: proxy, log: log.NewNopLogger(), requester: &requestMock}
|
||||
ruler := &LotexRuler{AlertingProxy: proxy, log: log.NewNopLogger(), features: featuremgmt.WithFeatures(), defaultManageAlertsEnabled: true, requester: &requestMock}
|
||||
|
||||
// Setup request context.
|
||||
httpReq, err := http.NewRequest(http.MethodGet, tt.datasource.URL+tt.urlParams, nil)
|
||||
|
||||
Reference in New Issue
Block a user