mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
FeatureFlags: manage feature flags outside of settings.Cfg (#43692)
This commit is contained in:
parent
7fbc7d019a
commit
f94c0decbd
1
.gitignore
vendored
1
.gitignore
vendored
@ -156,6 +156,7 @@ compilation-stats.json
|
|||||||
|
|
||||||
# auto generated Go files
|
# auto generated Go files
|
||||||
*_gen.go
|
*_gen.go
|
||||||
|
!pkg/services/featuremgmt/toggles_gen.go
|
||||||
|
|
||||||
# Auto-generated localisation files
|
# Auto-generated localisation files
|
||||||
public/locales/_build/
|
public/locales/_build/
|
||||||
|
@ -40,7 +40,6 @@ export interface LicenseInfo {
|
|||||||
licenseUrl: string;
|
licenseUrl: string;
|
||||||
stateInfo: string;
|
stateInfo: string;
|
||||||
edition: GrafanaEdition;
|
edition: GrafanaEdition;
|
||||||
enabledFeatures: { [key: string]: boolean };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,3 +1,9 @@
|
|||||||
|
// NOTE: This file was auto generated. DO NOT EDIT DIRECTLY!
|
||||||
|
// To change feature flags, edit:
|
||||||
|
// pkg/services/featuremgmt/registry.go
|
||||||
|
// Then run tests in:
|
||||||
|
// pkg/services/featuremgmt/toggles_gen_test.go
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Describes available feature toggles in Grafana. These can be configured via
|
* Describes available feature toggles in Grafana. These can be configured via
|
||||||
* conf/custom.ini to enable features under development or not yet available in
|
* conf/custom.ini to enable features under development or not yet available in
|
||||||
|
@ -1,6 +1,12 @@
|
|||||||
|
import { FeatureToggles } from '@grafana/data';
|
||||||
import { config } from '../config';
|
import { config } from '../config';
|
||||||
|
|
||||||
export const featureEnabled = (feature: string): boolean => {
|
export const featureEnabled = (feature: boolean | undefined | keyof FeatureToggles): boolean => {
|
||||||
const { enabledFeatures } = config.licenseInfo;
|
if (feature === true || feature === false) {
|
||||||
return enabledFeatures && enabledFeatures[feature];
|
return feature;
|
||||||
|
}
|
||||||
|
if (feature == null || !config?.featureToggles) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return Boolean(config.featureToggles[feature]);
|
||||||
};
|
};
|
||||||
|
@ -437,7 +437,7 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
// Some channels may have info
|
// Some channels may have info
|
||||||
liveRoute.Get("/info/*", routing.Wrap(hs.Live.HandleInfoHTTP))
|
liveRoute.Get("/info/*", routing.Wrap(hs.Live.HandleInfoHTTP))
|
||||||
|
|
||||||
if hs.Cfg.FeatureToggles["live-pipeline"] {
|
if hs.Features.Toggles().IsLivePipelineEnabled() {
|
||||||
// POST Live data to be processed according to channel rules.
|
// POST Live data to be processed according to channel rules.
|
||||||
liveRoute.Post("/pipeline/push/*", hs.LivePushGateway.HandlePipelinePush)
|
liveRoute.Post("/pipeline/push/*", hs.LivePushGateway.HandlePipelinePush)
|
||||||
liveRoute.Post("/pipeline-convert-test", routing.Wrap(hs.Live.HandlePipelineConvertTestHTTP), reqOrgAdmin)
|
liveRoute.Post("/pipeline-convert-test", routing.Wrap(hs.Live.HandlePipelineConvertTestHTTP), reqOrgAdmin)
|
||||||
@ -460,6 +460,9 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
// admin api
|
// admin api
|
||||||
r.Group("/api/admin", func(adminRoute routing.RouteRegister) {
|
r.Group("/api/admin", func(adminRoute routing.RouteRegister) {
|
||||||
adminRoute.Get("/settings", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionSettingsRead)), routing.Wrap(hs.AdminGetSettings))
|
adminRoute.Get("/settings", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionSettingsRead)), routing.Wrap(hs.AdminGetSettings))
|
||||||
|
if hs.Features.Toggles().IsShowFeatureFlagsInUIEnabled() {
|
||||||
|
adminRoute.Get("/settings/features", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionSettingsRead)), hs.Features.HandleGetSettings)
|
||||||
|
}
|
||||||
adminRoute.Get("/stats", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionServerStatsRead)), routing.Wrap(AdminGetStats))
|
adminRoute.Get("/stats", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionServerStatsRead)), routing.Wrap(AdminGetStats))
|
||||||
adminRoute.Post("/pause-all-alerts", reqGrafanaAdmin, routing.Wrap(PauseAllAlerts))
|
adminRoute.Post("/pause-all-alerts", reqGrafanaAdmin, routing.Wrap(PauseAllAlerts))
|
||||||
|
|
||||||
|
@ -83,7 +83,7 @@ func (hs *HTTPServer) AddAPIKey(c *models.ReqContext) response.Response {
|
|||||||
}
|
}
|
||||||
cmd.OrgId = c.OrgId
|
cmd.OrgId = c.OrgId
|
||||||
var err error
|
var err error
|
||||||
if hs.Cfg.FeatureToggles["service-accounts"] {
|
if hs.Features.Toggles().IsServiceAccountsEnabled() {
|
||||||
// Api keys should now be created with addadditionalapikey endpoint
|
// Api keys should now be created with addadditionalapikey endpoint
|
||||||
return response.Error(400, "API keys should now be added via the AdditionalAPIKey endpoint.", err)
|
return response.Error(400, "API keys should now be added via the AdditionalAPIKey endpoint.", err)
|
||||||
}
|
}
|
||||||
@ -120,7 +120,7 @@ func (hs *HTTPServer) AdditionalAPIKey(c *models.ReqContext) response.Response {
|
|||||||
if err := web.Bind(c.Req, &cmd); err != nil {
|
if err := web.Bind(c.Req, &cmd); err != nil {
|
||||||
return response.Error(http.StatusBadRequest, "bad request data", err)
|
return response.Error(http.StatusBadRequest, "bad request data", err)
|
||||||
}
|
}
|
||||||
if !hs.Cfg.FeatureToggles["service-accounts"] {
|
if !hs.Features.Toggles().IsServiceAccountsEnabled() {
|
||||||
return response.Error(500, "Requires services-accounts feature", errors.New("feature missing"))
|
return response.Error(500, "Requires services-accounts feature", errors.New("feature missing"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,6 +28,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol"
|
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/services/auth"
|
"github.com/grafana/grafana/pkg/services/auth"
|
||||||
"github.com/grafana/grafana/pkg/services/contexthandler"
|
"github.com/grafana/grafana/pkg/services/contexthandler"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/quota"
|
"github.com/grafana/grafana/pkg/services/quota"
|
||||||
"github.com/grafana/grafana/pkg/services/rendering"
|
"github.com/grafana/grafana/pkg/services/rendering"
|
||||||
"github.com/grafana/grafana/pkg/services/searchusers"
|
"github.com/grafana/grafana/pkg/services/searchusers"
|
||||||
@ -213,8 +214,8 @@ func (s *fakeRenderService) Init() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func setupAccessControlScenarioContext(t *testing.T, cfg *setting.Cfg, url string, permissions []*accesscontrol.Permission) (*scenarioContext, *HTTPServer) {
|
func setupAccessControlScenarioContext(t *testing.T, cfg *setting.Cfg, url string, permissions []*accesscontrol.Permission) (*scenarioContext, *HTTPServer) {
|
||||||
cfg.FeatureToggles = make(map[string]bool)
|
features := featuremgmt.WithFeatures("accesscontrol")
|
||||||
cfg.FeatureToggles["accesscontrol"] = true
|
cfg.IsFeatureToggleEnabled = features.IsEnabled
|
||||||
cfg.Quota.Enabled = false
|
cfg.Quota.Enabled = false
|
||||||
|
|
||||||
bus := bus.GetBus()
|
bus := bus.GetBus()
|
||||||
@ -222,6 +223,7 @@ func setupAccessControlScenarioContext(t *testing.T, cfg *setting.Cfg, url strin
|
|||||||
Cfg: cfg,
|
Cfg: cfg,
|
||||||
Bus: bus,
|
Bus: bus,
|
||||||
Live: newTestLive(t),
|
Live: newTestLive(t),
|
||||||
|
Features: features,
|
||||||
QuotaService: "a.QuotaService{Cfg: cfg},
|
QuotaService: "a.QuotaService{Cfg: cfg},
|
||||||
RouteRegister: routing.NewRouteRegister(),
|
RouteRegister: routing.NewRouteRegister(),
|
||||||
AccessControl: accesscontrolmock.New().WithPermissions(permissions),
|
AccessControl: accesscontrolmock.New().WithPermissions(permissions),
|
||||||
@ -296,13 +298,25 @@ func setInitCtxSignedInOrgAdmin(initCtx *models.ReqContext) {
|
|||||||
initCtx.SignedInUser = &models.SignedInUser{UserId: testUserID, OrgId: 1, OrgRole: models.ROLE_ADMIN, Login: testUserLogin}
|
initCtx.SignedInUser = &models.SignedInUser{UserId: testUserID, OrgId: 1, OrgRole: models.ROLE_ADMIN, Login: testUserLogin}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setupSimpleHTTPServer(features *featuremgmt.FeatureManager) *HTTPServer {
|
||||||
|
if features == nil {
|
||||||
|
features = featuremgmt.WithFeatures()
|
||||||
|
}
|
||||||
|
cfg := setting.NewCfg()
|
||||||
|
cfg.IsFeatureToggleEnabled = features.IsEnabled
|
||||||
|
|
||||||
|
return &HTTPServer{
|
||||||
|
Cfg: cfg,
|
||||||
|
Features: features,
|
||||||
|
Bus: bus.GetBus(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func setupHTTPServer(t *testing.T, useFakeAccessControl bool, enableAccessControl bool) accessControlScenarioContext {
|
func setupHTTPServer(t *testing.T, useFakeAccessControl bool, enableAccessControl bool) accessControlScenarioContext {
|
||||||
// Use a new conf
|
// Use a new conf
|
||||||
|
features := featuremgmt.WithFeatures("accesscontrol", enableAccessControl)
|
||||||
cfg := setting.NewCfg()
|
cfg := setting.NewCfg()
|
||||||
cfg.FeatureToggles = make(map[string]bool)
|
cfg.IsFeatureToggleEnabled = features.IsEnabled
|
||||||
if enableAccessControl {
|
|
||||||
cfg.FeatureToggles["accesscontrol"] = enableAccessControl
|
|
||||||
}
|
|
||||||
|
|
||||||
return setupHTTPServerWithCfg(t, useFakeAccessControl, enableAccessControl, cfg)
|
return setupHTTPServerWithCfg(t, useFakeAccessControl, enableAccessControl, cfg)
|
||||||
}
|
}
|
||||||
@ -310,6 +324,9 @@ func setupHTTPServer(t *testing.T, useFakeAccessControl bool, enableAccessContro
|
|||||||
func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessControl bool, cfg *setting.Cfg) accessControlScenarioContext {
|
func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessControl bool, cfg *setting.Cfg) accessControlScenarioContext {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
features := featuremgmt.WithFeatures("accesscontrol", enableAccessControl)
|
||||||
|
cfg.IsFeatureToggleEnabled = features.IsEnabled
|
||||||
|
|
||||||
var acmock *accesscontrolmock.Mock
|
var acmock *accesscontrolmock.Mock
|
||||||
var ac *ossaccesscontrol.OSSAccessControlService
|
var ac *ossaccesscontrol.OSSAccessControlService
|
||||||
|
|
||||||
@ -322,6 +339,7 @@ func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessCont
|
|||||||
// Create minimal HTTP Server
|
// Create minimal HTTP Server
|
||||||
hs := &HTTPServer{
|
hs := &HTTPServer{
|
||||||
Cfg: cfg,
|
Cfg: cfg,
|
||||||
|
Features: features,
|
||||||
Bus: bus,
|
Bus: bus,
|
||||||
Live: newTestLive(t),
|
Live: newTestLive(t),
|
||||||
QuotaService: "a.QuotaService{Cfg: cfg},
|
QuotaService: "a.QuotaService{Cfg: cfg},
|
||||||
@ -338,7 +356,7 @@ func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessCont
|
|||||||
}
|
}
|
||||||
hs.AccessControl = acmock
|
hs.AccessControl = acmock
|
||||||
} else {
|
} else {
|
||||||
ac = ossaccesscontrol.ProvideService(cfg, &usagestats.UsageStatsMock{T: t})
|
ac = ossaccesscontrol.ProvideService(hs.Features.Toggles(), &usagestats.UsageStatsMock{T: t})
|
||||||
hs.AccessControl = ac
|
hs.AccessControl = ac
|
||||||
// Perform role registration
|
// Perform role registration
|
||||||
err := hs.declareFixedRoles()
|
err := hs.declareFixedRoles()
|
||||||
|
@ -20,6 +20,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/services/alerting"
|
"github.com/grafana/grafana/pkg/services/alerting"
|
||||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/libraryelements"
|
"github.com/grafana/grafana/pkg/services/libraryelements"
|
||||||
"github.com/grafana/grafana/pkg/services/live"
|
"github.com/grafana/grafana/pkg/services/live"
|
||||||
"github.com/grafana/grafana/pkg/services/provisioning"
|
"github.com/grafana/grafana/pkg/services/provisioning"
|
||||||
@ -88,8 +89,17 @@ type testState struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newTestLive(t *testing.T) *live.GrafanaLive {
|
func newTestLive(t *testing.T) *live.GrafanaLive {
|
||||||
|
features := featuremgmt.WithToggles()
|
||||||
cfg := &setting.Cfg{AppURL: "http://localhost:3000/"}
|
cfg := &setting.Cfg{AppURL: "http://localhost:3000/"}
|
||||||
gLive, err := live.ProvideService(nil, cfg, routing.NewRouteRegister(), nil, nil, nil, sqlstore.InitTestDB(t), nil, &usagestats.UsageStatsMock{T: t}, nil)
|
cfg.IsFeatureToggleEnabled = features.IsEnabled
|
||||||
|
gLive, err := live.ProvideService(nil, cfg,
|
||||||
|
routing.NewRouteRegister(),
|
||||||
|
nil, nil, nil,
|
||||||
|
sqlstore.InitTestDB(t),
|
||||||
|
nil,
|
||||||
|
&usagestats.UsageStatsMock{T: t},
|
||||||
|
nil,
|
||||||
|
features)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
return gLive
|
return gLive
|
||||||
}
|
}
|
||||||
|
@ -243,13 +243,12 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i
|
|||||||
"env": setting.Env,
|
"env": setting.Env,
|
||||||
},
|
},
|
||||||
"licenseInfo": map[string]interface{}{
|
"licenseInfo": map[string]interface{}{
|
||||||
"expiry": hs.License.Expiry(),
|
"expiry": hs.License.Expiry(),
|
||||||
"stateInfo": hs.License.StateInfo(),
|
"stateInfo": hs.License.StateInfo(),
|
||||||
"licenseUrl": hs.License.LicenseURL(hasAccess(accesscontrol.ReqGrafanaAdmin, accesscontrol.LicensingPageReaderAccess)),
|
"licenseUrl": hs.License.LicenseURL(hasAccess(accesscontrol.ReqGrafanaAdmin, accesscontrol.LicensingPageReaderAccess)),
|
||||||
"edition": hs.License.Edition(),
|
"edition": hs.License.Edition(),
|
||||||
"enabledFeatures": hs.License.EnabledFeatures(),
|
|
||||||
},
|
},
|
||||||
"featureToggles": hs.Cfg.FeatureToggles,
|
"featureToggles": hs.Features.GetEnabled(c.Req.Context()),
|
||||||
"rendererAvailable": hs.RenderService.IsAvailable(),
|
"rendererAvailable": hs.RenderService.IsAvailable(),
|
||||||
"rendererVersion": hs.RenderService.Version(),
|
"rendererVersion": hs.RenderService.Version(),
|
||||||
"http2Enabled": hs.Cfg.Protocol == setting.HTTP2Scheme,
|
"http2Enabled": hs.Cfg.Protocol == setting.HTTP2Scheme,
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/licensing"
|
"github.com/grafana/grafana/pkg/services/licensing"
|
||||||
"github.com/grafana/grafana/pkg/services/rendering"
|
"github.com/grafana/grafana/pkg/services/rendering"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
@ -19,9 +20,10 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func setupTestEnvironment(t *testing.T, cfg *setting.Cfg) (*web.Mux, *HTTPServer) {
|
func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features *featuremgmt.FeatureManager) (*web.Mux, *HTTPServer) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
sqlstore.InitTestDB(t)
|
sqlstore.InitTestDB(t)
|
||||||
|
cfg.IsFeatureToggleEnabled = features.IsEnabled
|
||||||
|
|
||||||
{
|
{
|
||||||
oldVersion := setting.BuildVersion
|
oldVersion := setting.BuildVersion
|
||||||
@ -37,9 +39,10 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg) (*web.Mux, *HTTPServer
|
|||||||
sqlStore := sqlstore.InitTestDB(t)
|
sqlStore := sqlstore.InitTestDB(t)
|
||||||
|
|
||||||
hs := &HTTPServer{
|
hs := &HTTPServer{
|
||||||
Cfg: cfg,
|
Cfg: cfg,
|
||||||
Bus: bus.GetBus(),
|
Features: features,
|
||||||
License: &licensing.OSSLicensingService{Cfg: cfg},
|
Bus: bus.GetBus(),
|
||||||
|
License: &licensing.OSSLicensingService{Cfg: cfg},
|
||||||
RenderService: &rendering.RenderingService{
|
RenderService: &rendering.RenderingService{
|
||||||
Cfg: cfg,
|
Cfg: cfg,
|
||||||
RendererPluginManager: &fakeRendererManager{},
|
RendererPluginManager: &fakeRendererManager{},
|
||||||
@ -73,7 +76,8 @@ func TestHTTPServer_GetFrontendSettings_hideVersionAnonymous(t *testing.T) {
|
|||||||
cfg.Env = "testing"
|
cfg.Env = "testing"
|
||||||
cfg.BuildVersion = "7.8.9"
|
cfg.BuildVersion = "7.8.9"
|
||||||
cfg.BuildCommit = "01234567"
|
cfg.BuildCommit = "01234567"
|
||||||
m, hs := setupTestEnvironment(t, cfg)
|
|
||||||
|
m, hs := setupTestEnvironment(t, cfg, featuremgmt.WithFeatures())
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/api/frontend/settings", nil)
|
req := httptest.NewRequest(http.MethodGet, "/api/frontend/settings", nil)
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/query"
|
"github.com/grafana/grafana/pkg/services/query"
|
||||||
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
||||||
"github.com/grafana/grafana/pkg/services/thumbs"
|
"github.com/grafana/grafana/pkg/services/thumbs"
|
||||||
@ -75,6 +76,7 @@ type HTTPServer struct {
|
|||||||
Bus bus.Bus
|
Bus bus.Bus
|
||||||
RenderService rendering.Service
|
RenderService rendering.Service
|
||||||
Cfg *setting.Cfg
|
Cfg *setting.Cfg
|
||||||
|
Features *featuremgmt.FeatureManager
|
||||||
SettingsProvider setting.Provider
|
SettingsProvider setting.Provider
|
||||||
HooksService *hooks.HooksService
|
HooksService *hooks.HooksService
|
||||||
CacheService *localcache.CacheService
|
CacheService *localcache.CacheService
|
||||||
@ -135,7 +137,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
|
|||||||
loginService login.Service, accessControl accesscontrol.AccessControl,
|
loginService login.Service, accessControl accesscontrol.AccessControl,
|
||||||
dataSourceProxy *datasourceproxy.DataSourceProxyService, searchService *search.SearchService,
|
dataSourceProxy *datasourceproxy.DataSourceProxyService, searchService *search.SearchService,
|
||||||
live *live.GrafanaLive, livePushGateway *pushhttp.Gateway, plugCtxProvider *plugincontext.Provider,
|
live *live.GrafanaLive, livePushGateway *pushhttp.Gateway, plugCtxProvider *plugincontext.Provider,
|
||||||
contextHandler *contexthandler.ContextHandler,
|
contextHandler *contexthandler.ContextHandler, features *featuremgmt.FeatureManager,
|
||||||
schemaService *schemaloader.SchemaLoaderService, alertNG *ngalert.AlertNG,
|
schemaService *schemaloader.SchemaLoaderService, alertNG *ngalert.AlertNG,
|
||||||
libraryPanelService librarypanels.Service, libraryElementService libraryelements.Service,
|
libraryPanelService librarypanels.Service, libraryElementService libraryelements.Service,
|
||||||
quotaService *quota.QuotaService, socialService social.Service, tracer tracing.Tracer,
|
quotaService *quota.QuotaService, socialService social.Service, tracer tracing.Tracer,
|
||||||
@ -167,6 +169,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
|
|||||||
AuthTokenService: userTokenService,
|
AuthTokenService: userTokenService,
|
||||||
cleanUpService: cleanUpService,
|
cleanUpService: cleanUpService,
|
||||||
ShortURLService: shortURLService,
|
ShortURLService: shortURLService,
|
||||||
|
Features: features,
|
||||||
ThumbService: thumbService,
|
ThumbService: thumbService,
|
||||||
RemoteCacheService: remoteCache,
|
RemoteCacheService: remoteCache,
|
||||||
ProvisioningService: provisioningService,
|
ProvisioningService: provisioningService,
|
||||||
|
@ -85,7 +85,7 @@ func (hs *HTTPServer) getAppLinks(c *models.ReqContext) ([]*dtos.NavLink, error)
|
|||||||
SortWeight: dtos.WeightPlugin,
|
SortWeight: dtos.WeightPlugin,
|
||||||
}
|
}
|
||||||
|
|
||||||
if hs.Cfg.IsNewNavigationEnabled() {
|
if hs.Features.Toggles().IsNewNavigationEnabled() {
|
||||||
appLink.Section = dtos.NavSectionPlugin
|
appLink.Section = dtos.NavSectionPlugin
|
||||||
} else {
|
} else {
|
||||||
appLink.Section = dtos.NavSectionCore
|
appLink.Section = dtos.NavSectionCore
|
||||||
@ -143,7 +143,7 @@ func (hs *HTTPServer) getAppLinks(c *models.ReqContext) ([]*dtos.NavLink, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func enableServiceAccount(hs *HTTPServer, c *models.ReqContext) bool {
|
func enableServiceAccount(hs *HTTPServer, c *models.ReqContext) bool {
|
||||||
return c.OrgRole == models.ROLE_ADMIN && hs.Cfg.IsServiceAccountEnabled() && hs.serviceAccountsService.Migrated(c.Req.Context(), c.OrgId)
|
return c.OrgRole == models.ROLE_ADMIN && hs.Features.Toggles().IsServiceAccountsEnabled() && hs.serviceAccountsService.Migrated(c.Req.Context(), c.OrgId)
|
||||||
}
|
}
|
||||||
|
|
||||||
func enableTeams(hs *HTTPServer, c *models.ReqContext) bool {
|
func enableTeams(hs *HTTPServer, c *models.ReqContext) bool {
|
||||||
@ -154,7 +154,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
|
|||||||
hasAccess := ac.HasAccess(hs.AccessControl, c)
|
hasAccess := ac.HasAccess(hs.AccessControl, c)
|
||||||
navTree := []*dtos.NavLink{}
|
navTree := []*dtos.NavLink{}
|
||||||
|
|
||||||
if hs.Cfg.IsNewNavigationEnabled() {
|
if hs.Features.Toggles().IsNewNavigationEnabled() {
|
||||||
navTree = append(navTree, &dtos.NavLink{
|
navTree = append(navTree, &dtos.NavLink{
|
||||||
Text: "Home",
|
Text: "Home",
|
||||||
Id: "home",
|
Id: "home",
|
||||||
@ -165,7 +165,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasEditPerm && !hs.Cfg.IsNewNavigationEnabled() {
|
if hasEditPerm && !hs.Features.Toggles().IsNewNavigationEnabled() {
|
||||||
children := hs.buildCreateNavLinks(c)
|
children := hs.buildCreateNavLinks(c)
|
||||||
navTree = append(navTree, &dtos.NavLink{
|
navTree = append(navTree, &dtos.NavLink{
|
||||||
Text: "Create",
|
Text: "Create",
|
||||||
@ -181,7 +181,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
|
|||||||
dashboardChildLinks := hs.buildDashboardNavLinks(c, hasEditPerm)
|
dashboardChildLinks := hs.buildDashboardNavLinks(c, hasEditPerm)
|
||||||
|
|
||||||
dashboardsUrl := "/"
|
dashboardsUrl := "/"
|
||||||
if hs.Cfg.IsNewNavigationEnabled() {
|
if hs.Features.Toggles().IsNewNavigationEnabled() {
|
||||||
dashboardsUrl = "/dashboards"
|
dashboardsUrl = "/dashboards"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -312,7 +312,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if hs.Cfg.FeatureToggles["live-pipeline"] {
|
if hs.Features.Toggles().IsLivePipelineEnabled() {
|
||||||
liveNavLinks := []*dtos.NavLink{}
|
liveNavLinks := []*dtos.NavLink{}
|
||||||
|
|
||||||
liveNavLinks = append(liveNavLinks, &dtos.NavLink{
|
liveNavLinks = append(liveNavLinks, &dtos.NavLink{
|
||||||
@ -346,7 +346,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
|
|||||||
SortWeight: dtos.WeightConfig,
|
SortWeight: dtos.WeightConfig,
|
||||||
Children: configNodes,
|
Children: configNodes,
|
||||||
}
|
}
|
||||||
if hs.Cfg.IsNewNavigationEnabled() {
|
if hs.Features.Toggles().IsNewNavigationEnabled() {
|
||||||
configNode.Section = dtos.NavSectionConfig
|
configNode.Section = dtos.NavSectionConfig
|
||||||
} else {
|
} else {
|
||||||
configNode.Section = dtos.NavSectionCore
|
configNode.Section = dtos.NavSectionCore
|
||||||
@ -358,7 +358,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
|
|||||||
|
|
||||||
if len(adminNavLinks) > 0 {
|
if len(adminNavLinks) > 0 {
|
||||||
navSection := dtos.NavSectionCore
|
navSection := dtos.NavSectionCore
|
||||||
if hs.Cfg.IsNewNavigationEnabled() {
|
if hs.Features.Toggles().IsNewNavigationEnabled() {
|
||||||
navSection = dtos.NavSectionConfig
|
navSection = dtos.NavSectionConfig
|
||||||
}
|
}
|
||||||
serverAdminNode := navlinks.GetServerAdminNode(adminNavLinks, navSection)
|
serverAdminNode := navlinks.GetServerAdminNode(adminNavLinks, navSection)
|
||||||
@ -386,7 +386,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
|
|||||||
|
|
||||||
func (hs *HTTPServer) buildDashboardNavLinks(c *models.ReqContext, hasEditPerm bool) []*dtos.NavLink {
|
func (hs *HTTPServer) buildDashboardNavLinks(c *models.ReqContext, hasEditPerm bool) []*dtos.NavLink {
|
||||||
dashboardChildNavs := []*dtos.NavLink{}
|
dashboardChildNavs := []*dtos.NavLink{}
|
||||||
if !hs.Cfg.IsNewNavigationEnabled() {
|
if !hs.Features.Toggles().IsNewNavigationEnabled() {
|
||||||
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{
|
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{
|
||||||
Text: "Home", Id: "home", Url: hs.Cfg.AppSubURL + "/", Icon: "home-alt", HideFromTabs: true,
|
Text: "Home", Id: "home", Url: hs.Cfg.AppSubURL + "/", Icon: "home-alt", HideFromTabs: true,
|
||||||
})
|
})
|
||||||
@ -417,7 +417,7 @@ func (hs *HTTPServer) buildDashboardNavLinks(c *models.ReqContext, hasEditPerm b
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasEditPerm && hs.Cfg.IsNewNavigationEnabled() {
|
if hasEditPerm && hs.Features.Toggles().IsNewNavigationEnabled() {
|
||||||
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{
|
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{
|
||||||
Text: "Divider", Divider: true, Id: "divider", HideFromTabs: true,
|
Text: "Divider", Divider: true, Id: "divider", HideFromTabs: true,
|
||||||
})
|
})
|
||||||
@ -622,7 +622,7 @@ func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewDat
|
|||||||
LoadingLogo: "public/img/grafana_icon.svg",
|
LoadingLogo: "public/img/grafana_icon.svg",
|
||||||
}
|
}
|
||||||
|
|
||||||
if hs.Cfg.FeatureToggles["accesscontrol"] {
|
if hs.Features.Toggles().IsAccesscontrolEnabled() {
|
||||||
userPermissions, err := hs.AccessControl.GetUserPermissions(c.Req.Context(), c.SignedInUser)
|
userPermissions, err := hs.AccessControl.GetUserPermissions(c.Req.Context(), c.SignedInUser)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -89,7 +89,7 @@ func (hs *HTTPServer) CreateOrg(c *models.ReqContext) response.Response {
|
|||||||
if err := web.Bind(c.Req, &cmd); err != nil {
|
if err := web.Bind(c.Req, &cmd); err != nil {
|
||||||
return response.Error(http.StatusBadRequest, "bad request data", err)
|
return response.Error(http.StatusBadRequest, "bad request data", err)
|
||||||
}
|
}
|
||||||
acEnabled := hs.Cfg.FeatureToggles["accesscontrol"]
|
acEnabled := hs.Features.Toggles().IsAccesscontrolEnabled()
|
||||||
if !acEnabled && !(setting.AllowUserOrgCreate || c.IsGrafanaAdmin) {
|
if !acEnabled && !(setting.AllowUserOrgCreate || c.IsGrafanaAdmin) {
|
||||||
return response.Error(403, "Access denied", nil)
|
return response.Error(403, "Access denied", nil)
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
@ -34,8 +35,8 @@ func setUpGetOrgUsersDB(t *testing.T, sqlStore *sqlstore.SQLStore) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestOrgUsersAPIEndpoint_userLoggedIn(t *testing.T) {
|
func TestOrgUsersAPIEndpoint_userLoggedIn(t *testing.T) {
|
||||||
settings := setting.NewCfg()
|
hs := setupSimpleHTTPServer(featuremgmt.WithFeatures())
|
||||||
hs := &HTTPServer{Cfg: settings}
|
settings := hs.Cfg
|
||||||
|
|
||||||
sqlStore := sqlstore.InitTestDB(t)
|
sqlStore := sqlstore.InitTestDB(t)
|
||||||
sqlStore.Cfg = settings
|
sqlStore.Cfg = settings
|
||||||
|
@ -5,7 +5,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/middleware"
|
"github.com/grafana/grafana/pkg/middleware"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/web"
|
"github.com/grafana/grafana/pkg/web"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -52,8 +52,8 @@ type RouteRegister interface {
|
|||||||
|
|
||||||
type RegisterNamedMiddleware func(name string) web.Handler
|
type RegisterNamedMiddleware func(name string) web.Handler
|
||||||
|
|
||||||
func ProvideRegister(cfg *setting.Cfg) *RouteRegisterImpl {
|
func ProvideRegister(features *featuremgmt.FeatureToggles) *RouteRegisterImpl {
|
||||||
return NewRouteRegister(middleware.ProvideRouteOperationName, middleware.RequestMetrics(cfg))
|
return NewRouteRegister(middleware.ProvideRouteOperationName, middleware.RequestMetrics(features))
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRouteRegister creates a new RouteRegister with all middlewares sent as params
|
// NewRouteRegister creates a new RouteRegister with all middlewares sent as params
|
||||||
|
@ -20,7 +20,7 @@ func (hs *HTTPServer) CreateTeam(c *models.ReqContext) response.Response {
|
|||||||
if err := web.Bind(c.Req, &cmd); err != nil {
|
if err := web.Bind(c.Req, &cmd); err != nil {
|
||||||
return response.Error(http.StatusBadRequest, "bad request data", err)
|
return response.Error(http.StatusBadRequest, "bad request data", err)
|
||||||
}
|
}
|
||||||
accessControlEnabled := hs.Cfg.FeatureToggles["accesscontrol"]
|
accessControlEnabled := hs.Features.Toggles().IsAccesscontrolEnabled()
|
||||||
if !accessControlEnabled && c.OrgRole == models.ROLE_VIEWER {
|
if !accessControlEnabled && c.OrgRole == models.ROLE_VIEWER {
|
||||||
return response.Error(403, "Not allowed to create team.", nil)
|
return response.Error(403, "Not allowed to create team.", nil)
|
||||||
}
|
}
|
||||||
|
@ -40,9 +40,7 @@ func TestTeamAPIEndpoint(t *testing.T) {
|
|||||||
TotalCount: 2,
|
TotalCount: 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
hs := &HTTPServer{
|
hs := setupSimpleHTTPServer(nil)
|
||||||
Cfg: setting.NewCfg(),
|
|
||||||
}
|
|
||||||
|
|
||||||
loggedInUserScenario(t, "When calling GET on", "/api/teams/search", "/api/teams/search", func(sc *scenarioContext) {
|
loggedInUserScenario(t, "When calling GET on", "/api/teams/search", "/api/teams/search", func(sc *scenarioContext) {
|
||||||
var sentLimit int
|
var sentLimit int
|
||||||
@ -92,10 +90,7 @@ func TestTeamAPIEndpoint(t *testing.T) {
|
|||||||
t.Run("When creating team with API key", func(t *testing.T) {
|
t.Run("When creating team with API key", func(t *testing.T) {
|
||||||
defer bus.ClearBusHandlers()
|
defer bus.ClearBusHandlers()
|
||||||
|
|
||||||
hs := &HTTPServer{
|
hs := setupSimpleHTTPServer(nil)
|
||||||
Cfg: setting.NewCfg(),
|
|
||||||
Bus: bus.GetBus(),
|
|
||||||
}
|
|
||||||
hs.Cfg.EditorsCanAdmin = true
|
hs.Cfg.EditorsCanAdmin = true
|
||||||
|
|
||||||
teamName := "team foo"
|
teamName := "team foo"
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/infra/metrics/metricutil"
|
"github.com/grafana/grafana/pkg/infra/metrics/metricutil"
|
||||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/mwitkow/go-conntrack"
|
"github.com/mwitkow/go-conntrack"
|
||||||
)
|
)
|
||||||
@ -16,7 +17,7 @@ import (
|
|||||||
var newProviderFunc = sdkhttpclient.NewProvider
|
var newProviderFunc = sdkhttpclient.NewProvider
|
||||||
|
|
||||||
// New creates a new HTTP client provider with pre-configured middlewares.
|
// New creates a new HTTP client provider with pre-configured middlewares.
|
||||||
func New(cfg *setting.Cfg, tracer tracing.Tracer) *sdkhttpclient.Provider {
|
func New(cfg *setting.Cfg, tracer tracing.Tracer, features *featuremgmt.FeatureToggles) *sdkhttpclient.Provider {
|
||||||
logger := log.New("httpclient")
|
logger := log.New("httpclient")
|
||||||
userAgent := fmt.Sprintf("Grafana/%s", cfg.BuildVersion)
|
userAgent := fmt.Sprintf("Grafana/%s", cfg.BuildVersion)
|
||||||
|
|
||||||
@ -35,7 +36,7 @@ func New(cfg *setting.Cfg, tracer tracing.Tracer) *sdkhttpclient.Provider {
|
|||||||
|
|
||||||
setDefaultTimeoutOptions(cfg)
|
setDefaultTimeoutOptions(cfg)
|
||||||
|
|
||||||
if cfg.FeatureToggles["httpclientprovider_azure_auth"] {
|
if features.IsHttpclientproviderAzureAuthEnabled() {
|
||||||
middlewares = append(middlewares, AzureMiddleware(cfg))
|
middlewares = append(middlewares, AzureMiddleware(cfg))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
|
sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
|
||||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@ -22,7 +23,7 @@ func TestHTTPClientProvider(t *testing.T) {
|
|||||||
})
|
})
|
||||||
tracer, err := tracing.InitializeTracerForTest()
|
tracer, err := tracing.InitializeTracerForTest()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
_ = New(&setting.Cfg{SigV4AuthEnabled: false}, tracer)
|
_ = New(&setting.Cfg{SigV4AuthEnabled: false}, tracer, featuremgmt.WithToggles())
|
||||||
require.Len(t, providerOpts, 1)
|
require.Len(t, providerOpts, 1)
|
||||||
o := providerOpts[0]
|
o := providerOpts[0]
|
||||||
require.Len(t, o.Middlewares, 6)
|
require.Len(t, o.Middlewares, 6)
|
||||||
@ -46,7 +47,7 @@ func TestHTTPClientProvider(t *testing.T) {
|
|||||||
})
|
})
|
||||||
tracer, err := tracing.InitializeTracerForTest()
|
tracer, err := tracing.InitializeTracerForTest()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
_ = New(&setting.Cfg{SigV4AuthEnabled: true}, tracer)
|
_ = New(&setting.Cfg{SigV4AuthEnabled: true}, tracer, featuremgmt.WithToggles())
|
||||||
require.Len(t, providerOpts, 1)
|
require.Len(t, providerOpts, 1)
|
||||||
o := providerOpts[0]
|
o := providerOpts[0]
|
||||||
require.Len(t, o.Middlewares, 7)
|
require.Len(t, o.Middlewares, 7)
|
||||||
|
@ -7,7 +7,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/web"
|
"github.com/grafana/grafana/pkg/web"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
cw "github.com/weaveworks/common/tracing"
|
cw "github.com/weaveworks/common/tracing"
|
||||||
@ -45,7 +45,7 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RequestMetrics is a middleware handler that instruments the request.
|
// RequestMetrics is a middleware handler that instruments the request.
|
||||||
func RequestMetrics(cfg *setting.Cfg) func(handler string) web.Handler {
|
func RequestMetrics(features *featuremgmt.FeatureToggles) func(handler string) web.Handler {
|
||||||
return func(handler string) web.Handler {
|
return func(handler string) web.Handler {
|
||||||
return func(res http.ResponseWriter, req *http.Request, c *web.Context) {
|
return func(res http.ResponseWriter, req *http.Request, c *web.Context) {
|
||||||
rw := res.(web.ResponseWriter)
|
rw := res.(web.ResponseWriter)
|
||||||
@ -60,7 +60,7 @@ func RequestMetrics(cfg *setting.Cfg) func(handler string) web.Handler {
|
|||||||
method := sanitizeMethod(req.Method)
|
method := sanitizeMethod(req.Method)
|
||||||
|
|
||||||
// enable histogram and disable summaries + counters for http requests.
|
// enable histogram and disable summaries + counters for http requests.
|
||||||
if cfg.IsHTTPRequestHistogramDisabled() {
|
if features.IsDisableHttpRequestHistogramEnabled() {
|
||||||
duration := time.Since(now).Nanoseconds() / int64(time.Millisecond)
|
duration := time.Since(now).Nanoseconds() / int64(time.Millisecond)
|
||||||
metrics.MHttpRequestTotal.WithLabelValues(handler, code, method).Inc()
|
metrics.MHttpRequestTotal.WithLabelValues(handler, code, method).Inc()
|
||||||
metrics.MHttpRequestSummary.WithLabelValues(handler, code, method).Observe(float64(duration))
|
metrics.MHttpRequestSummary.WithLabelValues(handler, code, method).Observe(float64(duration))
|
||||||
|
@ -14,8 +14,6 @@ type Licensing interface {
|
|||||||
|
|
||||||
StateInfo() string
|
StateInfo() string
|
||||||
|
|
||||||
EnabledFeatures() map[string]bool
|
|
||||||
|
|
||||||
FeatureEnabled(feature string) bool
|
FeatureEnabled(feature string) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,7 +85,6 @@ func pluginScenario(t *testing.T, desc string, fn func(*testing.T, *PluginManage
|
|||||||
|
|
||||||
t.Run("Given a plugin", func(t *testing.T) {
|
t.Run("Given a plugin", func(t *testing.T) {
|
||||||
cfg := &setting.Cfg{
|
cfg := &setting.Cfg{
|
||||||
FeatureToggles: map[string]bool{},
|
|
||||||
PluginSettings: setting.PluginSettings{
|
PluginSettings: setting.PluginSettings{
|
||||||
"test-app": map[string]string{
|
"test-app": map[string]string{
|
||||||
"path": "testdata/test-app",
|
"path": "testdata/test-app",
|
||||||
|
@ -18,7 +18,6 @@ import (
|
|||||||
|
|
||||||
func TestGetPluginDashboards(t *testing.T) {
|
func TestGetPluginDashboards(t *testing.T) {
|
||||||
cfg := &setting.Cfg{
|
cfg := &setting.Cfg{
|
||||||
FeatureToggles: map[string]bool{},
|
|
||||||
PluginSettings: setting.PluginSettings{
|
PluginSettings: setting.PluginSettings{
|
||||||
"test-app": map[string]string{
|
"test-app": map[string]string{
|
||||||
"path": "testdata/test-app",
|
"path": "testdata/test-app",
|
||||||
|
@ -17,6 +17,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/plugins/backendplugin/provider"
|
"github.com/grafana/grafana/pkg/plugins/backendplugin/provider"
|
||||||
"github.com/grafana/grafana/pkg/plugins/manager/loader"
|
"github.com/grafana/grafana/pkg/plugins/manager/loader"
|
||||||
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/licensing"
|
"github.com/grafana/grafana/pkg/services/licensing"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/tsdb/azuremonitor"
|
"github.com/grafana/grafana/pkg/tsdb/azuremonitor"
|
||||||
@ -50,11 +51,13 @@ func TestPluginManager_int_init(t *testing.T) {
|
|||||||
bundledPluginsPath, err := filepath.Abs("../../../plugins-bundled/internal")
|
bundledPluginsPath, err := filepath.Abs("../../../plugins-bundled/internal")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
features := featuremgmt.WithToggles()
|
||||||
cfg := &setting.Cfg{
|
cfg := &setting.Cfg{
|
||||||
Raw: ini.Empty(),
|
Raw: ini.Empty(),
|
||||||
Env: setting.Prod,
|
Env: setting.Prod,
|
||||||
StaticRootPath: staticRootPath,
|
StaticRootPath: staticRootPath,
|
||||||
BundledPluginsPath: bundledPluginsPath,
|
BundledPluginsPath: bundledPluginsPath,
|
||||||
|
IsFeatureToggleEnabled: features.IsEnabled,
|
||||||
PluginSettings: map[string]map[string]string{
|
PluginSettings: map[string]map[string]string{
|
||||||
"plugin.datasource-id": {
|
"plugin.datasource-id": {
|
||||||
"path": "testdata/test-app",
|
"path": "testdata/test-app",
|
||||||
@ -79,7 +82,7 @@ func TestPluginManager_int_init(t *testing.T) {
|
|||||||
otsdb := opentsdb.ProvideService(hcp)
|
otsdb := opentsdb.ProvideService(hcp)
|
||||||
pr := prometheus.ProvideService(hcp, tracer)
|
pr := prometheus.ProvideService(hcp, tracer)
|
||||||
tmpo := tempo.ProvideService(hcp)
|
tmpo := tempo.ProvideService(hcp)
|
||||||
td := testdatasource.ProvideService(cfg)
|
td := testdatasource.ProvideService(cfg, features)
|
||||||
pg := postgres.ProvideService(cfg)
|
pg := postgres.ProvideService(cfg)
|
||||||
my := mysql.ProvideService(cfg, hcp)
|
my := mysql.ProvideService(cfg, hcp)
|
||||||
ms := mssql.ProvideService(cfg)
|
ms := mssql.ProvideService(cfg)
|
||||||
|
@ -35,6 +35,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
|
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
|
||||||
"github.com/grafana/grafana/pkg/services/datasourceproxy"
|
"github.com/grafana/grafana/pkg/services/datasourceproxy"
|
||||||
"github.com/grafana/grafana/pkg/services/datasources"
|
"github.com/grafana/grafana/pkg/services/datasources"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/hooks"
|
"github.com/grafana/grafana/pkg/services/hooks"
|
||||||
"github.com/grafana/grafana/pkg/services/libraryelements"
|
"github.com/grafana/grafana/pkg/services/libraryelements"
|
||||||
"github.com/grafana/grafana/pkg/services/librarypanels"
|
"github.com/grafana/grafana/pkg/services/librarypanels"
|
||||||
@ -182,6 +183,8 @@ var wireBasicSet = wire.NewSet(
|
|||||||
wire.Bind(new(teamguardian.Store), new(*teamguardianDatabase.TeamGuardianStoreImpl)),
|
wire.Bind(new(teamguardian.Store), new(*teamguardianDatabase.TeamGuardianStoreImpl)),
|
||||||
teamguardianManager.ProvideService,
|
teamguardianManager.ProvideService,
|
||||||
wire.Bind(new(teamguardian.TeamGuardian), new(*teamguardianManager.Service)),
|
wire.Bind(new(teamguardian.TeamGuardian), new(*teamguardianManager.Service)),
|
||||||
|
featuremgmt.ProvideManagerService,
|
||||||
|
featuremgmt.ProvideToggles,
|
||||||
)
|
)
|
||||||
|
|
||||||
var wireSet = wire.NewSet(
|
var wireSet = wire.NewSet(
|
||||||
|
@ -9,13 +9,13 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/infra/usagestats"
|
"github.com/grafana/grafana/pkg/infra/usagestats"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ProvideService(cfg *setting.Cfg, usageStats usagestats.Service) *OSSAccessControlService {
|
func ProvideService(features *featuremgmt.FeatureToggles, usageStats usagestats.Service) *OSSAccessControlService {
|
||||||
s := &OSSAccessControlService{
|
s := &OSSAccessControlService{
|
||||||
Cfg: cfg,
|
features: features,
|
||||||
UsageStats: usageStats,
|
UsageStats: usageStats,
|
||||||
Log: log.New("accesscontrol"),
|
Log: log.New("accesscontrol"),
|
||||||
ScopeResolver: accesscontrol.NewScopeResolver(),
|
ScopeResolver: accesscontrol.NewScopeResolver(),
|
||||||
@ -26,7 +26,7 @@ func ProvideService(cfg *setting.Cfg, usageStats usagestats.Service) *OSSAccessC
|
|||||||
|
|
||||||
// OSSAccessControlService is the service implementing role based access control.
|
// OSSAccessControlService is the service implementing role based access control.
|
||||||
type OSSAccessControlService struct {
|
type OSSAccessControlService struct {
|
||||||
Cfg *setting.Cfg
|
features *featuremgmt.FeatureToggles
|
||||||
UsageStats usagestats.Service
|
UsageStats usagestats.Service
|
||||||
Log log.Logger
|
Log log.Logger
|
||||||
registrations accesscontrol.RegistrationList
|
registrations accesscontrol.RegistrationList
|
||||||
@ -34,10 +34,10 @@ type OSSAccessControlService struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ac *OSSAccessControlService) IsDisabled() bool {
|
func (ac *OSSAccessControlService) IsDisabled() bool {
|
||||||
if ac.Cfg == nil {
|
if ac.features == nil {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return !ac.Cfg.FeatureToggles["accesscontrol"]
|
return !ac.features.IsAccesscontrolEnabled()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ac *OSSAccessControlService) registerUsageMetrics() {
|
func (ac *OSSAccessControlService) registerUsageMetrics() {
|
||||||
|
@ -12,17 +12,14 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/infra/usagestats"
|
"github.com/grafana/grafana/pkg/infra/usagestats"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
func setupTestEnv(t testing.TB) *OSSAccessControlService {
|
func setupTestEnv(t testing.TB) *OSSAccessControlService {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
cfg := setting.NewCfg()
|
|
||||||
cfg.FeatureToggles = map[string]bool{"accesscontrol": true}
|
|
||||||
|
|
||||||
ac := &OSSAccessControlService{
|
ac := &OSSAccessControlService{
|
||||||
Cfg: cfg,
|
features: featuremgmt.WithToggles("accesscontrol"),
|
||||||
UsageStats: &usagestats.UsageStatsMock{T: t},
|
UsageStats: &usagestats.UsageStatsMock{T: t},
|
||||||
Log: log.New("accesscontrol"),
|
Log: log.New("accesscontrol"),
|
||||||
registrations: accesscontrol.RegistrationList{},
|
registrations: accesscontrol.RegistrationList{},
|
||||||
@ -148,12 +145,9 @@ func TestUsageMetrics(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
cfg := setting.NewCfg()
|
features := featuremgmt.WithToggles("accesscontrol", tt.enabled)
|
||||||
if tt.enabled {
|
|
||||||
cfg.FeatureToggles = map[string]bool{"accesscontrol": true}
|
|
||||||
}
|
|
||||||
|
|
||||||
s := ProvideService(cfg, &usagestats.UsageStatsMock{T: t})
|
s := ProvideService(features, &usagestats.UsageStatsMock{T: t})
|
||||||
report, err := s.UsageStats.GetUsageReport(context.Background())
|
report, err := s.UsageStats.GetUsageReport(context.Background())
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
@ -267,7 +261,7 @@ func TestOSSAccessControlService_RegisterFixedRole(t *testing.T) {
|
|||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
ac := &OSSAccessControlService{
|
ac := &OSSAccessControlService{
|
||||||
Cfg: setting.NewCfg(),
|
features: featuremgmt.WithToggles(),
|
||||||
UsageStats: &usagestats.UsageStatsMock{T: t},
|
UsageStats: &usagestats.UsageStatsMock{T: t},
|
||||||
Log: log.New("accesscontrol-test"),
|
Log: log.New("accesscontrol-test"),
|
||||||
}
|
}
|
||||||
@ -386,12 +380,11 @@ func TestOSSAccessControlService_DeclareFixedRoles(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
ac := &OSSAccessControlService{
|
ac := &OSSAccessControlService{
|
||||||
Cfg: setting.NewCfg(),
|
features: featuremgmt.WithToggles("accesscontrol"),
|
||||||
UsageStats: &usagestats.UsageStatsMock{T: t},
|
UsageStats: &usagestats.UsageStatsMock{T: t},
|
||||||
Log: log.New("accesscontrol-test"),
|
Log: log.New("accesscontrol-test"),
|
||||||
registrations: accesscontrol.RegistrationList{},
|
registrations: accesscontrol.RegistrationList{},
|
||||||
}
|
}
|
||||||
ac.Cfg.FeatureToggles = map[string]bool{"accesscontrol": true}
|
|
||||||
|
|
||||||
// Test
|
// Test
|
||||||
err := ac.DeclareFixedRoles(tt.registrations...)
|
err := ac.DeclareFixedRoles(tt.registrations...)
|
||||||
@ -459,9 +452,6 @@ func TestOSSAccessControlService_RegisterFixedRoles(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
cfg := setting.NewCfg()
|
|
||||||
cfg.FeatureToggles = map[string]bool{"accesscontrol": true}
|
|
||||||
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
// Remove any inserted role after the test case has been run
|
// Remove any inserted role after the test case has been run
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
@ -472,12 +462,11 @@ func TestOSSAccessControlService_RegisterFixedRoles(t *testing.T) {
|
|||||||
|
|
||||||
// Setup
|
// Setup
|
||||||
ac := &OSSAccessControlService{
|
ac := &OSSAccessControlService{
|
||||||
Cfg: setting.NewCfg(),
|
features: featuremgmt.WithToggles("accesscontrol"),
|
||||||
UsageStats: &usagestats.UsageStatsMock{T: t},
|
UsageStats: &usagestats.UsageStatsMock{T: t},
|
||||||
Log: log.New("accesscontrol-test"),
|
Log: log.New("accesscontrol-test"),
|
||||||
registrations: accesscontrol.RegistrationList{},
|
registrations: accesscontrol.RegistrationList{},
|
||||||
}
|
}
|
||||||
ac.Cfg.FeatureToggles = map[string]bool{"accesscontrol": true}
|
|
||||||
ac.registrations.Append(tt.registrations...)
|
ac.registrations.Append(tt.registrations...)
|
||||||
|
|
||||||
// Test
|
// Test
|
||||||
@ -552,7 +541,7 @@ func TestOSSAccessControlService_GetUserPermissions(t *testing.T) {
|
|||||||
|
|
||||||
// Setup
|
// Setup
|
||||||
ac := setupTestEnv(t)
|
ac := setupTestEnv(t)
|
||||||
ac.Cfg.FeatureToggles = map[string]bool{"accesscontrol": true}
|
ac.features = featuremgmt.WithToggles("accesscontrol")
|
||||||
|
|
||||||
registration.Role.Permissions = []accesscontrol.Permission{tt.rawPerm}
|
registration.Role.Permissions = []accesscontrol.Permission{tt.rawPerm}
|
||||||
err := ac.DeclareFixedRoles(registration)
|
err := ac.DeclareFixedRoles(registration)
|
||||||
@ -638,7 +627,6 @@ func TestOSSAccessControlService_Evaluate(t *testing.T) {
|
|||||||
|
|
||||||
// Setup
|
// Setup
|
||||||
ac := setupTestEnv(t)
|
ac := setupTestEnv(t)
|
||||||
ac.Cfg.FeatureToggles = map[string]bool{"accesscontrol": true}
|
|
||||||
ac.RegisterAttributeScopeResolver("users:login:", userLoginScopeSolver)
|
ac.RegisterAttributeScopeResolver("users:login:", userLoginScopeSolver)
|
||||||
|
|
||||||
registration.Role.Permissions = []accesscontrol.Permission{tt.rawPerm}
|
registration.Role.Permissions = []accesscontrol.Permission{tt.rawPerm}
|
||||||
|
95
pkg/services/featuremgmt/features.go
Normal file
95
pkg/services/featuremgmt/features.go
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
package featuremgmt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FeatureToggleState indicates the quality level
|
||||||
|
type FeatureToggleState int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// FeatureStateUnknown indicates that no state is specified
|
||||||
|
FeatureStateUnknown FeatureToggleState = iota
|
||||||
|
|
||||||
|
// FeatureStateAlpha the feature is in active development and may change at any time
|
||||||
|
FeatureStateAlpha
|
||||||
|
|
||||||
|
// FeatureStateBeta the feature is still in development, but settings will have migrations
|
||||||
|
FeatureStateBeta
|
||||||
|
|
||||||
|
// FeatureStateStable this is a stable feature
|
||||||
|
FeatureStateStable
|
||||||
|
|
||||||
|
// FeatureStateDeprecated the feature will be removed in the future
|
||||||
|
FeatureStateDeprecated
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s FeatureToggleState) String() string {
|
||||||
|
switch s {
|
||||||
|
case FeatureStateAlpha:
|
||||||
|
return "alpha"
|
||||||
|
case FeatureStateBeta:
|
||||||
|
return "beta"
|
||||||
|
case FeatureStateStable:
|
||||||
|
return "stable"
|
||||||
|
case FeatureStateDeprecated:
|
||||||
|
return "deprecated"
|
||||||
|
case FeatureStateUnknown:
|
||||||
|
}
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON marshals the enum as a quoted json string
|
||||||
|
func (s FeatureToggleState) MarshalJSON() ([]byte, error) {
|
||||||
|
buffer := bytes.NewBufferString(`"`)
|
||||||
|
buffer.WriteString(s.String())
|
||||||
|
buffer.WriteString(`"`)
|
||||||
|
return buffer.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON unmarshals a quoted json string to the enum value
|
||||||
|
func (s *FeatureToggleState) UnmarshalJSON(b []byte) error {
|
||||||
|
var j string
|
||||||
|
err := json.Unmarshal(b, &j)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch j {
|
||||||
|
case "alpha":
|
||||||
|
*s = FeatureStateAlpha
|
||||||
|
|
||||||
|
case "beta":
|
||||||
|
*s = FeatureStateBeta
|
||||||
|
|
||||||
|
case "stable":
|
||||||
|
*s = FeatureStateStable
|
||||||
|
|
||||||
|
case "deprecated":
|
||||||
|
*s = FeatureStateDeprecated
|
||||||
|
|
||||||
|
default:
|
||||||
|
*s = FeatureStateUnknown
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type FeatureFlag struct {
|
||||||
|
Name string `json:"name" yaml:"name"` // Unique name
|
||||||
|
Description string `json:"description"`
|
||||||
|
State FeatureToggleState `json:"state,omitempty"`
|
||||||
|
DocsURL string `json:"docsURL,omitempty"`
|
||||||
|
|
||||||
|
// CEL-GO expression. Using the value "true" will mean this is on by default
|
||||||
|
Expression string `json:"expression,omitempty"`
|
||||||
|
|
||||||
|
// Special behavior flags
|
||||||
|
RequiresDevMode bool `json:"requiresDevMode,omitempty"` // can not be enabled in production
|
||||||
|
RequiresRestart bool `json:"requiresRestart,omitempty"` // The server must be initialized with the value
|
||||||
|
RequiresLicense bool `json:"requiresLicense,omitempty"` // Must be enabled in the license
|
||||||
|
FrontendOnly bool `json:"frontend,omitempty"` // change is only seen in the frontend
|
||||||
|
|
||||||
|
// Internal properties
|
||||||
|
// expr string `json:-`
|
||||||
|
}
|
195
pkg/services/featuremgmt/manager.go
Normal file
195
pkg/services/featuremgmt/manager.go
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
package featuremgmt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/api/response"
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FeatureManager struct {
|
||||||
|
isDevMod bool
|
||||||
|
licensing models.Licensing
|
||||||
|
flags map[string]*FeatureFlag
|
||||||
|
enabled map[string]bool // only the "on" values
|
||||||
|
toggles *FeatureToggles
|
||||||
|
config string // path to config file
|
||||||
|
vars map[string]interface{}
|
||||||
|
log log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// This will merge the flags with the current configuration
|
||||||
|
func (fm *FeatureManager) registerFlags(flags ...FeatureFlag) {
|
||||||
|
for idx, add := range flags {
|
||||||
|
if add.Name == "" {
|
||||||
|
continue // skip it with warning?
|
||||||
|
}
|
||||||
|
flag, ok := fm.flags[add.Name]
|
||||||
|
if !ok {
|
||||||
|
fm.flags[add.Name] = &flags[idx]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Selectively update properties
|
||||||
|
if add.Description != "" {
|
||||||
|
flag.Description = add.Description
|
||||||
|
}
|
||||||
|
if add.DocsURL != "" {
|
||||||
|
flag.DocsURL = add.DocsURL
|
||||||
|
}
|
||||||
|
if add.Expression != "" {
|
||||||
|
flag.Expression = add.Expression
|
||||||
|
}
|
||||||
|
|
||||||
|
// The most recently defined state
|
||||||
|
if add.State != FeatureStateUnknown {
|
||||||
|
flag.State = add.State
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only gets more restrictive
|
||||||
|
if add.RequiresDevMode {
|
||||||
|
flag.RequiresDevMode = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if add.RequiresLicense {
|
||||||
|
flag.RequiresLicense = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if add.RequiresRestart {
|
||||||
|
flag.RequiresRestart = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This will evaluate all flags
|
||||||
|
fm.update()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *FeatureManager) evaluate(ff *FeatureFlag) bool {
|
||||||
|
if ff.RequiresDevMode && !fm.isDevMod {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if ff.RequiresLicense && (fm.licensing == nil || !fm.licensing.FeatureEnabled(ff.Name)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: CEL - expression
|
||||||
|
return ff.Expression == "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update
|
||||||
|
func (fm *FeatureManager) update() {
|
||||||
|
enabled := make(map[string]bool)
|
||||||
|
for _, flag := range fm.flags {
|
||||||
|
val := fm.evaluate(flag)
|
||||||
|
|
||||||
|
// Update the registry
|
||||||
|
track := 0.0
|
||||||
|
if val {
|
||||||
|
track = 1
|
||||||
|
enabled[flag.Name] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register value with prometheus metric
|
||||||
|
featureToggleInfo.WithLabelValues(flag.Name).Set(track)
|
||||||
|
}
|
||||||
|
fm.enabled = enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run is called by background services
|
||||||
|
func (fm *FeatureManager) readFile() error {
|
||||||
|
if fm.config == "" {
|
||||||
|
return nil // not configured
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := readConfigFile(fm.config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fm.registerFlags(cfg.Flags...)
|
||||||
|
fm.vars = cfg.Vars
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEnabled checks if a feature is enabled
|
||||||
|
func (fm *FeatureManager) IsEnabled(flag string) bool {
|
||||||
|
return fm.enabled[flag]
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEnabled returns a map contaning only the features that are enabled
|
||||||
|
func (fm *FeatureManager) GetEnabled(ctx context.Context) map[string]bool {
|
||||||
|
enabled := make(map[string]bool, len(fm.enabled))
|
||||||
|
for key, val := range fm.enabled {
|
||||||
|
if val {
|
||||||
|
enabled[key] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggles returns FeatureToggles.
|
||||||
|
func (fm *FeatureManager) Toggles() *FeatureToggles {
|
||||||
|
if fm.toggles == nil {
|
||||||
|
fm.toggles = &FeatureToggles{manager: fm}
|
||||||
|
}
|
||||||
|
return fm.toggles
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFlags returns all flag definitions
|
||||||
|
func (fm *FeatureManager) GetFlags() []FeatureFlag {
|
||||||
|
v := make([]FeatureFlag, 0, len(fm.flags))
|
||||||
|
for _, value := range fm.flags {
|
||||||
|
v = append(v, *value)
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *FeatureManager) HandleGetSettings(c *models.ReqContext) {
|
||||||
|
res := make(map[string]interface{}, 3)
|
||||||
|
res["enabled"] = fm.GetEnabled(c.Req.Context())
|
||||||
|
|
||||||
|
vv := make([]*FeatureFlag, 0, len(fm.flags))
|
||||||
|
for _, v := range fm.flags {
|
||||||
|
vv = append(vv, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
res["info"] = vv
|
||||||
|
|
||||||
|
response.JSON(200, res).WriteTo(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithFeatures is used to define feature toggles for testing.
|
||||||
|
// The arguments are a list of strings that are optionally followed by a boolean value
|
||||||
|
func WithFeatures(spec ...interface{}) *FeatureManager {
|
||||||
|
count := len(spec)
|
||||||
|
enabled := make(map[string]bool, count)
|
||||||
|
|
||||||
|
idx := 0
|
||||||
|
for idx < count {
|
||||||
|
key := fmt.Sprintf("%v", spec[idx])
|
||||||
|
val := true
|
||||||
|
idx++
|
||||||
|
if idx < count && reflect.TypeOf(spec[idx]).Kind() == reflect.Bool {
|
||||||
|
val = spec[idx].(bool)
|
||||||
|
idx++
|
||||||
|
}
|
||||||
|
|
||||||
|
if val {
|
||||||
|
enabled[key] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &FeatureManager{enabled: enabled}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithToggles(spec ...interface{}) *FeatureToggles {
|
||||||
|
return &FeatureToggles{
|
||||||
|
manager: WithFeatures(spec...),
|
||||||
|
}
|
||||||
|
}
|
77
pkg/services/featuremgmt/manager_test.go
Normal file
77
pkg/services/featuremgmt/manager_test.go
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
package featuremgmt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFeatureManager(t *testing.T) {
|
||||||
|
t.Run("check testing stubs", func(t *testing.T) {
|
||||||
|
ft := WithFeatures("a", "b", "c")
|
||||||
|
require.True(t, ft.IsEnabled("a"))
|
||||||
|
require.True(t, ft.IsEnabled("b"))
|
||||||
|
require.True(t, ft.IsEnabled("c"))
|
||||||
|
require.False(t, ft.IsEnabled("d"))
|
||||||
|
|
||||||
|
require.Equal(t, map[string]bool{"a": true, "b": true, "c": true}, ft.GetEnabled(context.Background()))
|
||||||
|
|
||||||
|
// Explicit values
|
||||||
|
ft = WithFeatures("a", true, "b", false)
|
||||||
|
require.True(t, ft.IsEnabled("a"))
|
||||||
|
require.False(t, ft.IsEnabled("b"))
|
||||||
|
require.Equal(t, map[string]bool{"a": true}, ft.GetEnabled(context.Background()))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("check license validation", func(t *testing.T) {
|
||||||
|
ft := FeatureManager{
|
||||||
|
flags: map[string]*FeatureFlag{},
|
||||||
|
}
|
||||||
|
ft.registerFlags(FeatureFlag{
|
||||||
|
Name: "a",
|
||||||
|
RequiresLicense: true,
|
||||||
|
RequiresDevMode: true,
|
||||||
|
Expression: "true",
|
||||||
|
}, FeatureFlag{
|
||||||
|
Name: "b",
|
||||||
|
Expression: "true",
|
||||||
|
})
|
||||||
|
require.False(t, ft.IsEnabled("a"))
|
||||||
|
require.True(t, ft.IsEnabled("b"))
|
||||||
|
require.False(t, ft.IsEnabled("c")) // uknown flag
|
||||||
|
|
||||||
|
// Try changing "requires license"
|
||||||
|
ft.registerFlags(FeatureFlag{
|
||||||
|
Name: "a",
|
||||||
|
RequiresLicense: false, // shuld still require license!
|
||||||
|
}, FeatureFlag{
|
||||||
|
Name: "b",
|
||||||
|
RequiresLicense: true, // expression is still "true"
|
||||||
|
})
|
||||||
|
require.False(t, ft.IsEnabled("a"))
|
||||||
|
require.False(t, ft.IsEnabled("b"))
|
||||||
|
require.False(t, ft.IsEnabled("c"))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("check description and docs configs", func(t *testing.T) {
|
||||||
|
ft := FeatureManager{
|
||||||
|
flags: map[string]*FeatureFlag{},
|
||||||
|
}
|
||||||
|
ft.registerFlags(FeatureFlag{
|
||||||
|
Name: "a",
|
||||||
|
Description: "first",
|
||||||
|
}, FeatureFlag{
|
||||||
|
Name: "a",
|
||||||
|
Description: "second",
|
||||||
|
}, FeatureFlag{
|
||||||
|
Name: "a",
|
||||||
|
DocsURL: "http://something",
|
||||||
|
}, FeatureFlag{
|
||||||
|
Name: "a",
|
||||||
|
})
|
||||||
|
flag := ft.flags["a"]
|
||||||
|
require.Equal(t, "second", flag.Description)
|
||||||
|
require.Equal(t, "http://something", flag.DocsURL)
|
||||||
|
})
|
||||||
|
}
|
163
pkg/services/featuremgmt/registry.go
Normal file
163
pkg/services/featuremgmt/registry.go
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
package featuremgmt
|
||||||
|
|
||||||
|
import "github.com/grafana/grafana/pkg/services/secrets"
|
||||||
|
|
||||||
|
var (
|
||||||
|
FLAG_database_metrics = "database_metrics"
|
||||||
|
FLAG_live_config = "live-config"
|
||||||
|
FLAG_recordedQueries = "recordedQueries"
|
||||||
|
|
||||||
|
// Register each toggle here
|
||||||
|
standardFeatureFlags = []FeatureFlag{
|
||||||
|
{
|
||||||
|
Name: FLAG_recordedQueries,
|
||||||
|
Description: "Supports saving queries that can be scraped by prometheus",
|
||||||
|
State: FeatureStateBeta,
|
||||||
|
RequiresLicense: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "teamsync",
|
||||||
|
Description: "Team sync lets you set up synchronization between your auth providers teams and teams in Grafana",
|
||||||
|
State: FeatureStateStable,
|
||||||
|
DocsURL: "https://grafana.com/docs/grafana/latest/enterprise/team-sync/",
|
||||||
|
RequiresLicense: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "ldapsync",
|
||||||
|
Description: "Enhanced LDAP integration",
|
||||||
|
State: FeatureStateStable,
|
||||||
|
DocsURL: "https://grafana.com/docs/grafana/latest/enterprise/enhanced_ldap/",
|
||||||
|
RequiresLicense: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "caching",
|
||||||
|
Description: "Temporarily store data source query results.",
|
||||||
|
State: FeatureStateStable,
|
||||||
|
DocsURL: "https://grafana.com/docs/grafana/latest/enterprise/query-caching/",
|
||||||
|
RequiresLicense: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "dspermissions",
|
||||||
|
Description: "Data source permissions",
|
||||||
|
State: FeatureStateStable,
|
||||||
|
DocsURL: "https://grafana.com/docs/grafana/latest/enterprise/datasource_permissions/",
|
||||||
|
RequiresLicense: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "analytics",
|
||||||
|
Description: "Analytics",
|
||||||
|
State: FeatureStateStable,
|
||||||
|
RequiresLicense: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "enterprise.plugins",
|
||||||
|
Description: "Enterprise plugins",
|
||||||
|
State: FeatureStateStable,
|
||||||
|
DocsURL: "https://grafana.com/grafana/plugins/?enterprise=1",
|
||||||
|
RequiresLicense: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "trimDefaults",
|
||||||
|
Description: "Use cue schema to remove values that will be applied automatically",
|
||||||
|
State: FeatureStateBeta,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: secrets.EnvelopeEncryptionFeatureToggle,
|
||||||
|
Description: "encrypt secrets",
|
||||||
|
State: FeatureStateBeta,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
Name: "httpclientprovider_azure_auth",
|
||||||
|
State: FeatureStateBeta,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "service-accounts",
|
||||||
|
Description: "support service accounts",
|
||||||
|
State: FeatureStateBeta,
|
||||||
|
RequiresLicense: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
Name: FLAG_database_metrics,
|
||||||
|
Description: "Add prometheus metrics for database tables",
|
||||||
|
State: FeatureStateStable,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "dashboardPreviews",
|
||||||
|
Description: "Create and show thumbnails for dashboard search results",
|
||||||
|
State: FeatureStateAlpha,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: FLAG_live_config,
|
||||||
|
Description: "Save grafana live configuration in SQL tables",
|
||||||
|
State: FeatureStateAlpha,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "live-pipeline",
|
||||||
|
Description: "enable a generic live processing pipeline",
|
||||||
|
State: FeatureStateAlpha,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "live-service-web-worker",
|
||||||
|
Description: "This will use a webworker thread to processes events rather than the main thread",
|
||||||
|
State: FeatureStateAlpha,
|
||||||
|
FrontendOnly: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "queryOverLive",
|
||||||
|
Description: "Use grafana live websocket to execute backend queries",
|
||||||
|
State: FeatureStateAlpha,
|
||||||
|
FrontendOnly: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "tempoSearch",
|
||||||
|
Description: "Enable searching in tempo datasources",
|
||||||
|
State: FeatureStateBeta,
|
||||||
|
FrontendOnly: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "tempoBackendSearch",
|
||||||
|
Description: "Use backend for tempo search",
|
||||||
|
State: FeatureStateBeta,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "tempoServiceGraph",
|
||||||
|
Description: "show service",
|
||||||
|
State: FeatureStateBeta,
|
||||||
|
FrontendOnly: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "fullRangeLogsVolume",
|
||||||
|
Description: "Show full range logs volume in expore",
|
||||||
|
State: FeatureStateBeta,
|
||||||
|
FrontendOnly: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "accesscontrol",
|
||||||
|
Description: "Support robust access control",
|
||||||
|
State: FeatureStateBeta,
|
||||||
|
RequiresLicense: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "prometheus_azure_auth",
|
||||||
|
Description: "Use azure authentication for prometheus datasource",
|
||||||
|
State: FeatureStateBeta,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "newNavigation",
|
||||||
|
Description: "Try the next gen naviation model",
|
||||||
|
State: FeatureStateAlpha,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "showFeatureFlagsInUI",
|
||||||
|
Description: "Show feature flags in the settings UI",
|
||||||
|
State: FeatureStateAlpha,
|
||||||
|
RequiresDevMode: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "disable_http_request_histogram",
|
||||||
|
State: FeatureStateAlpha,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
78
pkg/services/featuremgmt/service.go
Normal file
78
pkg/services/featuremgmt/service.go
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
package featuremgmt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// The values are updated each time
|
||||||
|
featureToggleInfo = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||||
|
Name: "feature_toggles_info",
|
||||||
|
Help: "info metric that exposes what feature toggles are enabled or not",
|
||||||
|
Namespace: "grafana",
|
||||||
|
}, []string{"name"})
|
||||||
|
)
|
||||||
|
|
||||||
|
func ProvideManagerService(cfg *setting.Cfg, licensing models.Licensing) (*FeatureManager, error) {
|
||||||
|
mgmt := &FeatureManager{
|
||||||
|
isDevMod: setting.Env != setting.Prod,
|
||||||
|
licensing: licensing,
|
||||||
|
flags: make(map[string]*FeatureFlag, 30),
|
||||||
|
enabled: make(map[string]bool),
|
||||||
|
log: log.New("featuremgmt"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the standard flags
|
||||||
|
mgmt.registerFlags(standardFeatureFlags...)
|
||||||
|
|
||||||
|
// Load the flags from `custom.ini` files
|
||||||
|
flags, err := setting.ReadFeatureTogglesFromInitFile(cfg.Raw.Section("feature_toggles"))
|
||||||
|
if err != nil {
|
||||||
|
return mgmt, err
|
||||||
|
}
|
||||||
|
for key, val := range flags {
|
||||||
|
flag, ok := mgmt.flags[key]
|
||||||
|
if !ok {
|
||||||
|
flag = &FeatureFlag{
|
||||||
|
Name: key,
|
||||||
|
State: FeatureStateUnknown,
|
||||||
|
}
|
||||||
|
mgmt.flags[key] = flag
|
||||||
|
}
|
||||||
|
flag.Expression = fmt.Sprintf("%t", val) // true | false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load config settings
|
||||||
|
configfile := filepath.Join(cfg.HomePath, "conf", "features.yaml")
|
||||||
|
if _, err := os.Stat(configfile); err == nil {
|
||||||
|
mgmt.log.Info("[experimental] loading features from config file", "path", configfile)
|
||||||
|
mgmt.config = configfile
|
||||||
|
err = mgmt.readFile()
|
||||||
|
if err != nil {
|
||||||
|
return mgmt, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update the values
|
||||||
|
mgmt.update()
|
||||||
|
|
||||||
|
// Minimum approach to avoid circular dependency
|
||||||
|
cfg.IsFeatureToggleEnabled = mgmt.IsEnabled
|
||||||
|
return mgmt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProvideToggles allows read-only access to the feature state
|
||||||
|
func ProvideToggles(mgmt *FeatureManager) *FeatureToggles {
|
||||||
|
return &FeatureToggles{
|
||||||
|
manager: mgmt,
|
||||||
|
}
|
||||||
|
}
|
34
pkg/services/featuremgmt/settings.go
Normal file
34
pkg/services/featuremgmt/settings.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package featuremgmt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type configBody struct {
|
||||||
|
// define variables that can be used in expressions
|
||||||
|
Vars map[string]interface{} `yaml:"vars"`
|
||||||
|
|
||||||
|
// Define and override feature flag properties
|
||||||
|
Flags []FeatureFlag `yaml:"flags"`
|
||||||
|
|
||||||
|
// keep track of where the fie was loaded from
|
||||||
|
filename string
|
||||||
|
}
|
||||||
|
|
||||||
|
// will read a single configfile
|
||||||
|
func readConfigFile(filename string) (*configBody, error) {
|
||||||
|
cfg := &configBody{}
|
||||||
|
|
||||||
|
// Can ignore gosec G304 because the file path is forced within config subfolder
|
||||||
|
//nolint:gosec
|
||||||
|
yamlFile, err := ioutil.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return cfg, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = yaml.Unmarshal(yamlFile, cfg)
|
||||||
|
cfg.filename = filename
|
||||||
|
return cfg, err
|
||||||
|
}
|
25
pkg/services/featuremgmt/settings_test.go
Normal file
25
pkg/services/featuremgmt/settings_test.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package featuremgmt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestReadingFeatureSettings(t *testing.T) {
|
||||||
|
config, err := readConfigFile("testdata/features.yaml")
|
||||||
|
require.NoError(t, err, "No error when reading feature configs")
|
||||||
|
|
||||||
|
assert.Equal(t, map[string]interface{}{
|
||||||
|
"level": "free",
|
||||||
|
"stack": "something",
|
||||||
|
"valA": "value from features.yaml",
|
||||||
|
}, config.Vars)
|
||||||
|
|
||||||
|
out, err := yaml.Marshal(config)
|
||||||
|
require.NoError(t, err)
|
||||||
|
fmt.Printf("%s", string(out))
|
||||||
|
}
|
33
pkg/services/featuremgmt/testdata/features.yaml
vendored
Normal file
33
pkg/services/featuremgmt/testdata/features.yaml
vendored
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
include:
|
||||||
|
- included.yaml # not yet supported
|
||||||
|
|
||||||
|
vars:
|
||||||
|
stack: something
|
||||||
|
level: free
|
||||||
|
valA: value from features.yaml
|
||||||
|
|
||||||
|
flags:
|
||||||
|
- name: feature1
|
||||||
|
description: feature1
|
||||||
|
expression: "false"
|
||||||
|
|
||||||
|
- name: feature3
|
||||||
|
description: feature3
|
||||||
|
expression: "true"
|
||||||
|
|
||||||
|
- name: feature3
|
||||||
|
description: feature3
|
||||||
|
expression: env.level == 'free'
|
||||||
|
|
||||||
|
- name: displaySwedishTheme
|
||||||
|
description: enable swedish background theme
|
||||||
|
expression: |
|
||||||
|
// restrict to users allowing swedish language
|
||||||
|
req.locale.contains("sv")
|
||||||
|
- name: displayFrenchFlag
|
||||||
|
description: sho background theme
|
||||||
|
expression: |
|
||||||
|
// only admins
|
||||||
|
user.id == 1
|
||||||
|
// show to users allowing french language
|
||||||
|
&& req.locale.contains("fr")
|
13
pkg/services/featuremgmt/testdata/included.yaml
vendored
Normal file
13
pkg/services/featuremgmt/testdata/included.yaml
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
include:
|
||||||
|
- features.yaml # make sure we avoid recusion!
|
||||||
|
|
||||||
|
# variables that can be used in expressions
|
||||||
|
vars:
|
||||||
|
stack: something
|
||||||
|
deep: 1
|
||||||
|
valA: value from included.yaml
|
||||||
|
|
||||||
|
flags:
|
||||||
|
- name: featureFromIncludedFile
|
||||||
|
description: an inlcuded file
|
||||||
|
expression: invalid expression string here
|
10
pkg/services/featuremgmt/toggles.go
Normal file
10
pkg/services/featuremgmt/toggles.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package featuremgmt
|
||||||
|
|
||||||
|
type FeatureToggles struct {
|
||||||
|
manager *FeatureManager
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEnabled checks if a feature is enabled
|
||||||
|
func (ft *FeatureToggles) IsEnabled(flag string) bool {
|
||||||
|
return ft.manager.IsEnabled(flag)
|
||||||
|
}
|
157
pkg/services/featuremgmt/toggles_gen.go
Normal file
157
pkg/services/featuremgmt/toggles_gen.go
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
// NOTE: This file is autogenerated
|
||||||
|
|
||||||
|
package featuremgmt
|
||||||
|
|
||||||
|
// IsRecordedQueriesEnabled checks for the flag: recordedQueries
|
||||||
|
// Supports saving queries that can be scraped by prometheus
|
||||||
|
func (ft *FeatureToggles) IsRecordedQueriesEnabled() bool {
|
||||||
|
return ft.manager.IsEnabled("recordedQueries")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsTeamsyncEnabled checks for the flag: teamsync
|
||||||
|
// Team sync lets you set up synchronization between your auth providers teams and teams in Grafana
|
||||||
|
func (ft *FeatureToggles) IsTeamsyncEnabled() bool {
|
||||||
|
return ft.manager.IsEnabled("teamsync")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsLdapsyncEnabled checks for the flag: ldapsync
|
||||||
|
// Enhanced LDAP integration
|
||||||
|
func (ft *FeatureToggles) IsLdapsyncEnabled() bool {
|
||||||
|
return ft.manager.IsEnabled("ldapsync")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsCachingEnabled checks for the flag: caching
|
||||||
|
// Temporarily store data source query results.
|
||||||
|
func (ft *FeatureToggles) IsCachingEnabled() bool {
|
||||||
|
return ft.manager.IsEnabled("caching")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDspermissionsEnabled checks for the flag: dspermissions
|
||||||
|
// Data source permissions
|
||||||
|
func (ft *FeatureToggles) IsDspermissionsEnabled() bool {
|
||||||
|
return ft.manager.IsEnabled("dspermissions")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAnalyticsEnabled checks for the flag: analytics
|
||||||
|
// Analytics
|
||||||
|
func (ft *FeatureToggles) IsAnalyticsEnabled() bool {
|
||||||
|
return ft.manager.IsEnabled("analytics")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEnterprisePluginsEnabled checks for the flag: enterprise.plugins
|
||||||
|
// Enterprise plugins
|
||||||
|
func (ft *FeatureToggles) IsEnterprisePluginsEnabled() bool {
|
||||||
|
return ft.manager.IsEnabled("enterprise.plugins")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsTrimDefaultsEnabled checks for the flag: trimDefaults
|
||||||
|
// Use cue schema to remove values that will be applied automatically
|
||||||
|
func (ft *FeatureToggles) IsTrimDefaultsEnabled() bool {
|
||||||
|
return ft.manager.IsEnabled("trimDefaults")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEnvelopeEncryptionEnabled checks for the flag: envelopeEncryption
|
||||||
|
// encrypt secrets
|
||||||
|
func (ft *FeatureToggles) IsEnvelopeEncryptionEnabled() bool {
|
||||||
|
return ft.manager.IsEnabled("envelopeEncryption")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsHttpclientproviderAzureAuthEnabled checks for the flag: httpclientprovider_azure_auth
|
||||||
|
func (ft *FeatureToggles) IsHttpclientproviderAzureAuthEnabled() bool {
|
||||||
|
return ft.manager.IsEnabled("httpclientprovider_azure_auth")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsServiceAccountsEnabled checks for the flag: service-accounts
|
||||||
|
// support service accounts
|
||||||
|
func (ft *FeatureToggles) IsServiceAccountsEnabled() bool {
|
||||||
|
return ft.manager.IsEnabled("service-accounts")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDatabaseMetricsEnabled checks for the flag: database_metrics
|
||||||
|
// Add prometheus metrics for database tables
|
||||||
|
func (ft *FeatureToggles) IsDatabaseMetricsEnabled() bool {
|
||||||
|
return ft.manager.IsEnabled("database_metrics")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDashboardPreviewsEnabled checks for the flag: dashboardPreviews
|
||||||
|
// Create and show thumbnails for dashboard search results
|
||||||
|
func (ft *FeatureToggles) IsDashboardPreviewsEnabled() bool {
|
||||||
|
return ft.manager.IsEnabled("dashboardPreviews")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsLiveConfigEnabled checks for the flag: live-config
|
||||||
|
// Save grafana live configuration in SQL tables
|
||||||
|
func (ft *FeatureToggles) IsLiveConfigEnabled() bool {
|
||||||
|
return ft.manager.IsEnabled("live-config")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsLivePipelineEnabled checks for the flag: live-pipeline
|
||||||
|
// enable a generic live processing pipeline
|
||||||
|
func (ft *FeatureToggles) IsLivePipelineEnabled() bool {
|
||||||
|
return ft.manager.IsEnabled("live-pipeline")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsLiveServiceWebWorkerEnabled checks for the flag: live-service-web-worker
|
||||||
|
// This will use a webworker thread to processes events rather than the main thread
|
||||||
|
func (ft *FeatureToggles) IsLiveServiceWebWorkerEnabled() bool {
|
||||||
|
return ft.manager.IsEnabled("live-service-web-worker")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsQueryOverLiveEnabled checks for the flag: queryOverLive
|
||||||
|
// Use grafana live websocket to execute backend queries
|
||||||
|
func (ft *FeatureToggles) IsQueryOverLiveEnabled() bool {
|
||||||
|
return ft.manager.IsEnabled("queryOverLive")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsTempoSearchEnabled checks for the flag: tempoSearch
|
||||||
|
// Enable searching in tempo datasources
|
||||||
|
func (ft *FeatureToggles) IsTempoSearchEnabled() bool {
|
||||||
|
return ft.manager.IsEnabled("tempoSearch")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsTempoBackendSearchEnabled checks for the flag: tempoBackendSearch
|
||||||
|
// Use backend for tempo search
|
||||||
|
func (ft *FeatureToggles) IsTempoBackendSearchEnabled() bool {
|
||||||
|
return ft.manager.IsEnabled("tempoBackendSearch")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsTempoServiceGraphEnabled checks for the flag: tempoServiceGraph
|
||||||
|
// show service
|
||||||
|
func (ft *FeatureToggles) IsTempoServiceGraphEnabled() bool {
|
||||||
|
return ft.manager.IsEnabled("tempoServiceGraph")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsFullRangeLogsVolumeEnabled checks for the flag: fullRangeLogsVolume
|
||||||
|
// Show full range logs volume in expore
|
||||||
|
func (ft *FeatureToggles) IsFullRangeLogsVolumeEnabled() bool {
|
||||||
|
return ft.manager.IsEnabled("fullRangeLogsVolume")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAccesscontrolEnabled checks for the flag: accesscontrol
|
||||||
|
// Support robust access control
|
||||||
|
func (ft *FeatureToggles) IsAccesscontrolEnabled() bool {
|
||||||
|
return ft.manager.IsEnabled("accesscontrol")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsPrometheusAzureAuthEnabled checks for the flag: prometheus_azure_auth
|
||||||
|
// Use azure authentication for prometheus datasource
|
||||||
|
func (ft *FeatureToggles) IsPrometheusAzureAuthEnabled() bool {
|
||||||
|
return ft.manager.IsEnabled("prometheus_azure_auth")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsNewNavigationEnabled checks for the flag: newNavigation
|
||||||
|
// Try the next gen naviation model
|
||||||
|
func (ft *FeatureToggles) IsNewNavigationEnabled() bool {
|
||||||
|
return ft.manager.IsEnabled("newNavigation")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsShowFeatureFlagsInUIEnabled checks for the flag: showFeatureFlagsInUI
|
||||||
|
// Show feature flags in the settings UI
|
||||||
|
func (ft *FeatureToggles) IsShowFeatureFlagsInUIEnabled() bool {
|
||||||
|
return ft.manager.IsEnabled("showFeatureFlagsInUI")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDisableHttpRequestHistogramEnabled checks for the flag: disable_http_request_histogram
|
||||||
|
func (ft *FeatureToggles) IsDisableHttpRequestHistogramEnabled() bool {
|
||||||
|
return ft.manager.IsEnabled("disable_http_request_histogram")
|
||||||
|
}
|
140
pkg/services/featuremgmt/toggles_gen_test.go
Normal file
140
pkg/services/featuremgmt/toggles_gen_test.go
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
package featuremgmt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFeatureToggleFiles(t *testing.T) {
|
||||||
|
// Typescript files
|
||||||
|
verifyAndGenerateFile(t,
|
||||||
|
"../../../packages/grafana-data/src/types/featureToggles.gen.ts",
|
||||||
|
generateTypeScript(),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Golang files
|
||||||
|
verifyAndGenerateFile(t,
|
||||||
|
"toggles_gen.go",
|
||||||
|
generateRegistry(t),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyAndGenerateFile(t *testing.T, fpath string, gen string) {
|
||||||
|
// nolint:gosec
|
||||||
|
// We can ignore the gosec G304 warning since this is a test and the function is only called explicitly above
|
||||||
|
body, err := ioutil.ReadFile(fpath)
|
||||||
|
if err == nil {
|
||||||
|
if diff := cmp.Diff(gen, string(body)); diff != "" {
|
||||||
|
str := fmt.Sprintf("body mismatch (-want +got):\n%s\n", diff)
|
||||||
|
err = fmt.Errorf(str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
e2 := os.WriteFile(fpath, []byte(gen), 0644)
|
||||||
|
if e2 != nil {
|
||||||
|
t.Errorf("error writing file: %s", e2.Error())
|
||||||
|
}
|
||||||
|
abs, _ := filepath.Abs(fpath)
|
||||||
|
t.Errorf("feature toggle do not match: %s (%s)", err.Error(), abs)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateTypeScript() string {
|
||||||
|
buf := `// NOTE: This file was auto generated. DO NOT EDIT DIRECTLY!
|
||||||
|
// To change feature flags, edit:
|
||||||
|
// pkg/services/featuremgmt/registry.go
|
||||||
|
// Then run tests in:
|
||||||
|
// pkg/services/featuremgmt/toggles_gen_test.go
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes available feature toggles in Grafana. These can be configured via
|
||||||
|
* conf/custom.ini to enable features under development or not yet available in
|
||||||
|
* stable version.
|
||||||
|
*
|
||||||
|
* Only enabled values will be returned in this interface
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export interface FeatureToggles {
|
||||||
|
[name: string]: boolean | undefined; // support any string value
|
||||||
|
|
||||||
|
`
|
||||||
|
for _, flag := range standardFeatureFlags {
|
||||||
|
buf += " " + getTypeScriptKey(flag.Name) + "?: boolean;\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
buf += "}\n"
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTypeScriptKey(key string) string {
|
||||||
|
if strings.Contains(key, "-") || strings.Contains(key, ".") {
|
||||||
|
return "['" + key + "']"
|
||||||
|
}
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
func isLetterOrNumber(c rune) bool {
|
||||||
|
return !unicode.IsLetter(c) && !unicode.IsNumber(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func asCamelCase(key string) string {
|
||||||
|
parts := strings.FieldsFunc(key, isLetterOrNumber)
|
||||||
|
for idx, part := range parts {
|
||||||
|
parts[idx] = strings.Title(part)
|
||||||
|
}
|
||||||
|
return strings.Join(parts, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateRegistry(t *testing.T) string {
|
||||||
|
tmpl, err := template.New("fn").Parse(`
|
||||||
|
// Is{{.CamleCase}}Enabled checks for the flag: {{.Flag.Name}}{{.Ext}}
|
||||||
|
func (ft *FeatureToggles) Is{{.CamleCase}}Enabled() bool {
|
||||||
|
return ft.manager.IsEnabled("{{.Flag.Name}}")
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("error reading template", "error", err.Error())
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
data := struct {
|
||||||
|
CamleCase string
|
||||||
|
Flag FeatureFlag
|
||||||
|
Ext string
|
||||||
|
}{
|
||||||
|
CamleCase: "?",
|
||||||
|
}
|
||||||
|
|
||||||
|
var buff bytes.Buffer
|
||||||
|
|
||||||
|
buff.WriteString(`// NOTE: This file is autogenerated
|
||||||
|
|
||||||
|
package featuremgmt
|
||||||
|
`)
|
||||||
|
|
||||||
|
for _, flag := range standardFeatureFlags {
|
||||||
|
data.CamleCase = asCamelCase(flag.Name)
|
||||||
|
data.Flag = flag
|
||||||
|
data.Ext = ""
|
||||||
|
|
||||||
|
if flag.Description != "" {
|
||||||
|
data.Ext += "\n// " + flag.Description
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = tmpl.Execute(&buff, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buff.String()
|
||||||
|
}
|
@ -15,6 +15,7 @@ import (
|
|||||||
|
|
||||||
jsoniter "github.com/json-iterator/go"
|
jsoniter "github.com/json-iterator/go"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/query"
|
"github.com/grafana/grafana/pkg/services/query"
|
||||||
|
|
||||||
"github.com/centrifugal/centrifuge"
|
"github.com/centrifugal/centrifuge"
|
||||||
@ -67,9 +68,10 @@ type CoreGrafanaScope struct {
|
|||||||
func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, routeRegister routing.RouteRegister,
|
func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, routeRegister routing.RouteRegister,
|
||||||
pluginStore plugins.Store, cacheService *localcache.CacheService,
|
pluginStore plugins.Store, cacheService *localcache.CacheService,
|
||||||
dataSourceCache datasources.CacheService, sqlStore *sqlstore.SQLStore, secretsService secrets.Service,
|
dataSourceCache datasources.CacheService, sqlStore *sqlstore.SQLStore, secretsService secrets.Service,
|
||||||
usageStatsService usagestats.Service, queryDataService *query.Service) (*GrafanaLive, error) {
|
usageStatsService usagestats.Service, queryDataService *query.Service, toggles *featuremgmt.FeatureToggles) (*GrafanaLive, error) {
|
||||||
g := &GrafanaLive{
|
g := &GrafanaLive{
|
||||||
Cfg: cfg,
|
Cfg: cfg,
|
||||||
|
Features: toggles,
|
||||||
PluginContextProvider: plugCtxProvider,
|
PluginContextProvider: plugCtxProvider,
|
||||||
RouteRegister: routeRegister,
|
RouteRegister: routeRegister,
|
||||||
pluginStore: pluginStore,
|
pluginStore: pluginStore,
|
||||||
@ -174,7 +176,7 @@ func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, r
|
|||||||
}
|
}
|
||||||
|
|
||||||
g.ManagedStreamRunner = managedStreamRunner
|
g.ManagedStreamRunner = managedStreamRunner
|
||||||
if enabled := g.Cfg.FeatureToggles["live-pipeline"]; enabled {
|
if g.Features.IsLivePipelineEnabled() {
|
||||||
var builder pipeline.RuleBuilder
|
var builder pipeline.RuleBuilder
|
||||||
if os.Getenv("GF_LIVE_DEV_BUILDER") != "" {
|
if os.Getenv("GF_LIVE_DEV_BUILDER") != "" {
|
||||||
builder = &pipeline.DevRuleBuilder{
|
builder = &pipeline.DevRuleBuilder{
|
||||||
@ -391,6 +393,7 @@ func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, r
|
|||||||
type GrafanaLive struct {
|
type GrafanaLive struct {
|
||||||
PluginContextProvider *plugincontext.Provider
|
PluginContextProvider *plugincontext.Provider
|
||||||
Cfg *setting.Cfg
|
Cfg *setting.Cfg
|
||||||
|
Features *featuremgmt.FeatureToggles
|
||||||
RouteRegister routing.RouteRegister
|
RouteRegister routing.RouteRegister
|
||||||
CacheService *localcache.CacheService
|
CacheService *localcache.CacheService
|
||||||
DataSourceCache datasources.CacheService
|
DataSourceCache datasources.CacheService
|
||||||
|
@ -8,9 +8,9 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
"github.com/grafana/grafana/pkg/schema"
|
"github.com/grafana/grafana/pkg/schema"
|
||||||
"github.com/grafana/grafana/pkg/schema/load"
|
"github.com/grafana/grafana/pkg/schema/load"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const ServiceName = "SchemaLoader"
|
const ServiceName = "SchemaLoader"
|
||||||
@ -26,13 +26,13 @@ type RenderUser struct {
|
|||||||
OrgRole string
|
OrgRole string
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProvideService(cfg *setting.Cfg) (*SchemaLoaderService, error) {
|
func ProvideService(features *featuremgmt.FeatureToggles) (*SchemaLoaderService, error) {
|
||||||
dashFam, err := load.BaseDashboardFamily(baseLoadPath)
|
dashFam, err := load.BaseDashboardFamily(baseLoadPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to load dashboard cue schema from path %q: %w", baseLoadPath, err)
|
return nil, fmt.Errorf("failed to load dashboard cue schema from path %q: %w", baseLoadPath, err)
|
||||||
}
|
}
|
||||||
s := &SchemaLoaderService{
|
s := &SchemaLoaderService{
|
||||||
Cfg: cfg,
|
features: features,
|
||||||
DashFamily: dashFam,
|
DashFamily: dashFam,
|
||||||
log: log.New("schemaloader"),
|
log: log.New("schemaloader"),
|
||||||
}
|
}
|
||||||
@ -42,14 +42,14 @@ func ProvideService(cfg *setting.Cfg) (*SchemaLoaderService, error) {
|
|||||||
type SchemaLoaderService struct {
|
type SchemaLoaderService struct {
|
||||||
log log.Logger
|
log log.Logger
|
||||||
DashFamily schema.VersionedCueSchema
|
DashFamily schema.VersionedCueSchema
|
||||||
Cfg *setting.Cfg
|
features *featuremgmt.FeatureToggles
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rs *SchemaLoaderService) IsDisabled() bool {
|
func (rs *SchemaLoaderService) IsDisabled() bool {
|
||||||
if rs.Cfg == nil {
|
if rs.features == nil {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return !rs.Cfg.IsTrimDefaultsEnabled()
|
return !rs.features.IsTrimDefaultsEnabled()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rs *SchemaLoaderService) DashboardApplyDefaults(input *simplejson.Json) (*simplejson.Json, error) {
|
func (rs *SchemaLoaderService) DashboardApplyDefaults(input *simplejson.Json) (*simplejson.Json, error) {
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/usagestats"
|
"github.com/grafana/grafana/pkg/infra/usagestats"
|
||||||
"github.com/grafana/grafana/pkg/services/encryption/ossencryption"
|
"github.com/grafana/grafana/pkg/services/encryption/ossencryption"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/kmsproviders/osskmsproviders"
|
"github.com/grafana/grafana/pkg/services/kmsproviders/osskmsproviders"
|
||||||
"github.com/grafana/grafana/pkg/services/secrets"
|
"github.com/grafana/grafana/pkg/services/secrets"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
@ -23,10 +24,15 @@ func SetupTestService(tb testing.TB, store secrets.Store) *SecretsService {
|
|||||||
[security]
|
[security]
|
||||||
secret_key = ` + defaultKey))
|
secret_key = ` + defaultKey))
|
||||||
require.NoError(tb, err)
|
require.NoError(tb, err)
|
||||||
|
|
||||||
|
features := featuremgmt.WithToggles("envelopeEncryption")
|
||||||
|
|
||||||
cfg := &setting.Cfg{Raw: raw}
|
cfg := &setting.Cfg{Raw: raw}
|
||||||
cfg.FeatureToggles = map[string]bool{secrets.EnvelopeEncryptionFeatureToggle: true}
|
cfg.IsFeatureToggleEnabled = features.IsEnabled
|
||||||
|
|
||||||
settings := &setting.OSSImpl{Cfg: cfg}
|
settings := &setting.OSSImpl{Cfg: cfg}
|
||||||
assert.True(tb, settings.IsFeatureToggleEnabled(secrets.EnvelopeEncryptionFeatureToggle))
|
assert.True(tb, settings.IsFeatureToggleEnabled("envelopeEncryption"))
|
||||||
|
assert.True(tb, features.IsEnvelopeEncryptionEnabled())
|
||||||
|
|
||||||
encryption := ossencryption.ProvideService()
|
encryption := ossencryption.ProvideService()
|
||||||
secretsService, err := ProvideSecretsService(
|
secretsService, err := ProvideSecretsService(
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/usagestats"
|
"github.com/grafana/grafana/pkg/infra/usagestats"
|
||||||
"github.com/grafana/grafana/pkg/services/encryption/ossencryption"
|
"github.com/grafana/grafana/pkg/services/encryption/ossencryption"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/kmsproviders/osskmsproviders"
|
"github.com/grafana/grafana/pkg/services/kmsproviders/osskmsproviders"
|
||||||
"github.com/grafana/grafana/pkg/services/secrets"
|
"github.com/grafana/grafana/pkg/services/secrets"
|
||||||
"github.com/grafana/grafana/pkg/services/secrets/database"
|
"github.com/grafana/grafana/pkg/services/secrets/database"
|
||||||
@ -180,8 +181,8 @@ func TestSecretsService_UseCurrentProvider(t *testing.T) {
|
|||||||
providerID := secrets.ProviderID("fakeProvider.v1")
|
providerID := secrets.ProviderID("fakeProvider.v1")
|
||||||
settings := &setting.OSSImpl{
|
settings := &setting.OSSImpl{
|
||||||
Cfg: &setting.Cfg{
|
Cfg: &setting.Cfg{
|
||||||
Raw: raw,
|
Raw: raw,
|
||||||
FeatureToggles: map[string]bool{secrets.EnvelopeEncryptionFeatureToggle: true},
|
IsFeatureToggleEnabled: featuremgmt.WithToggles(secrets.EnvelopeEncryptionFeatureToggle).IsEnabled,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
encr := ossencryption.ProvideService()
|
encr := ossencryption.ProvideService()
|
||||||
|
@ -13,8 +13,8 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
acmiddleware "github.com/grafana/grafana/pkg/services/accesscontrol/middleware"
|
acmiddleware "github.com/grafana/grafana/pkg/services/accesscontrol/middleware"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
|
||||||
"github.com/grafana/grafana/pkg/web"
|
"github.com/grafana/grafana/pkg/web"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -40,9 +40,9 @@ func NewServiceAccountsAPI(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (api *ServiceAccountsAPI) RegisterAPIEndpoints(
|
func (api *ServiceAccountsAPI) RegisterAPIEndpoints(
|
||||||
cfg *setting.Cfg,
|
features *featuremgmt.FeatureToggles,
|
||||||
) {
|
) {
|
||||||
if !cfg.FeatureToggles["service-accounts"] {
|
if !features.IsServiceAccountsEnabled() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,10 +13,10 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
||||||
"github.com/grafana/grafana/pkg/services/serviceaccounts/tests"
|
"github.com/grafana/grafana/pkg/services/serviceaccounts/tests"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
|
||||||
"github.com/grafana/grafana/pkg/web"
|
"github.com/grafana/grafana/pkg/web"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
@ -97,7 +97,7 @@ func serviceAccountRequestScenario(t *testing.T, httpMethod string, endpoint str
|
|||||||
|
|
||||||
func setupTestServer(t *testing.T, svc *tests.ServiceAccountMock, routerRegister routing.RouteRegister, acmock *accesscontrolmock.Mock, sqlStore *sqlstore.SQLStore) *web.Mux {
|
func setupTestServer(t *testing.T, svc *tests.ServiceAccountMock, routerRegister routing.RouteRegister, acmock *accesscontrolmock.Mock, sqlStore *sqlstore.SQLStore) *web.Mux {
|
||||||
a := NewServiceAccountsAPI(svc, acmock, routerRegister, database.NewServiceAccountsStore(sqlStore))
|
a := NewServiceAccountsAPI(svc, acmock, routerRegister, database.NewServiceAccountsStore(sqlStore))
|
||||||
a.RegisterAPIEndpoints(&setting.Cfg{FeatureToggles: map[string]bool{"service-accounts": true}})
|
a.RegisterAPIEndpoints(featuremgmt.WithToggles("service-accounts"))
|
||||||
|
|
||||||
m := web.New()
|
m := web.New()
|
||||||
signedUser := &models.SignedInUser{
|
signedUser := &models.SignedInUser{
|
||||||
|
@ -7,11 +7,11 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
||||||
"github.com/grafana/grafana/pkg/services/serviceaccounts/api"
|
"github.com/grafana/grafana/pkg/services/serviceaccounts/api"
|
||||||
"github.com/grafana/grafana/pkg/services/serviceaccounts/database"
|
"github.com/grafana/grafana/pkg/services/serviceaccounts/database"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -19,21 +19,21 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ServiceAccountsService struct {
|
type ServiceAccountsService struct {
|
||||||
store serviceaccounts.Store
|
store serviceaccounts.Store
|
||||||
cfg *setting.Cfg
|
features *featuremgmt.FeatureToggles
|
||||||
log log.Logger
|
log log.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProvideServiceAccountsService(
|
func ProvideServiceAccountsService(
|
||||||
cfg *setting.Cfg,
|
features *featuremgmt.FeatureToggles,
|
||||||
store *sqlstore.SQLStore,
|
store *sqlstore.SQLStore,
|
||||||
ac accesscontrol.AccessControl,
|
ac accesscontrol.AccessControl,
|
||||||
routeRegister routing.RouteRegister,
|
routeRegister routing.RouteRegister,
|
||||||
) (*ServiceAccountsService, error) {
|
) (*ServiceAccountsService, error) {
|
||||||
s := &ServiceAccountsService{
|
s := &ServiceAccountsService{
|
||||||
cfg: cfg,
|
features: features,
|
||||||
store: database.NewServiceAccountsStore(store),
|
store: database.NewServiceAccountsStore(store),
|
||||||
log: log.New("serviceaccounts"),
|
log: log.New("serviceaccounts"),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := RegisterRoles(ac); err != nil {
|
if err := RegisterRoles(ac); err != nil {
|
||||||
@ -41,13 +41,13 @@ func ProvideServiceAccountsService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
serviceaccountsAPI := api.NewServiceAccountsAPI(s, ac, routeRegister, s.store)
|
serviceaccountsAPI := api.NewServiceAccountsAPI(s, ac, routeRegister, s.store)
|
||||||
serviceaccountsAPI.RegisterAPIEndpoints(cfg)
|
serviceaccountsAPI.RegisterAPIEndpoints(features)
|
||||||
|
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sa *ServiceAccountsService) CreateServiceAccount(ctx context.Context, saForm *serviceaccounts.CreateServiceaccountForm) (*models.User, error) {
|
func (sa *ServiceAccountsService) CreateServiceAccount(ctx context.Context, saForm *serviceaccounts.CreateServiceaccountForm) (*models.User, error) {
|
||||||
if !sa.cfg.FeatureToggles["service-accounts"] {
|
if !sa.features.IsServiceAccountsEnabled() {
|
||||||
sa.log.Debug(ServiceAccountFeatureToggleNotFound)
|
sa.log.Debug(ServiceAccountFeatureToggleNotFound)
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
@ -55,7 +55,7 @@ func (sa *ServiceAccountsService) CreateServiceAccount(ctx context.Context, saFo
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (sa *ServiceAccountsService) DeleteServiceAccount(ctx context.Context, orgID, serviceAccountID int64) error {
|
func (sa *ServiceAccountsService) DeleteServiceAccount(ctx context.Context, orgID, serviceAccountID int64) error {
|
||||||
if !sa.cfg.FeatureToggles["service-accounts"] {
|
if !sa.features.IsServiceAccountsEnabled() {
|
||||||
sa.log.Debug(ServiceAccountFeatureToggleNotFound)
|
sa.log.Debug(ServiceAccountFeatureToggleNotFound)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -5,31 +5,29 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/serviceaccounts/tests"
|
"github.com/grafana/grafana/pkg/services/serviceaccounts/tests"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestProvideServiceAccount_DeleteServiceAccount(t *testing.T) {
|
func TestProvideServiceAccount_DeleteServiceAccount(t *testing.T) {
|
||||||
t.Run("feature toggle present, should call store function", func(t *testing.T) {
|
t.Run("feature toggle present, should call store function", func(t *testing.T) {
|
||||||
cfg := setting.NewCfg()
|
|
||||||
storeMock := &tests.ServiceAccountsStoreMock{Calls: tests.Calls{}}
|
storeMock := &tests.ServiceAccountsStoreMock{Calls: tests.Calls{}}
|
||||||
cfg.FeatureToggles = map[string]bool{"service-accounts": true}
|
svc := ServiceAccountsService{
|
||||||
svc := ServiceAccountsService{cfg: cfg, store: storeMock}
|
features: featuremgmt.WithToggles("service-accounts", true),
|
||||||
|
store: storeMock}
|
||||||
err := svc.DeleteServiceAccount(context.Background(), 1, 1)
|
err := svc.DeleteServiceAccount(context.Background(), 1, 1)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, storeMock.Calls.DeleteServiceAccount, 1)
|
assert.Len(t, storeMock.Calls.DeleteServiceAccount, 1)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("no feature toggle present, should not call store function", func(t *testing.T) {
|
t.Run("no feature toggle present, should not call store function", func(t *testing.T) {
|
||||||
cfg := setting.NewCfg()
|
|
||||||
svcMock := &tests.ServiceAccountsStoreMock{Calls: tests.Calls{}}
|
svcMock := &tests.ServiceAccountsStoreMock{Calls: tests.Calls{}}
|
||||||
cfg.FeatureToggles = map[string]bool{"service-accounts": false}
|
|
||||||
svc := ServiceAccountsService{
|
svc := ServiceAccountsService{
|
||||||
cfg: cfg,
|
features: featuremgmt.WithToggles("service-accounts", false),
|
||||||
store: svcMock,
|
store: svcMock,
|
||||||
log: log.New("serviceaccounts-manager-test"),
|
log: log.New("serviceaccounts-manager-test"),
|
||||||
}
|
}
|
||||||
err := svc.DeleteServiceAccount(context.Background(), 1, 1)
|
err := svc.DeleteServiceAccount(context.Background(), 1, 1)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -3,6 +3,7 @@ package migrations
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrations/accesscontrol"
|
"github.com/grafana/grafana/pkg/services/sqlstore/migrations/accesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrations/ualert"
|
"github.com/grafana/grafana/pkg/services/sqlstore/migrations/ualert"
|
||||||
. "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
. "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||||
@ -56,8 +57,8 @@ func (*OSSMigrations) AddMigration(mg *Migrator) {
|
|||||||
ualert.AddTablesMigrations(mg)
|
ualert.AddTablesMigrations(mg)
|
||||||
ualert.AddDashAlertMigration(mg)
|
ualert.AddDashAlertMigration(mg)
|
||||||
addLibraryElementsMigrations(mg)
|
addLibraryElementsMigrations(mg)
|
||||||
if mg.Cfg != nil {
|
if mg.Cfg.IsFeatureToggleEnabled != nil {
|
||||||
if mg.Cfg.IsLiveConfigEnabled() {
|
if mg.Cfg.IsFeatureToggleEnabled(featuremgmt.FLAG_live_config) {
|
||||||
addLiveChannelMigrations(mg)
|
addLiveChannelMigrations(mg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -117,7 +117,7 @@ func (ss *SQLStore) GetOrgUsers(ctx context.Context, query *models.GetOrgUsersQu
|
|||||||
// service accounts table in the modelling
|
// service accounts table in the modelling
|
||||||
whereConditions = append(whereConditions, fmt.Sprintf("%s.is_service_account = %t", x.Dialect().Quote("user"), query.IsServiceAccount))
|
whereConditions = append(whereConditions, fmt.Sprintf("%s.is_service_account = %t", x.Dialect().Quote("user"), query.IsServiceAccount))
|
||||||
|
|
||||||
if ss.Cfg.FeatureToggles["accesscontrol"] {
|
if ss.Cfg.IsFeatureToggleEnabled("accesscontrol") {
|
||||||
q, args, err := accesscontrol.Filter(ctx, ss.Dialect, "org_user.user_id", "users", "org.users:read", query.User)
|
q, args, err := accesscontrol.Filter(ctx, ss.Dialect, "org_user.user_id", "users", "org.users:read", query.User)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -180,7 +180,7 @@ func (ss *SQLStore) SearchOrgUsers(ctx context.Context, query *models.SearchOrgU
|
|||||||
// service accounts table in the modelling
|
// service accounts table in the modelling
|
||||||
whereConditions = append(whereConditions, fmt.Sprintf("%s.is_service_account = %t", x.Dialect().Quote("user"), query.IsServiceAccount))
|
whereConditions = append(whereConditions, fmt.Sprintf("%s.is_service_account = %t", x.Dialect().Quote("user"), query.IsServiceAccount))
|
||||||
|
|
||||||
if ss.Cfg.FeatureToggles["accesscontrol"] {
|
if ss.Cfg.IsFeatureToggleEnabled("accesscontrol") {
|
||||||
q, args, err := accesscontrol.Filter(ctx, ss.Dialect, "org_user.user_id", "users", "org.users:read", query.User)
|
q, args, err := accesscontrol.Filter(ctx, ss.Dialect, "org_user.user_id", "users", "org.users:read", query.User)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
|
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
type getOrgUsersTestCase struct {
|
type getOrgUsersTestCase struct {
|
||||||
@ -61,7 +62,7 @@ func TestSQLStore_GetOrgUsers(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
store := InitTestDB(t)
|
store := InitTestDB(t)
|
||||||
store.Cfg.FeatureToggles = map[string]bool{"accesscontrol": true}
|
store.Cfg.IsFeatureToggleEnabled = featuremgmt.WithToggles("accesscontrol").IsEnabled
|
||||||
seedOrgUsers(t, store, 10)
|
seedOrgUsers(t, store, 10)
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
@ -127,7 +128,7 @@ func TestSQLStore_SearchOrgUsers(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
store := InitTestDB(t)
|
store := InitTestDB(t)
|
||||||
store.Cfg.FeatureToggles = map[string]bool{"accesscontrol": true}
|
store.Cfg.IsFeatureToggleEnabled = featuremgmt.WithToggles("accesscontrol").IsEnabled
|
||||||
seedOrgUsers(t, store, 10)
|
seedOrgUsers(t, store, 10)
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
@ -23,6 +23,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/registry"
|
"github.com/grafana/grafana/pkg/registry"
|
||||||
"github.com/grafana/grafana/pkg/services/annotations"
|
"github.com/grafana/grafana/pkg/services/annotations"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrations"
|
"github.com/grafana/grafana/pkg/services/sqlstore/migrations"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
|
"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
|
||||||
@ -326,7 +327,7 @@ func (ss *SQLStore) initEngine(engine *xorm.Engine) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if ss.Cfg.IsDatabaseMetricsEnabled() {
|
if ss.Cfg.IsFeatureToggleEnabled(featuremgmt.FLAG_database_metrics) {
|
||||||
ss.dbCfg.Type = WrapDatabaseDriverWithHooks(ss.dbCfg.Type, ss.tracer)
|
ss.dbCfg.Type = WrapDatabaseDriverWithHooks(ss.dbCfg.Type, ss.tracer)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -492,6 +493,7 @@ func initTestDB(migration registry.DatabaseMigrator, opts ...InitTestDBOpt) (*SQ
|
|||||||
|
|
||||||
// set test db config
|
// set test db config
|
||||||
cfg := setting.NewCfg()
|
cfg := setting.NewCfg()
|
||||||
|
cfg.IsFeatureToggleEnabled = func(key string) bool { return false }
|
||||||
sec, err := cfg.Raw.NewSection("database")
|
sec, err := cfg.Raw.NewSection("database")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/api/response"
|
"github.com/grafana/grafana/pkg/api/response"
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/guardian"
|
"github.com/grafana/grafana/pkg/services/guardian"
|
||||||
"github.com/grafana/grafana/pkg/services/live"
|
"github.com/grafana/grafana/pkg/services/live"
|
||||||
"github.com/grafana/grafana/pkg/services/rendering"
|
"github.com/grafana/grafana/pkg/services/rendering"
|
||||||
@ -39,8 +40,8 @@ type Service interface {
|
|||||||
CrawlerStatus(c *models.ReqContext) response.Response
|
CrawlerStatus(c *models.ReqContext) response.Response
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProvideService(cfg *setting.Cfg, renderService rendering.Service, gl *live.GrafanaLive) Service {
|
func ProvideService(cfg *setting.Cfg, features *featuremgmt.FeatureToggles, renderService rendering.Service, gl *live.GrafanaLive) Service {
|
||||||
if !cfg.IsDashboardPreviesEnabled() {
|
if !features.IsDashboardPreviewsEnabled() {
|
||||||
return &dummyService{}
|
return &dummyService{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,7 +132,7 @@ func (o *OSSImpl) Section(section string) Section {
|
|||||||
func (OSSImpl) RegisterReloadHandler(string, ReloadHandler) {}
|
func (OSSImpl) RegisterReloadHandler(string, ReloadHandler) {}
|
||||||
|
|
||||||
func (o OSSImpl) IsFeatureToggleEnabled(name string) bool {
|
func (o OSSImpl) IsFeatureToggleEnabled(name string) bool {
|
||||||
return o.Cfg.FeatureToggles[name]
|
return o.Cfg.IsFeatureToggleEnabled(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
type keyValImpl struct {
|
type keyValImpl struct {
|
||||||
|
@ -342,8 +342,10 @@ type Cfg struct {
|
|||||||
|
|
||||||
ApiKeyMaxSecondsToLive int64
|
ApiKeyMaxSecondsToLive int64
|
||||||
|
|
||||||
// Use to enable new features which may still be in alpha/beta stage.
|
// Check if a feature toggle is enabled
|
||||||
FeatureToggles map[string]bool
|
// @deprecated
|
||||||
|
IsFeatureToggleEnabled func(key string) bool // filled in dynamically
|
||||||
|
|
||||||
AnonymousEnabled bool
|
AnonymousEnabled bool
|
||||||
AnonymousOrgName string
|
AnonymousOrgName string
|
||||||
AnonymousOrgRole string
|
AnonymousOrgRole string
|
||||||
@ -429,41 +431,6 @@ type Cfg struct {
|
|||||||
UnifiedAlerting UnifiedAlertingSettings
|
UnifiedAlerting UnifiedAlertingSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsLiveConfigEnabled returns true if live should be able to save configs to SQL tables
|
|
||||||
func (cfg Cfg) IsLiveConfigEnabled() bool {
|
|
||||||
return cfg.FeatureToggles["live-config"]
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsLiveConfigEnabled returns true if live should be able to save configs to SQL tables
|
|
||||||
func (cfg Cfg) IsDashboardPreviesEnabled() bool {
|
|
||||||
return cfg.FeatureToggles["dashboardPreviews"]
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsTrimDefaultsEnabled returns whether the standalone trim dashboard default feature is enabled.
|
|
||||||
func (cfg Cfg) IsTrimDefaultsEnabled() bool {
|
|
||||||
return cfg.FeatureToggles["trimDefaults"]
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsDatabaseMetricsEnabled returns whether the database instrumentation feature is enabled.
|
|
||||||
func (cfg Cfg) IsDatabaseMetricsEnabled() bool {
|
|
||||||
return cfg.FeatureToggles["database_metrics"]
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsHTTPRequestHistogramDisabled returns whether the request historgrams is disabled.
|
|
||||||
// This feature toggle will be removed in Grafana 8.x but gives the operator
|
|
||||||
// some graceperiod to update all the monitoring tools.
|
|
||||||
func (cfg Cfg) IsHTTPRequestHistogramDisabled() bool {
|
|
||||||
return cfg.FeatureToggles["disable_http_request_histogram"]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg Cfg) IsNewNavigationEnabled() bool {
|
|
||||||
return cfg.FeatureToggles["newNavigation"]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg Cfg) IsServiceAccountEnabled() bool {
|
|
||||||
return cfg.FeatureToggles["service-accounts"]
|
|
||||||
}
|
|
||||||
|
|
||||||
type CommandLineArgs struct {
|
type CommandLineArgs struct {
|
||||||
Config string
|
Config string
|
||||||
HomePath string
|
HomePath string
|
||||||
|
@ -3,42 +3,23 @@ package setting
|
|||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
|
||||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
"gopkg.in/ini.v1"
|
"gopkg.in/ini.v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
// @deprecated -- should use `featuremgmt.FeatureToggles`
|
||||||
featureToggleInfo = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
|
||||||
Name: "feature_toggles_info",
|
|
||||||
Help: "info metric that exposes what feature toggles are enabled or not",
|
|
||||||
Namespace: "grafana",
|
|
||||||
}, []string{"name"})
|
|
||||||
|
|
||||||
defaultFeatureToggles = map[string]bool{
|
|
||||||
"recordedQueries": false,
|
|
||||||
"accesscontrol": false,
|
|
||||||
"service-accounts": false,
|
|
||||||
"httpclientprovider_azure_auth": false,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func (cfg *Cfg) readFeatureToggles(iniFile *ini.File) error {
|
func (cfg *Cfg) readFeatureToggles(iniFile *ini.File) error {
|
||||||
toggles, err := overrideDefaultWithConfiguration(iniFile, defaultFeatureToggles)
|
section := iniFile.Section("feature_toggles")
|
||||||
|
toggles, err := ReadFeatureTogglesFromInitFile(section)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
cfg.IsFeatureToggleEnabled = func(key string) bool { return toggles[key] }
|
||||||
cfg.FeatureToggles = toggles
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func overrideDefaultWithConfiguration(iniFile *ini.File, featureToggles map[string]bool) (map[string]bool, error) {
|
func ReadFeatureTogglesFromInitFile(featureTogglesSection *ini.Section) (map[string]bool, error) {
|
||||||
// Read and populate feature toggles list
|
featureToggles := make(map[string]bool, 10)
|
||||||
featureTogglesSection := iniFile.Section("feature_toggles")
|
|
||||||
|
|
||||||
// parse the comma separated list in `enable`.
|
// parse the comma separated list in `enable`.
|
||||||
featuresTogglesStr := valueAsString(featureTogglesSection, "enable", "")
|
featuresTogglesStr := valueAsString(featureTogglesSection, "enable", "")
|
||||||
@ -60,15 +41,5 @@ func overrideDefaultWithConfiguration(iniFile *ini.File, featureToggles map[stri
|
|||||||
|
|
||||||
featureToggles[v.Name()] = b
|
featureToggles[v.Name()] = b
|
||||||
}
|
}
|
||||||
|
|
||||||
// track if feature toggles are enabled or not using an info metric
|
|
||||||
for k, v := range featureToggles {
|
|
||||||
if v {
|
|
||||||
featureToggleInfo.WithLabelValues(k).Set(1)
|
|
||||||
} else {
|
|
||||||
featureToggleInfo.WithLabelValues(k).Set(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return featureToggles, nil
|
return featureToggles, nil
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,6 @@ func TestFeatureToggles(t *testing.T) {
|
|||||||
conf map[string]string
|
conf map[string]string
|
||||||
err error
|
err error
|
||||||
expectedToggles map[string]bool
|
expectedToggles map[string]bool
|
||||||
defaultToggles map[string]bool
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "can parse feature toggles passed in the `enable` array",
|
name: "can parse feature toggles passed in the `enable` array",
|
||||||
@ -58,18 +57,6 @@ func TestFeatureToggles(t *testing.T) {
|
|||||||
expectedToggles: map[string]bool{},
|
expectedToggles: map[string]bool{},
|
||||||
err: strconv.ErrSyntax,
|
err: strconv.ErrSyntax,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "should override default feature toggles",
|
|
||||||
defaultToggles: map[string]bool{
|
|
||||||
"feature1": true,
|
|
||||||
},
|
|
||||||
conf: map[string]string{
|
|
||||||
"feature1": "false",
|
|
||||||
},
|
|
||||||
expectedToggles: map[string]bool{
|
|
||||||
"feature1": false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
@ -81,12 +68,7 @@ func TestFeatureToggles(t *testing.T) {
|
|||||||
require.ErrorIs(t, err, nil)
|
require.ErrorIs(t, err, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
dt := map[string]bool{}
|
featureToggles, err := ReadFeatureTogglesFromInitFile(toggles)
|
||||||
if len(tc.defaultToggles) > 0 {
|
|
||||||
dt = tc.defaultToggles
|
|
||||||
}
|
|
||||||
|
|
||||||
featureToggles, err := overrideDefaultWithConfiguration(f, dt)
|
|
||||||
require.ErrorIs(t, err, tc.err)
|
require.ErrorIs(t, err, tc.err)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
@ -79,7 +79,7 @@ func (cfg *Cfg) readUnifiedAlertingEnabledSetting(section *ini.Section) (*bool,
|
|||||||
// the unified alerting is not enabled by default. First, check the feature flag
|
// the unified alerting is not enabled by default. First, check the feature flag
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// TODO: Remove in Grafana v9
|
// TODO: Remove in Grafana v9
|
||||||
if cfg.FeatureToggles["ngalert"] {
|
if cfg.IsFeatureToggleEnabled("ngalert") {
|
||||||
cfg.Logger.Warn("ngalert feature flag is deprecated: use unified alerting enabled setting instead")
|
cfg.Logger.Warn("ngalert feature flag is deprecated: use unified alerting enabled setting instead")
|
||||||
enabled = true
|
enabled = true
|
||||||
// feature flag overrides the legacy alerting setting.
|
// feature flag overrides the legacy alerting setting.
|
||||||
|
@ -143,6 +143,7 @@ func TestUnifiedAlertingSettings(t *testing.T) {
|
|||||||
t.Run(tc.desc, func(t *testing.T) {
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
f := ini.Empty()
|
f := ini.Empty()
|
||||||
cfg := NewCfg()
|
cfg := NewCfg()
|
||||||
|
cfg.IsFeatureToggleEnabled = func(key string) bool { return false }
|
||||||
unifiedAlertingSec, err := f.NewSection("unified_alerting")
|
unifiedAlertingSec, err := f.NewSection("unified_alerting")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
for k, v := range tc.unifiedAlertingOptions {
|
for k, v := range tc.unifiedAlertingOptions {
|
||||||
|
@ -37,7 +37,7 @@ func (s *Service) SubscribeStream(_ context.Context, req *backend.SubscribeStrea
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.cfg.FeatureToggles["live-pipeline"] {
|
if s.features.IsLivePipelineEnabled() {
|
||||||
// While developing Live pipeline avoid sending initial data.
|
// While developing Live pipeline avoid sending initial data.
|
||||||
initialData = nil
|
initialData = nil
|
||||||
}
|
}
|
||||||
@ -126,7 +126,7 @@ func (s *Service) runTestStream(ctx context.Context, path string, conf testStrea
|
|||||||
}
|
}
|
||||||
|
|
||||||
mode := data.IncludeDataOnly
|
mode := data.IncludeDataOnly
|
||||||
if s.cfg.FeatureToggles["live-pipeline"] {
|
if s.features.IsLivePipelineEnabled() {
|
||||||
mode = data.IncludeAll
|
mode = data.IncludeAll
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,11 +10,13 @@ import (
|
|||||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ProvideService(cfg *setting.Cfg) *Service {
|
func ProvideService(cfg *setting.Cfg, features *featuremgmt.FeatureToggles) *Service {
|
||||||
s := &Service{
|
s := &Service{
|
||||||
|
features: features,
|
||||||
queryMux: datasource.NewQueryTypeMux(),
|
queryMux: datasource.NewQueryTypeMux(),
|
||||||
scenarios: map[string]*Scenario{},
|
scenarios: map[string]*Scenario{},
|
||||||
frame: data.NewFrame("testdata",
|
frame: data.NewFrame("testdata",
|
||||||
@ -46,6 +48,7 @@ type Service struct {
|
|||||||
labelFrame *data.Frame
|
labelFrame *data.Frame
|
||||||
queryMux *datasource.QueryTypeMux
|
queryMux *datasource.QueryTypeMux
|
||||||
resourceHandler backend.CallResourceHandler
|
resourceHandler backend.CallResourceHandler
|
||||||
|
features *featuremgmt.FeatureToggles
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||||
|
@ -83,13 +83,13 @@ export class ContextSrv {
|
|||||||
}
|
}
|
||||||
|
|
||||||
accessControlEnabled(): boolean {
|
accessControlEnabled(): boolean {
|
||||||
return featureEnabled('accesscontrol') && Boolean(config.featureToggles['accesscontrol']);
|
return featureEnabled(config.featureToggles.accesscontrol);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks whether user has required permission
|
// Checks whether user has required permission
|
||||||
hasPermissionInMetadata(action: AccessControlAction | string, object: WithAccessControlMetadata): boolean {
|
hasPermissionInMetadata(action: AccessControlAction | string, object: WithAccessControlMetadata): boolean {
|
||||||
// Fallback if access control disabled
|
// Fallback if access control disabled
|
||||||
if (!config.featureToggles['accesscontrol']) {
|
if (!config.featureToggles.accesscontrol) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,7 +99,7 @@ export class ContextSrv {
|
|||||||
// Checks whether user has required permission
|
// Checks whether user has required permission
|
||||||
hasPermission(action: AccessControlAction | string): boolean {
|
hasPermission(action: AccessControlAction | string): boolean {
|
||||||
// Fallback if access control disabled
|
// Fallback if access control disabled
|
||||||
if (!config.featureToggles['accesscontrol']) {
|
if (!config.featureToggles.accesscontrol) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,14 +126,14 @@ export class ContextSrv {
|
|||||||
}
|
}
|
||||||
|
|
||||||
hasAccessToExplore() {
|
hasAccessToExplore() {
|
||||||
if (config.featureToggles['accesscontrol']) {
|
if (config.featureToggles.accesscontrol) {
|
||||||
return this.hasPermission(AccessControlAction.DataSourcesExplore);
|
return this.hasPermission(AccessControlAction.DataSourcesExplore);
|
||||||
}
|
}
|
||||||
return (this.isEditor || config.viewersCanEdit) && config.exploreEnabled;
|
return (this.isEditor || config.viewersCanEdit) && config.exploreEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
hasAccess(action: string, fallBack: boolean) {
|
hasAccess(action: string, fallBack: boolean) {
|
||||||
if (!config.featureToggles['accesscontrol']) {
|
if (!config.featureToggles.accesscontrol) {
|
||||||
return fallBack;
|
return fallBack;
|
||||||
}
|
}
|
||||||
return this.hasPermission(action);
|
return this.hasPermission(action);
|
||||||
@ -141,7 +141,7 @@ export class ContextSrv {
|
|||||||
|
|
||||||
// evaluates access control permissions, granting access if the user has any of them; uses fallback if access control is disabled
|
// evaluates access control permissions, granting access if the user has any of them; uses fallback if access control is disabled
|
||||||
evaluatePermission(fallback: () => string[], actions: string[]) {
|
evaluatePermission(fallback: () => string[], actions: string[]) {
|
||||||
if (!config.featureToggles['accesscontrol']) {
|
if (!config.featureToggles.accesscontrol) {
|
||||||
return fallback();
|
return fallback();
|
||||||
}
|
}
|
||||||
if (actions.some((action) => this.hasPermission(action))) {
|
if (actions.some((action) => this.hasPermission(action))) {
|
||||||
|
@ -2,7 +2,7 @@ import config from '../../core/config';
|
|||||||
|
|
||||||
// accessControlQueryParam adds an additional accesscontrol=true param to params when accesscontrol is enabled
|
// accessControlQueryParam adds an additional accesscontrol=true param to params when accesscontrol is enabled
|
||||||
export function accessControlQueryParam(params = {}) {
|
export function accessControlQueryParam(params = {}) {
|
||||||
if (!config.featureToggles['accesscontrol']) {
|
if (!config.featureToggles.accesscontrol) {
|
||||||
return params;
|
return params;
|
||||||
}
|
}
|
||||||
return { ...params, accesscontrol: true };
|
return { ...params, accesscontrol: true };
|
||||||
|
@ -49,14 +49,14 @@ describe('PluginListItemBadges', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('renders an enterprise badge (when a license is valid)', () => {
|
it('renders an enterprise badge (when a license is valid)', () => {
|
||||||
config.licenseInfo.enabledFeatures = { 'enterprise.plugins': true };
|
config.featureToggles = { 'enterprise.plugins': true };
|
||||||
render(<PluginListItemBadges plugin={{ ...plugin, isEnterprise: true }} />);
|
render(<PluginListItemBadges plugin={{ ...plugin, isEnterprise: true }} />);
|
||||||
expect(screen.getByText(/enterprise/i)).toBeVisible();
|
expect(screen.getByText(/enterprise/i)).toBeVisible();
|
||||||
expect(screen.queryByRole('button', { name: /learn more/i })).not.toBeInTheDocument();
|
expect(screen.queryByRole('button', { name: /learn more/i })).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders an enterprise badge with icon and link (when a license is invalid)', () => {
|
it('renders an enterprise badge with icon and link (when a license is invalid)', () => {
|
||||||
config.licenseInfo.enabledFeatures = {};
|
config.featureToggles = {};
|
||||||
render(<PluginListItemBadges plugin={{ ...plugin, isEnterprise: true }} />);
|
render(<PluginListItemBadges plugin={{ ...plugin, isEnterprise: true }} />);
|
||||||
expect(screen.getByText(/enterprise/i)).toBeVisible();
|
expect(screen.getByText(/enterprise/i)).toBeVisible();
|
||||||
expect(screen.getByLabelText(/lock icon/i)).toBeInTheDocument();
|
expect(screen.getByLabelText(/lock icon/i)).toBeInTheDocument();
|
||||||
|
@ -90,7 +90,7 @@ describe('Plugin details page', () => {
|
|||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
config.pluginAdminExternalManageEnabled = false;
|
config.pluginAdminExternalManageEnabled = false;
|
||||||
config.licenseInfo.enabledFeatures = {};
|
config.featureToggles = {};
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
@ -325,7 +325,7 @@ describe('Plugin details page', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should display an install button for enterprise plugins if license is valid', async () => {
|
it('should display an install button for enterprise plugins if license is valid', async () => {
|
||||||
config.licenseInfo.enabledFeatures = { 'enterprise.plugins': true };
|
config.featureToggles = { 'enterprise.plugins': true };
|
||||||
|
|
||||||
const { queryByRole } = renderPluginDetails({ id, isInstalled: false, isEnterprise: true });
|
const { queryByRole } = renderPluginDetails({ id, isInstalled: false, isEnterprise: true });
|
||||||
|
|
||||||
@ -333,7 +333,7 @@ describe('Plugin details page', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not display install button for enterprise plugins if license is invalid', async () => {
|
it('should not display install button for enterprise plugins if license is invalid', async () => {
|
||||||
config.licenseInfo.enabledFeatures = {};
|
config.featureToggles = {};
|
||||||
|
|
||||||
const { queryByRole, queryByText } = renderPluginDetails({ id, isInstalled: true, isEnterprise: true });
|
const { queryByRole, queryByText } = renderPluginDetails({ id, isInstalled: true, isEnterprise: true });
|
||||||
|
|
||||||
@ -772,7 +772,7 @@ describe('Plugin details page', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not display an install button for enterprise plugins if license is valid', async () => {
|
it('should not display an install button for enterprise plugins if license is valid', async () => {
|
||||||
config.licenseInfo.enabledFeatures = { 'enterprise.plugins': true };
|
config.featureToggles = { 'enterprise.plugins': true };
|
||||||
const { queryByRole, queryByText } = renderPluginDetails({ id, isInstalled: false, isEnterprise: true });
|
const { queryByRole, queryByText } = renderPluginDetails({ id, isInstalled: false, isEnterprise: true });
|
||||||
|
|
||||||
await waitFor(() => expect(queryByText(PluginTabLabels.OVERVIEW)).toBeInTheDocument());
|
await waitFor(() => expect(queryByText(PluginTabLabels.OVERVIEW)).toBeInTheDocument());
|
||||||
|
@ -10,9 +10,7 @@ import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps
|
|||||||
jest.mock('@grafana/runtime/src/config', () => ({
|
jest.mock('@grafana/runtime/src/config', () => ({
|
||||||
...((jest.requireActual('@grafana/runtime/src/config') as unknown) as object),
|
...((jest.requireActual('@grafana/runtime/src/config') as unknown) as object),
|
||||||
config: {
|
config: {
|
||||||
licenseInfo: {
|
featureToggles: { teamsync: true },
|
||||||
enabledFeatures: { teamsync: true },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user