diff --git a/conf/defaults.ini b/conf/defaults.ini index 212377c4213..a9bf50562e6 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -893,3 +893,7 @@ use_browser_locale = false # Default timezone for user preferences. Options are 'browser' for the browser local timezone or a timezone name from IANA Time Zone database, e.g. 'UTC' or 'Europe/Amsterdam' etc. default_timezone = browser + +[expressions] +# Disable expressions & UI features +enabled = true diff --git a/conf/sample.ini b/conf/sample.ini index 069e69bf979..7fc783fee13 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -883,3 +883,7 @@ # Default timezone for user preferences. Options are 'browser' for the browser local timezone or a timezone name from IANA Time Zone database, e.g. 'UTC' or 'Europe/Amsterdam' etc. ;default_timezone = browser + +[expressions] +# Disable expressions & UI features +;enabled = true diff --git a/docs/sources/administration/configuration.md b/docs/sources/administration/configuration.md index c5b77a24418..926a6e7d87a 100644 --- a/docs/sources/administration/configuration.md +++ b/docs/sources/administration/configuration.md @@ -1505,3 +1505,8 @@ Set this to `true` to have date formats automatically derived from your browser ### default_timezone Used as the default time zone for user preferences. Can be either `browser` for the browser local time zone or a time zone name from the IANA Time Zone database, such as `UTC` or `Europe/Amsterdam`. + +## [expressions] +>Note: This is available in Grafana v7.4 and later versions. +### enabled +Set this to `false` to disable expressions and hide them in the Grafana UI. Default is `true`. diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index 2ddb3f00683..5685b0674ce 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -68,6 +68,7 @@ export class GrafanaBootConfig implements GrafanaConfig { sampleRate: 1, }; marketplaceUrl?: string; + expressionsEnabled = false; constructor(options: GrafanaBootConfig) { this.theme = options.bootData.user.lightTheme ? getTheme(GrafanaThemeType.Light) : getTheme(GrafanaThemeType.Dark); diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index e57c0a05a87..78f9c2b7a47 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -237,11 +237,12 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i "licenseUrl": hs.License.LicenseURL(c.SignedInUser), "edition": hs.License.Edition(), }, - "featureToggles": hs.Cfg.FeatureToggles, - "rendererAvailable": hs.RenderService.IsAvailable(), - "http2Enabled": hs.Cfg.Protocol == setting.HTTP2Scheme, - "sentry": hs.Cfg.Sentry, - "marketplaceUrl": hs.Cfg.MarketplaceURL, + "featureToggles": hs.Cfg.FeatureToggles, + "rendererAvailable": hs.RenderService.IsAvailable(), + "http2Enabled": hs.Cfg.Protocol == setting.HTTP2Scheme, + "sentry": hs.Cfg.Sentry, + "marketplaceUrl": hs.Cfg.MarketplaceURL, + "expressionsEnabled": hs.Cfg.ExpressionsEnabled, } return jsonObj, nil diff --git a/pkg/api/metrics.go b/pkg/api/metrics.go index a3e303bf6fd..8674709ec45 100644 --- a/pkg/api/metrics.go +++ b/pkg/api/metrics.go @@ -121,7 +121,8 @@ func (hs *HTTPServer) handleExpressions(c *models.ReqContext, reqDTO dtos.Metric }) } - resp, err := expr.WrapTransformData(c.Req.Context(), request) + exprService := expr.Service{Cfg: hs.Cfg} + resp, err := exprService.WrapTransformData(c.Req.Context(), request) if err != nil { return response.Error(500, "expression request error", err) } diff --git a/pkg/expr/service.go b/pkg/expr/service.go index c83a60903df..39a7bd93026 100644 --- a/pkg/expr/service.go +++ b/pkg/expr/service.go @@ -4,6 +4,7 @@ import ( "context" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/setting" ) // DatasourceName is the string constant used as the datasource name in requests @@ -20,6 +21,14 @@ const DatasourceUID = "-100" // Service is service representation for expression handling. type Service struct { + Cfg *setting.Cfg +} + +func (s *Service) isDisabled() bool { + if s.Cfg == nil { + return true + } + return !s.Cfg.ExpressionsEnabled } // BuildPipeline builds a pipeline from a request. diff --git a/pkg/expr/transform.go b/pkg/expr/transform.go index 8e61192c797..f9d30f1a756 100644 --- a/pkg/expr/transform.go +++ b/pkg/expr/transform.go @@ -16,7 +16,7 @@ import ( "google.golang.org/grpc/status" ) -func WrapTransformData(ctx context.Context, query *tsdb.TsdbQuery) (*tsdb.Response, error) { +func (s *Service) WrapTransformData(ctx context.Context, query *tsdb.TsdbQuery) (*tsdb.Response, error) { sdkReq := &backend.QueryDataRequest{ PluginContext: backend.PluginContext{ OrgID: query.User.OrgId, @@ -41,7 +41,7 @@ func WrapTransformData(ctx context.Context, query *tsdb.TsdbQuery) (*tsdb.Respon }, }) } - pbRes, err := TransformData(ctx, sdkReq) + pbRes, err := s.TransformData(ctx, sdkReq) if err != nil { return nil, err } @@ -69,17 +69,20 @@ func WrapTransformData(ctx context.Context, query *tsdb.TsdbQuery) (*tsdb.Respon // TransformData takes Queries which are either expressions nodes // or are datasource requests. -func TransformData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - svc := Service{} +func (s *Service) TransformData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { + if s.isDisabled() { + return nil, status.Error(codes.PermissionDenied, "Expressions are disabled") + } + // Build the pipeline from the request, checking for ordering issues (e.g. loops) // and parsing graph nodes from the queries. - pipeline, err := svc.BuildPipeline(req) + pipeline, err := s.BuildPipeline(req) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } // Execute the pipeline - responses, err := svc.ExecutePipeline(ctx, pipeline) + responses, err := s.ExecutePipeline(ctx, pipeline) if err != nil { return nil, status.Error(codes.Unknown, err.Error()) } diff --git a/pkg/services/ngalert/api.go b/pkg/services/ngalert/api.go index 90d1d17353c..49968f1346f 100644 --- a/pkg/services/ngalert/api.go +++ b/pkg/services/ngalert/api.go @@ -39,7 +39,8 @@ func (ng *AlertNG) conditionEvalEndpoint(c *models.ReqContext, dto evalAlertCond return response.Error(400, "invalid condition", err) } - evalResults, err := eval.ConditionEval(&dto.Condition, timeNow()) + evaluator := eval.Evaluator{Cfg: ng.Cfg} + evalResults, err := evaluator.ConditionEval(&dto.Condition, timeNow()) if err != nil { return response.Error(400, "Failed to evaluate conditions", err) } @@ -69,7 +70,8 @@ func (ng *AlertNG) alertDefinitionEvalEndpoint(c *models.ReqContext) response.Re return response.Error(400, "invalid condition", err) } - evalResults, err := eval.ConditionEval(condition, timeNow()) + evaluator := eval.Evaluator{Cfg: ng.Cfg} + evalResults, err := evaluator.ConditionEval(condition, timeNow()) if err != nil { return response.Error(400, "Failed to evaluate alert", err) } diff --git a/pkg/services/ngalert/eval/eval.go b/pkg/services/ngalert/eval/eval.go index fb95f04f247..72d4be0548d 100644 --- a/pkg/services/ngalert/eval/eval.go +++ b/pkg/services/ngalert/eval/eval.go @@ -7,6 +7,8 @@ import ( "fmt" "time" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana/pkg/expr" @@ -14,6 +16,10 @@ import ( const alertingEvaluationTimeout = 30 * time.Second +type Evaluator struct { + Cfg *setting.Cfg +} + // invalidEvalResultFormatError is an error for invalid format of the alert definition evaluation results. type invalidEvalResultFormatError struct { refID string @@ -87,7 +93,8 @@ func (c Condition) IsValid() bool { // AlertExecCtx is the context provided for executing an alert condition. type AlertExecCtx struct { - OrgID int64 + OrgID int64 + ExpressionsEnabled bool Ctx context.Context } @@ -133,7 +140,8 @@ func (c *Condition) execute(ctx AlertExecCtx, now time.Time) (*ExecutionResults, }) } - pbRes, err := expr.TransformData(ctx.Ctx, queryDataReq) + exprService := expr.Service{Cfg: &setting.Cfg{ExpressionsEnabled: ctx.ExpressionsEnabled}} + pbRes, err := exprService.TransformData(ctx.Ctx, queryDataReq) if err != nil { return &result, err } @@ -210,11 +218,11 @@ func (evalResults Results) AsDataFrame() data.Frame { } // ConditionEval executes conditions and evaluates the result. -func ConditionEval(condition *Condition, now time.Time) (Results, error) { +func (e *Evaluator) ConditionEval(condition *Condition, now time.Time) (Results, error) { alertCtx, cancelFn := context.WithTimeout(context.Background(), alertingEvaluationTimeout) defer cancelFn() - alertExecCtx := AlertExecCtx{OrgID: condition.OrgID, Ctx: alertCtx} + alertExecCtx := AlertExecCtx{OrgID: condition.OrgID, Ctx: alertCtx, ExpressionsEnabled: e.Cfg.ExpressionsEnabled} execResult, err := condition.execute(alertExecCtx, now) if err != nil { diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index d744aebdcf6..837a0e267d5 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -47,7 +47,13 @@ func (ng *AlertNG) Init() error { ng.log = log.New("ngalert") ng.registerAPIEndpoints() - ng.schedule = newScheduler(clock.New(), baseIntervalSeconds*time.Second, ng.log, nil) + schedCfg := schedulerCfg{ + c: clock.New(), + baseInterval: baseIntervalSeconds * time.Second, + logger: ng.log, + evaluator: eval.Evaluator{Cfg: ng.Cfg}, + } + ng.schedule = newScheduler(schedCfg) return nil } diff --git a/pkg/services/ngalert/schedule.go b/pkg/services/ngalert/schedule.go index 00b592aa8ca..effb8e4e7d1 100644 --- a/pkg/services/ngalert/schedule.go +++ b/pkg/services/ngalert/schedule.go @@ -47,7 +47,7 @@ func (ng *AlertNG) definitionRoutine(grafanaCtx context.Context, key alertDefini OrgID: alertDefinition.OrgID, QueriesAndExpressions: alertDefinition.Data, } - results, err := eval.ConditionEval(&condition, ctx.now) + results, err := ng.schedule.evaluator.ConditionEval(&condition, ctx.now) end = timeNow() if err != nil { // consider saving alert instance on error @@ -118,19 +118,30 @@ type schedule struct { stopApplied func(alertDefinitionKey) log log.Logger + + evaluator eval.Evaluator +} + +type schedulerCfg struct { + c clock.Clock + baseInterval time.Duration + logger log.Logger + evalApplied func(alertDefinitionKey, time.Time) + evaluator eval.Evaluator } // newScheduler returns a new schedule. -func newScheduler(c clock.Clock, baseInterval time.Duration, logger log.Logger, evalApplied func(alertDefinitionKey, time.Time)) *schedule { - ticker := alerting.NewTicker(c.Now(), time.Second*0, c, int64(baseInterval.Seconds())) +func newScheduler(cfg schedulerCfg) *schedule { + ticker := alerting.NewTicker(cfg.c.Now(), time.Second*0, cfg.c, int64(cfg.baseInterval.Seconds())) sch := schedule{ registry: alertDefinitionRegistry{alertDefinitionInfo: make(map[alertDefinitionKey]alertDefinitionInfo)}, maxAttempts: maxAttempts, - clock: c, - baseInterval: baseInterval, - log: logger, + clock: cfg.c, + baseInterval: cfg.baseInterval, + log: cfg.logger, heartbeat: ticker, - evalApplied: evalApplied, + evalApplied: cfg.evalApplied, + evaluator: cfg.evaluator, } return &sch } diff --git a/pkg/services/ngalert/schedule_test.go b/pkg/services/ngalert/schedule_test.go index 5fd8ced24b9..e86db6000fe 100644 --- a/pkg/services/ngalert/schedule_test.go +++ b/pkg/services/ngalert/schedule_test.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -26,7 +27,13 @@ func TestAlertingTicker(t *testing.T) { t.Cleanup(registry.ClearOverrides) mockedClock := clock.NewMock() - ng.schedule = newScheduler(mockedClock, time.Second, log.New("ngalert.schedule.test"), nil) + schefCfg := schedulerCfg{ + c: mockedClock, + baseInterval: time.Second, + logger: log.New("ngalert.schedule.test"), + evaluator: eval.Evaluator{Cfg: ng.Cfg}, + } + ng.schedule = newScheduler(schefCfg) alerts := make([]*AlertDefinition, 0) diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index d8f13cf4e6c..6b882dbc7ac 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -339,6 +339,9 @@ type Cfg struct { AutoAssignOrg bool AutoAssignOrgId int AutoAssignOrgRole string + + // ExpressionsEnabled specifies whether expressions are enabled. + ExpressionsEnabled bool } // IsLiveEnabled returns if grafana live should be enabled @@ -482,6 +485,11 @@ func (cfg *Cfg) readAnnotationSettings() { cfg.APIAnnotationCleanupSettings = newAnnotationCleanupSettings(apiIAnnotation, "max_age") } +func (cfg *Cfg) readExpressionsSettings() { + expressions := cfg.Raw.Section("expressions") + cfg.ExpressionsEnabled = expressions.Key("enabled").MustBool(true) +} + type AnnotationCleanupSettings struct { MaxAge time.Duration MaxCount int64 @@ -850,6 +858,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { cfg.readSmtpSettings() cfg.readQuotaSettings() cfg.readAnnotationSettings() + cfg.readExpressionsSettings() if err := cfg.readGrafanaEnvironmentMetrics(); err != nil { return err } diff --git a/public/app/features/query/components/QueryGroup.tsx b/public/app/features/query/components/QueryGroup.tsx index 1baded560c3..8f4727e4b65 100644 --- a/public/app/features/query/components/QueryGroup.tsx +++ b/public/app/features/query/components/QueryGroup.tsx @@ -315,7 +315,7 @@ export class QueryGroup extends PureComponent { )} {isAddingMixed && this.renderMixedPicker()} - {this.isExpressionsSupported(dsSettings) && ( + {config.expressionsEnabled && this.isExpressionsSupported(dsSettings) && (