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 | ||||
| *_gen.go | ||||
| !pkg/services/featuremgmt/toggles_gen.go | ||||
| 
 | ||||
| # Auto-generated localisation files | ||||
| public/locales/_build/ | ||||
|  |  | |||
|  | @ -40,7 +40,6 @@ export interface LicenseInfo { | |||
|   licenseUrl: string; | ||||
|   stateInfo: string; | ||||
|   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 | ||||
|  * 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'; | ||||
| 
 | ||||
| export const featureEnabled = (feature: string): boolean => { | ||||
|   const { enabledFeatures } = config.licenseInfo; | ||||
|   return enabledFeatures && enabledFeatures[feature]; | ||||
| export const featureEnabled = (feature: boolean | undefined | keyof FeatureToggles): boolean => { | ||||
|   if (feature === true || feature === false) { | ||||
|     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
 | ||||
| 			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.
 | ||||
| 				liveRoute.Post("/pipeline/push/*", hs.LivePushGateway.HandlePipelinePush) | ||||
| 				liveRoute.Post("/pipeline-convert-test", routing.Wrap(hs.Live.HandlePipelineConvertTestHTTP), reqOrgAdmin) | ||||
|  | @ -460,6 +460,9 @@ func (hs *HTTPServer) registerRoutes() { | |||
| 	// admin api
 | ||||
| 	r.Group("/api/admin", func(adminRoute routing.RouteRegister) { | ||||
| 		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.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 | ||||
| 	var err error | ||||
| 	if hs.Cfg.FeatureToggles["service-accounts"] { | ||||
| 	if hs.Features.Toggles().IsServiceAccountsEnabled() { | ||||
| 		// Api keys should now be created with addadditionalapikey endpoint
 | ||||
| 		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 { | ||||
| 		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")) | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
|  | @ -28,6 +28,7 @@ import ( | |||
| 	"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol" | ||||
| 	"github.com/grafana/grafana/pkg/services/auth" | ||||
| 	"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/rendering" | ||||
| 	"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) { | ||||
| 	cfg.FeatureToggles = make(map[string]bool) | ||||
| 	cfg.FeatureToggles["accesscontrol"] = true | ||||
| 	features := featuremgmt.WithFeatures("accesscontrol") | ||||
| 	cfg.IsFeatureToggleEnabled = features.IsEnabled | ||||
| 	cfg.Quota.Enabled = false | ||||
| 
 | ||||
| 	bus := bus.GetBus() | ||||
|  | @ -222,6 +223,7 @@ func setupAccessControlScenarioContext(t *testing.T, cfg *setting.Cfg, url strin | |||
| 		Cfg:                cfg, | ||||
| 		Bus:                bus, | ||||
| 		Live:               newTestLive(t), | ||||
| 		Features:           features, | ||||
| 		QuotaService:       "a.QuotaService{Cfg: cfg}, | ||||
| 		RouteRegister:      routing.NewRouteRegister(), | ||||
| 		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} | ||||
| } | ||||
| 
 | ||||
| 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 { | ||||
| 	// Use a new conf
 | ||||
| 	features := featuremgmt.WithFeatures("accesscontrol", enableAccessControl) | ||||
| 	cfg := setting.NewCfg() | ||||
| 	cfg.FeatureToggles = make(map[string]bool) | ||||
| 	if enableAccessControl { | ||||
| 		cfg.FeatureToggles["accesscontrol"] = enableAccessControl | ||||
| 	} | ||||
| 	cfg.IsFeatureToggleEnabled = features.IsEnabled | ||||
| 
 | ||||
| 	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 { | ||||
| 	t.Helper() | ||||
| 
 | ||||
| 	features := featuremgmt.WithFeatures("accesscontrol", enableAccessControl) | ||||
| 	cfg.IsFeatureToggleEnabled = features.IsEnabled | ||||
| 
 | ||||
| 	var acmock *accesscontrolmock.Mock | ||||
| 	var ac *ossaccesscontrol.OSSAccessControlService | ||||
| 
 | ||||
|  | @ -322,6 +339,7 @@ func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessCont | |||
| 	// Create minimal HTTP Server
 | ||||
| 	hs := &HTTPServer{ | ||||
| 		Cfg:                cfg, | ||||
| 		Features:           features, | ||||
| 		Bus:                bus, | ||||
| 		Live:               newTestLive(t), | ||||
| 		QuotaService:       "a.QuotaService{Cfg: cfg}, | ||||
|  | @ -338,7 +356,7 @@ func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessCont | |||
| 		} | ||||
| 		hs.AccessControl = acmock | ||||
| 	} else { | ||||
| 		ac = ossaccesscontrol.ProvideService(cfg, &usagestats.UsageStatsMock{T: t}) | ||||
| 		ac = ossaccesscontrol.ProvideService(hs.Features.Toggles(), &usagestats.UsageStatsMock{T: t}) | ||||
| 		hs.AccessControl = ac | ||||
| 		// Perform role registration
 | ||||
| 		err := hs.declareFixedRoles() | ||||
|  |  | |||
|  | @ -20,6 +20,7 @@ import ( | |||
| 	"github.com/grafana/grafana/pkg/models" | ||||
| 	"github.com/grafana/grafana/pkg/services/alerting" | ||||
| 	"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/live" | ||||
| 	"github.com/grafana/grafana/pkg/services/provisioning" | ||||
|  | @ -88,8 +89,17 @@ type testState struct { | |||
| } | ||||
| 
 | ||||
| func newTestLive(t *testing.T) *live.GrafanaLive { | ||||
| 	features := featuremgmt.WithToggles() | ||||
| 	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) | ||||
| 	return gLive | ||||
| } | ||||
|  |  | |||
|  | @ -243,13 +243,12 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i | |||
| 			"env":           setting.Env, | ||||
| 		}, | ||||
| 		"licenseInfo": map[string]interface{}{ | ||||
| 			"expiry":          hs.License.Expiry(), | ||||
| 			"stateInfo":       hs.License.StateInfo(), | ||||
| 			"licenseUrl":      hs.License.LicenseURL(hasAccess(accesscontrol.ReqGrafanaAdmin, accesscontrol.LicensingPageReaderAccess)), | ||||
| 			"edition":         hs.License.Edition(), | ||||
| 			"enabledFeatures": hs.License.EnabledFeatures(), | ||||
| 			"expiry":     hs.License.Expiry(), | ||||
| 			"stateInfo":  hs.License.StateInfo(), | ||||
| 			"licenseUrl": hs.License.LicenseURL(hasAccess(accesscontrol.ReqGrafanaAdmin, accesscontrol.LicensingPageReaderAccess)), | ||||
| 			"edition":    hs.License.Edition(), | ||||
| 		}, | ||||
| 		"featureToggles":                   hs.Cfg.FeatureToggles, | ||||
| 		"featureToggles":                   hs.Features.GetEnabled(c.Req.Context()), | ||||
| 		"rendererAvailable":                hs.RenderService.IsAvailable(), | ||||
| 		"rendererVersion":                  hs.RenderService.Version(), | ||||
| 		"http2Enabled":                     hs.Cfg.Protocol == setting.HTTP2Scheme, | ||||
|  |  | |||
|  | @ -9,6 +9,7 @@ import ( | |||
| 
 | ||||
| 	"github.com/grafana/grafana/pkg/bus" | ||||
| 	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/rendering" | ||||
| 	"github.com/grafana/grafana/pkg/services/sqlstore" | ||||
|  | @ -19,9 +20,10 @@ import ( | |||
| 	"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() | ||||
| 	sqlstore.InitTestDB(t) | ||||
| 	cfg.IsFeatureToggleEnabled = features.IsEnabled | ||||
| 
 | ||||
| 	{ | ||||
| 		oldVersion := setting.BuildVersion | ||||
|  | @ -37,9 +39,10 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg) (*web.Mux, *HTTPServer | |||
| 	sqlStore := sqlstore.InitTestDB(t) | ||||
| 
 | ||||
| 	hs := &HTTPServer{ | ||||
| 		Cfg:     cfg, | ||||
| 		Bus:     bus.GetBus(), | ||||
| 		License: &licensing.OSSLicensingService{Cfg: cfg}, | ||||
| 		Cfg:      cfg, | ||||
| 		Features: features, | ||||
| 		Bus:      bus.GetBus(), | ||||
| 		License:  &licensing.OSSLicensingService{Cfg: cfg}, | ||||
| 		RenderService: &rendering.RenderingService{ | ||||
| 			Cfg:                   cfg, | ||||
| 			RendererPluginManager: &fakeRendererManager{}, | ||||
|  | @ -73,7 +76,8 @@ func TestHTTPServer_GetFrontendSettings_hideVersionAnonymous(t *testing.T) { | |||
| 	cfg.Env = "testing" | ||||
| 	cfg.BuildVersion = "7.8.9" | ||||
| 	cfg.BuildCommit = "01234567" | ||||
| 	m, hs := setupTestEnvironment(t, cfg) | ||||
| 
 | ||||
| 	m, hs := setupTestEnvironment(t, cfg, featuremgmt.WithFeatures()) | ||||
| 
 | ||||
| 	req := httptest.NewRequest(http.MethodGet, "/api/frontend/settings", nil) | ||||
| 
 | ||||
|  |  | |||
|  | @ -13,6 +13,7 @@ import ( | |||
| 	"strings" | ||||
| 	"sync" | ||||
| 
 | ||||
| 	"github.com/grafana/grafana/pkg/services/featuremgmt" | ||||
| 	"github.com/grafana/grafana/pkg/services/query" | ||||
| 	"github.com/grafana/grafana/pkg/services/serviceaccounts" | ||||
| 	"github.com/grafana/grafana/pkg/services/thumbs" | ||||
|  | @ -75,6 +76,7 @@ type HTTPServer struct { | |||
| 	Bus                       bus.Bus | ||||
| 	RenderService             rendering.Service | ||||
| 	Cfg                       *setting.Cfg | ||||
| 	Features                  *featuremgmt.FeatureManager | ||||
| 	SettingsProvider          setting.Provider | ||||
| 	HooksService              *hooks.HooksService | ||||
| 	CacheService              *localcache.CacheService | ||||
|  | @ -135,7 +137,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi | |||
| 	loginService login.Service, accessControl accesscontrol.AccessControl, | ||||
| 	dataSourceProxy *datasourceproxy.DataSourceProxyService, searchService *search.SearchService, | ||||
| 	live *live.GrafanaLive, livePushGateway *pushhttp.Gateway, plugCtxProvider *plugincontext.Provider, | ||||
| 	contextHandler *contexthandler.ContextHandler, | ||||
| 	contextHandler *contexthandler.ContextHandler, features *featuremgmt.FeatureManager, | ||||
| 	schemaService *schemaloader.SchemaLoaderService, alertNG *ngalert.AlertNG, | ||||
| 	libraryPanelService librarypanels.Service, libraryElementService libraryelements.Service, | ||||
| 	quotaService *quota.QuotaService, socialService social.Service, tracer tracing.Tracer, | ||||
|  | @ -167,6 +169,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi | |||
| 		AuthTokenService:          userTokenService, | ||||
| 		cleanUpService:            cleanUpService, | ||||
| 		ShortURLService:           shortURLService, | ||||
| 		Features:                  features, | ||||
| 		ThumbService:              thumbService, | ||||
| 		RemoteCacheService:        remoteCache, | ||||
| 		ProvisioningService:       provisioningService, | ||||
|  |  | |||
|  | @ -85,7 +85,7 @@ func (hs *HTTPServer) getAppLinks(c *models.ReqContext) ([]*dtos.NavLink, error) | |||
| 			SortWeight: dtos.WeightPlugin, | ||||
| 		} | ||||
| 
 | ||||
| 		if hs.Cfg.IsNewNavigationEnabled() { | ||||
| 		if hs.Features.Toggles().IsNewNavigationEnabled() { | ||||
| 			appLink.Section = dtos.NavSectionPlugin | ||||
| 		} else { | ||||
| 			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 { | ||||
| 	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 { | ||||
|  | @ -154,7 +154,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto | |||
| 	hasAccess := ac.HasAccess(hs.AccessControl, c) | ||||
| 	navTree := []*dtos.NavLink{} | ||||
| 
 | ||||
| 	if hs.Cfg.IsNewNavigationEnabled() { | ||||
| 	if hs.Features.Toggles().IsNewNavigationEnabled() { | ||||
| 		navTree = append(navTree, &dtos.NavLink{ | ||||
| 			Text:       "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) | ||||
| 		navTree = append(navTree, &dtos.NavLink{ | ||||
| 			Text:       "Create", | ||||
|  | @ -181,7 +181,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto | |||
| 	dashboardChildLinks := hs.buildDashboardNavLinks(c, hasEditPerm) | ||||
| 
 | ||||
| 	dashboardsUrl := "/" | ||||
| 	if hs.Cfg.IsNewNavigationEnabled() { | ||||
| 	if hs.Features.Toggles().IsNewNavigationEnabled() { | ||||
| 		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 = append(liveNavLinks, &dtos.NavLink{ | ||||
|  | @ -346,7 +346,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto | |||
| 			SortWeight: dtos.WeightConfig, | ||||
| 			Children:   configNodes, | ||||
| 		} | ||||
| 		if hs.Cfg.IsNewNavigationEnabled() { | ||||
| 		if hs.Features.Toggles().IsNewNavigationEnabled() { | ||||
| 			configNode.Section = dtos.NavSectionConfig | ||||
| 		} else { | ||||
| 			configNode.Section = dtos.NavSectionCore | ||||
|  | @ -358,7 +358,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto | |||
| 
 | ||||
| 	if len(adminNavLinks) > 0 { | ||||
| 		navSection := dtos.NavSectionCore | ||||
| 		if hs.Cfg.IsNewNavigationEnabled() { | ||||
| 		if hs.Features.Toggles().IsNewNavigationEnabled() { | ||||
| 			navSection = dtos.NavSectionConfig | ||||
| 		} | ||||
| 		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 { | ||||
| 	dashboardChildNavs := []*dtos.NavLink{} | ||||
| 	if !hs.Cfg.IsNewNavigationEnabled() { | ||||
| 	if !hs.Features.Toggles().IsNewNavigationEnabled() { | ||||
| 		dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{ | ||||
| 			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{ | ||||
| 			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", | ||||
| 	} | ||||
| 
 | ||||
| 	if hs.Cfg.FeatureToggles["accesscontrol"] { | ||||
| 	if hs.Features.Toggles().IsAccesscontrolEnabled() { | ||||
| 		userPermissions, err := hs.AccessControl.GetUserPermissions(c.Req.Context(), c.SignedInUser) | ||||
| 		if err != nil { | ||||
| 			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 { | ||||
| 		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) { | ||||
| 		return response.Error(403, "Access denied", nil) | ||||
| 	} | ||||
|  |  | |||
|  | @ -16,6 +16,7 @@ import ( | |||
| 	"github.com/grafana/grafana/pkg/components/simplejson" | ||||
| 	"github.com/grafana/grafana/pkg/models" | ||||
| 	"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/setting" | ||||
| 	"github.com/grafana/grafana/pkg/util" | ||||
|  | @ -34,8 +35,8 @@ func setUpGetOrgUsersDB(t *testing.T, sqlStore *sqlstore.SQLStore) { | |||
| } | ||||
| 
 | ||||
| func TestOrgUsersAPIEndpoint_userLoggedIn(t *testing.T) { | ||||
| 	settings := setting.NewCfg() | ||||
| 	hs := &HTTPServer{Cfg: settings} | ||||
| 	hs := setupSimpleHTTPServer(featuremgmt.WithFeatures()) | ||||
| 	settings := hs.Cfg | ||||
| 
 | ||||
| 	sqlStore := sqlstore.InitTestDB(t) | ||||
| 	sqlStore.Cfg = settings | ||||
|  |  | |||
|  | @ -5,7 +5,7 @@ import ( | |||
| 	"strings" | ||||
| 
 | ||||
| 	"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" | ||||
| ) | ||||
| 
 | ||||
|  | @ -52,8 +52,8 @@ type RouteRegister interface { | |||
| 
 | ||||
| type RegisterNamedMiddleware func(name string) web.Handler | ||||
| 
 | ||||
| func ProvideRegister(cfg *setting.Cfg) *RouteRegisterImpl { | ||||
| 	return NewRouteRegister(middleware.ProvideRouteOperationName, middleware.RequestMetrics(cfg)) | ||||
| func ProvideRegister(features *featuremgmt.FeatureToggles) *RouteRegisterImpl { | ||||
| 	return NewRouteRegister(middleware.ProvideRouteOperationName, middleware.RequestMetrics(features)) | ||||
| } | ||||
| 
 | ||||
| // 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 { | ||||
| 		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 { | ||||
| 		return response.Error(403, "Not allowed to create team.", nil) | ||||
| 	} | ||||
|  |  | |||
|  | @ -40,9 +40,7 @@ func TestTeamAPIEndpoint(t *testing.T) { | |||
| 			TotalCount: 2, | ||||
| 		} | ||||
| 
 | ||||
| 		hs := &HTTPServer{ | ||||
| 			Cfg: setting.NewCfg(), | ||||
| 		} | ||||
| 		hs := setupSimpleHTTPServer(nil) | ||||
| 
 | ||||
| 		loggedInUserScenario(t, "When calling GET on", "/api/teams/search", "/api/teams/search", func(sc *scenarioContext) { | ||||
| 			var sentLimit int | ||||
|  | @ -92,10 +90,7 @@ func TestTeamAPIEndpoint(t *testing.T) { | |||
| 	t.Run("When creating team with API key", func(t *testing.T) { | ||||
| 		defer bus.ClearBusHandlers() | ||||
| 
 | ||||
| 		hs := &HTTPServer{ | ||||
| 			Cfg: setting.NewCfg(), | ||||
| 			Bus: bus.GetBus(), | ||||
| 		} | ||||
| 		hs := setupSimpleHTTPServer(nil) | ||||
| 		hs.Cfg.EditorsCanAdmin = true | ||||
| 
 | ||||
| 		teamName := "team foo" | ||||
|  |  | |||
|  | @ -9,6 +9,7 @@ import ( | |||
| 	"github.com/grafana/grafana/pkg/infra/log" | ||||
| 	"github.com/grafana/grafana/pkg/infra/metrics/metricutil" | ||||
| 	"github.com/grafana/grafana/pkg/infra/tracing" | ||||
| 	"github.com/grafana/grafana/pkg/services/featuremgmt" | ||||
| 	"github.com/grafana/grafana/pkg/setting" | ||||
| 	"github.com/mwitkow/go-conntrack" | ||||
| ) | ||||
|  | @ -16,7 +17,7 @@ import ( | |||
| var newProviderFunc = sdkhttpclient.NewProvider | ||||
| 
 | ||||
| // 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") | ||||
| 	userAgent := fmt.Sprintf("Grafana/%s", cfg.BuildVersion) | ||||
| 
 | ||||
|  | @ -35,7 +36,7 @@ func New(cfg *setting.Cfg, tracer tracing.Tracer) *sdkhttpclient.Provider { | |||
| 
 | ||||
| 	setDefaultTimeoutOptions(cfg) | ||||
| 
 | ||||
| 	if cfg.FeatureToggles["httpclientprovider_azure_auth"] { | ||||
| 	if features.IsHttpclientproviderAzureAuthEnabled() { | ||||
| 		middlewares = append(middlewares, AzureMiddleware(cfg)) | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ import ( | |||
| 
 | ||||
| 	sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" | ||||
| 	"github.com/grafana/grafana/pkg/infra/tracing" | ||||
| 	"github.com/grafana/grafana/pkg/services/featuremgmt" | ||||
| 	"github.com/grafana/grafana/pkg/setting" | ||||
| 	"github.com/stretchr/testify/require" | ||||
| ) | ||||
|  | @ -22,7 +23,7 @@ func TestHTTPClientProvider(t *testing.T) { | |||
| 		}) | ||||
| 		tracer, err := tracing.InitializeTracerForTest() | ||||
| 		require.NoError(t, err) | ||||
| 		_ = New(&setting.Cfg{SigV4AuthEnabled: false}, tracer) | ||||
| 		_ = New(&setting.Cfg{SigV4AuthEnabled: false}, tracer, featuremgmt.WithToggles()) | ||||
| 		require.Len(t, providerOpts, 1) | ||||
| 		o := providerOpts[0] | ||||
| 		require.Len(t, o.Middlewares, 6) | ||||
|  | @ -46,7 +47,7 @@ func TestHTTPClientProvider(t *testing.T) { | |||
| 		}) | ||||
| 		tracer, err := tracing.InitializeTracerForTest() | ||||
| 		require.NoError(t, err) | ||||
| 		_ = New(&setting.Cfg{SigV4AuthEnabled: true}, tracer) | ||||
| 		_ = New(&setting.Cfg{SigV4AuthEnabled: true}, tracer, featuremgmt.WithToggles()) | ||||
| 		require.Len(t, providerOpts, 1) | ||||
| 		o := providerOpts[0] | ||||
| 		require.Len(t, o.Middlewares, 7) | ||||
|  |  | |||
|  | @ -7,7 +7,7 @@ import ( | |||
| 	"time" | ||||
| 
 | ||||
| 	"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/prometheus/client_golang/prometheus" | ||||
| 	cw "github.com/weaveworks/common/tracing" | ||||
|  | @ -45,7 +45,7 @@ func init() { | |||
| } | ||||
| 
 | ||||
| // 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(res http.ResponseWriter, req *http.Request, c *web.Context) { | ||||
| 			rw := res.(web.ResponseWriter) | ||||
|  | @ -60,7 +60,7 @@ func RequestMetrics(cfg *setting.Cfg) func(handler string) web.Handler { | |||
| 			method := sanitizeMethod(req.Method) | ||||
| 
 | ||||
| 			// enable histogram and disable summaries + counters for http requests.
 | ||||
| 			if cfg.IsHTTPRequestHistogramDisabled() { | ||||
| 			if features.IsDisableHttpRequestHistogramEnabled() { | ||||
| 				duration := time.Since(now).Nanoseconds() / int64(time.Millisecond) | ||||
| 				metrics.MHttpRequestTotal.WithLabelValues(handler, code, method).Inc() | ||||
| 				metrics.MHttpRequestSummary.WithLabelValues(handler, code, method).Observe(float64(duration)) | ||||
|  |  | |||
|  | @ -14,8 +14,6 @@ type Licensing interface { | |||
| 
 | ||||
| 	StateInfo() string | ||||
| 
 | ||||
| 	EnabledFeatures() map[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) { | ||||
| 		cfg := &setting.Cfg{ | ||||
| 			FeatureToggles: map[string]bool{}, | ||||
| 			PluginSettings: setting.PluginSettings{ | ||||
| 				"test-app": map[string]string{ | ||||
| 					"path": "testdata/test-app", | ||||
|  |  | |||
|  | @ -18,7 +18,6 @@ import ( | |||
| 
 | ||||
| func TestGetPluginDashboards(t *testing.T) { | ||||
| 	cfg := &setting.Cfg{ | ||||
| 		FeatureToggles: map[string]bool{}, | ||||
| 		PluginSettings: setting.PluginSettings{ | ||||
| 			"test-app": map[string]string{ | ||||
| 				"path": "testdata/test-app", | ||||
|  |  | |||
|  | @ -17,6 +17,7 @@ import ( | |||
| 	"github.com/grafana/grafana/pkg/plugins/backendplugin/provider" | ||||
| 	"github.com/grafana/grafana/pkg/plugins/manager/loader" | ||||
| 	"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/setting" | ||||
| 	"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") | ||||
| 	require.NoError(t, err) | ||||
| 
 | ||||
| 	features := featuremgmt.WithToggles() | ||||
| 	cfg := &setting.Cfg{ | ||||
| 		Raw:                ini.Empty(), | ||||
| 		Env:                setting.Prod, | ||||
| 		StaticRootPath:     staticRootPath, | ||||
| 		BundledPluginsPath: bundledPluginsPath, | ||||
| 		Raw:                    ini.Empty(), | ||||
| 		Env:                    setting.Prod, | ||||
| 		StaticRootPath:         staticRootPath, | ||||
| 		BundledPluginsPath:     bundledPluginsPath, | ||||
| 		IsFeatureToggleEnabled: features.IsEnabled, | ||||
| 		PluginSettings: map[string]map[string]string{ | ||||
| 			"plugin.datasource-id": { | ||||
| 				"path": "testdata/test-app", | ||||
|  | @ -79,7 +82,7 @@ func TestPluginManager_int_init(t *testing.T) { | |||
| 	otsdb := opentsdb.ProvideService(hcp) | ||||
| 	pr := prometheus.ProvideService(hcp, tracer) | ||||
| 	tmpo := tempo.ProvideService(hcp) | ||||
| 	td := testdatasource.ProvideService(cfg) | ||||
| 	td := testdatasource.ProvideService(cfg, features) | ||||
| 	pg := postgres.ProvideService(cfg) | ||||
| 	my := mysql.ProvideService(cfg, hcp) | ||||
| 	ms := mssql.ProvideService(cfg) | ||||
|  |  | |||
|  | @ -35,6 +35,7 @@ import ( | |||
| 	"github.com/grafana/grafana/pkg/services/dashboardsnapshots" | ||||
| 	"github.com/grafana/grafana/pkg/services/datasourceproxy" | ||||
| 	"github.com/grafana/grafana/pkg/services/datasources" | ||||
| 	"github.com/grafana/grafana/pkg/services/featuremgmt" | ||||
| 	"github.com/grafana/grafana/pkg/services/hooks" | ||||
| 	"github.com/grafana/grafana/pkg/services/libraryelements" | ||||
| 	"github.com/grafana/grafana/pkg/services/librarypanels" | ||||
|  | @ -182,6 +183,8 @@ var wireBasicSet = wire.NewSet( | |||
| 	wire.Bind(new(teamguardian.Store), new(*teamguardianDatabase.TeamGuardianStoreImpl)), | ||||
| 	teamguardianManager.ProvideService, | ||||
| 	wire.Bind(new(teamguardian.TeamGuardian), new(*teamguardianManager.Service)), | ||||
| 	featuremgmt.ProvideManagerService, | ||||
| 	featuremgmt.ProvideToggles, | ||||
| ) | ||||
| 
 | ||||
| var wireSet = wire.NewSet( | ||||
|  |  | |||
|  | @ -9,13 +9,13 @@ import ( | |||
| 	"github.com/grafana/grafana/pkg/infra/usagestats" | ||||
| 	"github.com/grafana/grafana/pkg/models" | ||||
| 	"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" | ||||
| ) | ||||
| 
 | ||||
| func ProvideService(cfg *setting.Cfg, usageStats usagestats.Service) *OSSAccessControlService { | ||||
| func ProvideService(features *featuremgmt.FeatureToggles, usageStats usagestats.Service) *OSSAccessControlService { | ||||
| 	s := &OSSAccessControlService{ | ||||
| 		Cfg:           cfg, | ||||
| 		features:      features, | ||||
| 		UsageStats:    usageStats, | ||||
| 		Log:           log.New("accesscontrol"), | ||||
| 		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.
 | ||||
| type OSSAccessControlService struct { | ||||
| 	Cfg           *setting.Cfg | ||||
| 	features      *featuremgmt.FeatureToggles | ||||
| 	UsageStats    usagestats.Service | ||||
| 	Log           log.Logger | ||||
| 	registrations accesscontrol.RegistrationList | ||||
|  | @ -34,10 +34,10 @@ type OSSAccessControlService struct { | |||
| } | ||||
| 
 | ||||
| func (ac *OSSAccessControlService) IsDisabled() bool { | ||||
| 	if ac.Cfg == nil { | ||||
| 	if ac.features == nil { | ||||
| 		return true | ||||
| 	} | ||||
| 	return !ac.Cfg.FeatureToggles["accesscontrol"] | ||||
| 	return !ac.features.IsAccesscontrolEnabled() | ||||
| } | ||||
| 
 | ||||
| func (ac *OSSAccessControlService) registerUsageMetrics() { | ||||
|  |  | |||
|  | @ -12,17 +12,14 @@ import ( | |||
| 	"github.com/grafana/grafana/pkg/infra/usagestats" | ||||
| 	"github.com/grafana/grafana/pkg/models" | ||||
| 	"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 { | ||||
| 	t.Helper() | ||||
| 
 | ||||
| 	cfg := setting.NewCfg() | ||||
| 	cfg.FeatureToggles = map[string]bool{"accesscontrol": true} | ||||
| 
 | ||||
| 	ac := &OSSAccessControlService{ | ||||
| 		Cfg:           cfg, | ||||
| 		features:      featuremgmt.WithToggles("accesscontrol"), | ||||
| 		UsageStats:    &usagestats.UsageStatsMock{T: t}, | ||||
| 		Log:           log.New("accesscontrol"), | ||||
| 		registrations: accesscontrol.RegistrationList{}, | ||||
|  | @ -148,12 +145,9 @@ func TestUsageMetrics(t *testing.T) { | |||
| 
 | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			cfg := setting.NewCfg() | ||||
| 			if tt.enabled { | ||||
| 				cfg.FeatureToggles = map[string]bool{"accesscontrol": true} | ||||
| 			} | ||||
| 			features := featuremgmt.WithToggles("accesscontrol", tt.enabled) | ||||
| 
 | ||||
| 			s := ProvideService(cfg, &usagestats.UsageStatsMock{T: t}) | ||||
| 			s := ProvideService(features, &usagestats.UsageStatsMock{T: t}) | ||||
| 			report, err := s.UsageStats.GetUsageReport(context.Background()) | ||||
| 			assert.Nil(t, err) | ||||
| 
 | ||||
|  | @ -267,7 +261,7 @@ func TestOSSAccessControlService_RegisterFixedRole(t *testing.T) { | |||
| 	for _, tc := range tests { | ||||
| 		t.Run(tc.name, func(t *testing.T) { | ||||
| 			ac := &OSSAccessControlService{ | ||||
| 				Cfg:        setting.NewCfg(), | ||||
| 				features:   featuremgmt.WithToggles(), | ||||
| 				UsageStats: &usagestats.UsageStatsMock{T: t}, | ||||
| 				Log:        log.New("accesscontrol-test"), | ||||
| 			} | ||||
|  | @ -386,12 +380,11 @@ func TestOSSAccessControlService_DeclareFixedRoles(t *testing.T) { | |||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			ac := &OSSAccessControlService{ | ||||
| 				Cfg:           setting.NewCfg(), | ||||
| 				features:      featuremgmt.WithToggles("accesscontrol"), | ||||
| 				UsageStats:    &usagestats.UsageStatsMock{T: t}, | ||||
| 				Log:           log.New("accesscontrol-test"), | ||||
| 				registrations: accesscontrol.RegistrationList{}, | ||||
| 			} | ||||
| 			ac.Cfg.FeatureToggles = map[string]bool{"accesscontrol": true} | ||||
| 
 | ||||
| 			// Test
 | ||||
| 			err := ac.DeclareFixedRoles(tt.registrations...) | ||||
|  | @ -459,9 +452,6 @@ func TestOSSAccessControlService_RegisterFixedRoles(t *testing.T) { | |||
| 	} | ||||
| 
 | ||||
| 	for _, tt := range tests { | ||||
| 		cfg := setting.NewCfg() | ||||
| 		cfg.FeatureToggles = map[string]bool{"accesscontrol": true} | ||||
| 
 | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			// Remove any inserted role after the test case has been run
 | ||||
| 			t.Cleanup(func() { | ||||
|  | @ -472,12 +462,11 @@ func TestOSSAccessControlService_RegisterFixedRoles(t *testing.T) { | |||
| 
 | ||||
| 			// Setup
 | ||||
| 			ac := &OSSAccessControlService{ | ||||
| 				Cfg:           setting.NewCfg(), | ||||
| 				features:      featuremgmt.WithToggles("accesscontrol"), | ||||
| 				UsageStats:    &usagestats.UsageStatsMock{T: t}, | ||||
| 				Log:           log.New("accesscontrol-test"), | ||||
| 				registrations: accesscontrol.RegistrationList{}, | ||||
| 			} | ||||
| 			ac.Cfg.FeatureToggles = map[string]bool{"accesscontrol": true} | ||||
| 			ac.registrations.Append(tt.registrations...) | ||||
| 
 | ||||
| 			// Test
 | ||||
|  | @ -552,7 +541,7 @@ func TestOSSAccessControlService_GetUserPermissions(t *testing.T) { | |||
| 
 | ||||
| 			// Setup
 | ||||
| 			ac := setupTestEnv(t) | ||||
| 			ac.Cfg.FeatureToggles = map[string]bool{"accesscontrol": true} | ||||
| 			ac.features = featuremgmt.WithToggles("accesscontrol") | ||||
| 
 | ||||
| 			registration.Role.Permissions = []accesscontrol.Permission{tt.rawPerm} | ||||
| 			err := ac.DeclareFixedRoles(registration) | ||||
|  | @ -638,7 +627,6 @@ func TestOSSAccessControlService_Evaluate(t *testing.T) { | |||
| 
 | ||||
| 			// Setup
 | ||||
| 			ac := setupTestEnv(t) | ||||
| 			ac.Cfg.FeatureToggles = map[string]bool{"accesscontrol": true} | ||||
| 			ac.RegisterAttributeScopeResolver("users:login:", userLoginScopeSolver) | ||||
| 
 | ||||
| 			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" | ||||
| 
 | ||||
| 	"github.com/grafana/grafana/pkg/services/featuremgmt" | ||||
| 	"github.com/grafana/grafana/pkg/services/query" | ||||
| 
 | ||||
| 	"github.com/centrifugal/centrifuge" | ||||
|  | @ -67,9 +68,10 @@ type CoreGrafanaScope struct { | |||
| func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, routeRegister routing.RouteRegister, | ||||
| 	pluginStore plugins.Store, cacheService *localcache.CacheService, | ||||
| 	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{ | ||||
| 		Cfg:                   cfg, | ||||
| 		Features:              toggles, | ||||
| 		PluginContextProvider: plugCtxProvider, | ||||
| 		RouteRegister:         routeRegister, | ||||
| 		pluginStore:           pluginStore, | ||||
|  | @ -174,7 +176,7 @@ func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, r | |||
| 	} | ||||
| 
 | ||||
| 	g.ManagedStreamRunner = managedStreamRunner | ||||
| 	if enabled := g.Cfg.FeatureToggles["live-pipeline"]; enabled { | ||||
| 	if g.Features.IsLivePipelineEnabled() { | ||||
| 		var builder pipeline.RuleBuilder | ||||
| 		if os.Getenv("GF_LIVE_DEV_BUILDER") != "" { | ||||
| 			builder = &pipeline.DevRuleBuilder{ | ||||
|  | @ -391,6 +393,7 @@ func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, r | |||
| type GrafanaLive struct { | ||||
| 	PluginContextProvider *plugincontext.Provider | ||||
| 	Cfg                   *setting.Cfg | ||||
| 	Features              *featuremgmt.FeatureToggles | ||||
| 	RouteRegister         routing.RouteRegister | ||||
| 	CacheService          *localcache.CacheService | ||||
| 	DataSourceCache       datasources.CacheService | ||||
|  |  | |||
|  | @ -8,9 +8,9 @@ import ( | |||
| 	"github.com/grafana/grafana/pkg/components/simplejson" | ||||
| 	"github.com/grafana/grafana/pkg/schema" | ||||
| 	"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/setting" | ||||
| ) | ||||
| 
 | ||||
| const ServiceName = "SchemaLoader" | ||||
|  | @ -26,13 +26,13 @@ type RenderUser struct { | |||
| 	OrgRole string | ||||
| } | ||||
| 
 | ||||
| func ProvideService(cfg *setting.Cfg) (*SchemaLoaderService, error) { | ||||
| func ProvideService(features *featuremgmt.FeatureToggles) (*SchemaLoaderService, error) { | ||||
| 	dashFam, err := load.BaseDashboardFamily(baseLoadPath) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to load dashboard cue schema from path %q: %w", baseLoadPath, err) | ||||
| 	} | ||||
| 	s := &SchemaLoaderService{ | ||||
| 		Cfg:        cfg, | ||||
| 		features:   features, | ||||
| 		DashFamily: dashFam, | ||||
| 		log:        log.New("schemaloader"), | ||||
| 	} | ||||
|  | @ -42,14 +42,14 @@ func ProvideService(cfg *setting.Cfg) (*SchemaLoaderService, error) { | |||
| type SchemaLoaderService struct { | ||||
| 	log        log.Logger | ||||
| 	DashFamily schema.VersionedCueSchema | ||||
| 	Cfg        *setting.Cfg | ||||
| 	features   *featuremgmt.FeatureToggles | ||||
| } | ||||
| 
 | ||||
| func (rs *SchemaLoaderService) IsDisabled() bool { | ||||
| 	if rs.Cfg == nil { | ||||
| 	if rs.features == nil { | ||||
| 		return true | ||||
| 	} | ||||
| 	return !rs.Cfg.IsTrimDefaultsEnabled() | ||||
| 	return !rs.features.IsTrimDefaultsEnabled() | ||||
| } | ||||
| 
 | ||||
| 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/services/encryption/ossencryption" | ||||
| 	"github.com/grafana/grafana/pkg/services/featuremgmt" | ||||
| 	"github.com/grafana/grafana/pkg/services/kmsproviders/osskmsproviders" | ||||
| 	"github.com/grafana/grafana/pkg/services/secrets" | ||||
| 	"github.com/grafana/grafana/pkg/setting" | ||||
|  | @ -23,10 +24,15 @@ func SetupTestService(tb testing.TB, store secrets.Store) *SecretsService { | |||
| 		[security] | ||||
| 		secret_key = ` + defaultKey)) | ||||
| 	require.NoError(tb, err) | ||||
| 
 | ||||
| 	features := featuremgmt.WithToggles("envelopeEncryption") | ||||
| 
 | ||||
| 	cfg := &setting.Cfg{Raw: raw} | ||||
| 	cfg.FeatureToggles = map[string]bool{secrets.EnvelopeEncryptionFeatureToggle: true} | ||||
| 	cfg.IsFeatureToggleEnabled = features.IsEnabled | ||||
| 
 | ||||
| 	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() | ||||
| 	secretsService, err := ProvideSecretsService( | ||||
|  |  | |||
|  | @ -7,6 +7,7 @@ import ( | |||
| 
 | ||||
| 	"github.com/grafana/grafana/pkg/infra/usagestats" | ||||
| 	"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/secrets" | ||||
| 	"github.com/grafana/grafana/pkg/services/secrets/database" | ||||
|  | @ -180,8 +181,8 @@ func TestSecretsService_UseCurrentProvider(t *testing.T) { | |||
| 		providerID := secrets.ProviderID("fakeProvider.v1") | ||||
| 		settings := &setting.OSSImpl{ | ||||
| 			Cfg: &setting.Cfg{ | ||||
| 				Raw:            raw, | ||||
| 				FeatureToggles: map[string]bool{secrets.EnvelopeEncryptionFeatureToggle: true}, | ||||
| 				Raw:                    raw, | ||||
| 				IsFeatureToggleEnabled: featuremgmt.WithToggles(secrets.EnvelopeEncryptionFeatureToggle).IsEnabled, | ||||
| 			}, | ||||
| 		} | ||||
| 		encr := ossencryption.ProvideService() | ||||
|  |  | |||
|  | @ -13,8 +13,8 @@ import ( | |||
| 	"github.com/grafana/grafana/pkg/models" | ||||
| 	"github.com/grafana/grafana/pkg/services/accesscontrol" | ||||
| 	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/setting" | ||||
| 	"github.com/grafana/grafana/pkg/web" | ||||
| ) | ||||
| 
 | ||||
|  | @ -40,9 +40,9 @@ func NewServiceAccountsAPI( | |||
| } | ||||
| 
 | ||||
| func (api *ServiceAccountsAPI) RegisterAPIEndpoints( | ||||
| 	cfg *setting.Cfg, | ||||
| 	features *featuremgmt.FeatureToggles, | ||||
| ) { | ||||
| 	if !cfg.FeatureToggles["service-accounts"] { | ||||
| 	if !features.IsServiceAccountsEnabled() { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
|  | @ -13,10 +13,10 @@ import ( | |||
| 	"github.com/grafana/grafana/pkg/models" | ||||
| 	"github.com/grafana/grafana/pkg/services/accesscontrol" | ||||
| 	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/tests" | ||||
| 	"github.com/grafana/grafana/pkg/services/sqlstore" | ||||
| 	"github.com/grafana/grafana/pkg/setting" | ||||
| 	"github.com/grafana/grafana/pkg/web" | ||||
| 	"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 { | ||||
| 	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() | ||||
| 	signedUser := &models.SignedInUser{ | ||||
|  |  | |||
|  | @ -7,11 +7,11 @@ import ( | |||
| 	"github.com/grafana/grafana/pkg/infra/log" | ||||
| 	"github.com/grafana/grafana/pkg/models" | ||||
| 	"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/api" | ||||
| 	"github.com/grafana/grafana/pkg/services/serviceaccounts/database" | ||||
| 	"github.com/grafana/grafana/pkg/services/sqlstore" | ||||
| 	"github.com/grafana/grafana/pkg/setting" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
|  | @ -19,21 +19,21 @@ var ( | |||
| ) | ||||
| 
 | ||||
| type ServiceAccountsService struct { | ||||
| 	store serviceaccounts.Store | ||||
| 	cfg   *setting.Cfg | ||||
| 	log   log.Logger | ||||
| 	store    serviceaccounts.Store | ||||
| 	features *featuremgmt.FeatureToggles | ||||
| 	log      log.Logger | ||||
| } | ||||
| 
 | ||||
| func ProvideServiceAccountsService( | ||||
| 	cfg *setting.Cfg, | ||||
| 	features *featuremgmt.FeatureToggles, | ||||
| 	store *sqlstore.SQLStore, | ||||
| 	ac accesscontrol.AccessControl, | ||||
| 	routeRegister routing.RouteRegister, | ||||
| ) (*ServiceAccountsService, error) { | ||||
| 	s := &ServiceAccountsService{ | ||||
| 		cfg:   cfg, | ||||
| 		store: database.NewServiceAccountsStore(store), | ||||
| 		log:   log.New("serviceaccounts"), | ||||
| 		features: features, | ||||
| 		store:    database.NewServiceAccountsStore(store), | ||||
| 		log:      log.New("serviceaccounts"), | ||||
| 	} | ||||
| 
 | ||||
| 	if err := RegisterRoles(ac); err != nil { | ||||
|  | @ -41,13 +41,13 @@ func ProvideServiceAccountsService( | |||
| 	} | ||||
| 
 | ||||
| 	serviceaccountsAPI := api.NewServiceAccountsAPI(s, ac, routeRegister, s.store) | ||||
| 	serviceaccountsAPI.RegisterAPIEndpoints(cfg) | ||||
| 	serviceaccountsAPI.RegisterAPIEndpoints(features) | ||||
| 
 | ||||
| 	return s, nil | ||||
| } | ||||
| 
 | ||||
| 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) | ||||
| 		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 { | ||||
| 	if !sa.cfg.FeatureToggles["service-accounts"] { | ||||
| 	if !sa.features.IsServiceAccountsEnabled() { | ||||
| 		sa.log.Debug(ServiceAccountFeatureToggleNotFound) | ||||
| 		return nil | ||||
| 	} | ||||
|  |  | |||
|  | @ -5,31 +5,29 @@ import ( | |||
| 	"testing" | ||||
| 
 | ||||
| 	"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/setting" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"github.com/stretchr/testify/require" | ||||
| ) | ||||
| 
 | ||||
| func TestProvideServiceAccount_DeleteServiceAccount(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{}} | ||||
| 		cfg.FeatureToggles = map[string]bool{"service-accounts": true} | ||||
| 		svc := ServiceAccountsService{cfg: cfg, store: storeMock} | ||||
| 		svc := ServiceAccountsService{ | ||||
| 			features: featuremgmt.WithToggles("service-accounts", true), | ||||
| 			store:    storeMock} | ||||
| 		err := svc.DeleteServiceAccount(context.Background(), 1, 1) | ||||
| 		require.NoError(t, err) | ||||
| 		assert.Len(t, storeMock.Calls.DeleteServiceAccount, 1) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("no feature toggle present, should not call store function", func(t *testing.T) { | ||||
| 		cfg := setting.NewCfg() | ||||
| 		svcMock := &tests.ServiceAccountsStoreMock{Calls: tests.Calls{}} | ||||
| 		cfg.FeatureToggles = map[string]bool{"service-accounts": false} | ||||
| 		svc := ServiceAccountsService{ | ||||
| 			cfg:   cfg, | ||||
| 			store: svcMock, | ||||
| 			log:   log.New("serviceaccounts-manager-test"), | ||||
| 			features: featuremgmt.WithToggles("service-accounts", false), | ||||
| 			store:    svcMock, | ||||
| 			log:      log.New("serviceaccounts-manager-test"), | ||||
| 		} | ||||
| 		err := svc.DeleteServiceAccount(context.Background(), 1, 1) | ||||
| 		require.NoError(t, err) | ||||
|  |  | |||
|  | @ -3,6 +3,7 @@ package migrations | |||
| import ( | ||||
| 	"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/ualert" | ||||
| 	. "github.com/grafana/grafana/pkg/services/sqlstore/migrator" | ||||
|  | @ -56,8 +57,8 @@ func (*OSSMigrations) AddMigration(mg *Migrator) { | |||
| 	ualert.AddTablesMigrations(mg) | ||||
| 	ualert.AddDashAlertMigration(mg) | ||||
| 	addLibraryElementsMigrations(mg) | ||||
| 	if mg.Cfg != nil { | ||||
| 		if mg.Cfg.IsLiveConfigEnabled() { | ||||
| 	if mg.Cfg.IsFeatureToggleEnabled != nil { | ||||
| 		if mg.Cfg.IsFeatureToggleEnabled(featuremgmt.FLAG_live_config) { | ||||
| 			addLiveChannelMigrations(mg) | ||||
| 		} | ||||
| 	} | ||||
|  |  | |||
|  | @ -117,7 +117,7 @@ func (ss *SQLStore) GetOrgUsers(ctx context.Context, query *models.GetOrgUsersQu | |||
| 	// service accounts table in the modelling
 | ||||
| 	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) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
|  | @ -180,7 +180,7 @@ func (ss *SQLStore) SearchOrgUsers(ctx context.Context, query *models.SearchOrgU | |||
| 	// service accounts table in the modelling
 | ||||
| 	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) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
|  |  | |||
|  | @ -11,6 +11,7 @@ import ( | |||
| 
 | ||||
| 	"github.com/grafana/grafana/pkg/models" | ||||
| 	ac "github.com/grafana/grafana/pkg/services/accesscontrol" | ||||
| 	"github.com/grafana/grafana/pkg/services/featuremgmt" | ||||
| ) | ||||
| 
 | ||||
| type getOrgUsersTestCase struct { | ||||
|  | @ -61,7 +62,7 @@ func TestSQLStore_GetOrgUsers(t *testing.T) { | |||
| 	} | ||||
| 
 | ||||
| 	store := InitTestDB(t) | ||||
| 	store.Cfg.FeatureToggles = map[string]bool{"accesscontrol": true} | ||||
| 	store.Cfg.IsFeatureToggleEnabled = featuremgmt.WithToggles("accesscontrol").IsEnabled | ||||
| 	seedOrgUsers(t, store, 10) | ||||
| 
 | ||||
| 	for _, tt := range tests { | ||||
|  | @ -127,7 +128,7 @@ func TestSQLStore_SearchOrgUsers(t *testing.T) { | |||
| 	} | ||||
| 
 | ||||
| 	store := InitTestDB(t) | ||||
| 	store.Cfg.FeatureToggles = map[string]bool{"accesscontrol": true} | ||||
| 	store.Cfg.IsFeatureToggleEnabled = featuremgmt.WithToggles("accesscontrol").IsEnabled | ||||
| 	seedOrgUsers(t, store, 10) | ||||
| 
 | ||||
| 	for _, tt := range tests { | ||||
|  |  | |||
|  | @ -23,6 +23,7 @@ import ( | |||
| 	"github.com/grafana/grafana/pkg/models" | ||||
| 	"github.com/grafana/grafana/pkg/registry" | ||||
| 	"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/migrator" | ||||
| 	"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil" | ||||
|  | @ -326,7 +327,7 @@ func (ss *SQLStore) initEngine(engine *xorm.Engine) error { | |||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if ss.Cfg.IsDatabaseMetricsEnabled() { | ||||
| 	if ss.Cfg.IsFeatureToggleEnabled(featuremgmt.FLAG_database_metrics) { | ||||
| 		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
 | ||||
| 		cfg := setting.NewCfg() | ||||
| 		cfg.IsFeatureToggleEnabled = func(key string) bool { return false } | ||||
| 		sec, err := cfg.Raw.NewSection("database") | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
|  |  | |||
|  | @ -14,6 +14,7 @@ import ( | |||
| 	"github.com/grafana/grafana/pkg/api/response" | ||||
| 	"github.com/grafana/grafana/pkg/bus" | ||||
| 	"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/live" | ||||
| 	"github.com/grafana/grafana/pkg/services/rendering" | ||||
|  | @ -39,8 +40,8 @@ type Service interface { | |||
| 	CrawlerStatus(c *models.ReqContext) response.Response | ||||
| } | ||||
| 
 | ||||
| func ProvideService(cfg *setting.Cfg, renderService rendering.Service, gl *live.GrafanaLive) Service { | ||||
| 	if !cfg.IsDashboardPreviesEnabled() { | ||||
| func ProvideService(cfg *setting.Cfg, features *featuremgmt.FeatureToggles, renderService rendering.Service, gl *live.GrafanaLive) Service { | ||||
| 	if !features.IsDashboardPreviewsEnabled() { | ||||
| 		return &dummyService{} | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
|  | @ -132,7 +132,7 @@ func (o *OSSImpl) Section(section string) Section { | |||
| func (OSSImpl) RegisterReloadHandler(string, ReloadHandler) {} | ||||
| 
 | ||||
| func (o OSSImpl) IsFeatureToggleEnabled(name string) bool { | ||||
| 	return o.Cfg.FeatureToggles[name] | ||||
| 	return o.Cfg.IsFeatureToggleEnabled(name) | ||||
| } | ||||
| 
 | ||||
| type keyValImpl struct { | ||||
|  |  | |||
|  | @ -342,8 +342,10 @@ type Cfg struct { | |||
| 
 | ||||
| 	ApiKeyMaxSecondsToLive int64 | ||||
| 
 | ||||
| 	// Use to enable new features which may still be in alpha/beta stage.
 | ||||
| 	FeatureToggles       map[string]bool | ||||
| 	// Check if a feature toggle is enabled
 | ||||
| 	// @deprecated
 | ||||
| 	IsFeatureToggleEnabled func(key string) bool // filled in dynamically
 | ||||
| 
 | ||||
| 	AnonymousEnabled     bool | ||||
| 	AnonymousOrgName     string | ||||
| 	AnonymousOrgRole     string | ||||
|  | @ -429,41 +431,6 @@ type Cfg struct { | |||
| 	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 { | ||||
| 	Config   string | ||||
| 	HomePath string | ||||
|  |  | |||
|  | @ -3,42 +3,23 @@ package setting | |||
| import ( | ||||
| 	"strconv" | ||||
| 
 | ||||
| 	"github.com/prometheus/client_golang/prometheus" | ||||
| 	"github.com/prometheus/client_golang/prometheus/promauto" | ||||
| 
 | ||||
| 	"github.com/grafana/grafana/pkg/util" | ||||
| 	"gopkg.in/ini.v1" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	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, | ||||
| 	} | ||||
| ) | ||||
| 
 | ||||
| // @deprecated -- should use `featuremgmt.FeatureToggles`
 | ||||
| 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 { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	cfg.FeatureToggles = toggles | ||||
| 
 | ||||
| 	cfg.IsFeatureToggleEnabled = func(key string) bool { return toggles[key] } | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func overrideDefaultWithConfiguration(iniFile *ini.File, featureToggles map[string]bool) (map[string]bool, error) { | ||||
| 	// Read and populate feature toggles list
 | ||||
| 	featureTogglesSection := iniFile.Section("feature_toggles") | ||||
| func ReadFeatureTogglesFromInitFile(featureTogglesSection *ini.Section) (map[string]bool, error) { | ||||
| 	featureToggles := make(map[string]bool, 10) | ||||
| 
 | ||||
| 	// parse the comma separated list in `enable`.
 | ||||
| 	featuresTogglesStr := valueAsString(featureTogglesSection, "enable", "") | ||||
|  | @ -60,15 +41,5 @@ func overrideDefaultWithConfiguration(iniFile *ini.File, featureToggles map[stri | |||
| 
 | ||||
| 		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 | ||||
| } | ||||
|  |  | |||
|  | @ -14,7 +14,6 @@ func TestFeatureToggles(t *testing.T) { | |||
| 		conf            map[string]string | ||||
| 		err             error | ||||
| 		expectedToggles map[string]bool | ||||
| 		defaultToggles  map[string]bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name: "can parse feature toggles passed in the `enable` array", | ||||
|  | @ -58,18 +57,6 @@ func TestFeatureToggles(t *testing.T) { | |||
| 			expectedToggles: map[string]bool{}, | ||||
| 			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 { | ||||
|  | @ -81,12 +68,7 @@ func TestFeatureToggles(t *testing.T) { | |||
| 			require.ErrorIs(t, err, nil) | ||||
| 		} | ||||
| 
 | ||||
| 		dt := map[string]bool{} | ||||
| 		if len(tc.defaultToggles) > 0 { | ||||
| 			dt = tc.defaultToggles | ||||
| 		} | ||||
| 
 | ||||
| 		featureToggles, err := overrideDefaultWithConfiguration(f, dt) | ||||
| 		featureToggles, err := ReadFeatureTogglesFromInitFile(toggles) | ||||
| 		require.ErrorIs(t, err, tc.err) | ||||
| 
 | ||||
| 		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
 | ||||
| 	if err != nil { | ||||
| 		// 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") | ||||
| 			enabled = true | ||||
| 			// feature flag overrides the legacy alerting setting.
 | ||||
|  |  | |||
|  | @ -143,6 +143,7 @@ func TestUnifiedAlertingSettings(t *testing.T) { | |||
| 		t.Run(tc.desc, func(t *testing.T) { | ||||
| 			f := ini.Empty() | ||||
| 			cfg := NewCfg() | ||||
| 			cfg.IsFeatureToggleEnabled = func(key string) bool { return false } | ||||
| 			unifiedAlertingSec, err := f.NewSection("unified_alerting") | ||||
| 			require.NoError(t, err) | ||||
| 			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.
 | ||||
| 		initialData = nil | ||||
| 	} | ||||
|  | @ -126,7 +126,7 @@ func (s *Service) runTestStream(ctx context.Context, path string, conf testStrea | |||
| 			} | ||||
| 
 | ||||
| 			mode := data.IncludeDataOnly | ||||
| 			if s.cfg.FeatureToggles["live-pipeline"] { | ||||
| 			if s.features.IsLivePipelineEnabled() { | ||||
| 				mode = data.IncludeAll | ||||
| 			} | ||||
| 
 | ||||
|  |  | |||
|  | @ -10,11 +10,13 @@ import ( | |||
| 	"github.com/grafana/grafana-plugin-sdk-go/data" | ||||
| 
 | ||||
| 	"github.com/grafana/grafana/pkg/infra/log" | ||||
| 	"github.com/grafana/grafana/pkg/services/featuremgmt" | ||||
| 	"github.com/grafana/grafana/pkg/setting" | ||||
| ) | ||||
| 
 | ||||
| func ProvideService(cfg *setting.Cfg) *Service { | ||||
| func ProvideService(cfg *setting.Cfg, features *featuremgmt.FeatureToggles) *Service { | ||||
| 	s := &Service{ | ||||
| 		features:  features, | ||||
| 		queryMux:  datasource.NewQueryTypeMux(), | ||||
| 		scenarios: map[string]*Scenario{}, | ||||
| 		frame: data.NewFrame("testdata", | ||||
|  | @ -46,6 +48,7 @@ type Service struct { | |||
| 	labelFrame      *data.Frame | ||||
| 	queryMux        *datasource.QueryTypeMux | ||||
| 	resourceHandler backend.CallResourceHandler | ||||
| 	features        *featuremgmt.FeatureToggles | ||||
| } | ||||
| 
 | ||||
| func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { | ||||
|  |  | |||
|  | @ -83,13 +83,13 @@ export class ContextSrv { | |||
|   } | ||||
| 
 | ||||
|   accessControlEnabled(): boolean { | ||||
|     return featureEnabled('accesscontrol') && Boolean(config.featureToggles['accesscontrol']); | ||||
|     return featureEnabled(config.featureToggles.accesscontrol); | ||||
|   } | ||||
| 
 | ||||
|   // Checks whether user has required permission
 | ||||
|   hasPermissionInMetadata(action: AccessControlAction | string, object: WithAccessControlMetadata): boolean { | ||||
|     // Fallback if access control disabled
 | ||||
|     if (!config.featureToggles['accesscontrol']) { | ||||
|     if (!config.featureToggles.accesscontrol) { | ||||
|       return true; | ||||
|     } | ||||
| 
 | ||||
|  | @ -99,7 +99,7 @@ export class ContextSrv { | |||
|   // Checks whether user has required permission
 | ||||
|   hasPermission(action: AccessControlAction | string): boolean { | ||||
|     // Fallback if access control disabled
 | ||||
|     if (!config.featureToggles['accesscontrol']) { | ||||
|     if (!config.featureToggles.accesscontrol) { | ||||
|       return true; | ||||
|     } | ||||
| 
 | ||||
|  | @ -126,14 +126,14 @@ export class ContextSrv { | |||
|   } | ||||
| 
 | ||||
|   hasAccessToExplore() { | ||||
|     if (config.featureToggles['accesscontrol']) { | ||||
|     if (config.featureToggles.accesscontrol) { | ||||
|       return this.hasPermission(AccessControlAction.DataSourcesExplore); | ||||
|     } | ||||
|     return (this.isEditor || config.viewersCanEdit) && config.exploreEnabled; | ||||
|   } | ||||
| 
 | ||||
|   hasAccess(action: string, fallBack: boolean) { | ||||
|     if (!config.featureToggles['accesscontrol']) { | ||||
|     if (!config.featureToggles.accesscontrol) { | ||||
|       return fallBack; | ||||
|     } | ||||
|     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
 | ||||
|   evaluatePermission(fallback: () => string[], actions: string[]) { | ||||
|     if (!config.featureToggles['accesscontrol']) { | ||||
|     if (!config.featureToggles.accesscontrol) { | ||||
|       return fallback(); | ||||
|     } | ||||
|     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
 | ||||
| export function accessControlQueryParam(params = {}) { | ||||
|   if (!config.featureToggles['accesscontrol']) { | ||||
|   if (!config.featureToggles.accesscontrol) { | ||||
|     return params; | ||||
|   } | ||||
|   return { ...params, accesscontrol: true }; | ||||
|  |  | |||
|  | @ -49,14 +49,14 @@ describe('PluginListItemBadges', () => { | |||
|   }); | ||||
| 
 | ||||
|   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 }} />); | ||||
|     expect(screen.getByText(/enterprise/i)).toBeVisible(); | ||||
|     expect(screen.queryByRole('button', { name: /learn more/i })).not.toBeInTheDocument(); | ||||
|   }); | ||||
| 
 | ||||
|   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 }} />); | ||||
|     expect(screen.getByText(/enterprise/i)).toBeVisible(); | ||||
|     expect(screen.getByLabelText(/lock icon/i)).toBeInTheDocument(); | ||||
|  |  | |||
|  | @ -90,7 +90,7 @@ describe('Plugin details page', () => { | |||
|   afterEach(() => { | ||||
|     jest.clearAllMocks(); | ||||
|     config.pluginAdminExternalManageEnabled = false; | ||||
|     config.licenseInfo.enabledFeatures = {}; | ||||
|     config.featureToggles = {}; | ||||
|   }); | ||||
| 
 | ||||
|   afterAll(() => { | ||||
|  | @ -325,7 +325,7 @@ describe('Plugin details page', () => { | |||
|     }); | ||||
| 
 | ||||
|     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 }); | ||||
| 
 | ||||
|  | @ -333,7 +333,7 @@ describe('Plugin details page', () => { | |||
|     }); | ||||
| 
 | ||||
|     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 }); | ||||
| 
 | ||||
|  | @ -772,7 +772,7 @@ describe('Plugin details page', () => { | |||
|     }); | ||||
| 
 | ||||
|     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 }); | ||||
| 
 | ||||
|       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.requireActual('@grafana/runtime/src/config') as unknown) as object), | ||||
|   config: { | ||||
|     licenseInfo: { | ||||
|       enabledFeatures: { teamsync: true }, | ||||
|     }, | ||||
|     featureToggles: { teamsync: true }, | ||||
|   }, | ||||
| })); | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue