FeatureFlags: manage feature flags outside of settings.Cfg (#43692)

This commit is contained in:
Ryan McKinley 2022-01-20 13:42:05 -08:00 committed by GitHub
parent 7fbc7d019a
commit f94c0decbd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
65 changed files with 1244 additions and 252 deletions

1
.gitignore vendored
View File

@ -156,6 +156,7 @@ compilation-stats.json
# auto generated Go files # auto generated Go files
*_gen.go *_gen.go
!pkg/services/featuremgmt/toggles_gen.go
# Auto-generated localisation files # Auto-generated localisation files
public/locales/_build/ public/locales/_build/

View File

@ -40,7 +40,6 @@ export interface LicenseInfo {
licenseUrl: string; licenseUrl: string;
stateInfo: string; stateInfo: string;
edition: GrafanaEdition; edition: GrafanaEdition;
enabledFeatures: { [key: string]: boolean };
} }
/** /**

View File

@ -1,3 +1,9 @@
// NOTE: This file was auto generated. DO NOT EDIT DIRECTLY!
// To change feature flags, edit:
// pkg/services/featuremgmt/registry.go
// Then run tests in:
// pkg/services/featuremgmt/toggles_gen_test.go
/** /**
* Describes available feature toggles in Grafana. These can be configured via * Describes available feature toggles in Grafana. These can be configured via
* conf/custom.ini to enable features under development or not yet available in * conf/custom.ini to enable features under development or not yet available in

View File

@ -1,6 +1,12 @@
import { FeatureToggles } from '@grafana/data';
import { config } from '../config'; import { config } from '../config';
export const featureEnabled = (feature: string): boolean => { export const featureEnabled = (feature: boolean | undefined | keyof FeatureToggles): boolean => {
const { enabledFeatures } = config.licenseInfo; if (feature === true || feature === false) {
return enabledFeatures && enabledFeatures[feature]; return feature;
}
if (feature == null || !config?.featureToggles) {
return false;
}
return Boolean(config.featureToggles[feature]);
}; };

View File

@ -437,7 +437,7 @@ func (hs *HTTPServer) registerRoutes() {
// Some channels may have info // Some channels may have info
liveRoute.Get("/info/*", routing.Wrap(hs.Live.HandleInfoHTTP)) liveRoute.Get("/info/*", routing.Wrap(hs.Live.HandleInfoHTTP))
if hs.Cfg.FeatureToggles["live-pipeline"] { if hs.Features.Toggles().IsLivePipelineEnabled() {
// POST Live data to be processed according to channel rules. // POST Live data to be processed according to channel rules.
liveRoute.Post("/pipeline/push/*", hs.LivePushGateway.HandlePipelinePush) liveRoute.Post("/pipeline/push/*", hs.LivePushGateway.HandlePipelinePush)
liveRoute.Post("/pipeline-convert-test", routing.Wrap(hs.Live.HandlePipelineConvertTestHTTP), reqOrgAdmin) liveRoute.Post("/pipeline-convert-test", routing.Wrap(hs.Live.HandlePipelineConvertTestHTTP), reqOrgAdmin)
@ -460,6 +460,9 @@ func (hs *HTTPServer) registerRoutes() {
// admin api // admin api
r.Group("/api/admin", func(adminRoute routing.RouteRegister) { r.Group("/api/admin", func(adminRoute routing.RouteRegister) {
adminRoute.Get("/settings", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionSettingsRead)), routing.Wrap(hs.AdminGetSettings)) adminRoute.Get("/settings", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionSettingsRead)), routing.Wrap(hs.AdminGetSettings))
if hs.Features.Toggles().IsShowFeatureFlagsInUIEnabled() {
adminRoute.Get("/settings/features", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionSettingsRead)), hs.Features.HandleGetSettings)
}
adminRoute.Get("/stats", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionServerStatsRead)), routing.Wrap(AdminGetStats)) adminRoute.Get("/stats", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionServerStatsRead)), routing.Wrap(AdminGetStats))
adminRoute.Post("/pause-all-alerts", reqGrafanaAdmin, routing.Wrap(PauseAllAlerts)) adminRoute.Post("/pause-all-alerts", reqGrafanaAdmin, routing.Wrap(PauseAllAlerts))

View File

@ -83,7 +83,7 @@ func (hs *HTTPServer) AddAPIKey(c *models.ReqContext) response.Response {
} }
cmd.OrgId = c.OrgId cmd.OrgId = c.OrgId
var err error var err error
if hs.Cfg.FeatureToggles["service-accounts"] { if hs.Features.Toggles().IsServiceAccountsEnabled() {
// Api keys should now be created with addadditionalapikey endpoint // Api keys should now be created with addadditionalapikey endpoint
return response.Error(400, "API keys should now be added via the AdditionalAPIKey endpoint.", err) return response.Error(400, "API keys should now be added via the AdditionalAPIKey endpoint.", err)
} }
@ -120,7 +120,7 @@ func (hs *HTTPServer) AdditionalAPIKey(c *models.ReqContext) response.Response {
if err := web.Bind(c.Req, &cmd); err != nil { if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err) return response.Error(http.StatusBadRequest, "bad request data", err)
} }
if !hs.Cfg.FeatureToggles["service-accounts"] { if !hs.Features.Toggles().IsServiceAccountsEnabled() {
return response.Error(500, "Requires services-accounts feature", errors.New("feature missing")) return response.Error(500, "Requires services-accounts feature", errors.New("feature missing"))
} }

View File

@ -28,6 +28,7 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol"
"github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/contexthandler" "github.com/grafana/grafana/pkg/services/contexthandler"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/services/searchusers" "github.com/grafana/grafana/pkg/services/searchusers"
@ -213,8 +214,8 @@ func (s *fakeRenderService) Init() error {
} }
func setupAccessControlScenarioContext(t *testing.T, cfg *setting.Cfg, url string, permissions []*accesscontrol.Permission) (*scenarioContext, *HTTPServer) { func setupAccessControlScenarioContext(t *testing.T, cfg *setting.Cfg, url string, permissions []*accesscontrol.Permission) (*scenarioContext, *HTTPServer) {
cfg.FeatureToggles = make(map[string]bool) features := featuremgmt.WithFeatures("accesscontrol")
cfg.FeatureToggles["accesscontrol"] = true cfg.IsFeatureToggleEnabled = features.IsEnabled
cfg.Quota.Enabled = false cfg.Quota.Enabled = false
bus := bus.GetBus() bus := bus.GetBus()
@ -222,6 +223,7 @@ func setupAccessControlScenarioContext(t *testing.T, cfg *setting.Cfg, url strin
Cfg: cfg, Cfg: cfg,
Bus: bus, Bus: bus,
Live: newTestLive(t), Live: newTestLive(t),
Features: features,
QuotaService: &quota.QuotaService{Cfg: cfg}, QuotaService: &quota.QuotaService{Cfg: cfg},
RouteRegister: routing.NewRouteRegister(), RouteRegister: routing.NewRouteRegister(),
AccessControl: accesscontrolmock.New().WithPermissions(permissions), AccessControl: accesscontrolmock.New().WithPermissions(permissions),
@ -296,13 +298,25 @@ func setInitCtxSignedInOrgAdmin(initCtx *models.ReqContext) {
initCtx.SignedInUser = &models.SignedInUser{UserId: testUserID, OrgId: 1, OrgRole: models.ROLE_ADMIN, Login: testUserLogin} initCtx.SignedInUser = &models.SignedInUser{UserId: testUserID, OrgId: 1, OrgRole: models.ROLE_ADMIN, Login: testUserLogin}
} }
func setupSimpleHTTPServer(features *featuremgmt.FeatureManager) *HTTPServer {
if features == nil {
features = featuremgmt.WithFeatures()
}
cfg := setting.NewCfg()
cfg.IsFeatureToggleEnabled = features.IsEnabled
return &HTTPServer{
Cfg: cfg,
Features: features,
Bus: bus.GetBus(),
}
}
func setupHTTPServer(t *testing.T, useFakeAccessControl bool, enableAccessControl bool) accessControlScenarioContext { func setupHTTPServer(t *testing.T, useFakeAccessControl bool, enableAccessControl bool) accessControlScenarioContext {
// Use a new conf // Use a new conf
features := featuremgmt.WithFeatures("accesscontrol", enableAccessControl)
cfg := setting.NewCfg() cfg := setting.NewCfg()
cfg.FeatureToggles = make(map[string]bool) cfg.IsFeatureToggleEnabled = features.IsEnabled
if enableAccessControl {
cfg.FeatureToggles["accesscontrol"] = enableAccessControl
}
return setupHTTPServerWithCfg(t, useFakeAccessControl, enableAccessControl, cfg) return setupHTTPServerWithCfg(t, useFakeAccessControl, enableAccessControl, cfg)
} }
@ -310,6 +324,9 @@ func setupHTTPServer(t *testing.T, useFakeAccessControl bool, enableAccessContro
func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessControl bool, cfg *setting.Cfg) accessControlScenarioContext { func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessControl bool, cfg *setting.Cfg) accessControlScenarioContext {
t.Helper() t.Helper()
features := featuremgmt.WithFeatures("accesscontrol", enableAccessControl)
cfg.IsFeatureToggleEnabled = features.IsEnabled
var acmock *accesscontrolmock.Mock var acmock *accesscontrolmock.Mock
var ac *ossaccesscontrol.OSSAccessControlService var ac *ossaccesscontrol.OSSAccessControlService
@ -322,6 +339,7 @@ func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessCont
// Create minimal HTTP Server // Create minimal HTTP Server
hs := &HTTPServer{ hs := &HTTPServer{
Cfg: cfg, Cfg: cfg,
Features: features,
Bus: bus, Bus: bus,
Live: newTestLive(t), Live: newTestLive(t),
QuotaService: &quota.QuotaService{Cfg: cfg}, QuotaService: &quota.QuotaService{Cfg: cfg},
@ -338,7 +356,7 @@ func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessCont
} }
hs.AccessControl = acmock hs.AccessControl = acmock
} else { } else {
ac = ossaccesscontrol.ProvideService(cfg, &usagestats.UsageStatsMock{T: t}) ac = ossaccesscontrol.ProvideService(hs.Features.Toggles(), &usagestats.UsageStatsMock{T: t})
hs.AccessControl = ac hs.AccessControl = ac
// Perform role registration // Perform role registration
err := hs.declareFixedRoles() err := hs.declareFixedRoles()

View File

@ -20,6 +20,7 @@ import (
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/libraryelements" "github.com/grafana/grafana/pkg/services/libraryelements"
"github.com/grafana/grafana/pkg/services/live" "github.com/grafana/grafana/pkg/services/live"
"github.com/grafana/grafana/pkg/services/provisioning" "github.com/grafana/grafana/pkg/services/provisioning"
@ -88,8 +89,17 @@ type testState struct {
} }
func newTestLive(t *testing.T) *live.GrafanaLive { func newTestLive(t *testing.T) *live.GrafanaLive {
features := featuremgmt.WithToggles()
cfg := &setting.Cfg{AppURL: "http://localhost:3000/"} cfg := &setting.Cfg{AppURL: "http://localhost:3000/"}
gLive, err := live.ProvideService(nil, cfg, routing.NewRouteRegister(), nil, nil, nil, sqlstore.InitTestDB(t), nil, &usagestats.UsageStatsMock{T: t}, nil) cfg.IsFeatureToggleEnabled = features.IsEnabled
gLive, err := live.ProvideService(nil, cfg,
routing.NewRouteRegister(),
nil, nil, nil,
sqlstore.InitTestDB(t),
nil,
&usagestats.UsageStatsMock{T: t},
nil,
features)
require.NoError(t, err) require.NoError(t, err)
return gLive return gLive
} }

View File

@ -243,13 +243,12 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i
"env": setting.Env, "env": setting.Env,
}, },
"licenseInfo": map[string]interface{}{ "licenseInfo": map[string]interface{}{
"expiry": hs.License.Expiry(), "expiry": hs.License.Expiry(),
"stateInfo": hs.License.StateInfo(), "stateInfo": hs.License.StateInfo(),
"licenseUrl": hs.License.LicenseURL(hasAccess(accesscontrol.ReqGrafanaAdmin, accesscontrol.LicensingPageReaderAccess)), "licenseUrl": hs.License.LicenseURL(hasAccess(accesscontrol.ReqGrafanaAdmin, accesscontrol.LicensingPageReaderAccess)),
"edition": hs.License.Edition(), "edition": hs.License.Edition(),
"enabledFeatures": hs.License.EnabledFeatures(),
}, },
"featureToggles": hs.Cfg.FeatureToggles, "featureToggles": hs.Features.GetEnabled(c.Req.Context()),
"rendererAvailable": hs.RenderService.IsAvailable(), "rendererAvailable": hs.RenderService.IsAvailable(),
"rendererVersion": hs.RenderService.Version(), "rendererVersion": hs.RenderService.Version(),
"http2Enabled": hs.Cfg.Protocol == setting.HTTP2Scheme, "http2Enabled": hs.Cfg.Protocol == setting.HTTP2Scheme,

View File

@ -9,6 +9,7 @@ import (
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/licensing" "github.com/grafana/grafana/pkg/services/licensing"
"github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore"
@ -19,9 +20,10 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func setupTestEnvironment(t *testing.T, cfg *setting.Cfg) (*web.Mux, *HTTPServer) { func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features *featuremgmt.FeatureManager) (*web.Mux, *HTTPServer) {
t.Helper() t.Helper()
sqlstore.InitTestDB(t) sqlstore.InitTestDB(t)
cfg.IsFeatureToggleEnabled = features.IsEnabled
{ {
oldVersion := setting.BuildVersion oldVersion := setting.BuildVersion
@ -37,9 +39,10 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg) (*web.Mux, *HTTPServer
sqlStore := sqlstore.InitTestDB(t) sqlStore := sqlstore.InitTestDB(t)
hs := &HTTPServer{ hs := &HTTPServer{
Cfg: cfg, Cfg: cfg,
Bus: bus.GetBus(), Features: features,
License: &licensing.OSSLicensingService{Cfg: cfg}, Bus: bus.GetBus(),
License: &licensing.OSSLicensingService{Cfg: cfg},
RenderService: &rendering.RenderingService{ RenderService: &rendering.RenderingService{
Cfg: cfg, Cfg: cfg,
RendererPluginManager: &fakeRendererManager{}, RendererPluginManager: &fakeRendererManager{},
@ -73,7 +76,8 @@ func TestHTTPServer_GetFrontendSettings_hideVersionAnonymous(t *testing.T) {
cfg.Env = "testing" cfg.Env = "testing"
cfg.BuildVersion = "7.8.9" cfg.BuildVersion = "7.8.9"
cfg.BuildCommit = "01234567" cfg.BuildCommit = "01234567"
m, hs := setupTestEnvironment(t, cfg)
m, hs := setupTestEnvironment(t, cfg, featuremgmt.WithFeatures())
req := httptest.NewRequest(http.MethodGet, "/api/frontend/settings", nil) req := httptest.NewRequest(http.MethodGet, "/api/frontend/settings", nil)

View File

@ -13,6 +13,7 @@ import (
"strings" "strings"
"sync" "sync"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/query" "github.com/grafana/grafana/pkg/services/query"
"github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/services/serviceaccounts"
"github.com/grafana/grafana/pkg/services/thumbs" "github.com/grafana/grafana/pkg/services/thumbs"
@ -75,6 +76,7 @@ type HTTPServer struct {
Bus bus.Bus Bus bus.Bus
RenderService rendering.Service RenderService rendering.Service
Cfg *setting.Cfg Cfg *setting.Cfg
Features *featuremgmt.FeatureManager
SettingsProvider setting.Provider SettingsProvider setting.Provider
HooksService *hooks.HooksService HooksService *hooks.HooksService
CacheService *localcache.CacheService CacheService *localcache.CacheService
@ -135,7 +137,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
loginService login.Service, accessControl accesscontrol.AccessControl, loginService login.Service, accessControl accesscontrol.AccessControl,
dataSourceProxy *datasourceproxy.DataSourceProxyService, searchService *search.SearchService, dataSourceProxy *datasourceproxy.DataSourceProxyService, searchService *search.SearchService,
live *live.GrafanaLive, livePushGateway *pushhttp.Gateway, plugCtxProvider *plugincontext.Provider, live *live.GrafanaLive, livePushGateway *pushhttp.Gateway, plugCtxProvider *plugincontext.Provider,
contextHandler *contexthandler.ContextHandler, contextHandler *contexthandler.ContextHandler, features *featuremgmt.FeatureManager,
schemaService *schemaloader.SchemaLoaderService, alertNG *ngalert.AlertNG, schemaService *schemaloader.SchemaLoaderService, alertNG *ngalert.AlertNG,
libraryPanelService librarypanels.Service, libraryElementService libraryelements.Service, libraryPanelService librarypanels.Service, libraryElementService libraryelements.Service,
quotaService *quota.QuotaService, socialService social.Service, tracer tracing.Tracer, quotaService *quota.QuotaService, socialService social.Service, tracer tracing.Tracer,
@ -167,6 +169,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
AuthTokenService: userTokenService, AuthTokenService: userTokenService,
cleanUpService: cleanUpService, cleanUpService: cleanUpService,
ShortURLService: shortURLService, ShortURLService: shortURLService,
Features: features,
ThumbService: thumbService, ThumbService: thumbService,
RemoteCacheService: remoteCache, RemoteCacheService: remoteCache,
ProvisioningService: provisioningService, ProvisioningService: provisioningService,

View File

@ -85,7 +85,7 @@ func (hs *HTTPServer) getAppLinks(c *models.ReqContext) ([]*dtos.NavLink, error)
SortWeight: dtos.WeightPlugin, SortWeight: dtos.WeightPlugin,
} }
if hs.Cfg.IsNewNavigationEnabled() { if hs.Features.Toggles().IsNewNavigationEnabled() {
appLink.Section = dtos.NavSectionPlugin appLink.Section = dtos.NavSectionPlugin
} else { } else {
appLink.Section = dtos.NavSectionCore appLink.Section = dtos.NavSectionCore
@ -143,7 +143,7 @@ func (hs *HTTPServer) getAppLinks(c *models.ReqContext) ([]*dtos.NavLink, error)
} }
func enableServiceAccount(hs *HTTPServer, c *models.ReqContext) bool { func enableServiceAccount(hs *HTTPServer, c *models.ReqContext) bool {
return c.OrgRole == models.ROLE_ADMIN && hs.Cfg.IsServiceAccountEnabled() && hs.serviceAccountsService.Migrated(c.Req.Context(), c.OrgId) return c.OrgRole == models.ROLE_ADMIN && hs.Features.Toggles().IsServiceAccountsEnabled() && hs.serviceAccountsService.Migrated(c.Req.Context(), c.OrgId)
} }
func enableTeams(hs *HTTPServer, c *models.ReqContext) bool { func enableTeams(hs *HTTPServer, c *models.ReqContext) bool {
@ -154,7 +154,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
hasAccess := ac.HasAccess(hs.AccessControl, c) hasAccess := ac.HasAccess(hs.AccessControl, c)
navTree := []*dtos.NavLink{} navTree := []*dtos.NavLink{}
if hs.Cfg.IsNewNavigationEnabled() { if hs.Features.Toggles().IsNewNavigationEnabled() {
navTree = append(navTree, &dtos.NavLink{ navTree = append(navTree, &dtos.NavLink{
Text: "Home", Text: "Home",
Id: "home", Id: "home",
@ -165,7 +165,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
}) })
} }
if hasEditPerm && !hs.Cfg.IsNewNavigationEnabled() { if hasEditPerm && !hs.Features.Toggles().IsNewNavigationEnabled() {
children := hs.buildCreateNavLinks(c) children := hs.buildCreateNavLinks(c)
navTree = append(navTree, &dtos.NavLink{ navTree = append(navTree, &dtos.NavLink{
Text: "Create", Text: "Create",
@ -181,7 +181,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
dashboardChildLinks := hs.buildDashboardNavLinks(c, hasEditPerm) dashboardChildLinks := hs.buildDashboardNavLinks(c, hasEditPerm)
dashboardsUrl := "/" dashboardsUrl := "/"
if hs.Cfg.IsNewNavigationEnabled() { if hs.Features.Toggles().IsNewNavigationEnabled() {
dashboardsUrl = "/dashboards" dashboardsUrl = "/dashboards"
} }
@ -312,7 +312,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
}) })
} }
if hs.Cfg.FeatureToggles["live-pipeline"] { if hs.Features.Toggles().IsLivePipelineEnabled() {
liveNavLinks := []*dtos.NavLink{} liveNavLinks := []*dtos.NavLink{}
liveNavLinks = append(liveNavLinks, &dtos.NavLink{ liveNavLinks = append(liveNavLinks, &dtos.NavLink{
@ -346,7 +346,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
SortWeight: dtos.WeightConfig, SortWeight: dtos.WeightConfig,
Children: configNodes, Children: configNodes,
} }
if hs.Cfg.IsNewNavigationEnabled() { if hs.Features.Toggles().IsNewNavigationEnabled() {
configNode.Section = dtos.NavSectionConfig configNode.Section = dtos.NavSectionConfig
} else { } else {
configNode.Section = dtos.NavSectionCore configNode.Section = dtos.NavSectionCore
@ -358,7 +358,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
if len(adminNavLinks) > 0 { if len(adminNavLinks) > 0 {
navSection := dtos.NavSectionCore navSection := dtos.NavSectionCore
if hs.Cfg.IsNewNavigationEnabled() { if hs.Features.Toggles().IsNewNavigationEnabled() {
navSection = dtos.NavSectionConfig navSection = dtos.NavSectionConfig
} }
serverAdminNode := navlinks.GetServerAdminNode(adminNavLinks, navSection) serverAdminNode := navlinks.GetServerAdminNode(adminNavLinks, navSection)
@ -386,7 +386,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
func (hs *HTTPServer) buildDashboardNavLinks(c *models.ReqContext, hasEditPerm bool) []*dtos.NavLink { func (hs *HTTPServer) buildDashboardNavLinks(c *models.ReqContext, hasEditPerm bool) []*dtos.NavLink {
dashboardChildNavs := []*dtos.NavLink{} dashboardChildNavs := []*dtos.NavLink{}
if !hs.Cfg.IsNewNavigationEnabled() { if !hs.Features.Toggles().IsNewNavigationEnabled() {
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{ dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{
Text: "Home", Id: "home", Url: hs.Cfg.AppSubURL + "/", Icon: "home-alt", HideFromTabs: true, Text: "Home", Id: "home", Url: hs.Cfg.AppSubURL + "/", Icon: "home-alt", HideFromTabs: true,
}) })
@ -417,7 +417,7 @@ func (hs *HTTPServer) buildDashboardNavLinks(c *models.ReqContext, hasEditPerm b
}) })
} }
if hasEditPerm && hs.Cfg.IsNewNavigationEnabled() { if hasEditPerm && hs.Features.Toggles().IsNewNavigationEnabled() {
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{ dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{
Text: "Divider", Divider: true, Id: "divider", HideFromTabs: true, Text: "Divider", Divider: true, Id: "divider", HideFromTabs: true,
}) })
@ -622,7 +622,7 @@ func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewDat
LoadingLogo: "public/img/grafana_icon.svg", LoadingLogo: "public/img/grafana_icon.svg",
} }
if hs.Cfg.FeatureToggles["accesscontrol"] { if hs.Features.Toggles().IsAccesscontrolEnabled() {
userPermissions, err := hs.AccessControl.GetUserPermissions(c.Req.Context(), c.SignedInUser) userPermissions, err := hs.AccessControl.GetUserPermissions(c.Req.Context(), c.SignedInUser)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -89,7 +89,7 @@ func (hs *HTTPServer) CreateOrg(c *models.ReqContext) response.Response {
if err := web.Bind(c.Req, &cmd); err != nil { if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err) return response.Error(http.StatusBadRequest, "bad request data", err)
} }
acEnabled := hs.Cfg.FeatureToggles["accesscontrol"] acEnabled := hs.Features.Toggles().IsAccesscontrolEnabled()
if !acEnabled && !(setting.AllowUserOrgCreate || c.IsGrafanaAdmin) { if !acEnabled && !(setting.AllowUserOrgCreate || c.IsGrafanaAdmin) {
return response.Error(403, "Access denied", nil) return response.Error(403, "Access denied", nil)
} }

View File

@ -16,6 +16,7 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
@ -34,8 +35,8 @@ func setUpGetOrgUsersDB(t *testing.T, sqlStore *sqlstore.SQLStore) {
} }
func TestOrgUsersAPIEndpoint_userLoggedIn(t *testing.T) { func TestOrgUsersAPIEndpoint_userLoggedIn(t *testing.T) {
settings := setting.NewCfg() hs := setupSimpleHTTPServer(featuremgmt.WithFeatures())
hs := &HTTPServer{Cfg: settings} settings := hs.Cfg
sqlStore := sqlstore.InitTestDB(t) sqlStore := sqlstore.InitTestDB(t)
sqlStore.Cfg = settings sqlStore.Cfg = settings

View File

@ -5,7 +5,7 @@ import (
"strings" "strings"
"github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/web" "github.com/grafana/grafana/pkg/web"
) )
@ -52,8 +52,8 @@ type RouteRegister interface {
type RegisterNamedMiddleware func(name string) web.Handler type RegisterNamedMiddleware func(name string) web.Handler
func ProvideRegister(cfg *setting.Cfg) *RouteRegisterImpl { func ProvideRegister(features *featuremgmt.FeatureToggles) *RouteRegisterImpl {
return NewRouteRegister(middleware.ProvideRouteOperationName, middleware.RequestMetrics(cfg)) return NewRouteRegister(middleware.ProvideRouteOperationName, middleware.RequestMetrics(features))
} }
// NewRouteRegister creates a new RouteRegister with all middlewares sent as params // NewRouteRegister creates a new RouteRegister with all middlewares sent as params

View File

@ -20,7 +20,7 @@ func (hs *HTTPServer) CreateTeam(c *models.ReqContext) response.Response {
if err := web.Bind(c.Req, &cmd); err != nil { if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err) return response.Error(http.StatusBadRequest, "bad request data", err)
} }
accessControlEnabled := hs.Cfg.FeatureToggles["accesscontrol"] accessControlEnabled := hs.Features.Toggles().IsAccesscontrolEnabled()
if !accessControlEnabled && c.OrgRole == models.ROLE_VIEWER { if !accessControlEnabled && c.OrgRole == models.ROLE_VIEWER {
return response.Error(403, "Not allowed to create team.", nil) return response.Error(403, "Not allowed to create team.", nil)
} }

View File

@ -40,9 +40,7 @@ func TestTeamAPIEndpoint(t *testing.T) {
TotalCount: 2, TotalCount: 2,
} }
hs := &HTTPServer{ hs := setupSimpleHTTPServer(nil)
Cfg: setting.NewCfg(),
}
loggedInUserScenario(t, "When calling GET on", "/api/teams/search", "/api/teams/search", func(sc *scenarioContext) { loggedInUserScenario(t, "When calling GET on", "/api/teams/search", "/api/teams/search", func(sc *scenarioContext) {
var sentLimit int var sentLimit int
@ -92,10 +90,7 @@ func TestTeamAPIEndpoint(t *testing.T) {
t.Run("When creating team with API key", func(t *testing.T) { t.Run("When creating team with API key", func(t *testing.T) {
defer bus.ClearBusHandlers() defer bus.ClearBusHandlers()
hs := &HTTPServer{ hs := setupSimpleHTTPServer(nil)
Cfg: setting.NewCfg(),
Bus: bus.GetBus(),
}
hs.Cfg.EditorsCanAdmin = true hs.Cfg.EditorsCanAdmin = true
teamName := "team foo" teamName := "team foo"

View File

@ -9,6 +9,7 @@ import (
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/metrics/metricutil" "github.com/grafana/grafana/pkg/infra/metrics/metricutil"
"github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/mwitkow/go-conntrack" "github.com/mwitkow/go-conntrack"
) )
@ -16,7 +17,7 @@ import (
var newProviderFunc = sdkhttpclient.NewProvider var newProviderFunc = sdkhttpclient.NewProvider
// New creates a new HTTP client provider with pre-configured middlewares. // New creates a new HTTP client provider with pre-configured middlewares.
func New(cfg *setting.Cfg, tracer tracing.Tracer) *sdkhttpclient.Provider { func New(cfg *setting.Cfg, tracer tracing.Tracer, features *featuremgmt.FeatureToggles) *sdkhttpclient.Provider {
logger := log.New("httpclient") logger := log.New("httpclient")
userAgent := fmt.Sprintf("Grafana/%s", cfg.BuildVersion) userAgent := fmt.Sprintf("Grafana/%s", cfg.BuildVersion)
@ -35,7 +36,7 @@ func New(cfg *setting.Cfg, tracer tracing.Tracer) *sdkhttpclient.Provider {
setDefaultTimeoutOptions(cfg) setDefaultTimeoutOptions(cfg)
if cfg.FeatureToggles["httpclientprovider_azure_auth"] { if features.IsHttpclientproviderAzureAuthEnabled() {
middlewares = append(middlewares, AzureMiddleware(cfg)) middlewares = append(middlewares, AzureMiddleware(cfg))
} }

View File

@ -5,6 +5,7 @@ import (
sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -22,7 +23,7 @@ func TestHTTPClientProvider(t *testing.T) {
}) })
tracer, err := tracing.InitializeTracerForTest() tracer, err := tracing.InitializeTracerForTest()
require.NoError(t, err) require.NoError(t, err)
_ = New(&setting.Cfg{SigV4AuthEnabled: false}, tracer) _ = New(&setting.Cfg{SigV4AuthEnabled: false}, tracer, featuremgmt.WithToggles())
require.Len(t, providerOpts, 1) require.Len(t, providerOpts, 1)
o := providerOpts[0] o := providerOpts[0]
require.Len(t, o.Middlewares, 6) require.Len(t, o.Middlewares, 6)
@ -46,7 +47,7 @@ func TestHTTPClientProvider(t *testing.T) {
}) })
tracer, err := tracing.InitializeTracerForTest() tracer, err := tracing.InitializeTracerForTest()
require.NoError(t, err) require.NoError(t, err)
_ = New(&setting.Cfg{SigV4AuthEnabled: true}, tracer) _ = New(&setting.Cfg{SigV4AuthEnabled: true}, tracer, featuremgmt.WithToggles())
require.Len(t, providerOpts, 1) require.Len(t, providerOpts, 1)
o := providerOpts[0] o := providerOpts[0]
require.Len(t, o.Middlewares, 7) require.Len(t, o.Middlewares, 7)

View File

@ -7,7 +7,7 @@ import (
"time" "time"
"github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/web" "github.com/grafana/grafana/pkg/web"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
cw "github.com/weaveworks/common/tracing" cw "github.com/weaveworks/common/tracing"
@ -45,7 +45,7 @@ func init() {
} }
// RequestMetrics is a middleware handler that instruments the request. // RequestMetrics is a middleware handler that instruments the request.
func RequestMetrics(cfg *setting.Cfg) func(handler string) web.Handler { func RequestMetrics(features *featuremgmt.FeatureToggles) func(handler string) web.Handler {
return func(handler string) web.Handler { return func(handler string) web.Handler {
return func(res http.ResponseWriter, req *http.Request, c *web.Context) { return func(res http.ResponseWriter, req *http.Request, c *web.Context) {
rw := res.(web.ResponseWriter) rw := res.(web.ResponseWriter)
@ -60,7 +60,7 @@ func RequestMetrics(cfg *setting.Cfg) func(handler string) web.Handler {
method := sanitizeMethod(req.Method) method := sanitizeMethod(req.Method)
// enable histogram and disable summaries + counters for http requests. // enable histogram and disable summaries + counters for http requests.
if cfg.IsHTTPRequestHistogramDisabled() { if features.IsDisableHttpRequestHistogramEnabled() {
duration := time.Since(now).Nanoseconds() / int64(time.Millisecond) duration := time.Since(now).Nanoseconds() / int64(time.Millisecond)
metrics.MHttpRequestTotal.WithLabelValues(handler, code, method).Inc() metrics.MHttpRequestTotal.WithLabelValues(handler, code, method).Inc()
metrics.MHttpRequestSummary.WithLabelValues(handler, code, method).Observe(float64(duration)) metrics.MHttpRequestSummary.WithLabelValues(handler, code, method).Observe(float64(duration))

View File

@ -14,8 +14,6 @@ type Licensing interface {
StateInfo() string StateInfo() string
EnabledFeatures() map[string]bool
FeatureEnabled(feature string) bool FeatureEnabled(feature string) bool
} }

View File

@ -85,7 +85,6 @@ func pluginScenario(t *testing.T, desc string, fn func(*testing.T, *PluginManage
t.Run("Given a plugin", func(t *testing.T) { t.Run("Given a plugin", func(t *testing.T) {
cfg := &setting.Cfg{ cfg := &setting.Cfg{
FeatureToggles: map[string]bool{},
PluginSettings: setting.PluginSettings{ PluginSettings: setting.PluginSettings{
"test-app": map[string]string{ "test-app": map[string]string{
"path": "testdata/test-app", "path": "testdata/test-app",

View File

@ -18,7 +18,6 @@ import (
func TestGetPluginDashboards(t *testing.T) { func TestGetPluginDashboards(t *testing.T) {
cfg := &setting.Cfg{ cfg := &setting.Cfg{
FeatureToggles: map[string]bool{},
PluginSettings: setting.PluginSettings{ PluginSettings: setting.PluginSettings{
"test-app": map[string]string{ "test-app": map[string]string{
"path": "testdata/test-app", "path": "testdata/test-app",

View File

@ -17,6 +17,7 @@ import (
"github.com/grafana/grafana/pkg/plugins/backendplugin/provider" "github.com/grafana/grafana/pkg/plugins/backendplugin/provider"
"github.com/grafana/grafana/pkg/plugins/manager/loader" "github.com/grafana/grafana/pkg/plugins/manager/loader"
"github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/plugins/manager/signature"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/licensing" "github.com/grafana/grafana/pkg/services/licensing"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb/azuremonitor" "github.com/grafana/grafana/pkg/tsdb/azuremonitor"
@ -50,11 +51,13 @@ func TestPluginManager_int_init(t *testing.T) {
bundledPluginsPath, err := filepath.Abs("../../../plugins-bundled/internal") bundledPluginsPath, err := filepath.Abs("../../../plugins-bundled/internal")
require.NoError(t, err) require.NoError(t, err)
features := featuremgmt.WithToggles()
cfg := &setting.Cfg{ cfg := &setting.Cfg{
Raw: ini.Empty(), Raw: ini.Empty(),
Env: setting.Prod, Env: setting.Prod,
StaticRootPath: staticRootPath, StaticRootPath: staticRootPath,
BundledPluginsPath: bundledPluginsPath, BundledPluginsPath: bundledPluginsPath,
IsFeatureToggleEnabled: features.IsEnabled,
PluginSettings: map[string]map[string]string{ PluginSettings: map[string]map[string]string{
"plugin.datasource-id": { "plugin.datasource-id": {
"path": "testdata/test-app", "path": "testdata/test-app",
@ -79,7 +82,7 @@ func TestPluginManager_int_init(t *testing.T) {
otsdb := opentsdb.ProvideService(hcp) otsdb := opentsdb.ProvideService(hcp)
pr := prometheus.ProvideService(hcp, tracer) pr := prometheus.ProvideService(hcp, tracer)
tmpo := tempo.ProvideService(hcp) tmpo := tempo.ProvideService(hcp)
td := testdatasource.ProvideService(cfg) td := testdatasource.ProvideService(cfg, features)
pg := postgres.ProvideService(cfg) pg := postgres.ProvideService(cfg)
my := mysql.ProvideService(cfg, hcp) my := mysql.ProvideService(cfg, hcp)
ms := mssql.ProvideService(cfg) ms := mssql.ProvideService(cfg)

View File

@ -35,6 +35,7 @@ import (
"github.com/grafana/grafana/pkg/services/dashboardsnapshots" "github.com/grafana/grafana/pkg/services/dashboardsnapshots"
"github.com/grafana/grafana/pkg/services/datasourceproxy" "github.com/grafana/grafana/pkg/services/datasourceproxy"
"github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/hooks" "github.com/grafana/grafana/pkg/services/hooks"
"github.com/grafana/grafana/pkg/services/libraryelements" "github.com/grafana/grafana/pkg/services/libraryelements"
"github.com/grafana/grafana/pkg/services/librarypanels" "github.com/grafana/grafana/pkg/services/librarypanels"
@ -182,6 +183,8 @@ var wireBasicSet = wire.NewSet(
wire.Bind(new(teamguardian.Store), new(*teamguardianDatabase.TeamGuardianStoreImpl)), wire.Bind(new(teamguardian.Store), new(*teamguardianDatabase.TeamGuardianStoreImpl)),
teamguardianManager.ProvideService, teamguardianManager.ProvideService,
wire.Bind(new(teamguardian.TeamGuardian), new(*teamguardianManager.Service)), wire.Bind(new(teamguardian.TeamGuardian), new(*teamguardianManager.Service)),
featuremgmt.ProvideManagerService,
featuremgmt.ProvideToggles,
) )
var wireSet = wire.NewSet( var wireSet = wire.NewSet(

View File

@ -9,13 +9,13 @@ import (
"github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
) )
func ProvideService(cfg *setting.Cfg, usageStats usagestats.Service) *OSSAccessControlService { func ProvideService(features *featuremgmt.FeatureToggles, usageStats usagestats.Service) *OSSAccessControlService {
s := &OSSAccessControlService{ s := &OSSAccessControlService{
Cfg: cfg, features: features,
UsageStats: usageStats, UsageStats: usageStats,
Log: log.New("accesscontrol"), Log: log.New("accesscontrol"),
ScopeResolver: accesscontrol.NewScopeResolver(), ScopeResolver: accesscontrol.NewScopeResolver(),
@ -26,7 +26,7 @@ func ProvideService(cfg *setting.Cfg, usageStats usagestats.Service) *OSSAccessC
// OSSAccessControlService is the service implementing role based access control. // OSSAccessControlService is the service implementing role based access control.
type OSSAccessControlService struct { type OSSAccessControlService struct {
Cfg *setting.Cfg features *featuremgmt.FeatureToggles
UsageStats usagestats.Service UsageStats usagestats.Service
Log log.Logger Log log.Logger
registrations accesscontrol.RegistrationList registrations accesscontrol.RegistrationList
@ -34,10 +34,10 @@ type OSSAccessControlService struct {
} }
func (ac *OSSAccessControlService) IsDisabled() bool { func (ac *OSSAccessControlService) IsDisabled() bool {
if ac.Cfg == nil { if ac.features == nil {
return true return true
} }
return !ac.Cfg.FeatureToggles["accesscontrol"] return !ac.features.IsAccesscontrolEnabled()
} }
func (ac *OSSAccessControlService) registerUsageMetrics() { func (ac *OSSAccessControlService) registerUsageMetrics() {

View File

@ -12,17 +12,14 @@ import (
"github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/services/featuremgmt"
) )
func setupTestEnv(t testing.TB) *OSSAccessControlService { func setupTestEnv(t testing.TB) *OSSAccessControlService {
t.Helper() t.Helper()
cfg := setting.NewCfg()
cfg.FeatureToggles = map[string]bool{"accesscontrol": true}
ac := &OSSAccessControlService{ ac := &OSSAccessControlService{
Cfg: cfg, features: featuremgmt.WithToggles("accesscontrol"),
UsageStats: &usagestats.UsageStatsMock{T: t}, UsageStats: &usagestats.UsageStatsMock{T: t},
Log: log.New("accesscontrol"), Log: log.New("accesscontrol"),
registrations: accesscontrol.RegistrationList{}, registrations: accesscontrol.RegistrationList{},
@ -148,12 +145,9 @@ func TestUsageMetrics(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
cfg := setting.NewCfg() features := featuremgmt.WithToggles("accesscontrol", tt.enabled)
if tt.enabled {
cfg.FeatureToggles = map[string]bool{"accesscontrol": true}
}
s := ProvideService(cfg, &usagestats.UsageStatsMock{T: t}) s := ProvideService(features, &usagestats.UsageStatsMock{T: t})
report, err := s.UsageStats.GetUsageReport(context.Background()) report, err := s.UsageStats.GetUsageReport(context.Background())
assert.Nil(t, err) assert.Nil(t, err)
@ -267,7 +261,7 @@ func TestOSSAccessControlService_RegisterFixedRole(t *testing.T) {
for _, tc := range tests { for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
ac := &OSSAccessControlService{ ac := &OSSAccessControlService{
Cfg: setting.NewCfg(), features: featuremgmt.WithToggles(),
UsageStats: &usagestats.UsageStatsMock{T: t}, UsageStats: &usagestats.UsageStatsMock{T: t},
Log: log.New("accesscontrol-test"), Log: log.New("accesscontrol-test"),
} }
@ -386,12 +380,11 @@ func TestOSSAccessControlService_DeclareFixedRoles(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
ac := &OSSAccessControlService{ ac := &OSSAccessControlService{
Cfg: setting.NewCfg(), features: featuremgmt.WithToggles("accesscontrol"),
UsageStats: &usagestats.UsageStatsMock{T: t}, UsageStats: &usagestats.UsageStatsMock{T: t},
Log: log.New("accesscontrol-test"), Log: log.New("accesscontrol-test"),
registrations: accesscontrol.RegistrationList{}, registrations: accesscontrol.RegistrationList{},
} }
ac.Cfg.FeatureToggles = map[string]bool{"accesscontrol": true}
// Test // Test
err := ac.DeclareFixedRoles(tt.registrations...) err := ac.DeclareFixedRoles(tt.registrations...)
@ -459,9 +452,6 @@ func TestOSSAccessControlService_RegisterFixedRoles(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
cfg := setting.NewCfg()
cfg.FeatureToggles = map[string]bool{"accesscontrol": true}
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
// Remove any inserted role after the test case has been run // Remove any inserted role after the test case has been run
t.Cleanup(func() { t.Cleanup(func() {
@ -472,12 +462,11 @@ func TestOSSAccessControlService_RegisterFixedRoles(t *testing.T) {
// Setup // Setup
ac := &OSSAccessControlService{ ac := &OSSAccessControlService{
Cfg: setting.NewCfg(), features: featuremgmt.WithToggles("accesscontrol"),
UsageStats: &usagestats.UsageStatsMock{T: t}, UsageStats: &usagestats.UsageStatsMock{T: t},
Log: log.New("accesscontrol-test"), Log: log.New("accesscontrol-test"),
registrations: accesscontrol.RegistrationList{}, registrations: accesscontrol.RegistrationList{},
} }
ac.Cfg.FeatureToggles = map[string]bool{"accesscontrol": true}
ac.registrations.Append(tt.registrations...) ac.registrations.Append(tt.registrations...)
// Test // Test
@ -552,7 +541,7 @@ func TestOSSAccessControlService_GetUserPermissions(t *testing.T) {
// Setup // Setup
ac := setupTestEnv(t) ac := setupTestEnv(t)
ac.Cfg.FeatureToggles = map[string]bool{"accesscontrol": true} ac.features = featuremgmt.WithToggles("accesscontrol")
registration.Role.Permissions = []accesscontrol.Permission{tt.rawPerm} registration.Role.Permissions = []accesscontrol.Permission{tt.rawPerm}
err := ac.DeclareFixedRoles(registration) err := ac.DeclareFixedRoles(registration)
@ -638,7 +627,6 @@ func TestOSSAccessControlService_Evaluate(t *testing.T) {
// Setup // Setup
ac := setupTestEnv(t) ac := setupTestEnv(t)
ac.Cfg.FeatureToggles = map[string]bool{"accesscontrol": true}
ac.RegisterAttributeScopeResolver("users:login:", userLoginScopeSolver) ac.RegisterAttributeScopeResolver("users:login:", userLoginScopeSolver)
registration.Role.Permissions = []accesscontrol.Permission{tt.rawPerm} registration.Role.Permissions = []accesscontrol.Permission{tt.rawPerm}

View File

@ -0,0 +1,95 @@
package featuremgmt
import (
"bytes"
"encoding/json"
)
// FeatureToggleState indicates the quality level
type FeatureToggleState int
const (
// FeatureStateUnknown indicates that no state is specified
FeatureStateUnknown FeatureToggleState = iota
// FeatureStateAlpha the feature is in active development and may change at any time
FeatureStateAlpha
// FeatureStateBeta the feature is still in development, but settings will have migrations
FeatureStateBeta
// FeatureStateStable this is a stable feature
FeatureStateStable
// FeatureStateDeprecated the feature will be removed in the future
FeatureStateDeprecated
)
func (s FeatureToggleState) String() string {
switch s {
case FeatureStateAlpha:
return "alpha"
case FeatureStateBeta:
return "beta"
case FeatureStateStable:
return "stable"
case FeatureStateDeprecated:
return "deprecated"
case FeatureStateUnknown:
}
return "unknown"
}
// MarshalJSON marshals the enum as a quoted json string
func (s FeatureToggleState) MarshalJSON() ([]byte, error) {
buffer := bytes.NewBufferString(`"`)
buffer.WriteString(s.String())
buffer.WriteString(`"`)
return buffer.Bytes(), nil
}
// UnmarshalJSON unmarshals a quoted json string to the enum value
func (s *FeatureToggleState) UnmarshalJSON(b []byte) error {
var j string
err := json.Unmarshal(b, &j)
if err != nil {
return err
}
switch j {
case "alpha":
*s = FeatureStateAlpha
case "beta":
*s = FeatureStateBeta
case "stable":
*s = FeatureStateStable
case "deprecated":
*s = FeatureStateDeprecated
default:
*s = FeatureStateUnknown
}
return nil
}
type FeatureFlag struct {
Name string `json:"name" yaml:"name"` // Unique name
Description string `json:"description"`
State FeatureToggleState `json:"state,omitempty"`
DocsURL string `json:"docsURL,omitempty"`
// CEL-GO expression. Using the value "true" will mean this is on by default
Expression string `json:"expression,omitempty"`
// Special behavior flags
RequiresDevMode bool `json:"requiresDevMode,omitempty"` // can not be enabled in production
RequiresRestart bool `json:"requiresRestart,omitempty"` // The server must be initialized with the value
RequiresLicense bool `json:"requiresLicense,omitempty"` // Must be enabled in the license
FrontendOnly bool `json:"frontend,omitempty"` // change is only seen in the frontend
// Internal properties
// expr string `json:-`
}

View File

@ -0,0 +1,195 @@
package featuremgmt
import (
"context"
"fmt"
"reflect"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/models"
)
type FeatureManager struct {
isDevMod bool
licensing models.Licensing
flags map[string]*FeatureFlag
enabled map[string]bool // only the "on" values
toggles *FeatureToggles
config string // path to config file
vars map[string]interface{}
log log.Logger
}
// This will merge the flags with the current configuration
func (fm *FeatureManager) registerFlags(flags ...FeatureFlag) {
for idx, add := range flags {
if add.Name == "" {
continue // skip it with warning?
}
flag, ok := fm.flags[add.Name]
if !ok {
fm.flags[add.Name] = &flags[idx]
continue
}
// Selectively update properties
if add.Description != "" {
flag.Description = add.Description
}
if add.DocsURL != "" {
flag.DocsURL = add.DocsURL
}
if add.Expression != "" {
flag.Expression = add.Expression
}
// The most recently defined state
if add.State != FeatureStateUnknown {
flag.State = add.State
}
// Only gets more restrictive
if add.RequiresDevMode {
flag.RequiresDevMode = true
}
if add.RequiresLicense {
flag.RequiresLicense = true
}
if add.RequiresRestart {
flag.RequiresRestart = true
}
}
// This will evaluate all flags
fm.update()
}
func (fm *FeatureManager) evaluate(ff *FeatureFlag) bool {
if ff.RequiresDevMode && !fm.isDevMod {
return false
}
if ff.RequiresLicense && (fm.licensing == nil || !fm.licensing.FeatureEnabled(ff.Name)) {
return false
}
// TODO: CEL - expression
return ff.Expression == "true"
}
// Update
func (fm *FeatureManager) update() {
enabled := make(map[string]bool)
for _, flag := range fm.flags {
val := fm.evaluate(flag)
// Update the registry
track := 0.0
if val {
track = 1
enabled[flag.Name] = true
}
// Register value with prometheus metric
featureToggleInfo.WithLabelValues(flag.Name).Set(track)
}
fm.enabled = enabled
}
// Run is called by background services
func (fm *FeatureManager) readFile() error {
if fm.config == "" {
return nil // not configured
}
cfg, err := readConfigFile(fm.config)
if err != nil {
return err
}
fm.registerFlags(cfg.Flags...)
fm.vars = cfg.Vars
return nil
}
// IsEnabled checks if a feature is enabled
func (fm *FeatureManager) IsEnabled(flag string) bool {
return fm.enabled[flag]
}
// GetEnabled returns a map contaning only the features that are enabled
func (fm *FeatureManager) GetEnabled(ctx context.Context) map[string]bool {
enabled := make(map[string]bool, len(fm.enabled))
for key, val := range fm.enabled {
if val {
enabled[key] = true
}
}
return enabled
}
// Toggles returns FeatureToggles.
func (fm *FeatureManager) Toggles() *FeatureToggles {
if fm.toggles == nil {
fm.toggles = &FeatureToggles{manager: fm}
}
return fm.toggles
}
// GetFlags returns all flag definitions
func (fm *FeatureManager) GetFlags() []FeatureFlag {
v := make([]FeatureFlag, 0, len(fm.flags))
for _, value := range fm.flags {
v = append(v, *value)
}
return v
}
func (fm *FeatureManager) HandleGetSettings(c *models.ReqContext) {
res := make(map[string]interface{}, 3)
res["enabled"] = fm.GetEnabled(c.Req.Context())
vv := make([]*FeatureFlag, 0, len(fm.flags))
for _, v := range fm.flags {
vv = append(vv, v)
}
res["info"] = vv
response.JSON(200, res).WriteTo(c)
}
// WithFeatures is used to define feature toggles for testing.
// The arguments are a list of strings that are optionally followed by a boolean value
func WithFeatures(spec ...interface{}) *FeatureManager {
count := len(spec)
enabled := make(map[string]bool, count)
idx := 0
for idx < count {
key := fmt.Sprintf("%v", spec[idx])
val := true
idx++
if idx < count && reflect.TypeOf(spec[idx]).Kind() == reflect.Bool {
val = spec[idx].(bool)
idx++
}
if val {
enabled[key] = true
}
}
return &FeatureManager{enabled: enabled}
}
func WithToggles(spec ...interface{}) *FeatureToggles {
return &FeatureToggles{
manager: WithFeatures(spec...),
}
}

View File

@ -0,0 +1,77 @@
package featuremgmt
import (
"context"
"testing"
"github.com/stretchr/testify/require"
)
func TestFeatureManager(t *testing.T) {
t.Run("check testing stubs", func(t *testing.T) {
ft := WithFeatures("a", "b", "c")
require.True(t, ft.IsEnabled("a"))
require.True(t, ft.IsEnabled("b"))
require.True(t, ft.IsEnabled("c"))
require.False(t, ft.IsEnabled("d"))
require.Equal(t, map[string]bool{"a": true, "b": true, "c": true}, ft.GetEnabled(context.Background()))
// Explicit values
ft = WithFeatures("a", true, "b", false)
require.True(t, ft.IsEnabled("a"))
require.False(t, ft.IsEnabled("b"))
require.Equal(t, map[string]bool{"a": true}, ft.GetEnabled(context.Background()))
})
t.Run("check license validation", func(t *testing.T) {
ft := FeatureManager{
flags: map[string]*FeatureFlag{},
}
ft.registerFlags(FeatureFlag{
Name: "a",
RequiresLicense: true,
RequiresDevMode: true,
Expression: "true",
}, FeatureFlag{
Name: "b",
Expression: "true",
})
require.False(t, ft.IsEnabled("a"))
require.True(t, ft.IsEnabled("b"))
require.False(t, ft.IsEnabled("c")) // uknown flag
// Try changing "requires license"
ft.registerFlags(FeatureFlag{
Name: "a",
RequiresLicense: false, // shuld still require license!
}, FeatureFlag{
Name: "b",
RequiresLicense: true, // expression is still "true"
})
require.False(t, ft.IsEnabled("a"))
require.False(t, ft.IsEnabled("b"))
require.False(t, ft.IsEnabled("c"))
})
t.Run("check description and docs configs", func(t *testing.T) {
ft := FeatureManager{
flags: map[string]*FeatureFlag{},
}
ft.registerFlags(FeatureFlag{
Name: "a",
Description: "first",
}, FeatureFlag{
Name: "a",
Description: "second",
}, FeatureFlag{
Name: "a",
DocsURL: "http://something",
}, FeatureFlag{
Name: "a",
})
flag := ft.flags["a"]
require.Equal(t, "second", flag.Description)
require.Equal(t, "http://something", flag.DocsURL)
})
}

View File

@ -0,0 +1,163 @@
package featuremgmt
import "github.com/grafana/grafana/pkg/services/secrets"
var (
FLAG_database_metrics = "database_metrics"
FLAG_live_config = "live-config"
FLAG_recordedQueries = "recordedQueries"
// Register each toggle here
standardFeatureFlags = []FeatureFlag{
{
Name: FLAG_recordedQueries,
Description: "Supports saving queries that can be scraped by prometheus",
State: FeatureStateBeta,
RequiresLicense: true,
},
{
Name: "teamsync",
Description: "Team sync lets you set up synchronization between your auth providers teams and teams in Grafana",
State: FeatureStateStable,
DocsURL: "https://grafana.com/docs/grafana/latest/enterprise/team-sync/",
RequiresLicense: true,
},
{
Name: "ldapsync",
Description: "Enhanced LDAP integration",
State: FeatureStateStable,
DocsURL: "https://grafana.com/docs/grafana/latest/enterprise/enhanced_ldap/",
RequiresLicense: true,
},
{
Name: "caching",
Description: "Temporarily store data source query results.",
State: FeatureStateStable,
DocsURL: "https://grafana.com/docs/grafana/latest/enterprise/query-caching/",
RequiresLicense: true,
},
{
Name: "dspermissions",
Description: "Data source permissions",
State: FeatureStateStable,
DocsURL: "https://grafana.com/docs/grafana/latest/enterprise/datasource_permissions/",
RequiresLicense: true,
},
{
Name: "analytics",
Description: "Analytics",
State: FeatureStateStable,
RequiresLicense: true,
},
{
Name: "enterprise.plugins",
Description: "Enterprise plugins",
State: FeatureStateStable,
DocsURL: "https://grafana.com/grafana/plugins/?enterprise=1",
RequiresLicense: true,
},
{
Name: "trimDefaults",
Description: "Use cue schema to remove values that will be applied automatically",
State: FeatureStateBeta,
},
{
Name: secrets.EnvelopeEncryptionFeatureToggle,
Description: "encrypt secrets",
State: FeatureStateBeta,
},
{
Name: "httpclientprovider_azure_auth",
State: FeatureStateBeta,
},
{
Name: "service-accounts",
Description: "support service accounts",
State: FeatureStateBeta,
RequiresLicense: true,
},
{
Name: FLAG_database_metrics,
Description: "Add prometheus metrics for database tables",
State: FeatureStateStable,
},
{
Name: "dashboardPreviews",
Description: "Create and show thumbnails for dashboard search results",
State: FeatureStateAlpha,
},
{
Name: FLAG_live_config,
Description: "Save grafana live configuration in SQL tables",
State: FeatureStateAlpha,
},
{
Name: "live-pipeline",
Description: "enable a generic live processing pipeline",
State: FeatureStateAlpha,
},
{
Name: "live-service-web-worker",
Description: "This will use a webworker thread to processes events rather than the main thread",
State: FeatureStateAlpha,
FrontendOnly: true,
},
{
Name: "queryOverLive",
Description: "Use grafana live websocket to execute backend queries",
State: FeatureStateAlpha,
FrontendOnly: true,
},
{
Name: "tempoSearch",
Description: "Enable searching in tempo datasources",
State: FeatureStateBeta,
FrontendOnly: true,
},
{
Name: "tempoBackendSearch",
Description: "Use backend for tempo search",
State: FeatureStateBeta,
},
{
Name: "tempoServiceGraph",
Description: "show service",
State: FeatureStateBeta,
FrontendOnly: true,
},
{
Name: "fullRangeLogsVolume",
Description: "Show full range logs volume in expore",
State: FeatureStateBeta,
FrontendOnly: true,
},
{
Name: "accesscontrol",
Description: "Support robust access control",
State: FeatureStateBeta,
RequiresLicense: true,
},
{
Name: "prometheus_azure_auth",
Description: "Use azure authentication for prometheus datasource",
State: FeatureStateBeta,
},
{
Name: "newNavigation",
Description: "Try the next gen naviation model",
State: FeatureStateAlpha,
},
{
Name: "showFeatureFlagsInUI",
Description: "Show feature flags in the settings UI",
State: FeatureStateAlpha,
RequiresDevMode: true,
},
{
Name: "disable_http_request_histogram",
State: FeatureStateAlpha,
},
}
)

View File

@ -0,0 +1,78 @@
package featuremgmt
import (
"fmt"
"os"
"path/filepath"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var (
// The values are updated each time
featureToggleInfo = promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "feature_toggles_info",
Help: "info metric that exposes what feature toggles are enabled or not",
Namespace: "grafana",
}, []string{"name"})
)
func ProvideManagerService(cfg *setting.Cfg, licensing models.Licensing) (*FeatureManager, error) {
mgmt := &FeatureManager{
isDevMod: setting.Env != setting.Prod,
licensing: licensing,
flags: make(map[string]*FeatureFlag, 30),
enabled: make(map[string]bool),
log: log.New("featuremgmt"),
}
// Register the standard flags
mgmt.registerFlags(standardFeatureFlags...)
// Load the flags from `custom.ini` files
flags, err := setting.ReadFeatureTogglesFromInitFile(cfg.Raw.Section("feature_toggles"))
if err != nil {
return mgmt, err
}
for key, val := range flags {
flag, ok := mgmt.flags[key]
if !ok {
flag = &FeatureFlag{
Name: key,
State: FeatureStateUnknown,
}
mgmt.flags[key] = flag
}
flag.Expression = fmt.Sprintf("%t", val) // true | false
}
// Load config settings
configfile := filepath.Join(cfg.HomePath, "conf", "features.yaml")
if _, err := os.Stat(configfile); err == nil {
mgmt.log.Info("[experimental] loading features from config file", "path", configfile)
mgmt.config = configfile
err = mgmt.readFile()
if err != nil {
return mgmt, err
}
}
// update the values
mgmt.update()
// Minimum approach to avoid circular dependency
cfg.IsFeatureToggleEnabled = mgmt.IsEnabled
return mgmt, nil
}
// ProvideToggles allows read-only access to the feature state
func ProvideToggles(mgmt *FeatureManager) *FeatureToggles {
return &FeatureToggles{
manager: mgmt,
}
}

View File

@ -0,0 +1,34 @@
package featuremgmt
import (
"io/ioutil"
"gopkg.in/yaml.v2"
)
type configBody struct {
// define variables that can be used in expressions
Vars map[string]interface{} `yaml:"vars"`
// Define and override feature flag properties
Flags []FeatureFlag `yaml:"flags"`
// keep track of where the fie was loaded from
filename string
}
// will read a single configfile
func readConfigFile(filename string) (*configBody, error) {
cfg := &configBody{}
// Can ignore gosec G304 because the file path is forced within config subfolder
//nolint:gosec
yamlFile, err := ioutil.ReadFile(filename)
if err != nil {
return cfg, err
}
err = yaml.Unmarshal(yamlFile, cfg)
cfg.filename = filename
return cfg, err
}

View File

@ -0,0 +1,25 @@
package featuremgmt
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)
func TestReadingFeatureSettings(t *testing.T) {
config, err := readConfigFile("testdata/features.yaml")
require.NoError(t, err, "No error when reading feature configs")
assert.Equal(t, map[string]interface{}{
"level": "free",
"stack": "something",
"valA": "value from features.yaml",
}, config.Vars)
out, err := yaml.Marshal(config)
require.NoError(t, err)
fmt.Printf("%s", string(out))
}

View File

@ -0,0 +1,33 @@
include:
- included.yaml # not yet supported
vars:
stack: something
level: free
valA: value from features.yaml
flags:
- name: feature1
description: feature1
expression: "false"
- name: feature3
description: feature3
expression: "true"
- name: feature3
description: feature3
expression: env.level == 'free'
- name: displaySwedishTheme
description: enable swedish background theme
expression: |
// restrict to users allowing swedish language
req.locale.contains("sv")
- name: displayFrenchFlag
description: sho background theme
expression: |
// only admins
user.id == 1
// show to users allowing french language
&& req.locale.contains("fr")

View File

@ -0,0 +1,13 @@
include:
- features.yaml # make sure we avoid recusion!
# variables that can be used in expressions
vars:
stack: something
deep: 1
valA: value from included.yaml
flags:
- name: featureFromIncludedFile
description: an inlcuded file
expression: invalid expression string here

View File

@ -0,0 +1,10 @@
package featuremgmt
type FeatureToggles struct {
manager *FeatureManager
}
// IsEnabled checks if a feature is enabled
func (ft *FeatureToggles) IsEnabled(flag string) bool {
return ft.manager.IsEnabled(flag)
}

View File

@ -0,0 +1,157 @@
// NOTE: This file is autogenerated
package featuremgmt
// IsRecordedQueriesEnabled checks for the flag: recordedQueries
// Supports saving queries that can be scraped by prometheus
func (ft *FeatureToggles) IsRecordedQueriesEnabled() bool {
return ft.manager.IsEnabled("recordedQueries")
}
// IsTeamsyncEnabled checks for the flag: teamsync
// Team sync lets you set up synchronization between your auth providers teams and teams in Grafana
func (ft *FeatureToggles) IsTeamsyncEnabled() bool {
return ft.manager.IsEnabled("teamsync")
}
// IsLdapsyncEnabled checks for the flag: ldapsync
// Enhanced LDAP integration
func (ft *FeatureToggles) IsLdapsyncEnabled() bool {
return ft.manager.IsEnabled("ldapsync")
}
// IsCachingEnabled checks for the flag: caching
// Temporarily store data source query results.
func (ft *FeatureToggles) IsCachingEnabled() bool {
return ft.manager.IsEnabled("caching")
}
// IsDspermissionsEnabled checks for the flag: dspermissions
// Data source permissions
func (ft *FeatureToggles) IsDspermissionsEnabled() bool {
return ft.manager.IsEnabled("dspermissions")
}
// IsAnalyticsEnabled checks for the flag: analytics
// Analytics
func (ft *FeatureToggles) IsAnalyticsEnabled() bool {
return ft.manager.IsEnabled("analytics")
}
// IsEnterprisePluginsEnabled checks for the flag: enterprise.plugins
// Enterprise plugins
func (ft *FeatureToggles) IsEnterprisePluginsEnabled() bool {
return ft.manager.IsEnabled("enterprise.plugins")
}
// IsTrimDefaultsEnabled checks for the flag: trimDefaults
// Use cue schema to remove values that will be applied automatically
func (ft *FeatureToggles) IsTrimDefaultsEnabled() bool {
return ft.manager.IsEnabled("trimDefaults")
}
// IsEnvelopeEncryptionEnabled checks for the flag: envelopeEncryption
// encrypt secrets
func (ft *FeatureToggles) IsEnvelopeEncryptionEnabled() bool {
return ft.manager.IsEnabled("envelopeEncryption")
}
// IsHttpclientproviderAzureAuthEnabled checks for the flag: httpclientprovider_azure_auth
func (ft *FeatureToggles) IsHttpclientproviderAzureAuthEnabled() bool {
return ft.manager.IsEnabled("httpclientprovider_azure_auth")
}
// IsServiceAccountsEnabled checks for the flag: service-accounts
// support service accounts
func (ft *FeatureToggles) IsServiceAccountsEnabled() bool {
return ft.manager.IsEnabled("service-accounts")
}
// IsDatabaseMetricsEnabled checks for the flag: database_metrics
// Add prometheus metrics for database tables
func (ft *FeatureToggles) IsDatabaseMetricsEnabled() bool {
return ft.manager.IsEnabled("database_metrics")
}
// IsDashboardPreviewsEnabled checks for the flag: dashboardPreviews
// Create and show thumbnails for dashboard search results
func (ft *FeatureToggles) IsDashboardPreviewsEnabled() bool {
return ft.manager.IsEnabled("dashboardPreviews")
}
// IsLiveConfigEnabled checks for the flag: live-config
// Save grafana live configuration in SQL tables
func (ft *FeatureToggles) IsLiveConfigEnabled() bool {
return ft.manager.IsEnabled("live-config")
}
// IsLivePipelineEnabled checks for the flag: live-pipeline
// enable a generic live processing pipeline
func (ft *FeatureToggles) IsLivePipelineEnabled() bool {
return ft.manager.IsEnabled("live-pipeline")
}
// IsLiveServiceWebWorkerEnabled checks for the flag: live-service-web-worker
// This will use a webworker thread to processes events rather than the main thread
func (ft *FeatureToggles) IsLiveServiceWebWorkerEnabled() bool {
return ft.manager.IsEnabled("live-service-web-worker")
}
// IsQueryOverLiveEnabled checks for the flag: queryOverLive
// Use grafana live websocket to execute backend queries
func (ft *FeatureToggles) IsQueryOverLiveEnabled() bool {
return ft.manager.IsEnabled("queryOverLive")
}
// IsTempoSearchEnabled checks for the flag: tempoSearch
// Enable searching in tempo datasources
func (ft *FeatureToggles) IsTempoSearchEnabled() bool {
return ft.manager.IsEnabled("tempoSearch")
}
// IsTempoBackendSearchEnabled checks for the flag: tempoBackendSearch
// Use backend for tempo search
func (ft *FeatureToggles) IsTempoBackendSearchEnabled() bool {
return ft.manager.IsEnabled("tempoBackendSearch")
}
// IsTempoServiceGraphEnabled checks for the flag: tempoServiceGraph
// show service
func (ft *FeatureToggles) IsTempoServiceGraphEnabled() bool {
return ft.manager.IsEnabled("tempoServiceGraph")
}
// IsFullRangeLogsVolumeEnabled checks for the flag: fullRangeLogsVolume
// Show full range logs volume in expore
func (ft *FeatureToggles) IsFullRangeLogsVolumeEnabled() bool {
return ft.manager.IsEnabled("fullRangeLogsVolume")
}
// IsAccesscontrolEnabled checks for the flag: accesscontrol
// Support robust access control
func (ft *FeatureToggles) IsAccesscontrolEnabled() bool {
return ft.manager.IsEnabled("accesscontrol")
}
// IsPrometheusAzureAuthEnabled checks for the flag: prometheus_azure_auth
// Use azure authentication for prometheus datasource
func (ft *FeatureToggles) IsPrometheusAzureAuthEnabled() bool {
return ft.manager.IsEnabled("prometheus_azure_auth")
}
// IsNewNavigationEnabled checks for the flag: newNavigation
// Try the next gen naviation model
func (ft *FeatureToggles) IsNewNavigationEnabled() bool {
return ft.manager.IsEnabled("newNavigation")
}
// IsShowFeatureFlagsInUIEnabled checks for the flag: showFeatureFlagsInUI
// Show feature flags in the settings UI
func (ft *FeatureToggles) IsShowFeatureFlagsInUIEnabled() bool {
return ft.manager.IsEnabled("showFeatureFlagsInUI")
}
// IsDisableHttpRequestHistogramEnabled checks for the flag: disable_http_request_histogram
func (ft *FeatureToggles) IsDisableHttpRequestHistogramEnabled() bool {
return ft.manager.IsEnabled("disable_http_request_histogram")
}

View File

@ -0,0 +1,140 @@
package featuremgmt
import (
"bytes"
"fmt"
"html/template"
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
"unicode"
"github.com/google/go-cmp/cmp"
)
func TestFeatureToggleFiles(t *testing.T) {
// Typescript files
verifyAndGenerateFile(t,
"../../../packages/grafana-data/src/types/featureToggles.gen.ts",
generateTypeScript(),
)
// Golang files
verifyAndGenerateFile(t,
"toggles_gen.go",
generateRegistry(t),
)
}
func verifyAndGenerateFile(t *testing.T, fpath string, gen string) {
// nolint:gosec
// We can ignore the gosec G304 warning since this is a test and the function is only called explicitly above
body, err := ioutil.ReadFile(fpath)
if err == nil {
if diff := cmp.Diff(gen, string(body)); diff != "" {
str := fmt.Sprintf("body mismatch (-want +got):\n%s\n", diff)
err = fmt.Errorf(str)
}
}
if err != nil {
e2 := os.WriteFile(fpath, []byte(gen), 0644)
if e2 != nil {
t.Errorf("error writing file: %s", e2.Error())
}
abs, _ := filepath.Abs(fpath)
t.Errorf("feature toggle do not match: %s (%s)", err.Error(), abs)
t.Fail()
}
}
func generateTypeScript() string {
buf := `// NOTE: This file was auto generated. DO NOT EDIT DIRECTLY!
// To change feature flags, edit:
// pkg/services/featuremgmt/registry.go
// Then run tests in:
// pkg/services/featuremgmt/toggles_gen_test.go
/**
* Describes available feature toggles in Grafana. These can be configured via
* conf/custom.ini to enable features under development or not yet available in
* stable version.
*
* Only enabled values will be returned in this interface
*
* @public
*/
export interface FeatureToggles {
[name: string]: boolean | undefined; // support any string value
`
for _, flag := range standardFeatureFlags {
buf += " " + getTypeScriptKey(flag.Name) + "?: boolean;\n"
}
buf += "}\n"
return buf
}
func getTypeScriptKey(key string) string {
if strings.Contains(key, "-") || strings.Contains(key, ".") {
return "['" + key + "']"
}
return key
}
func isLetterOrNumber(c rune) bool {
return !unicode.IsLetter(c) && !unicode.IsNumber(c)
}
func asCamelCase(key string) string {
parts := strings.FieldsFunc(key, isLetterOrNumber)
for idx, part := range parts {
parts[idx] = strings.Title(part)
}
return strings.Join(parts, "")
}
func generateRegistry(t *testing.T) string {
tmpl, err := template.New("fn").Parse(`
// Is{{.CamleCase}}Enabled checks for the flag: {{.Flag.Name}}{{.Ext}}
func (ft *FeatureToggles) Is{{.CamleCase}}Enabled() bool {
return ft.manager.IsEnabled("{{.Flag.Name}}")
}
`)
if err != nil {
t.Fatal("error reading template", "error", err.Error())
return ""
}
data := struct {
CamleCase string
Flag FeatureFlag
Ext string
}{
CamleCase: "?",
}
var buff bytes.Buffer
buff.WriteString(`// NOTE: This file is autogenerated
package featuremgmt
`)
for _, flag := range standardFeatureFlags {
data.CamleCase = asCamelCase(flag.Name)
data.Flag = flag
data.Ext = ""
if flag.Description != "" {
data.Ext += "\n// " + flag.Description
}
_ = tmpl.Execute(&buff, data)
}
return buff.String()
}

View File

@ -15,6 +15,7 @@ import (
jsoniter "github.com/json-iterator/go" jsoniter "github.com/json-iterator/go"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/query" "github.com/grafana/grafana/pkg/services/query"
"github.com/centrifugal/centrifuge" "github.com/centrifugal/centrifuge"
@ -67,9 +68,10 @@ type CoreGrafanaScope struct {
func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, routeRegister routing.RouteRegister, func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, routeRegister routing.RouteRegister,
pluginStore plugins.Store, cacheService *localcache.CacheService, pluginStore plugins.Store, cacheService *localcache.CacheService,
dataSourceCache datasources.CacheService, sqlStore *sqlstore.SQLStore, secretsService secrets.Service, dataSourceCache datasources.CacheService, sqlStore *sqlstore.SQLStore, secretsService secrets.Service,
usageStatsService usagestats.Service, queryDataService *query.Service) (*GrafanaLive, error) { usageStatsService usagestats.Service, queryDataService *query.Service, toggles *featuremgmt.FeatureToggles) (*GrafanaLive, error) {
g := &GrafanaLive{ g := &GrafanaLive{
Cfg: cfg, Cfg: cfg,
Features: toggles,
PluginContextProvider: plugCtxProvider, PluginContextProvider: plugCtxProvider,
RouteRegister: routeRegister, RouteRegister: routeRegister,
pluginStore: pluginStore, pluginStore: pluginStore,
@ -174,7 +176,7 @@ func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, r
} }
g.ManagedStreamRunner = managedStreamRunner g.ManagedStreamRunner = managedStreamRunner
if enabled := g.Cfg.FeatureToggles["live-pipeline"]; enabled { if g.Features.IsLivePipelineEnabled() {
var builder pipeline.RuleBuilder var builder pipeline.RuleBuilder
if os.Getenv("GF_LIVE_DEV_BUILDER") != "" { if os.Getenv("GF_LIVE_DEV_BUILDER") != "" {
builder = &pipeline.DevRuleBuilder{ builder = &pipeline.DevRuleBuilder{
@ -391,6 +393,7 @@ func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, r
type GrafanaLive struct { type GrafanaLive struct {
PluginContextProvider *plugincontext.Provider PluginContextProvider *plugincontext.Provider
Cfg *setting.Cfg Cfg *setting.Cfg
Features *featuremgmt.FeatureToggles
RouteRegister routing.RouteRegister RouteRegister routing.RouteRegister
CacheService *localcache.CacheService CacheService *localcache.CacheService
DataSourceCache datasources.CacheService DataSourceCache datasources.CacheService

View File

@ -8,9 +8,9 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/schema" "github.com/grafana/grafana/pkg/schema"
"github.com/grafana/grafana/pkg/schema/load" "github.com/grafana/grafana/pkg/schema/load"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/setting"
) )
const ServiceName = "SchemaLoader" const ServiceName = "SchemaLoader"
@ -26,13 +26,13 @@ type RenderUser struct {
OrgRole string OrgRole string
} }
func ProvideService(cfg *setting.Cfg) (*SchemaLoaderService, error) { func ProvideService(features *featuremgmt.FeatureToggles) (*SchemaLoaderService, error) {
dashFam, err := load.BaseDashboardFamily(baseLoadPath) dashFam, err := load.BaseDashboardFamily(baseLoadPath)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to load dashboard cue schema from path %q: %w", baseLoadPath, err) return nil, fmt.Errorf("failed to load dashboard cue schema from path %q: %w", baseLoadPath, err)
} }
s := &SchemaLoaderService{ s := &SchemaLoaderService{
Cfg: cfg, features: features,
DashFamily: dashFam, DashFamily: dashFam,
log: log.New("schemaloader"), log: log.New("schemaloader"),
} }
@ -42,14 +42,14 @@ func ProvideService(cfg *setting.Cfg) (*SchemaLoaderService, error) {
type SchemaLoaderService struct { type SchemaLoaderService struct {
log log.Logger log log.Logger
DashFamily schema.VersionedCueSchema DashFamily schema.VersionedCueSchema
Cfg *setting.Cfg features *featuremgmt.FeatureToggles
} }
func (rs *SchemaLoaderService) IsDisabled() bool { func (rs *SchemaLoaderService) IsDisabled() bool {
if rs.Cfg == nil { if rs.features == nil {
return true return true
} }
return !rs.Cfg.IsTrimDefaultsEnabled() return !rs.features.IsTrimDefaultsEnabled()
} }
func (rs *SchemaLoaderService) DashboardApplyDefaults(input *simplejson.Json) (*simplejson.Json, error) { func (rs *SchemaLoaderService) DashboardApplyDefaults(input *simplejson.Json) (*simplejson.Json, error) {

View File

@ -5,6 +5,7 @@ import (
"github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/services/encryption/ossencryption" "github.com/grafana/grafana/pkg/services/encryption/ossencryption"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/kmsproviders/osskmsproviders" "github.com/grafana/grafana/pkg/services/kmsproviders/osskmsproviders"
"github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
@ -23,10 +24,15 @@ func SetupTestService(tb testing.TB, store secrets.Store) *SecretsService {
[security] [security]
secret_key = ` + defaultKey)) secret_key = ` + defaultKey))
require.NoError(tb, err) require.NoError(tb, err)
features := featuremgmt.WithToggles("envelopeEncryption")
cfg := &setting.Cfg{Raw: raw} cfg := &setting.Cfg{Raw: raw}
cfg.FeatureToggles = map[string]bool{secrets.EnvelopeEncryptionFeatureToggle: true} cfg.IsFeatureToggleEnabled = features.IsEnabled
settings := &setting.OSSImpl{Cfg: cfg} settings := &setting.OSSImpl{Cfg: cfg}
assert.True(tb, settings.IsFeatureToggleEnabled(secrets.EnvelopeEncryptionFeatureToggle)) assert.True(tb, settings.IsFeatureToggleEnabled("envelopeEncryption"))
assert.True(tb, features.IsEnvelopeEncryptionEnabled())
encryption := ossencryption.ProvideService() encryption := ossencryption.ProvideService()
secretsService, err := ProvideSecretsService( secretsService, err := ProvideSecretsService(

View File

@ -7,6 +7,7 @@ import (
"github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/services/encryption/ossencryption" "github.com/grafana/grafana/pkg/services/encryption/ossencryption"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/kmsproviders/osskmsproviders" "github.com/grafana/grafana/pkg/services/kmsproviders/osskmsproviders"
"github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/services/secrets/database" "github.com/grafana/grafana/pkg/services/secrets/database"
@ -180,8 +181,8 @@ func TestSecretsService_UseCurrentProvider(t *testing.T) {
providerID := secrets.ProviderID("fakeProvider.v1") providerID := secrets.ProviderID("fakeProvider.v1")
settings := &setting.OSSImpl{ settings := &setting.OSSImpl{
Cfg: &setting.Cfg{ Cfg: &setting.Cfg{
Raw: raw, Raw: raw,
FeatureToggles: map[string]bool{secrets.EnvelopeEncryptionFeatureToggle: true}, IsFeatureToggleEnabled: featuremgmt.WithToggles(secrets.EnvelopeEncryptionFeatureToggle).IsEnabled,
}, },
} }
encr := ossencryption.ProvideService() encr := ossencryption.ProvideService()

View File

@ -13,8 +13,8 @@ import (
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol"
acmiddleware "github.com/grafana/grafana/pkg/services/accesscontrol/middleware" acmiddleware "github.com/grafana/grafana/pkg/services/accesscontrol/middleware"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/services/serviceaccounts"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web" "github.com/grafana/grafana/pkg/web"
) )
@ -40,9 +40,9 @@ func NewServiceAccountsAPI(
} }
func (api *ServiceAccountsAPI) RegisterAPIEndpoints( func (api *ServiceAccountsAPI) RegisterAPIEndpoints(
cfg *setting.Cfg, features *featuremgmt.FeatureToggles,
) { ) {
if !cfg.FeatureToggles["service-accounts"] { if !features.IsServiceAccountsEnabled() {
return return
} }

View File

@ -13,10 +13,10 @@ import (
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol"
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/services/serviceaccounts"
"github.com/grafana/grafana/pkg/services/serviceaccounts/tests" "github.com/grafana/grafana/pkg/services/serviceaccounts/tests"
"github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web" "github.com/grafana/grafana/pkg/web"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -97,7 +97,7 @@ func serviceAccountRequestScenario(t *testing.T, httpMethod string, endpoint str
func setupTestServer(t *testing.T, svc *tests.ServiceAccountMock, routerRegister routing.RouteRegister, acmock *accesscontrolmock.Mock, sqlStore *sqlstore.SQLStore) *web.Mux { func setupTestServer(t *testing.T, svc *tests.ServiceAccountMock, routerRegister routing.RouteRegister, acmock *accesscontrolmock.Mock, sqlStore *sqlstore.SQLStore) *web.Mux {
a := NewServiceAccountsAPI(svc, acmock, routerRegister, database.NewServiceAccountsStore(sqlStore)) a := NewServiceAccountsAPI(svc, acmock, routerRegister, database.NewServiceAccountsStore(sqlStore))
a.RegisterAPIEndpoints(&setting.Cfg{FeatureToggles: map[string]bool{"service-accounts": true}}) a.RegisterAPIEndpoints(featuremgmt.WithToggles("service-accounts"))
m := web.New() m := web.New()
signedUser := &models.SignedInUser{ signedUser := &models.SignedInUser{

View File

@ -7,11 +7,11 @@ import (
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/services/serviceaccounts"
"github.com/grafana/grafana/pkg/services/serviceaccounts/api" "github.com/grafana/grafana/pkg/services/serviceaccounts/api"
"github.com/grafana/grafana/pkg/services/serviceaccounts/database" "github.com/grafana/grafana/pkg/services/serviceaccounts/database"
"github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
) )
var ( var (
@ -19,21 +19,21 @@ var (
) )
type ServiceAccountsService struct { type ServiceAccountsService struct {
store serviceaccounts.Store store serviceaccounts.Store
cfg *setting.Cfg features *featuremgmt.FeatureToggles
log log.Logger log log.Logger
} }
func ProvideServiceAccountsService( func ProvideServiceAccountsService(
cfg *setting.Cfg, features *featuremgmt.FeatureToggles,
store *sqlstore.SQLStore, store *sqlstore.SQLStore,
ac accesscontrol.AccessControl, ac accesscontrol.AccessControl,
routeRegister routing.RouteRegister, routeRegister routing.RouteRegister,
) (*ServiceAccountsService, error) { ) (*ServiceAccountsService, error) {
s := &ServiceAccountsService{ s := &ServiceAccountsService{
cfg: cfg, features: features,
store: database.NewServiceAccountsStore(store), store: database.NewServiceAccountsStore(store),
log: log.New("serviceaccounts"), log: log.New("serviceaccounts"),
} }
if err := RegisterRoles(ac); err != nil { if err := RegisterRoles(ac); err != nil {
@ -41,13 +41,13 @@ func ProvideServiceAccountsService(
} }
serviceaccountsAPI := api.NewServiceAccountsAPI(s, ac, routeRegister, s.store) serviceaccountsAPI := api.NewServiceAccountsAPI(s, ac, routeRegister, s.store)
serviceaccountsAPI.RegisterAPIEndpoints(cfg) serviceaccountsAPI.RegisterAPIEndpoints(features)
return s, nil return s, nil
} }
func (sa *ServiceAccountsService) CreateServiceAccount(ctx context.Context, saForm *serviceaccounts.CreateServiceaccountForm) (*models.User, error) { func (sa *ServiceAccountsService) CreateServiceAccount(ctx context.Context, saForm *serviceaccounts.CreateServiceaccountForm) (*models.User, error) {
if !sa.cfg.FeatureToggles["service-accounts"] { if !sa.features.IsServiceAccountsEnabled() {
sa.log.Debug(ServiceAccountFeatureToggleNotFound) sa.log.Debug(ServiceAccountFeatureToggleNotFound)
return nil, nil return nil, nil
} }
@ -55,7 +55,7 @@ func (sa *ServiceAccountsService) CreateServiceAccount(ctx context.Context, saFo
} }
func (sa *ServiceAccountsService) DeleteServiceAccount(ctx context.Context, orgID, serviceAccountID int64) error { func (sa *ServiceAccountsService) DeleteServiceAccount(ctx context.Context, orgID, serviceAccountID int64) error {
if !sa.cfg.FeatureToggles["service-accounts"] { if !sa.features.IsServiceAccountsEnabled() {
sa.log.Debug(ServiceAccountFeatureToggleNotFound) sa.log.Debug(ServiceAccountFeatureToggleNotFound)
return nil return nil
} }

View File

@ -5,31 +5,29 @@ import (
"testing" "testing"
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/serviceaccounts/tests" "github.com/grafana/grafana/pkg/services/serviceaccounts/tests"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestProvideServiceAccount_DeleteServiceAccount(t *testing.T) { func TestProvideServiceAccount_DeleteServiceAccount(t *testing.T) {
t.Run("feature toggle present, should call store function", func(t *testing.T) { t.Run("feature toggle present, should call store function", func(t *testing.T) {
cfg := setting.NewCfg()
storeMock := &tests.ServiceAccountsStoreMock{Calls: tests.Calls{}} storeMock := &tests.ServiceAccountsStoreMock{Calls: tests.Calls{}}
cfg.FeatureToggles = map[string]bool{"service-accounts": true} svc := ServiceAccountsService{
svc := ServiceAccountsService{cfg: cfg, store: storeMock} features: featuremgmt.WithToggles("service-accounts", true),
store: storeMock}
err := svc.DeleteServiceAccount(context.Background(), 1, 1) err := svc.DeleteServiceAccount(context.Background(), 1, 1)
require.NoError(t, err) require.NoError(t, err)
assert.Len(t, storeMock.Calls.DeleteServiceAccount, 1) assert.Len(t, storeMock.Calls.DeleteServiceAccount, 1)
}) })
t.Run("no feature toggle present, should not call store function", func(t *testing.T) { t.Run("no feature toggle present, should not call store function", func(t *testing.T) {
cfg := setting.NewCfg()
svcMock := &tests.ServiceAccountsStoreMock{Calls: tests.Calls{}} svcMock := &tests.ServiceAccountsStoreMock{Calls: tests.Calls{}}
cfg.FeatureToggles = map[string]bool{"service-accounts": false}
svc := ServiceAccountsService{ svc := ServiceAccountsService{
cfg: cfg, features: featuremgmt.WithToggles("service-accounts", false),
store: svcMock, store: svcMock,
log: log.New("serviceaccounts-manager-test"), log: log.New("serviceaccounts-manager-test"),
} }
err := svc.DeleteServiceAccount(context.Background(), 1, 1) err := svc.DeleteServiceAccount(context.Background(), 1, 1)
require.NoError(t, err) require.NoError(t, err)

View File

@ -3,6 +3,7 @@ package migrations
import ( import (
"os" "os"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/sqlstore/migrations/accesscontrol" "github.com/grafana/grafana/pkg/services/sqlstore/migrations/accesscontrol"
"github.com/grafana/grafana/pkg/services/sqlstore/migrations/ualert" "github.com/grafana/grafana/pkg/services/sqlstore/migrations/ualert"
. "github.com/grafana/grafana/pkg/services/sqlstore/migrator" . "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
@ -56,8 +57,8 @@ func (*OSSMigrations) AddMigration(mg *Migrator) {
ualert.AddTablesMigrations(mg) ualert.AddTablesMigrations(mg)
ualert.AddDashAlertMigration(mg) ualert.AddDashAlertMigration(mg)
addLibraryElementsMigrations(mg) addLibraryElementsMigrations(mg)
if mg.Cfg != nil { if mg.Cfg.IsFeatureToggleEnabled != nil {
if mg.Cfg.IsLiveConfigEnabled() { if mg.Cfg.IsFeatureToggleEnabled(featuremgmt.FLAG_live_config) {
addLiveChannelMigrations(mg) addLiveChannelMigrations(mg)
} }
} }

View File

@ -117,7 +117,7 @@ func (ss *SQLStore) GetOrgUsers(ctx context.Context, query *models.GetOrgUsersQu
// service accounts table in the modelling // service accounts table in the modelling
whereConditions = append(whereConditions, fmt.Sprintf("%s.is_service_account = %t", x.Dialect().Quote("user"), query.IsServiceAccount)) whereConditions = append(whereConditions, fmt.Sprintf("%s.is_service_account = %t", x.Dialect().Quote("user"), query.IsServiceAccount))
if ss.Cfg.FeatureToggles["accesscontrol"] { if ss.Cfg.IsFeatureToggleEnabled("accesscontrol") {
q, args, err := accesscontrol.Filter(ctx, ss.Dialect, "org_user.user_id", "users", "org.users:read", query.User) q, args, err := accesscontrol.Filter(ctx, ss.Dialect, "org_user.user_id", "users", "org.users:read", query.User)
if err != nil { if err != nil {
return err return err
@ -180,7 +180,7 @@ func (ss *SQLStore) SearchOrgUsers(ctx context.Context, query *models.SearchOrgU
// service accounts table in the modelling // service accounts table in the modelling
whereConditions = append(whereConditions, fmt.Sprintf("%s.is_service_account = %t", x.Dialect().Quote("user"), query.IsServiceAccount)) whereConditions = append(whereConditions, fmt.Sprintf("%s.is_service_account = %t", x.Dialect().Quote("user"), query.IsServiceAccount))
if ss.Cfg.FeatureToggles["accesscontrol"] { if ss.Cfg.IsFeatureToggleEnabled("accesscontrol") {
q, args, err := accesscontrol.Filter(ctx, ss.Dialect, "org_user.user_id", "users", "org.users:read", query.User) q, args, err := accesscontrol.Filter(ctx, ss.Dialect, "org_user.user_id", "users", "org.users:read", query.User)
if err != nil { if err != nil {
return err return err

View File

@ -11,6 +11,7 @@ import (
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
ac "github.com/grafana/grafana/pkg/services/accesscontrol" ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/featuremgmt"
) )
type getOrgUsersTestCase struct { type getOrgUsersTestCase struct {
@ -61,7 +62,7 @@ func TestSQLStore_GetOrgUsers(t *testing.T) {
} }
store := InitTestDB(t) store := InitTestDB(t)
store.Cfg.FeatureToggles = map[string]bool{"accesscontrol": true} store.Cfg.IsFeatureToggleEnabled = featuremgmt.WithToggles("accesscontrol").IsEnabled
seedOrgUsers(t, store, 10) seedOrgUsers(t, store, 10)
for _, tt := range tests { for _, tt := range tests {
@ -127,7 +128,7 @@ func TestSQLStore_SearchOrgUsers(t *testing.T) {
} }
store := InitTestDB(t) store := InitTestDB(t)
store.Cfg.FeatureToggles = map[string]bool{"accesscontrol": true} store.Cfg.IsFeatureToggleEnabled = featuremgmt.WithToggles("accesscontrol").IsEnabled
seedOrgUsers(t, store, 10) seedOrgUsers(t, store, 10)
for _, tt := range tests { for _, tt := range tests {

View File

@ -23,6 +23,7 @@ import (
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/annotations"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/sqlstore/migrations" "github.com/grafana/grafana/pkg/services/sqlstore/migrations"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil" "github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
@ -326,7 +327,7 @@ func (ss *SQLStore) initEngine(engine *xorm.Engine) error {
return err return err
} }
if ss.Cfg.IsDatabaseMetricsEnabled() { if ss.Cfg.IsFeatureToggleEnabled(featuremgmt.FLAG_database_metrics) {
ss.dbCfg.Type = WrapDatabaseDriverWithHooks(ss.dbCfg.Type, ss.tracer) ss.dbCfg.Type = WrapDatabaseDriverWithHooks(ss.dbCfg.Type, ss.tracer)
} }
@ -492,6 +493,7 @@ func initTestDB(migration registry.DatabaseMigrator, opts ...InitTestDBOpt) (*SQ
// set test db config // set test db config
cfg := setting.NewCfg() cfg := setting.NewCfg()
cfg.IsFeatureToggleEnabled = func(key string) bool { return false }
sec, err := cfg.Raw.NewSection("database") sec, err := cfg.Raw.NewSection("database")
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -14,6 +14,7 @@ import (
"github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/live" "github.com/grafana/grafana/pkg/services/live"
"github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/services/rendering"
@ -39,8 +40,8 @@ type Service interface {
CrawlerStatus(c *models.ReqContext) response.Response CrawlerStatus(c *models.ReqContext) response.Response
} }
func ProvideService(cfg *setting.Cfg, renderService rendering.Service, gl *live.GrafanaLive) Service { func ProvideService(cfg *setting.Cfg, features *featuremgmt.FeatureToggles, renderService rendering.Service, gl *live.GrafanaLive) Service {
if !cfg.IsDashboardPreviesEnabled() { if !features.IsDashboardPreviewsEnabled() {
return &dummyService{} return &dummyService{}
} }

View File

@ -132,7 +132,7 @@ func (o *OSSImpl) Section(section string) Section {
func (OSSImpl) RegisterReloadHandler(string, ReloadHandler) {} func (OSSImpl) RegisterReloadHandler(string, ReloadHandler) {}
func (o OSSImpl) IsFeatureToggleEnabled(name string) bool { func (o OSSImpl) IsFeatureToggleEnabled(name string) bool {
return o.Cfg.FeatureToggles[name] return o.Cfg.IsFeatureToggleEnabled(name)
} }
type keyValImpl struct { type keyValImpl struct {

View File

@ -342,8 +342,10 @@ type Cfg struct {
ApiKeyMaxSecondsToLive int64 ApiKeyMaxSecondsToLive int64
// Use to enable new features which may still be in alpha/beta stage. // Check if a feature toggle is enabled
FeatureToggles map[string]bool // @deprecated
IsFeatureToggleEnabled func(key string) bool // filled in dynamically
AnonymousEnabled bool AnonymousEnabled bool
AnonymousOrgName string AnonymousOrgName string
AnonymousOrgRole string AnonymousOrgRole string
@ -429,41 +431,6 @@ type Cfg struct {
UnifiedAlerting UnifiedAlertingSettings UnifiedAlerting UnifiedAlertingSettings
} }
// IsLiveConfigEnabled returns true if live should be able to save configs to SQL tables
func (cfg Cfg) IsLiveConfigEnabled() bool {
return cfg.FeatureToggles["live-config"]
}
// IsLiveConfigEnabled returns true if live should be able to save configs to SQL tables
func (cfg Cfg) IsDashboardPreviesEnabled() bool {
return cfg.FeatureToggles["dashboardPreviews"]
}
// IsTrimDefaultsEnabled returns whether the standalone trim dashboard default feature is enabled.
func (cfg Cfg) IsTrimDefaultsEnabled() bool {
return cfg.FeatureToggles["trimDefaults"]
}
// IsDatabaseMetricsEnabled returns whether the database instrumentation feature is enabled.
func (cfg Cfg) IsDatabaseMetricsEnabled() bool {
return cfg.FeatureToggles["database_metrics"]
}
// IsHTTPRequestHistogramDisabled returns whether the request historgrams is disabled.
// This feature toggle will be removed in Grafana 8.x but gives the operator
// some graceperiod to update all the monitoring tools.
func (cfg Cfg) IsHTTPRequestHistogramDisabled() bool {
return cfg.FeatureToggles["disable_http_request_histogram"]
}
func (cfg Cfg) IsNewNavigationEnabled() bool {
return cfg.FeatureToggles["newNavigation"]
}
func (cfg Cfg) IsServiceAccountEnabled() bool {
return cfg.FeatureToggles["service-accounts"]
}
type CommandLineArgs struct { type CommandLineArgs struct {
Config string Config string
HomePath string HomePath string

View File

@ -3,42 +3,23 @@ package setting
import ( import (
"strconv" "strconv"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
"gopkg.in/ini.v1" "gopkg.in/ini.v1"
) )
var ( // @deprecated -- should use `featuremgmt.FeatureToggles`
featureToggleInfo = promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "feature_toggles_info",
Help: "info metric that exposes what feature toggles are enabled or not",
Namespace: "grafana",
}, []string{"name"})
defaultFeatureToggles = map[string]bool{
"recordedQueries": false,
"accesscontrol": false,
"service-accounts": false,
"httpclientprovider_azure_auth": false,
}
)
func (cfg *Cfg) readFeatureToggles(iniFile *ini.File) error { func (cfg *Cfg) readFeatureToggles(iniFile *ini.File) error {
toggles, err := overrideDefaultWithConfiguration(iniFile, defaultFeatureToggles) section := iniFile.Section("feature_toggles")
toggles, err := ReadFeatureTogglesFromInitFile(section)
if err != nil { if err != nil {
return err return err
} }
cfg.IsFeatureToggleEnabled = func(key string) bool { return toggles[key] }
cfg.FeatureToggles = toggles
return nil return nil
} }
func overrideDefaultWithConfiguration(iniFile *ini.File, featureToggles map[string]bool) (map[string]bool, error) { func ReadFeatureTogglesFromInitFile(featureTogglesSection *ini.Section) (map[string]bool, error) {
// Read and populate feature toggles list featureToggles := make(map[string]bool, 10)
featureTogglesSection := iniFile.Section("feature_toggles")
// parse the comma separated list in `enable`. // parse the comma separated list in `enable`.
featuresTogglesStr := valueAsString(featureTogglesSection, "enable", "") featuresTogglesStr := valueAsString(featureTogglesSection, "enable", "")
@ -60,15 +41,5 @@ func overrideDefaultWithConfiguration(iniFile *ini.File, featureToggles map[stri
featureToggles[v.Name()] = b featureToggles[v.Name()] = b
} }
// track if feature toggles are enabled or not using an info metric
for k, v := range featureToggles {
if v {
featureToggleInfo.WithLabelValues(k).Set(1)
} else {
featureToggleInfo.WithLabelValues(k).Set(0)
}
}
return featureToggles, nil return featureToggles, nil
} }

View File

@ -14,7 +14,6 @@ func TestFeatureToggles(t *testing.T) {
conf map[string]string conf map[string]string
err error err error
expectedToggles map[string]bool expectedToggles map[string]bool
defaultToggles map[string]bool
}{ }{
{ {
name: "can parse feature toggles passed in the `enable` array", name: "can parse feature toggles passed in the `enable` array",
@ -58,18 +57,6 @@ func TestFeatureToggles(t *testing.T) {
expectedToggles: map[string]bool{}, expectedToggles: map[string]bool{},
err: strconv.ErrSyntax, err: strconv.ErrSyntax,
}, },
{
name: "should override default feature toggles",
defaultToggles: map[string]bool{
"feature1": true,
},
conf: map[string]string{
"feature1": "false",
},
expectedToggles: map[string]bool{
"feature1": false,
},
},
} }
for _, tc := range testCases { for _, tc := range testCases {
@ -81,12 +68,7 @@ func TestFeatureToggles(t *testing.T) {
require.ErrorIs(t, err, nil) require.ErrorIs(t, err, nil)
} }
dt := map[string]bool{} featureToggles, err := ReadFeatureTogglesFromInitFile(toggles)
if len(tc.defaultToggles) > 0 {
dt = tc.defaultToggles
}
featureToggles, err := overrideDefaultWithConfiguration(f, dt)
require.ErrorIs(t, err, tc.err) require.ErrorIs(t, err, tc.err)
if err == nil { if err == nil {

View File

@ -79,7 +79,7 @@ func (cfg *Cfg) readUnifiedAlertingEnabledSetting(section *ini.Section) (*bool,
// the unified alerting is not enabled by default. First, check the feature flag // the unified alerting is not enabled by default. First, check the feature flag
if err != nil { if err != nil {
// TODO: Remove in Grafana v9 // TODO: Remove in Grafana v9
if cfg.FeatureToggles["ngalert"] { if cfg.IsFeatureToggleEnabled("ngalert") {
cfg.Logger.Warn("ngalert feature flag is deprecated: use unified alerting enabled setting instead") cfg.Logger.Warn("ngalert feature flag is deprecated: use unified alerting enabled setting instead")
enabled = true enabled = true
// feature flag overrides the legacy alerting setting. // feature flag overrides the legacy alerting setting.

View File

@ -143,6 +143,7 @@ func TestUnifiedAlertingSettings(t *testing.T) {
t.Run(tc.desc, func(t *testing.T) { t.Run(tc.desc, func(t *testing.T) {
f := ini.Empty() f := ini.Empty()
cfg := NewCfg() cfg := NewCfg()
cfg.IsFeatureToggleEnabled = func(key string) bool { return false }
unifiedAlertingSec, err := f.NewSection("unified_alerting") unifiedAlertingSec, err := f.NewSection("unified_alerting")
require.NoError(t, err) require.NoError(t, err)
for k, v := range tc.unifiedAlertingOptions { for k, v := range tc.unifiedAlertingOptions {

View File

@ -37,7 +37,7 @@ func (s *Service) SubscribeStream(_ context.Context, req *backend.SubscribeStrea
} }
} }
if s.cfg.FeatureToggles["live-pipeline"] { if s.features.IsLivePipelineEnabled() {
// While developing Live pipeline avoid sending initial data. // While developing Live pipeline avoid sending initial data.
initialData = nil initialData = nil
} }
@ -126,7 +126,7 @@ func (s *Service) runTestStream(ctx context.Context, path string, conf testStrea
} }
mode := data.IncludeDataOnly mode := data.IncludeDataOnly
if s.cfg.FeatureToggles["live-pipeline"] { if s.features.IsLivePipelineEnabled() {
mode = data.IncludeAll mode = data.IncludeAll
} }

View File

@ -10,11 +10,13 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
) )
func ProvideService(cfg *setting.Cfg) *Service { func ProvideService(cfg *setting.Cfg, features *featuremgmt.FeatureToggles) *Service {
s := &Service{ s := &Service{
features: features,
queryMux: datasource.NewQueryTypeMux(), queryMux: datasource.NewQueryTypeMux(),
scenarios: map[string]*Scenario{}, scenarios: map[string]*Scenario{},
frame: data.NewFrame("testdata", frame: data.NewFrame("testdata",
@ -46,6 +48,7 @@ type Service struct {
labelFrame *data.Frame labelFrame *data.Frame
queryMux *datasource.QueryTypeMux queryMux *datasource.QueryTypeMux
resourceHandler backend.CallResourceHandler resourceHandler backend.CallResourceHandler
features *featuremgmt.FeatureToggles
} }
func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {

View File

@ -83,13 +83,13 @@ export class ContextSrv {
} }
accessControlEnabled(): boolean { accessControlEnabled(): boolean {
return featureEnabled('accesscontrol') && Boolean(config.featureToggles['accesscontrol']); return featureEnabled(config.featureToggles.accesscontrol);
} }
// Checks whether user has required permission // Checks whether user has required permission
hasPermissionInMetadata(action: AccessControlAction | string, object: WithAccessControlMetadata): boolean { hasPermissionInMetadata(action: AccessControlAction | string, object: WithAccessControlMetadata): boolean {
// Fallback if access control disabled // Fallback if access control disabled
if (!config.featureToggles['accesscontrol']) { if (!config.featureToggles.accesscontrol) {
return true; return true;
} }
@ -99,7 +99,7 @@ export class ContextSrv {
// Checks whether user has required permission // Checks whether user has required permission
hasPermission(action: AccessControlAction | string): boolean { hasPermission(action: AccessControlAction | string): boolean {
// Fallback if access control disabled // Fallback if access control disabled
if (!config.featureToggles['accesscontrol']) { if (!config.featureToggles.accesscontrol) {
return true; return true;
} }
@ -126,14 +126,14 @@ export class ContextSrv {
} }
hasAccessToExplore() { hasAccessToExplore() {
if (config.featureToggles['accesscontrol']) { if (config.featureToggles.accesscontrol) {
return this.hasPermission(AccessControlAction.DataSourcesExplore); return this.hasPermission(AccessControlAction.DataSourcesExplore);
} }
return (this.isEditor || config.viewersCanEdit) && config.exploreEnabled; return (this.isEditor || config.viewersCanEdit) && config.exploreEnabled;
} }
hasAccess(action: string, fallBack: boolean) { hasAccess(action: string, fallBack: boolean) {
if (!config.featureToggles['accesscontrol']) { if (!config.featureToggles.accesscontrol) {
return fallBack; return fallBack;
} }
return this.hasPermission(action); return this.hasPermission(action);
@ -141,7 +141,7 @@ export class ContextSrv {
// evaluates access control permissions, granting access if the user has any of them; uses fallback if access control is disabled // evaluates access control permissions, granting access if the user has any of them; uses fallback if access control is disabled
evaluatePermission(fallback: () => string[], actions: string[]) { evaluatePermission(fallback: () => string[], actions: string[]) {
if (!config.featureToggles['accesscontrol']) { if (!config.featureToggles.accesscontrol) {
return fallback(); return fallback();
} }
if (actions.some((action) => this.hasPermission(action))) { if (actions.some((action) => this.hasPermission(action))) {

View File

@ -2,7 +2,7 @@ import config from '../../core/config';
// accessControlQueryParam adds an additional accesscontrol=true param to params when accesscontrol is enabled // accessControlQueryParam adds an additional accesscontrol=true param to params when accesscontrol is enabled
export function accessControlQueryParam(params = {}) { export function accessControlQueryParam(params = {}) {
if (!config.featureToggles['accesscontrol']) { if (!config.featureToggles.accesscontrol) {
return params; return params;
} }
return { ...params, accesscontrol: true }; return { ...params, accesscontrol: true };

View File

@ -49,14 +49,14 @@ describe('PluginListItemBadges', () => {
}); });
it('renders an enterprise badge (when a license is valid)', () => { it('renders an enterprise badge (when a license is valid)', () => {
config.licenseInfo.enabledFeatures = { 'enterprise.plugins': true }; config.featureToggles = { 'enterprise.plugins': true };
render(<PluginListItemBadges plugin={{ ...plugin, isEnterprise: true }} />); render(<PluginListItemBadges plugin={{ ...plugin, isEnterprise: true }} />);
expect(screen.getByText(/enterprise/i)).toBeVisible(); expect(screen.getByText(/enterprise/i)).toBeVisible();
expect(screen.queryByRole('button', { name: /learn more/i })).not.toBeInTheDocument(); expect(screen.queryByRole('button', { name: /learn more/i })).not.toBeInTheDocument();
}); });
it('renders an enterprise badge with icon and link (when a license is invalid)', () => { it('renders an enterprise badge with icon and link (when a license is invalid)', () => {
config.licenseInfo.enabledFeatures = {}; config.featureToggles = {};
render(<PluginListItemBadges plugin={{ ...plugin, isEnterprise: true }} />); render(<PluginListItemBadges plugin={{ ...plugin, isEnterprise: true }} />);
expect(screen.getByText(/enterprise/i)).toBeVisible(); expect(screen.getByText(/enterprise/i)).toBeVisible();
expect(screen.getByLabelText(/lock icon/i)).toBeInTheDocument(); expect(screen.getByLabelText(/lock icon/i)).toBeInTheDocument();

View File

@ -90,7 +90,7 @@ describe('Plugin details page', () => {
afterEach(() => { afterEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
config.pluginAdminExternalManageEnabled = false; config.pluginAdminExternalManageEnabled = false;
config.licenseInfo.enabledFeatures = {}; config.featureToggles = {};
}); });
afterAll(() => { afterAll(() => {
@ -325,7 +325,7 @@ describe('Plugin details page', () => {
}); });
it('should display an install button for enterprise plugins if license is valid', async () => { it('should display an install button for enterprise plugins if license is valid', async () => {
config.licenseInfo.enabledFeatures = { 'enterprise.plugins': true }; config.featureToggles = { 'enterprise.plugins': true };
const { queryByRole } = renderPluginDetails({ id, isInstalled: false, isEnterprise: true }); const { queryByRole } = renderPluginDetails({ id, isInstalled: false, isEnterprise: true });
@ -333,7 +333,7 @@ describe('Plugin details page', () => {
}); });
it('should not display install button for enterprise plugins if license is invalid', async () => { it('should not display install button for enterprise plugins if license is invalid', async () => {
config.licenseInfo.enabledFeatures = {}; config.featureToggles = {};
const { queryByRole, queryByText } = renderPluginDetails({ id, isInstalled: true, isEnterprise: true }); const { queryByRole, queryByText } = renderPluginDetails({ id, isInstalled: true, isEnterprise: true });
@ -772,7 +772,7 @@ describe('Plugin details page', () => {
}); });
it('should not display an install button for enterprise plugins if license is valid', async () => { it('should not display an install button for enterprise plugins if license is valid', async () => {
config.licenseInfo.enabledFeatures = { 'enterprise.plugins': true }; config.featureToggles = { 'enterprise.plugins': true };
const { queryByRole, queryByText } = renderPluginDetails({ id, isInstalled: false, isEnterprise: true }); const { queryByRole, queryByText } = renderPluginDetails({ id, isInstalled: false, isEnterprise: true });
await waitFor(() => expect(queryByText(PluginTabLabels.OVERVIEW)).toBeInTheDocument()); await waitFor(() => expect(queryByText(PluginTabLabels.OVERVIEW)).toBeInTheDocument());

View File

@ -10,9 +10,7 @@ import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps
jest.mock('@grafana/runtime/src/config', () => ({ jest.mock('@grafana/runtime/src/config', () => ({
...((jest.requireActual('@grafana/runtime/src/config') as unknown) as object), ...((jest.requireActual('@grafana/runtime/src/config') as unknown) as object),
config: { config: {
licenseInfo: { featureToggles: { teamsync: true },
enabledFeatures: { teamsync: true },
},
}, },
})); }));