mirror of https://github.com/grafana/grafana.git
				
				
				
			FeatureFlags: manage feature flags outside of settings.Cfg (#43692)
This commit is contained in:
		
							parent
							
								
									7fbc7d019a
								
							
						
					
					
						commit
						f94c0decbd
					
				|  | @ -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} | ||||||
|  |  | ||||||
|  | @ -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:-`
 | ||||||
|  | } | ||||||
|  | @ -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...), | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -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) | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | @ -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, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | ) | ||||||
|  | @ -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, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -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 | ||||||
|  | } | ||||||
|  | @ -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)) | ||||||
|  | } | ||||||
|  | @ -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") | ||||||
|  | @ -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 | ||||||
|  | @ -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) | ||||||
|  | } | ||||||
|  | @ -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") | ||||||
|  | } | ||||||
|  | @ -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