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

This commit is contained in:
Ryan McKinley
2022-01-20 13:42:05 -08:00
committed by GitHub
parent 7fbc7d019a
commit f94c0decbd
65 changed files with 1244 additions and 252 deletions
+4 -1
View File
@@ -437,7 +437,7 @@ func (hs *HTTPServer) registerRoutes() {
// Some channels may have info
liveRoute.Get("/info/*", routing.Wrap(hs.Live.HandleInfoHTTP))
if hs.Cfg.FeatureToggles["live-pipeline"] {
if hs.Features.Toggles().IsLivePipelineEnabled() {
// POST Live data to be processed according to channel rules.
liveRoute.Post("/pipeline/push/*", hs.LivePushGateway.HandlePipelinePush)
liveRoute.Post("/pipeline-convert-test", routing.Wrap(hs.Live.HandlePipelineConvertTestHTTP), reqOrgAdmin)
@@ -460,6 +460,9 @@ func (hs *HTTPServer) registerRoutes() {
// admin api
r.Group("/api/admin", func(adminRoute routing.RouteRegister) {
adminRoute.Get("/settings", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionSettingsRead)), routing.Wrap(hs.AdminGetSettings))
if hs.Features.Toggles().IsShowFeatureFlagsInUIEnabled() {
adminRoute.Get("/settings/features", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionSettingsRead)), hs.Features.HandleGetSettings)
}
adminRoute.Get("/stats", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionServerStatsRead)), routing.Wrap(AdminGetStats))
adminRoute.Post("/pause-all-alerts", reqGrafanaAdmin, routing.Wrap(PauseAllAlerts))
+2 -2
View File
@@ -83,7 +83,7 @@ func (hs *HTTPServer) AddAPIKey(c *models.ReqContext) response.Response {
}
cmd.OrgId = c.OrgId
var err error
if hs.Cfg.FeatureToggles["service-accounts"] {
if hs.Features.Toggles().IsServiceAccountsEnabled() {
// Api keys should now be created with addadditionalapikey endpoint
return response.Error(400, "API keys should now be added via the AdditionalAPIKey endpoint.", err)
}
@@ -120,7 +120,7 @@ func (hs *HTTPServer) AdditionalAPIKey(c *models.ReqContext) response.Response {
if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
if !hs.Cfg.FeatureToggles["service-accounts"] {
if !hs.Features.Toggles().IsServiceAccountsEnabled() {
return response.Error(500, "Requires services-accounts feature", errors.New("feature missing"))
}
+25 -7
View File
@@ -28,6 +28,7 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol"
"github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/contexthandler"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/services/searchusers"
@@ -213,8 +214,8 @@ func (s *fakeRenderService) Init() error {
}
func setupAccessControlScenarioContext(t *testing.T, cfg *setting.Cfg, url string, permissions []*accesscontrol.Permission) (*scenarioContext, *HTTPServer) {
cfg.FeatureToggles = make(map[string]bool)
cfg.FeatureToggles["accesscontrol"] = true
features := featuremgmt.WithFeatures("accesscontrol")
cfg.IsFeatureToggleEnabled = features.IsEnabled
cfg.Quota.Enabled = false
bus := bus.GetBus()
@@ -222,6 +223,7 @@ func setupAccessControlScenarioContext(t *testing.T, cfg *setting.Cfg, url strin
Cfg: cfg,
Bus: bus,
Live: newTestLive(t),
Features: features,
QuotaService: &quota.QuotaService{Cfg: cfg},
RouteRegister: routing.NewRouteRegister(),
AccessControl: accesscontrolmock.New().WithPermissions(permissions),
@@ -296,13 +298,25 @@ func setInitCtxSignedInOrgAdmin(initCtx *models.ReqContext) {
initCtx.SignedInUser = &models.SignedInUser{UserId: testUserID, OrgId: 1, OrgRole: models.ROLE_ADMIN, Login: testUserLogin}
}
func setupSimpleHTTPServer(features *featuremgmt.FeatureManager) *HTTPServer {
if features == nil {
features = featuremgmt.WithFeatures()
}
cfg := setting.NewCfg()
cfg.IsFeatureToggleEnabled = features.IsEnabled
return &HTTPServer{
Cfg: cfg,
Features: features,
Bus: bus.GetBus(),
}
}
func setupHTTPServer(t *testing.T, useFakeAccessControl bool, enableAccessControl bool) accessControlScenarioContext {
// Use a new conf
features := featuremgmt.WithFeatures("accesscontrol", enableAccessControl)
cfg := setting.NewCfg()
cfg.FeatureToggles = make(map[string]bool)
if enableAccessControl {
cfg.FeatureToggles["accesscontrol"] = enableAccessControl
}
cfg.IsFeatureToggleEnabled = features.IsEnabled
return setupHTTPServerWithCfg(t, useFakeAccessControl, enableAccessControl, cfg)
}
@@ -310,6 +324,9 @@ func setupHTTPServer(t *testing.T, useFakeAccessControl bool, enableAccessContro
func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessControl bool, cfg *setting.Cfg) accessControlScenarioContext {
t.Helper()
features := featuremgmt.WithFeatures("accesscontrol", enableAccessControl)
cfg.IsFeatureToggleEnabled = features.IsEnabled
var acmock *accesscontrolmock.Mock
var ac *ossaccesscontrol.OSSAccessControlService
@@ -322,6 +339,7 @@ func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessCont
// Create minimal HTTP Server
hs := &HTTPServer{
Cfg: cfg,
Features: features,
Bus: bus,
Live: newTestLive(t),
QuotaService: &quota.QuotaService{Cfg: cfg},
@@ -338,7 +356,7 @@ func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessCont
}
hs.AccessControl = acmock
} else {
ac = ossaccesscontrol.ProvideService(cfg, &usagestats.UsageStatsMock{T: t})
ac = ossaccesscontrol.ProvideService(hs.Features.Toggles(), &usagestats.UsageStatsMock{T: t})
hs.AccessControl = ac
// Perform role registration
err := hs.declareFixedRoles()
+11 -1
View File
@@ -20,6 +20,7 @@ import (
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/libraryelements"
"github.com/grafana/grafana/pkg/services/live"
"github.com/grafana/grafana/pkg/services/provisioning"
@@ -88,8 +89,17 @@ type testState struct {
}
func newTestLive(t *testing.T) *live.GrafanaLive {
features := featuremgmt.WithToggles()
cfg := &setting.Cfg{AppURL: "http://localhost:3000/"}
gLive, err := live.ProvideService(nil, cfg, routing.NewRouteRegister(), nil, nil, nil, sqlstore.InitTestDB(t), nil, &usagestats.UsageStatsMock{T: t}, nil)
cfg.IsFeatureToggleEnabled = features.IsEnabled
gLive, err := live.ProvideService(nil, cfg,
routing.NewRouteRegister(),
nil, nil, nil,
sqlstore.InitTestDB(t),
nil,
&usagestats.UsageStatsMock{T: t},
nil,
features)
require.NoError(t, err)
return gLive
}
+5 -6
View File
@@ -243,13 +243,12 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i
"env": setting.Env,
},
"licenseInfo": map[string]interface{}{
"expiry": hs.License.Expiry(),
"stateInfo": hs.License.StateInfo(),
"licenseUrl": hs.License.LicenseURL(hasAccess(accesscontrol.ReqGrafanaAdmin, accesscontrol.LicensingPageReaderAccess)),
"edition": hs.License.Edition(),
"enabledFeatures": hs.License.EnabledFeatures(),
"expiry": hs.License.Expiry(),
"stateInfo": hs.License.StateInfo(),
"licenseUrl": hs.License.LicenseURL(hasAccess(accesscontrol.ReqGrafanaAdmin, accesscontrol.LicensingPageReaderAccess)),
"edition": hs.License.Edition(),
},
"featureToggles": hs.Cfg.FeatureToggles,
"featureToggles": hs.Features.GetEnabled(c.Req.Context()),
"rendererAvailable": hs.RenderService.IsAvailable(),
"rendererVersion": hs.RenderService.Version(),
"http2Enabled": hs.Cfg.Protocol == setting.HTTP2Scheme,
+9 -5
View File
@@ -9,6 +9,7 @@ import (
"github.com/grafana/grafana/pkg/bus"
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/licensing"
"github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/services/sqlstore"
@@ -19,9 +20,10 @@ import (
"github.com/stretchr/testify/require"
)
func setupTestEnvironment(t *testing.T, cfg *setting.Cfg) (*web.Mux, *HTTPServer) {
func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features *featuremgmt.FeatureManager) (*web.Mux, *HTTPServer) {
t.Helper()
sqlstore.InitTestDB(t)
cfg.IsFeatureToggleEnabled = features.IsEnabled
{
oldVersion := setting.BuildVersion
@@ -37,9 +39,10 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg) (*web.Mux, *HTTPServer
sqlStore := sqlstore.InitTestDB(t)
hs := &HTTPServer{
Cfg: cfg,
Bus: bus.GetBus(),
License: &licensing.OSSLicensingService{Cfg: cfg},
Cfg: cfg,
Features: features,
Bus: bus.GetBus(),
License: &licensing.OSSLicensingService{Cfg: cfg},
RenderService: &rendering.RenderingService{
Cfg: cfg,
RendererPluginManager: &fakeRendererManager{},
@@ -73,7 +76,8 @@ func TestHTTPServer_GetFrontendSettings_hideVersionAnonymous(t *testing.T) {
cfg.Env = "testing"
cfg.BuildVersion = "7.8.9"
cfg.BuildCommit = "01234567"
m, hs := setupTestEnvironment(t, cfg)
m, hs := setupTestEnvironment(t, cfg, featuremgmt.WithFeatures())
req := httptest.NewRequest(http.MethodGet, "/api/frontend/settings", nil)
+4 -1
View File
@@ -13,6 +13,7 @@ import (
"strings"
"sync"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/query"
"github.com/grafana/grafana/pkg/services/serviceaccounts"
"github.com/grafana/grafana/pkg/services/thumbs"
@@ -75,6 +76,7 @@ type HTTPServer struct {
Bus bus.Bus
RenderService rendering.Service
Cfg *setting.Cfg
Features *featuremgmt.FeatureManager
SettingsProvider setting.Provider
HooksService *hooks.HooksService
CacheService *localcache.CacheService
@@ -135,7 +137,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
loginService login.Service, accessControl accesscontrol.AccessControl,
dataSourceProxy *datasourceproxy.DataSourceProxyService, searchService *search.SearchService,
live *live.GrafanaLive, livePushGateway *pushhttp.Gateway, plugCtxProvider *plugincontext.Provider,
contextHandler *contexthandler.ContextHandler,
contextHandler *contexthandler.ContextHandler, features *featuremgmt.FeatureManager,
schemaService *schemaloader.SchemaLoaderService, alertNG *ngalert.AlertNG,
libraryPanelService librarypanels.Service, libraryElementService libraryelements.Service,
quotaService *quota.QuotaService, socialService social.Service, tracer tracing.Tracer,
@@ -167,6 +169,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
AuthTokenService: userTokenService,
cleanUpService: cleanUpService,
ShortURLService: shortURLService,
Features: features,
ThumbService: thumbService,
RemoteCacheService: remoteCache,
ProvisioningService: provisioningService,
+11 -11
View File
@@ -85,7 +85,7 @@ func (hs *HTTPServer) getAppLinks(c *models.ReqContext) ([]*dtos.NavLink, error)
SortWeight: dtos.WeightPlugin,
}
if hs.Cfg.IsNewNavigationEnabled() {
if hs.Features.Toggles().IsNewNavigationEnabled() {
appLink.Section = dtos.NavSectionPlugin
} else {
appLink.Section = dtos.NavSectionCore
@@ -143,7 +143,7 @@ func (hs *HTTPServer) getAppLinks(c *models.ReqContext) ([]*dtos.NavLink, error)
}
func enableServiceAccount(hs *HTTPServer, c *models.ReqContext) bool {
return c.OrgRole == models.ROLE_ADMIN && hs.Cfg.IsServiceAccountEnabled() && hs.serviceAccountsService.Migrated(c.Req.Context(), c.OrgId)
return c.OrgRole == models.ROLE_ADMIN && hs.Features.Toggles().IsServiceAccountsEnabled() && hs.serviceAccountsService.Migrated(c.Req.Context(), c.OrgId)
}
func enableTeams(hs *HTTPServer, c *models.ReqContext) bool {
@@ -154,7 +154,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
hasAccess := ac.HasAccess(hs.AccessControl, c)
navTree := []*dtos.NavLink{}
if hs.Cfg.IsNewNavigationEnabled() {
if hs.Features.Toggles().IsNewNavigationEnabled() {
navTree = append(navTree, &dtos.NavLink{
Text: "Home",
Id: "home",
@@ -165,7 +165,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
})
}
if hasEditPerm && !hs.Cfg.IsNewNavigationEnabled() {
if hasEditPerm && !hs.Features.Toggles().IsNewNavigationEnabled() {
children := hs.buildCreateNavLinks(c)
navTree = append(navTree, &dtos.NavLink{
Text: "Create",
@@ -181,7 +181,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
dashboardChildLinks := hs.buildDashboardNavLinks(c, hasEditPerm)
dashboardsUrl := "/"
if hs.Cfg.IsNewNavigationEnabled() {
if hs.Features.Toggles().IsNewNavigationEnabled() {
dashboardsUrl = "/dashboards"
}
@@ -312,7 +312,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
})
}
if hs.Cfg.FeatureToggles["live-pipeline"] {
if hs.Features.Toggles().IsLivePipelineEnabled() {
liveNavLinks := []*dtos.NavLink{}
liveNavLinks = append(liveNavLinks, &dtos.NavLink{
@@ -346,7 +346,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
SortWeight: dtos.WeightConfig,
Children: configNodes,
}
if hs.Cfg.IsNewNavigationEnabled() {
if hs.Features.Toggles().IsNewNavigationEnabled() {
configNode.Section = dtos.NavSectionConfig
} else {
configNode.Section = dtos.NavSectionCore
@@ -358,7 +358,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
if len(adminNavLinks) > 0 {
navSection := dtos.NavSectionCore
if hs.Cfg.IsNewNavigationEnabled() {
if hs.Features.Toggles().IsNewNavigationEnabled() {
navSection = dtos.NavSectionConfig
}
serverAdminNode := navlinks.GetServerAdminNode(adminNavLinks, navSection)
@@ -386,7 +386,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
func (hs *HTTPServer) buildDashboardNavLinks(c *models.ReqContext, hasEditPerm bool) []*dtos.NavLink {
dashboardChildNavs := []*dtos.NavLink{}
if !hs.Cfg.IsNewNavigationEnabled() {
if !hs.Features.Toggles().IsNewNavigationEnabled() {
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{
Text: "Home", Id: "home", Url: hs.Cfg.AppSubURL + "/", Icon: "home-alt", HideFromTabs: true,
})
@@ -417,7 +417,7 @@ func (hs *HTTPServer) buildDashboardNavLinks(c *models.ReqContext, hasEditPerm b
})
}
if hasEditPerm && hs.Cfg.IsNewNavigationEnabled() {
if hasEditPerm && hs.Features.Toggles().IsNewNavigationEnabled() {
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{
Text: "Divider", Divider: true, Id: "divider", HideFromTabs: true,
})
@@ -622,7 +622,7 @@ func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewDat
LoadingLogo: "public/img/grafana_icon.svg",
}
if hs.Cfg.FeatureToggles["accesscontrol"] {
if hs.Features.Toggles().IsAccesscontrolEnabled() {
userPermissions, err := hs.AccessControl.GetUserPermissions(c.Req.Context(), c.SignedInUser)
if err != nil {
return nil, err
+1 -1
View File
@@ -89,7 +89,7 @@ func (hs *HTTPServer) CreateOrg(c *models.ReqContext) response.Response {
if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
acEnabled := hs.Cfg.FeatureToggles["accesscontrol"]
acEnabled := hs.Features.Toggles().IsAccesscontrolEnabled()
if !acEnabled && !(setting.AllowUserOrgCreate || c.IsGrafanaAdmin) {
return response.Error(403, "Access denied", nil)
}
+3 -2
View File
@@ -16,6 +16,7 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
@@ -34,8 +35,8 @@ func setUpGetOrgUsersDB(t *testing.T, sqlStore *sqlstore.SQLStore) {
}
func TestOrgUsersAPIEndpoint_userLoggedIn(t *testing.T) {
settings := setting.NewCfg()
hs := &HTTPServer{Cfg: settings}
hs := setupSimpleHTTPServer(featuremgmt.WithFeatures())
settings := hs.Cfg
sqlStore := sqlstore.InitTestDB(t)
sqlStore.Cfg = settings
+3 -3
View File
@@ -5,7 +5,7 @@ import (
"strings"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/web"
)
@@ -52,8 +52,8 @@ type RouteRegister interface {
type RegisterNamedMiddleware func(name string) web.Handler
func ProvideRegister(cfg *setting.Cfg) *RouteRegisterImpl {
return NewRouteRegister(middleware.ProvideRouteOperationName, middleware.RequestMetrics(cfg))
func ProvideRegister(features *featuremgmt.FeatureToggles) *RouteRegisterImpl {
return NewRouteRegister(middleware.ProvideRouteOperationName, middleware.RequestMetrics(features))
}
// NewRouteRegister creates a new RouteRegister with all middlewares sent as params
+1 -1
View File
@@ -20,7 +20,7 @@ func (hs *HTTPServer) CreateTeam(c *models.ReqContext) response.Response {
if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
accessControlEnabled := hs.Cfg.FeatureToggles["accesscontrol"]
accessControlEnabled := hs.Features.Toggles().IsAccesscontrolEnabled()
if !accessControlEnabled && c.OrgRole == models.ROLE_VIEWER {
return response.Error(403, "Not allowed to create team.", nil)
}
+2 -7
View File
@@ -40,9 +40,7 @@ func TestTeamAPIEndpoint(t *testing.T) {
TotalCount: 2,
}
hs := &HTTPServer{
Cfg: setting.NewCfg(),
}
hs := setupSimpleHTTPServer(nil)
loggedInUserScenario(t, "When calling GET on", "/api/teams/search", "/api/teams/search", func(sc *scenarioContext) {
var sentLimit int
@@ -92,10 +90,7 @@ func TestTeamAPIEndpoint(t *testing.T) {
t.Run("When creating team with API key", func(t *testing.T) {
defer bus.ClearBusHandlers()
hs := &HTTPServer{
Cfg: setting.NewCfg(),
Bus: bus.GetBus(),
}
hs := setupSimpleHTTPServer(nil)
hs.Cfg.EditorsCanAdmin = true
teamName := "team foo"