FeatureFlags: define features outside settings.Cfg (take 3) (#44443)

This commit is contained in:
Ryan McKinley
2022-01-26 09:44:20 -08:00
committed by GitHub
parent 84a5910e56
commit 5d66194ec5
64 changed files with 1193 additions and 248 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

@@ -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
@@ -10,13 +16,6 @@
export interface FeatureToggles { export interface FeatureToggles {
[name: string]: boolean | undefined; // support any string value [name: string]: boolean | undefined; // support any string value
recordedQueries?: boolean;
teamsync?: boolean;
ldapsync?: boolean;
caching?: boolean;
dspermissions?: boolean;
analytics?: boolean;
['enterprise.plugins']?: boolean;
trimDefaults?: boolean; trimDefaults?: boolean;
envelopeEncryption?: boolean; envelopeEncryption?: boolean;
httpclientprovider_azure_auth?: boolean; httpclientprovider_azure_auth?: boolean;
@@ -36,4 +35,5 @@ export interface FeatureToggles {
newNavigation?: boolean; newNavigation?: boolean;
showFeatureFlagsInUI?: boolean; showFeatureFlagsInUI?: boolean;
disable_http_request_histogram?: boolean; disable_http_request_histogram?: boolean;
validatedQueries?: boolean;
} }

View File

@@ -12,6 +12,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"
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"
sa "github.com/grafana/grafana/pkg/services/serviceaccounts/manager" sa "github.com/grafana/grafana/pkg/services/serviceaccounts/manager"
) )
@@ -437,7 +438,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.IsEnabled(featuremgmt.FlagLivePipeline) {
// 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 +461,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.IsEnabled(featuremgmt.FlagShowFeatureFlagsInUI) {
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

@@ -11,6 +11,7 @@ import (
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/apikeygen" "github.com/grafana/grafana/pkg/components/apikeygen"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/web" "github.com/grafana/grafana/pkg/web"
) )
@@ -83,7 +84,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.IsEnabled(featuremgmt.FlagServiceAccounts) {
// 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 +121,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.IsEnabled(featuremgmt.FlagServiceAccounts) {
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

@@ -30,6 +30,7 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol/resourceservices" "github.com/grafana/grafana/pkg/services/accesscontrol/resourceservices"
"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"
@@ -215,8 +216,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(featuremgmt.FlagAccesscontrol)
cfg.FeatureToggles["accesscontrol"] = true cfg.IsFeatureToggleEnabled = features.IsEnabled
cfg.Quota.Enabled = false cfg.Quota.Enabled = false
bus := bus.GetBus() bus := bus.GetBus()
@@ -224,6 +225,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),
@@ -298,13 +300,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)
} }
@@ -312,6 +326,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
@@ -325,6 +342,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},
@@ -344,7 +362,7 @@ func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessCont
require.NoError(t, err) require.NoError(t, err)
hs.TeamPermissionsService = teamPermissionService hs.TeamPermissionsService = teamPermissionService
} else { } else {
ac = ossaccesscontrol.ProvideService(cfg, &usagestats.UsageStatsMock{T: t}) ac = ossaccesscontrol.ProvideService(hs.Features, &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.WithFeatures()
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

@@ -249,7 +249,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i
"edition": hs.License.Edition(), "edition": hs.License.Edition(),
"enabledFeatures": hs.License.EnabledFeatures(), "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

@@ -36,6 +36,7 @@ import (
"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/encryption" "github.com/grafana/grafana/pkg/services/encryption"
"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"
@@ -77,6 +78,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
@@ -138,7 +140,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,
@@ -171,6 +173,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

@@ -11,6 +11,7 @@ import (
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins"
ac "github.com/grafana/grafana/pkg/services/accesscontrol" ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
) )
@@ -85,7 +86,7 @@ func (hs *HTTPServer) getAppLinks(c *models.ReqContext) ([]*dtos.NavLink, error)
SortWeight: dtos.WeightPlugin, SortWeight: dtos.WeightPlugin,
} }
if hs.Cfg.IsNewNavigationEnabled() { if hs.Features.IsEnabled(featuremgmt.FlagNewNavigation) {
appLink.Section = dtos.NavSectionPlugin appLink.Section = dtos.NavSectionPlugin
} else { } else {
appLink.Section = dtos.NavSectionCore appLink.Section = dtos.NavSectionCore
@@ -143,7 +144,9 @@ 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.IsEnabled(featuremgmt.FlagServiceAccounts) &&
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 +157,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.IsEnabled(featuremgmt.FlagNewNavigation) {
navTree = append(navTree, &dtos.NavLink{ navTree = append(navTree, &dtos.NavLink{
Text: "Home", Text: "Home",
Id: "home", Id: "home",
@@ -165,7 +168,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
}) })
} }
if hasEditPerm && !hs.Cfg.IsNewNavigationEnabled() { if hasEditPerm && !hs.Features.IsEnabled(featuremgmt.FlagNewNavigation) {
children := hs.buildCreateNavLinks(c) children := hs.buildCreateNavLinks(c)
navTree = append(navTree, &dtos.NavLink{ navTree = append(navTree, &dtos.NavLink{
Text: "Create", Text: "Create",
@@ -181,7 +184,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.IsEnabled(featuremgmt.FlagNewNavigation) {
dashboardsUrl = "/dashboards" dashboardsUrl = "/dashboards"
} }
@@ -312,7 +315,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
}) })
} }
if hs.Cfg.FeatureToggles["live-pipeline"] { if hs.Features.IsEnabled(featuremgmt.FlagLivePipeline) {
liveNavLinks := []*dtos.NavLink{} liveNavLinks := []*dtos.NavLink{}
liveNavLinks = append(liveNavLinks, &dtos.NavLink{ liveNavLinks = append(liveNavLinks, &dtos.NavLink{
@@ -346,7 +349,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.IsEnabled(featuremgmt.FlagNewNavigation) {
configNode.Section = dtos.NavSectionConfig configNode.Section = dtos.NavSectionConfig
} else { } else {
configNode.Section = dtos.NavSectionCore configNode.Section = dtos.NavSectionCore
@@ -358,7 +361,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.IsEnabled(featuremgmt.FlagNewNavigation) {
navSection = dtos.NavSectionConfig navSection = dtos.NavSectionConfig
} }
serverAdminNode := navlinks.GetServerAdminNode(adminNavLinks, navSection) serverAdminNode := navlinks.GetServerAdminNode(adminNavLinks, navSection)
@@ -386,7 +389,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.IsEnabled(featuremgmt.FlagNewNavigation) {
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 +420,7 @@ func (hs *HTTPServer) buildDashboardNavLinks(c *models.ReqContext, hasEditPerm b
}) })
} }
if hasEditPerm && hs.Cfg.IsNewNavigationEnabled() { if hasEditPerm && hs.Features.IsEnabled(featuremgmt.FlagNewNavigation) {
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 +625,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.IsEnabled(featuremgmt.FlagAccesscontrol) {
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

@@ -10,6 +10,7 @@ import (
"github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/infra/metrics"
"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/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"
@@ -89,7 +90,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.IsEnabled(featuremgmt.FlagAccesscontrol)
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

@@ -8,6 +8,7 @@ import (
"github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/response"
"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/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web" "github.com/grafana/grafana/pkg/web"
@@ -19,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.IsEnabled(featuremgmt.FlagAccesscontrol)
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

@@ -11,6 +11,7 @@ import (
"github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web" "github.com/grafana/grafana/pkg/web"
) )
@@ -61,7 +62,7 @@ func (hs *HTTPServer) AddTeamMember(c *models.ReqContext) response.Response {
return response.Error(http.StatusBadRequest, "teamId is invalid", err) return response.Error(http.StatusBadRequest, "teamId is invalid", err)
} }
if !hs.Cfg.FeatureToggles["accesscontrol"] { if !hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol) {
if err := hs.teamGuardian.CanAdmin(c.Req.Context(), cmd.OrgId, cmd.TeamId, c.SignedInUser); err != nil { if err := hs.teamGuardian.CanAdmin(c.Req.Context(), cmd.OrgId, cmd.TeamId, c.SignedInUser); err != nil {
return response.Error(403, "Not allowed to add team member", err) return response.Error(403, "Not allowed to add team member", err)
} }
@@ -101,7 +102,7 @@ func (hs *HTTPServer) UpdateTeamMember(c *models.ReqContext) response.Response {
} }
orgId := c.OrgId orgId := c.OrgId
if !hs.Cfg.FeatureToggles["accesscontrol"] { if !hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol) {
if err := hs.teamGuardian.CanAdmin(c.Req.Context(), orgId, teamId, c.SignedInUser); err != nil { if err := hs.teamGuardian.CanAdmin(c.Req.Context(), orgId, teamId, c.SignedInUser); err != nil {
return response.Error(403, "Not allowed to update team member", err) return response.Error(403, "Not allowed to update team member", err)
} }
@@ -144,7 +145,7 @@ func (hs *HTTPServer) RemoveTeamMember(c *models.ReqContext) response.Response {
return response.Error(http.StatusBadRequest, "userId is invalid", err) return response.Error(http.StatusBadRequest, "userId is invalid", err)
} }
if !hs.Cfg.FeatureToggles["accesscontrol"] { if !hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol) {
if err := hs.teamGuardian.CanAdmin(c.Req.Context(), orgId, teamId, c.SignedInUser); err != nil { if err := hs.teamGuardian.CanAdmin(c.Req.Context(), orgId, teamId, c.SignedInUser); err != nil {
return response.Error(403, "Not allowed to remove team member", err) return response.Error(403, "Not allowed to remove team member", err)
} }

View File

@@ -32,9 +32,7 @@ func (stub *testLogger) Warn(testMessage string, ctx ...interface{}) {
func TestTeamAPIEndpoint(t *testing.T) { func TestTeamAPIEndpoint(t *testing.T) {
t.Run("Given two teams", func(t *testing.T) { t.Run("Given two teams", func(t *testing.T) {
hs := &HTTPServer{ hs := setupSimpleHTTPServer(nil)
Cfg: setting.NewCfg(),
}
hs.SQLStore = sqlstore.InitTestDB(t) hs.SQLStore = sqlstore.InitTestDB(t)
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) {
@@ -73,9 +71,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) {
hs := &HTTPServer{ hs := setupSimpleHTTPServer(nil)
Cfg: setting.NewCfg(),
}
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/cmd/grafana-cli/logger" "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/runner" "github.com/grafana/grafana/pkg/cmd/grafana-cli/runner"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/utils" "github.com/grafana/grafana/pkg/cmd/grafana-cli/utils"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/ngalert/notifier" "github.com/grafana/grafana/pkg/services/ngalert/notifier"
"github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/services/secrets/manager" "github.com/grafana/grafana/pkg/services/secrets/manager"
@@ -178,7 +179,7 @@ func (s alertingSecret) reencrypt(secretsSrv *manager.SecretsService, sess *xorm
} }
func ReEncryptSecrets(_ utils.CommandLine, runner runner.Runner) error { func ReEncryptSecrets(_ utils.CommandLine, runner runner.Runner) error {
if !runner.SettingsProvider.IsFeatureToggleEnabled(secrets.EnvelopeEncryptionFeatureToggle) { if !runner.SettingsProvider.IsFeatureToggleEnabled(featuremgmt.FlagEnvelopeEncryption) {
logger.Warn("Envelope encryption is not enabled, quitting...") logger.Warn("Envelope encryption is not enabled, quitting...")
return nil return nil
} }

View File

@@ -12,6 +12,7 @@ import (
"github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/infra/localcache"
"github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/secrets"
secretsDatabase "github.com/grafana/grafana/pkg/services/secrets/database" secretsDatabase "github.com/grafana/grafana/pkg/services/secrets/database"
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager" secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
@@ -25,6 +26,8 @@ var wireSet = wire.NewSet(
localcache.ProvideService, localcache.ProvideService,
tracing.ProvideService, tracing.ProvideService,
bus.ProvideBus, bus.ProvideBus,
featuremgmt.ProvideManagerService,
featuremgmt.ProvideToggles,
wire.Bind(new(bus.Bus), new(*bus.InProcBus)), wire.Bind(new(bus.Bus), new(*bus.InProcBus)),
sqlstore.ProvideService, sqlstore.ProvideService,
wire.InterfaceValue(new(usagestats.Service), noOpUsageStats{}), wire.InterfaceValue(new(usagestats.Service), noOpUsageStats{}),

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.IsEnabled(featuremgmt.FlagHttpclientproviderAzureAuth) {
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.WithFeatures())
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.WithFeatures())
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.IsEnabled(featuremgmt.FlagDisableHttpRequestHistogram) {
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

@@ -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.WithFeatures()
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

@@ -36,6 +36,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"
@@ -183,6 +184,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,
resourceservices.ProvideResourceServices, resourceservices.ProvideResourceServices,
) )

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.IsEnabled(featuremgmt.FlagAccesscontrol)
} }
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.WithFeatures(featuremgmt.FlagAccesscontrol),
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.WithFeatures("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.WithFeatures(),
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.WithFeatures(featuremgmt.FlagAccesscontrol),
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.WithFeatures(featuremgmt.FlagAccesscontrol),
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.WithFeatures(featuremgmt.FlagAccesscontrol)
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,96 @@
package featuremgmt
import (
"bytes"
"encoding/json"
)
type FeatureToggles interface {
IsEnabled(flag string) bool
}
// FeatureFlagState indicates the quality level
type FeatureFlagState int
const (
// FeatureStateUnknown indicates that no state is specified
FeatureStateUnknown FeatureFlagState = 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 FeatureFlagState) 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 FeatureFlagState) 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 *FeatureFlagState) 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 FeatureFlagState `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
}

View File

@@ -0,0 +1,185 @@
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"
)
var (
_ FeatureToggles = (*FeatureManager)(nil)
)
type FeatureManager struct {
isDevMod bool
licensing models.Licensing
flags map[string]*FeatureFlag
enabled map[string]bool // only the "on" values
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 _, add := range flags {
if add.Name == "" {
continue // skip it with warning?
}
flag, ok := fm.flags[add.Name]
if !ok {
f := add // make a copy
fm.flags[add.Name] = &f
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
}
// 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}
}

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,118 @@
package featuremgmt
var (
// Register each toggle here
standardFeatureFlags = []FeatureFlag{
{
Name: "trimDefaults",
Description: "Use cue schema to remove values that will be applied automatically",
State: FeatureStateBeta,
},
{
Name: "envelopeEncryption",
Description: "encrypt secrets",
State: FeatureStateBeta,
},
{
Name: "httpclientprovider_azure_auth",
Description: "use http client for azure auth",
State: FeatureStateBeta,
},
{
Name: "service-accounts",
Description: "support service accounts",
State: FeatureStateBeta,
RequiresLicense: true,
},
{
Name: "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: "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 explore",
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 navigation model",
State: FeatureStateAlpha,
},
{
Name: "showFeatureFlagsInUI",
Description: "Show feature flags in the settings UI",
State: FeatureStateAlpha,
RequiresDevMode: true,
},
{
Name: "disable_http_request_histogram",
Description: "Do not create histograms for http requests",
State: FeatureStateAlpha,
},
{
Name: "validatedQueries",
Description: "only execute the query saved in a panel",
State: FeatureStateAlpha,
RequiresDevMode: true,
},
}
)

View File

@@ -0,0 +1,76 @@
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 mgmt
}

View File

@@ -0,0 +1,84 @@
package featuremgmt
import (
"testing"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/require"
)
func TestFeatureService(t *testing.T) {
license := stubLicenseServier{
flags: []FeatureFlag{
{
Name: "a.yes.default",
RequiresLicense: true,
Expression: "true",
},
{
Name: "a.yes",
RequiresLicense: true,
Expression: "",
},
{
Name: "b.no",
RequiresLicense: true,
},
},
enabled: map[string]bool{
"a.yes.default": true,
"a.yes": true,
},
}
require.False(t, license.FeatureEnabled("unknown"))
require.False(t, license.FeatureEnabled("b.no"))
require.True(t, license.FeatureEnabled("a.yes"))
require.True(t, license.FeatureEnabled("a.yes.default"))
cfg := setting.NewCfg()
mgmt, err := ProvideManagerService(cfg, license)
require.NoError(t, err)
require.NotNil(t, mgmt)
// Enterprise features do not fall though automatically
require.False(t, mgmt.IsEnabled("a.yes.default"))
require.False(t, mgmt.IsEnabled("a.yes")) // licensed, but not enabled
}
var (
_ models.Licensing = (*stubLicenseServier)(nil)
)
type stubLicenseServier struct {
flags []FeatureFlag
enabled map[string]bool
}
func (s stubLicenseServier) Expiry() int64 {
return 100
}
func (s stubLicenseServier) Edition() string {
return "test"
}
func (s stubLicenseServier) ContentDeliveryPrefix() string {
return ""
}
func (s stubLicenseServier) LicenseURL(showAdminLicensingPage bool) string {
return "http://??"
}
func (s stubLicenseServier) StateInfo() string {
return "ok"
}
func (s stubLicenseServier) EnabledFeatures() map[string]bool {
return map[string]bool{}
}
func (s stubLicenseServier) FeatureEnabled(feature string) bool {
return s.enabled[feature]
}

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,85 @@
// NOTE: This file is autogenerated
package featuremgmt
const (
// FlagTrimDefaults
// Use cue schema to remove values that will be applied automatically
FlagTrimDefaults = "trimDefaults"
// FlagEnvelopeEncryption
// encrypt secrets
FlagEnvelopeEncryption = "envelopeEncryption"
// FlagHttpclientproviderAzureAuth
// use http client for azure auth
FlagHttpclientproviderAzureAuth = "httpclientprovider_azure_auth"
// FlagServiceAccounts
// support service accounts
FlagServiceAccounts = "service-accounts"
// FlagDatabaseMetrics
// Add prometheus metrics for database tables
FlagDatabaseMetrics = "database_metrics"
// FlagDashboardPreviews
// Create and show thumbnails for dashboard search results
FlagDashboardPreviews = "dashboardPreviews"
// FlagLiveConfig
// Save grafana live configuration in SQL tables
FlagLiveConfig = "live-config"
// FlagLivePipeline
// enable a generic live processing pipeline
FlagLivePipeline = "live-pipeline"
// FlagLiveServiceWebWorker
// This will use a webworker thread to processes events rather than the main thread
FlagLiveServiceWebWorker = "live-service-web-worker"
// FlagQueryOverLive
// Use grafana live websocket to execute backend queries
FlagQueryOverLive = "queryOverLive"
// FlagTempoSearch
// Enable searching in tempo datasources
FlagTempoSearch = "tempoSearch"
// FlagTempoBackendSearch
// Use backend for tempo search
FlagTempoBackendSearch = "tempoBackendSearch"
// FlagTempoServiceGraph
// show service
FlagTempoServiceGraph = "tempoServiceGraph"
// FlagFullRangeLogsVolume
// Show full range logs volume in explore
FlagFullRangeLogsVolume = "fullRangeLogsVolume"
// FlagAccesscontrol
// Support robust access control
FlagAccesscontrol = "accesscontrol"
// FlagPrometheusAzureAuth
// Use azure authentication for prometheus datasource
FlagPrometheusAzureAuth = "prometheus_azure_auth"
// FlagNewNavigation
// Try the next gen navigation model
FlagNewNavigation = "newNavigation"
// FlagShowFeatureFlagsInUI
// Show feature flags in the settings UI
FlagShowFeatureFlagsInUI = "showFeatureFlagsInUI"
// FlagDisableHttpRequestHistogram
// Do not create histograms for http requests
FlagDisableHttpRequestHistogram = "disable_http_request_histogram"
// FlagValidatedQueries
// only execute the query saved in a panel
FlagValidatedQueries = "validatedQueries"
)

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(`
{{"\t"}}// Flag{{.CamelCase}}{{.Ext}}
{{"\t"}}Flag{{.CamelCase}} = "{{.Flag.Name}}"
`)
if err != nil {
t.Fatal("error reading template", "error", err.Error())
return ""
}
data := struct {
CamelCase string
Flag FeatureFlag
Ext string
}{
CamelCase: "?",
}
var buff bytes.Buffer
buff.WriteString(`// NOTE: This file is autogenerated
package featuremgmt
const (`)
for _, flag := range standardFeatureFlags {
data.CamelCase = asCamelCase(flag.Name)
data.Flag = flag
data.Ext = ""
if flag.Description != "" {
data.Ext += "\n\t// " + flag.Description
}
_ = tmpl.Execute(&buff, data)
}
buff.WriteString(")\n")
return buff.String()
}

View File

@@ -2,6 +2,7 @@ package osskmsproviders
import ( import (
"github.com/grafana/grafana/pkg/services/encryption" "github.com/grafana/grafana/pkg/services/encryption"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/kmsproviders" "github.com/grafana/grafana/pkg/services/kmsproviders"
grafana "github.com/grafana/grafana/pkg/services/kmsproviders/defaultprovider" grafana "github.com/grafana/grafana/pkg/services/kmsproviders/defaultprovider"
"github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/secrets"
@@ -21,7 +22,7 @@ func ProvideService(enc encryption.Internal, settings setting.Provider) Service
} }
func (s Service) Provide() (map[secrets.ProviderID]secrets.Provider, error) { func (s Service) Provide() (map[secrets.ProviderID]secrets.Provider, error) {
if !s.settings.IsFeatureToggleEnabled(secrets.EnvelopeEncryptionFeatureToggle) { if !s.settings.IsFeatureToggleEnabled(featuremgmt.FlagEnvelopeEncryption) {
return nil, nil return nil, nil
} }

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.IsEnabled(featuremgmt.FlagLivePipeline) {
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.IsEnabled(featuremgmt.FlagTrimDefaults)
} }
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.WithFeatures(featuremgmt.FlagEnvelopeEncryption)
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(featuremgmt.FlagEnvelopeEncryption))
assert.True(tb, features.IsEnabled(featuremgmt.FlagEnvelopeEncryption))
encryption := ossencryption.ProvideService() encryption := ossencryption.ProvideService()
secretsService, err := ProvideSecretsService( secretsService, err := ProvideSecretsService(

View File

@@ -12,6 +12,7 @@ import (
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/services/encryption" "github.com/grafana/grafana/pkg/services/encryption"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/kmsproviders" "github.com/grafana/grafana/pkg/services/kmsproviders"
"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"
@@ -43,7 +44,7 @@ func ProvideSecretsService(
} }
logger := log.New("secrets") logger := log.New("secrets")
enabled := settings.IsFeatureToggleEnabled(secrets.EnvelopeEncryptionFeatureToggle) enabled := settings.IsFeatureToggleEnabled(featuremgmt.FlagEnvelopeEncryption)
currentProviderID := readCurrentProviderID(settings) currentProviderID := readCurrentProviderID(settings)
if _, ok := providers[currentProviderID]; enabled && !ok { if _, ok := providers[currentProviderID]; enabled && !ok {
@@ -87,7 +88,7 @@ func (s *SecretsService) registerUsageMetrics() {
// Enabled / disabled // Enabled / disabled
usageMetrics["stats.encryption.envelope_encryption_enabled.count"] = 0 usageMetrics["stats.encryption.envelope_encryption_enabled.count"] = 0
if s.settings.IsFeatureToggleEnabled(secrets.EnvelopeEncryptionFeatureToggle) { if s.settings.IsFeatureToggleEnabled(featuremgmt.FlagEnvelopeEncryption) {
usageMetrics["stats.encryption.envelope_encryption_enabled.count"] = 1 usageMetrics["stats.encryption.envelope_encryption_enabled.count"] = 1
} }
@@ -130,11 +131,11 @@ func (s *SecretsService) Encrypt(ctx context.Context, payload []byte, opt secret
func (s *SecretsService) EncryptWithDBSession(ctx context.Context, payload []byte, opt secrets.EncryptionOptions, sess *xorm.Session) ([]byte, error) { func (s *SecretsService) EncryptWithDBSession(ctx context.Context, payload []byte, opt secrets.EncryptionOptions, sess *xorm.Session) ([]byte, error) {
// Use legacy encryption service if envelopeEncryptionFeatureToggle toggle is off // Use legacy encryption service if envelopeEncryptionFeatureToggle toggle is off
if !s.settings.IsFeatureToggleEnabled(secrets.EnvelopeEncryptionFeatureToggle) { if !s.settings.IsFeatureToggleEnabled(featuremgmt.FlagEnvelopeEncryption) {
return s.enc.Encrypt(ctx, payload, setting.SecretKey) return s.enc.Encrypt(ctx, payload, setting.SecretKey)
} }
// If encryption secrets.EnvelopeEncryptionFeatureToggle toggle is on, use envelope encryption // If encryption featuremgmt.FlagEnvelopeEncryption toggle is on, use envelope encryption
scope := opt() scope := opt()
keyName := s.keyName(scope) keyName := s.keyName(scope)
@@ -172,12 +173,12 @@ func (s *SecretsService) keyName(scope string) string {
} }
func (s *SecretsService) Decrypt(ctx context.Context, payload []byte) ([]byte, error) { func (s *SecretsService) Decrypt(ctx context.Context, payload []byte) ([]byte, error) {
// Use legacy encryption service if secrets.EnvelopeEncryptionFeatureToggle toggle is off // Use legacy encryption service if featuremgmt.FlagEnvelopeEncryption toggle is off
if !s.settings.IsFeatureToggleEnabled(secrets.EnvelopeEncryptionFeatureToggle) { if !s.settings.IsFeatureToggleEnabled(featuremgmt.FlagEnvelopeEncryption) {
return s.enc.Decrypt(ctx, payload, setting.SecretKey) return s.enc.Decrypt(ctx, payload, setting.SecretKey)
} }
// If encryption secrets.EnvelopeEncryptionFeatureToggle toggle is on, use envelope encryption // If encryption featuremgmt.FlagEnvelopeEncryption toggle is on, use envelope encryption
if len(payload) == 0 { if len(payload) == 0 {
return nil, fmt.Errorf("unable to decrypt empty payload") return nil, fmt.Errorf("unable to decrypt empty payload")
} }

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.WithFeatures(featuremgmt.FlagEnvelopeEncryption).IsEnabled,
}, },
} }
encr := ossencryption.ProvideService() encr := ossencryption.ProvideService()

View File

@@ -8,10 +8,6 @@ import (
"xorm.io/xorm" "xorm.io/xorm"
) )
const (
EnvelopeEncryptionFeatureToggle = "envelopeEncryption"
)
// Service is an envelope encryption service in charge of encrypting/decrypting secrets. // Service is an envelope encryption service in charge of encrypting/decrypting secrets.
// It is a replacement for encryption.Service // It is a replacement for encryption.Service
type Service interface { type Service interface {

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.IsEnabled(featuremgmt.FlagServiceAccounts) {
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.WithFeatures(featuremgmt.FlagServiceAccounts))
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.IsEnabled(featuremgmt.FlagServiceAccounts) {
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.IsEnabled(featuremgmt.FlagServiceAccounts) {
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.WithFeatures("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.WithFeatures("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 != nil && mg.Cfg.IsFeatureToggleEnabled != nil {
if mg.Cfg.IsLiveConfigEnabled() { if mg.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagLiveConfig) {
addLiveChannelMigrations(mg) addLiveChannelMigrations(mg)
} }
} }

View File

@@ -9,6 +9,7 @@ import (
"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/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
) )
@@ -117,7 +118,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(featuremgmt.FlagAccesscontrol) {
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 +181,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(featuremgmt.FlagAccesscontrol) {
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.WithFeatures(featuremgmt.FlagAccesscontrol).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.WithFeatures(featuremgmt.FlagAccesscontrol).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"
@@ -330,7 +331,7 @@ func (ss *SQLStore) initEngine(engine *xorm.Engine) error {
return err return err
} }
if ss.Cfg.IsDatabaseMetricsEnabled() { if ss.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagDatabaseMetrics) {
ss.dbCfg.Type = WrapDatabaseDriverWithHooks(ss.dbCfg.Type, ss.tracer) ss.dbCfg.Type = WrapDatabaseDriverWithHooks(ss.dbCfg.Type, ss.tracer)
} }
@@ -496,6 +497,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.IsEnabled(featuremgmt.FlagDashboardPreviews) {
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

@@ -10,6 +10,7 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/services/featuremgmt"
) )
var random20HzStreamRegex = regexp.MustCompile(`random-20Hz-stream(-\d+)?`) var random20HzStreamRegex = regexp.MustCompile(`random-20Hz-stream(-\d+)?`)
@@ -37,7 +38,7 @@ func (s *Service) SubscribeStream(_ context.Context, req *backend.SubscribeStrea
} }
} }
if s.cfg.FeatureToggles["live-pipeline"] { if s.features.IsEnabled(featuremgmt.FlagLivePipeline) {
// While developing Live pipeline avoid sending initial data. // While developing Live pipeline avoid sending initial data.
initialData = nil initialData = nil
} }
@@ -126,7 +127,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.IsEnabled(featuremgmt.FlagLivePipeline) {
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

@@ -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 };