mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
FeatureFlags: manage feature flags outside of settings.Cfg (#43692)
This commit is contained in:
parent
7fbc7d019a
commit
f94c0decbd
1
.gitignore
vendored
1
.gitignore
vendored
@ -156,6 +156,7 @@ compilation-stats.json
|
||||
|
||||
# auto generated Go files
|
||||
*_gen.go
|
||||
!pkg/services/featuremgmt/toggles_gen.go
|
||||
|
||||
# Auto-generated localisation files
|
||||
public/locales/_build/
|
||||
|
@ -40,7 +40,6 @@ export interface LicenseInfo {
|
||||
licenseUrl: string;
|
||||
stateInfo: string;
|
||||
edition: GrafanaEdition;
|
||||
enabledFeatures: { [key: string]: boolean };
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,3 +1,9 @@
|
||||
// NOTE: This file was auto generated. DO NOT EDIT DIRECTLY!
|
||||
// To change feature flags, edit:
|
||||
// pkg/services/featuremgmt/registry.go
|
||||
// Then run tests in:
|
||||
// pkg/services/featuremgmt/toggles_gen_test.go
|
||||
|
||||
/**
|
||||
* Describes available feature toggles in Grafana. These can be configured via
|
||||
* conf/custom.ini to enable features under development or not yet available in
|
||||
|
@ -1,6 +1,12 @@
|
||||
import { FeatureToggles } from '@grafana/data';
|
||||
import { config } from '../config';
|
||||
|
||||
export const featureEnabled = (feature: string): boolean => {
|
||||
const { enabledFeatures } = config.licenseInfo;
|
||||
return enabledFeatures && enabledFeatures[feature];
|
||||
export const featureEnabled = (feature: boolean | undefined | keyof FeatureToggles): boolean => {
|
||||
if (feature === true || feature === false) {
|
||||
return feature;
|
||||
}
|
||||
if (feature == null || !config?.featureToggles) {
|
||||
return false;
|
||||
}
|
||||
return Boolean(config.featureToggles[feature]);
|
||||
};
|
||||
|
@ -437,7 +437,7 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
// Some channels may have info
|
||||
liveRoute.Get("/info/*", routing.Wrap(hs.Live.HandleInfoHTTP))
|
||||
|
||||
if hs.Cfg.FeatureToggles["live-pipeline"] {
|
||||
if hs.Features.Toggles().IsLivePipelineEnabled() {
|
||||
// POST Live data to be processed according to channel rules.
|
||||
liveRoute.Post("/pipeline/push/*", hs.LivePushGateway.HandlePipelinePush)
|
||||
liveRoute.Post("/pipeline-convert-test", routing.Wrap(hs.Live.HandlePipelineConvertTestHTTP), reqOrgAdmin)
|
||||
@ -460,6 +460,9 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
// admin api
|
||||
r.Group("/api/admin", func(adminRoute routing.RouteRegister) {
|
||||
adminRoute.Get("/settings", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionSettingsRead)), routing.Wrap(hs.AdminGetSettings))
|
||||
if hs.Features.Toggles().IsShowFeatureFlagsInUIEnabled() {
|
||||
adminRoute.Get("/settings/features", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionSettingsRead)), hs.Features.HandleGetSettings)
|
||||
}
|
||||
adminRoute.Get("/stats", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionServerStatsRead)), routing.Wrap(AdminGetStats))
|
||||
adminRoute.Post("/pause-all-alerts", reqGrafanaAdmin, routing.Wrap(PauseAllAlerts))
|
||||
|
||||
|
@ -83,7 +83,7 @@ func (hs *HTTPServer) AddAPIKey(c *models.ReqContext) response.Response {
|
||||
}
|
||||
cmd.OrgId = c.OrgId
|
||||
var err error
|
||||
if hs.Cfg.FeatureToggles["service-accounts"] {
|
||||
if hs.Features.Toggles().IsServiceAccountsEnabled() {
|
||||
// Api keys should now be created with addadditionalapikey endpoint
|
||||
return response.Error(400, "API keys should now be added via the AdditionalAPIKey endpoint.", err)
|
||||
}
|
||||
@ -120,7 +120,7 @@ func (hs *HTTPServer) AdditionalAPIKey(c *models.ReqContext) response.Response {
|
||||
if err := web.Bind(c.Req, &cmd); err != nil {
|
||||
return response.Error(http.StatusBadRequest, "bad request data", err)
|
||||
}
|
||||
if !hs.Cfg.FeatureToggles["service-accounts"] {
|
||||
if !hs.Features.Toggles().IsServiceAccountsEnabled() {
|
||||
return response.Error(500, "Requires services-accounts feature", errors.New("feature missing"))
|
||||
}
|
||||
|
||||
|
@ -28,6 +28,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/auth"
|
||||
"github.com/grafana/grafana/pkg/services/contexthandler"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/quota"
|
||||
"github.com/grafana/grafana/pkg/services/rendering"
|
||||
"github.com/grafana/grafana/pkg/services/searchusers"
|
||||
@ -213,8 +214,8 @@ func (s *fakeRenderService) Init() error {
|
||||
}
|
||||
|
||||
func setupAccessControlScenarioContext(t *testing.T, cfg *setting.Cfg, url string, permissions []*accesscontrol.Permission) (*scenarioContext, *HTTPServer) {
|
||||
cfg.FeatureToggles = make(map[string]bool)
|
||||
cfg.FeatureToggles["accesscontrol"] = true
|
||||
features := featuremgmt.WithFeatures("accesscontrol")
|
||||
cfg.IsFeatureToggleEnabled = features.IsEnabled
|
||||
cfg.Quota.Enabled = false
|
||||
|
||||
bus := bus.GetBus()
|
||||
@ -222,6 +223,7 @@ func setupAccessControlScenarioContext(t *testing.T, cfg *setting.Cfg, url strin
|
||||
Cfg: cfg,
|
||||
Bus: bus,
|
||||
Live: newTestLive(t),
|
||||
Features: features,
|
||||
QuotaService: "a.QuotaService{Cfg: cfg},
|
||||
RouteRegister: routing.NewRouteRegister(),
|
||||
AccessControl: accesscontrolmock.New().WithPermissions(permissions),
|
||||
@ -296,13 +298,25 @@ func setInitCtxSignedInOrgAdmin(initCtx *models.ReqContext) {
|
||||
initCtx.SignedInUser = &models.SignedInUser{UserId: testUserID, OrgId: 1, OrgRole: models.ROLE_ADMIN, Login: testUserLogin}
|
||||
}
|
||||
|
||||
func setupSimpleHTTPServer(features *featuremgmt.FeatureManager) *HTTPServer {
|
||||
if features == nil {
|
||||
features = featuremgmt.WithFeatures()
|
||||
}
|
||||
cfg := setting.NewCfg()
|
||||
cfg.IsFeatureToggleEnabled = features.IsEnabled
|
||||
|
||||
return &HTTPServer{
|
||||
Cfg: cfg,
|
||||
Features: features,
|
||||
Bus: bus.GetBus(),
|
||||
}
|
||||
}
|
||||
|
||||
func setupHTTPServer(t *testing.T, useFakeAccessControl bool, enableAccessControl bool) accessControlScenarioContext {
|
||||
// Use a new conf
|
||||
features := featuremgmt.WithFeatures("accesscontrol", enableAccessControl)
|
||||
cfg := setting.NewCfg()
|
||||
cfg.FeatureToggles = make(map[string]bool)
|
||||
if enableAccessControl {
|
||||
cfg.FeatureToggles["accesscontrol"] = enableAccessControl
|
||||
}
|
||||
cfg.IsFeatureToggleEnabled = features.IsEnabled
|
||||
|
||||
return setupHTTPServerWithCfg(t, useFakeAccessControl, enableAccessControl, cfg)
|
||||
}
|
||||
@ -310,6 +324,9 @@ func setupHTTPServer(t *testing.T, useFakeAccessControl bool, enableAccessContro
|
||||
func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessControl bool, cfg *setting.Cfg) accessControlScenarioContext {
|
||||
t.Helper()
|
||||
|
||||
features := featuremgmt.WithFeatures("accesscontrol", enableAccessControl)
|
||||
cfg.IsFeatureToggleEnabled = features.IsEnabled
|
||||
|
||||
var acmock *accesscontrolmock.Mock
|
||||
var ac *ossaccesscontrol.OSSAccessControlService
|
||||
|
||||
@ -322,6 +339,7 @@ func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessCont
|
||||
// Create minimal HTTP Server
|
||||
hs := &HTTPServer{
|
||||
Cfg: cfg,
|
||||
Features: features,
|
||||
Bus: bus,
|
||||
Live: newTestLive(t),
|
||||
QuotaService: "a.QuotaService{Cfg: cfg},
|
||||
@ -338,7 +356,7 @@ func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessCont
|
||||
}
|
||||
hs.AccessControl = acmock
|
||||
} else {
|
||||
ac = ossaccesscontrol.ProvideService(cfg, &usagestats.UsageStatsMock{T: t})
|
||||
ac = ossaccesscontrol.ProvideService(hs.Features.Toggles(), &usagestats.UsageStatsMock{T: t})
|
||||
hs.AccessControl = ac
|
||||
// Perform role registration
|
||||
err := hs.declareFixedRoles()
|
||||
|
@ -20,6 +20,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/libraryelements"
|
||||
"github.com/grafana/grafana/pkg/services/live"
|
||||
"github.com/grafana/grafana/pkg/services/provisioning"
|
||||
@ -88,8 +89,17 @@ type testState struct {
|
||||
}
|
||||
|
||||
func newTestLive(t *testing.T) *live.GrafanaLive {
|
||||
features := featuremgmt.WithToggles()
|
||||
cfg := &setting.Cfg{AppURL: "http://localhost:3000/"}
|
||||
gLive, err := live.ProvideService(nil, cfg, routing.NewRouteRegister(), nil, nil, nil, sqlstore.InitTestDB(t), nil, &usagestats.UsageStatsMock{T: t}, nil)
|
||||
cfg.IsFeatureToggleEnabled = features.IsEnabled
|
||||
gLive, err := live.ProvideService(nil, cfg,
|
||||
routing.NewRouteRegister(),
|
||||
nil, nil, nil,
|
||||
sqlstore.InitTestDB(t),
|
||||
nil,
|
||||
&usagestats.UsageStatsMock{T: t},
|
||||
nil,
|
||||
features)
|
||||
require.NoError(t, err)
|
||||
return gLive
|
||||
}
|
||||
|
@ -243,13 +243,12 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i
|
||||
"env": setting.Env,
|
||||
},
|
||||
"licenseInfo": map[string]interface{}{
|
||||
"expiry": hs.License.Expiry(),
|
||||
"stateInfo": hs.License.StateInfo(),
|
||||
"licenseUrl": hs.License.LicenseURL(hasAccess(accesscontrol.ReqGrafanaAdmin, accesscontrol.LicensingPageReaderAccess)),
|
||||
"edition": hs.License.Edition(),
|
||||
"enabledFeatures": hs.License.EnabledFeatures(),
|
||||
"expiry": hs.License.Expiry(),
|
||||
"stateInfo": hs.License.StateInfo(),
|
||||
"licenseUrl": hs.License.LicenseURL(hasAccess(accesscontrol.ReqGrafanaAdmin, accesscontrol.LicensingPageReaderAccess)),
|
||||
"edition": hs.License.Edition(),
|
||||
},
|
||||
"featureToggles": hs.Cfg.FeatureToggles,
|
||||
"featureToggles": hs.Features.GetEnabled(c.Req.Context()),
|
||||
"rendererAvailable": hs.RenderService.IsAvailable(),
|
||||
"rendererVersion": hs.RenderService.Version(),
|
||||
"http2Enabled": hs.Cfg.Protocol == setting.HTTP2Scheme,
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/licensing"
|
||||
"github.com/grafana/grafana/pkg/services/rendering"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
@ -19,9 +20,10 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func setupTestEnvironment(t *testing.T, cfg *setting.Cfg) (*web.Mux, *HTTPServer) {
|
||||
func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features *featuremgmt.FeatureManager) (*web.Mux, *HTTPServer) {
|
||||
t.Helper()
|
||||
sqlstore.InitTestDB(t)
|
||||
cfg.IsFeatureToggleEnabled = features.IsEnabled
|
||||
|
||||
{
|
||||
oldVersion := setting.BuildVersion
|
||||
@ -37,9 +39,10 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg) (*web.Mux, *HTTPServer
|
||||
sqlStore := sqlstore.InitTestDB(t)
|
||||
|
||||
hs := &HTTPServer{
|
||||
Cfg: cfg,
|
||||
Bus: bus.GetBus(),
|
||||
License: &licensing.OSSLicensingService{Cfg: cfg},
|
||||
Cfg: cfg,
|
||||
Features: features,
|
||||
Bus: bus.GetBus(),
|
||||
License: &licensing.OSSLicensingService{Cfg: cfg},
|
||||
RenderService: &rendering.RenderingService{
|
||||
Cfg: cfg,
|
||||
RendererPluginManager: &fakeRendererManager{},
|
||||
@ -73,7 +76,8 @@ func TestHTTPServer_GetFrontendSettings_hideVersionAnonymous(t *testing.T) {
|
||||
cfg.Env = "testing"
|
||||
cfg.BuildVersion = "7.8.9"
|
||||
cfg.BuildCommit = "01234567"
|
||||
m, hs := setupTestEnvironment(t, cfg)
|
||||
|
||||
m, hs := setupTestEnvironment(t, cfg, featuremgmt.WithFeatures())
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/frontend/settings", nil)
|
||||
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/query"
|
||||
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
||||
"github.com/grafana/grafana/pkg/services/thumbs"
|
||||
@ -75,6 +76,7 @@ type HTTPServer struct {
|
||||
Bus bus.Bus
|
||||
RenderService rendering.Service
|
||||
Cfg *setting.Cfg
|
||||
Features *featuremgmt.FeatureManager
|
||||
SettingsProvider setting.Provider
|
||||
HooksService *hooks.HooksService
|
||||
CacheService *localcache.CacheService
|
||||
@ -135,7 +137,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
|
||||
loginService login.Service, accessControl accesscontrol.AccessControl,
|
||||
dataSourceProxy *datasourceproxy.DataSourceProxyService, searchService *search.SearchService,
|
||||
live *live.GrafanaLive, livePushGateway *pushhttp.Gateway, plugCtxProvider *plugincontext.Provider,
|
||||
contextHandler *contexthandler.ContextHandler,
|
||||
contextHandler *contexthandler.ContextHandler, features *featuremgmt.FeatureManager,
|
||||
schemaService *schemaloader.SchemaLoaderService, alertNG *ngalert.AlertNG,
|
||||
libraryPanelService librarypanels.Service, libraryElementService libraryelements.Service,
|
||||
quotaService *quota.QuotaService, socialService social.Service, tracer tracing.Tracer,
|
||||
@ -167,6 +169,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
|
||||
AuthTokenService: userTokenService,
|
||||
cleanUpService: cleanUpService,
|
||||
ShortURLService: shortURLService,
|
||||
Features: features,
|
||||
ThumbService: thumbService,
|
||||
RemoteCacheService: remoteCache,
|
||||
ProvisioningService: provisioningService,
|
||||
|
@ -85,7 +85,7 @@ func (hs *HTTPServer) getAppLinks(c *models.ReqContext) ([]*dtos.NavLink, error)
|
||||
SortWeight: dtos.WeightPlugin,
|
||||
}
|
||||
|
||||
if hs.Cfg.IsNewNavigationEnabled() {
|
||||
if hs.Features.Toggles().IsNewNavigationEnabled() {
|
||||
appLink.Section = dtos.NavSectionPlugin
|
||||
} else {
|
||||
appLink.Section = dtos.NavSectionCore
|
||||
@ -143,7 +143,7 @@ func (hs *HTTPServer) getAppLinks(c *models.ReqContext) ([]*dtos.NavLink, error)
|
||||
}
|
||||
|
||||
func enableServiceAccount(hs *HTTPServer, c *models.ReqContext) bool {
|
||||
return c.OrgRole == models.ROLE_ADMIN && hs.Cfg.IsServiceAccountEnabled() && hs.serviceAccountsService.Migrated(c.Req.Context(), c.OrgId)
|
||||
return c.OrgRole == models.ROLE_ADMIN && hs.Features.Toggles().IsServiceAccountsEnabled() && hs.serviceAccountsService.Migrated(c.Req.Context(), c.OrgId)
|
||||
}
|
||||
|
||||
func enableTeams(hs *HTTPServer, c *models.ReqContext) bool {
|
||||
@ -154,7 +154,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
|
||||
hasAccess := ac.HasAccess(hs.AccessControl, c)
|
||||
navTree := []*dtos.NavLink{}
|
||||
|
||||
if hs.Cfg.IsNewNavigationEnabled() {
|
||||
if hs.Features.Toggles().IsNewNavigationEnabled() {
|
||||
navTree = append(navTree, &dtos.NavLink{
|
||||
Text: "Home",
|
||||
Id: "home",
|
||||
@ -165,7 +165,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
|
||||
})
|
||||
}
|
||||
|
||||
if hasEditPerm && !hs.Cfg.IsNewNavigationEnabled() {
|
||||
if hasEditPerm && !hs.Features.Toggles().IsNewNavigationEnabled() {
|
||||
children := hs.buildCreateNavLinks(c)
|
||||
navTree = append(navTree, &dtos.NavLink{
|
||||
Text: "Create",
|
||||
@ -181,7 +181,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
|
||||
dashboardChildLinks := hs.buildDashboardNavLinks(c, hasEditPerm)
|
||||
|
||||
dashboardsUrl := "/"
|
||||
if hs.Cfg.IsNewNavigationEnabled() {
|
||||
if hs.Features.Toggles().IsNewNavigationEnabled() {
|
||||
dashboardsUrl = "/dashboards"
|
||||
}
|
||||
|
||||
@ -312,7 +312,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
|
||||
})
|
||||
}
|
||||
|
||||
if hs.Cfg.FeatureToggles["live-pipeline"] {
|
||||
if hs.Features.Toggles().IsLivePipelineEnabled() {
|
||||
liveNavLinks := []*dtos.NavLink{}
|
||||
|
||||
liveNavLinks = append(liveNavLinks, &dtos.NavLink{
|
||||
@ -346,7 +346,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
|
||||
SortWeight: dtos.WeightConfig,
|
||||
Children: configNodes,
|
||||
}
|
||||
if hs.Cfg.IsNewNavigationEnabled() {
|
||||
if hs.Features.Toggles().IsNewNavigationEnabled() {
|
||||
configNode.Section = dtos.NavSectionConfig
|
||||
} else {
|
||||
configNode.Section = dtos.NavSectionCore
|
||||
@ -358,7 +358,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
|
||||
|
||||
if len(adminNavLinks) > 0 {
|
||||
navSection := dtos.NavSectionCore
|
||||
if hs.Cfg.IsNewNavigationEnabled() {
|
||||
if hs.Features.Toggles().IsNewNavigationEnabled() {
|
||||
navSection = dtos.NavSectionConfig
|
||||
}
|
||||
serverAdminNode := navlinks.GetServerAdminNode(adminNavLinks, navSection)
|
||||
@ -386,7 +386,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
|
||||
|
||||
func (hs *HTTPServer) buildDashboardNavLinks(c *models.ReqContext, hasEditPerm bool) []*dtos.NavLink {
|
||||
dashboardChildNavs := []*dtos.NavLink{}
|
||||
if !hs.Cfg.IsNewNavigationEnabled() {
|
||||
if !hs.Features.Toggles().IsNewNavigationEnabled() {
|
||||
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{
|
||||
Text: "Home", Id: "home", Url: hs.Cfg.AppSubURL + "/", Icon: "home-alt", HideFromTabs: true,
|
||||
})
|
||||
@ -417,7 +417,7 @@ func (hs *HTTPServer) buildDashboardNavLinks(c *models.ReqContext, hasEditPerm b
|
||||
})
|
||||
}
|
||||
|
||||
if hasEditPerm && hs.Cfg.IsNewNavigationEnabled() {
|
||||
if hasEditPerm && hs.Features.Toggles().IsNewNavigationEnabled() {
|
||||
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{
|
||||
Text: "Divider", Divider: true, Id: "divider", HideFromTabs: true,
|
||||
})
|
||||
@ -622,7 +622,7 @@ func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewDat
|
||||
LoadingLogo: "public/img/grafana_icon.svg",
|
||||
}
|
||||
|
||||
if hs.Cfg.FeatureToggles["accesscontrol"] {
|
||||
if hs.Features.Toggles().IsAccesscontrolEnabled() {
|
||||
userPermissions, err := hs.AccessControl.GetUserPermissions(c.Req.Context(), c.SignedInUser)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -89,7 +89,7 @@ func (hs *HTTPServer) CreateOrg(c *models.ReqContext) response.Response {
|
||||
if err := web.Bind(c.Req, &cmd); err != nil {
|
||||
return response.Error(http.StatusBadRequest, "bad request data", err)
|
||||
}
|
||||
acEnabled := hs.Cfg.FeatureToggles["accesscontrol"]
|
||||
acEnabled := hs.Features.Toggles().IsAccesscontrolEnabled()
|
||||
if !acEnabled && !(setting.AllowUserOrgCreate || c.IsGrafanaAdmin) {
|
||||
return response.Error(403, "Access denied", nil)
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
@ -34,8 +35,8 @@ func setUpGetOrgUsersDB(t *testing.T, sqlStore *sqlstore.SQLStore) {
|
||||
}
|
||||
|
||||
func TestOrgUsersAPIEndpoint_userLoggedIn(t *testing.T) {
|
||||
settings := setting.NewCfg()
|
||||
hs := &HTTPServer{Cfg: settings}
|
||||
hs := setupSimpleHTTPServer(featuremgmt.WithFeatures())
|
||||
settings := hs.Cfg
|
||||
|
||||
sqlStore := sqlstore.InitTestDB(t)
|
||||
sqlStore.Cfg = settings
|
||||
|
@ -5,7 +5,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
@ -52,8 +52,8 @@ type RouteRegister interface {
|
||||
|
||||
type RegisterNamedMiddleware func(name string) web.Handler
|
||||
|
||||
func ProvideRegister(cfg *setting.Cfg) *RouteRegisterImpl {
|
||||
return NewRouteRegister(middleware.ProvideRouteOperationName, middleware.RequestMetrics(cfg))
|
||||
func ProvideRegister(features *featuremgmt.FeatureToggles) *RouteRegisterImpl {
|
||||
return NewRouteRegister(middleware.ProvideRouteOperationName, middleware.RequestMetrics(features))
|
||||
}
|
||||
|
||||
// NewRouteRegister creates a new RouteRegister with all middlewares sent as params
|
||||
|
@ -20,7 +20,7 @@ func (hs *HTTPServer) CreateTeam(c *models.ReqContext) response.Response {
|
||||
if err := web.Bind(c.Req, &cmd); err != nil {
|
||||
return response.Error(http.StatusBadRequest, "bad request data", err)
|
||||
}
|
||||
accessControlEnabled := hs.Cfg.FeatureToggles["accesscontrol"]
|
||||
accessControlEnabled := hs.Features.Toggles().IsAccesscontrolEnabled()
|
||||
if !accessControlEnabled && c.OrgRole == models.ROLE_VIEWER {
|
||||
return response.Error(403, "Not allowed to create team.", nil)
|
||||
}
|
||||
|
@ -40,9 +40,7 @@ func TestTeamAPIEndpoint(t *testing.T) {
|
||||
TotalCount: 2,
|
||||
}
|
||||
|
||||
hs := &HTTPServer{
|
||||
Cfg: setting.NewCfg(),
|
||||
}
|
||||
hs := setupSimpleHTTPServer(nil)
|
||||
|
||||
loggedInUserScenario(t, "When calling GET on", "/api/teams/search", "/api/teams/search", func(sc *scenarioContext) {
|
||||
var sentLimit int
|
||||
@ -92,10 +90,7 @@ func TestTeamAPIEndpoint(t *testing.T) {
|
||||
t.Run("When creating team with API key", func(t *testing.T) {
|
||||
defer bus.ClearBusHandlers()
|
||||
|
||||
hs := &HTTPServer{
|
||||
Cfg: setting.NewCfg(),
|
||||
Bus: bus.GetBus(),
|
||||
}
|
||||
hs := setupSimpleHTTPServer(nil)
|
||||
hs.Cfg.EditorsCanAdmin = true
|
||||
|
||||
teamName := "team foo"
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/metrics/metricutil"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/mwitkow/go-conntrack"
|
||||
)
|
||||
@ -16,7 +17,7 @@ import (
|
||||
var newProviderFunc = sdkhttpclient.NewProvider
|
||||
|
||||
// New creates a new HTTP client provider with pre-configured middlewares.
|
||||
func New(cfg *setting.Cfg, tracer tracing.Tracer) *sdkhttpclient.Provider {
|
||||
func New(cfg *setting.Cfg, tracer tracing.Tracer, features *featuremgmt.FeatureToggles) *sdkhttpclient.Provider {
|
||||
logger := log.New("httpclient")
|
||||
userAgent := fmt.Sprintf("Grafana/%s", cfg.BuildVersion)
|
||||
|
||||
@ -35,7 +36,7 @@ func New(cfg *setting.Cfg, tracer tracing.Tracer) *sdkhttpclient.Provider {
|
||||
|
||||
setDefaultTimeoutOptions(cfg)
|
||||
|
||||
if cfg.FeatureToggles["httpclientprovider_azure_auth"] {
|
||||
if features.IsHttpclientproviderAzureAuthEnabled() {
|
||||
middlewares = append(middlewares, AzureMiddleware(cfg))
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
|
||||
sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@ -22,7 +23,7 @@ func TestHTTPClientProvider(t *testing.T) {
|
||||
})
|
||||
tracer, err := tracing.InitializeTracerForTest()
|
||||
require.NoError(t, err)
|
||||
_ = New(&setting.Cfg{SigV4AuthEnabled: false}, tracer)
|
||||
_ = New(&setting.Cfg{SigV4AuthEnabled: false}, tracer, featuremgmt.WithToggles())
|
||||
require.Len(t, providerOpts, 1)
|
||||
o := providerOpts[0]
|
||||
require.Len(t, o.Middlewares, 6)
|
||||
@ -46,7 +47,7 @@ func TestHTTPClientProvider(t *testing.T) {
|
||||
})
|
||||
tracer, err := tracing.InitializeTracerForTest()
|
||||
require.NoError(t, err)
|
||||
_ = New(&setting.Cfg{SigV4AuthEnabled: true}, tracer)
|
||||
_ = New(&setting.Cfg{SigV4AuthEnabled: true}, tracer, featuremgmt.WithToggles())
|
||||
require.Len(t, providerOpts, 1)
|
||||
o := providerOpts[0]
|
||||
require.Len(t, o.Middlewares, 7)
|
||||
|
@ -7,7 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
cw "github.com/weaveworks/common/tracing"
|
||||
@ -45,7 +45,7 @@ func init() {
|
||||
}
|
||||
|
||||
// RequestMetrics is a middleware handler that instruments the request.
|
||||
func RequestMetrics(cfg *setting.Cfg) func(handler string) web.Handler {
|
||||
func RequestMetrics(features *featuremgmt.FeatureToggles) func(handler string) web.Handler {
|
||||
return func(handler string) web.Handler {
|
||||
return func(res http.ResponseWriter, req *http.Request, c *web.Context) {
|
||||
rw := res.(web.ResponseWriter)
|
||||
@ -60,7 +60,7 @@ func RequestMetrics(cfg *setting.Cfg) func(handler string) web.Handler {
|
||||
method := sanitizeMethod(req.Method)
|
||||
|
||||
// enable histogram and disable summaries + counters for http requests.
|
||||
if cfg.IsHTTPRequestHistogramDisabled() {
|
||||
if features.IsDisableHttpRequestHistogramEnabled() {
|
||||
duration := time.Since(now).Nanoseconds() / int64(time.Millisecond)
|
||||
metrics.MHttpRequestTotal.WithLabelValues(handler, code, method).Inc()
|
||||
metrics.MHttpRequestSummary.WithLabelValues(handler, code, method).Observe(float64(duration))
|
||||
|
@ -14,8 +14,6 @@ type Licensing interface {
|
||||
|
||||
StateInfo() string
|
||||
|
||||
EnabledFeatures() map[string]bool
|
||||
|
||||
FeatureEnabled(feature string) bool
|
||||
}
|
||||
|
||||
|
@ -85,7 +85,6 @@ func pluginScenario(t *testing.T, desc string, fn func(*testing.T, *PluginManage
|
||||
|
||||
t.Run("Given a plugin", func(t *testing.T) {
|
||||
cfg := &setting.Cfg{
|
||||
FeatureToggles: map[string]bool{},
|
||||
PluginSettings: setting.PluginSettings{
|
||||
"test-app": map[string]string{
|
||||
"path": "testdata/test-app",
|
||||
|
@ -18,7 +18,6 @@ import (
|
||||
|
||||
func TestGetPluginDashboards(t *testing.T) {
|
||||
cfg := &setting.Cfg{
|
||||
FeatureToggles: map[string]bool{},
|
||||
PluginSettings: setting.PluginSettings{
|
||||
"test-app": map[string]string{
|
||||
"path": "testdata/test-app",
|
||||
|
@ -17,6 +17,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin/provider"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/loader"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/licensing"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/tsdb/azuremonitor"
|
||||
@ -50,11 +51,13 @@ func TestPluginManager_int_init(t *testing.T) {
|
||||
bundledPluginsPath, err := filepath.Abs("../../../plugins-bundled/internal")
|
||||
require.NoError(t, err)
|
||||
|
||||
features := featuremgmt.WithToggles()
|
||||
cfg := &setting.Cfg{
|
||||
Raw: ini.Empty(),
|
||||
Env: setting.Prod,
|
||||
StaticRootPath: staticRootPath,
|
||||
BundledPluginsPath: bundledPluginsPath,
|
||||
Raw: ini.Empty(),
|
||||
Env: setting.Prod,
|
||||
StaticRootPath: staticRootPath,
|
||||
BundledPluginsPath: bundledPluginsPath,
|
||||
IsFeatureToggleEnabled: features.IsEnabled,
|
||||
PluginSettings: map[string]map[string]string{
|
||||
"plugin.datasource-id": {
|
||||
"path": "testdata/test-app",
|
||||
@ -79,7 +82,7 @@ func TestPluginManager_int_init(t *testing.T) {
|
||||
otsdb := opentsdb.ProvideService(hcp)
|
||||
pr := prometheus.ProvideService(hcp, tracer)
|
||||
tmpo := tempo.ProvideService(hcp)
|
||||
td := testdatasource.ProvideService(cfg)
|
||||
td := testdatasource.ProvideService(cfg, features)
|
||||
pg := postgres.ProvideService(cfg)
|
||||
my := mysql.ProvideService(cfg, hcp)
|
||||
ms := mssql.ProvideService(cfg)
|
||||
|
@ -35,6 +35,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
|
||||
"github.com/grafana/grafana/pkg/services/datasourceproxy"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/hooks"
|
||||
"github.com/grafana/grafana/pkg/services/libraryelements"
|
||||
"github.com/grafana/grafana/pkg/services/librarypanels"
|
||||
@ -182,6 +183,8 @@ var wireBasicSet = wire.NewSet(
|
||||
wire.Bind(new(teamguardian.Store), new(*teamguardianDatabase.TeamGuardianStoreImpl)),
|
||||
teamguardianManager.ProvideService,
|
||||
wire.Bind(new(teamguardian.TeamGuardian), new(*teamguardianManager.Service)),
|
||||
featuremgmt.ProvideManagerService,
|
||||
featuremgmt.ProvideToggles,
|
||||
)
|
||||
|
||||
var wireSet = wire.NewSet(
|
||||
|
@ -9,13 +9,13 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/usagestats"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
func ProvideService(cfg *setting.Cfg, usageStats usagestats.Service) *OSSAccessControlService {
|
||||
func ProvideService(features *featuremgmt.FeatureToggles, usageStats usagestats.Service) *OSSAccessControlService {
|
||||
s := &OSSAccessControlService{
|
||||
Cfg: cfg,
|
||||
features: features,
|
||||
UsageStats: usageStats,
|
||||
Log: log.New("accesscontrol"),
|
||||
ScopeResolver: accesscontrol.NewScopeResolver(),
|
||||
@ -26,7 +26,7 @@ func ProvideService(cfg *setting.Cfg, usageStats usagestats.Service) *OSSAccessC
|
||||
|
||||
// OSSAccessControlService is the service implementing role based access control.
|
||||
type OSSAccessControlService struct {
|
||||
Cfg *setting.Cfg
|
||||
features *featuremgmt.FeatureToggles
|
||||
UsageStats usagestats.Service
|
||||
Log log.Logger
|
||||
registrations accesscontrol.RegistrationList
|
||||
@ -34,10 +34,10 @@ type OSSAccessControlService struct {
|
||||
}
|
||||
|
||||
func (ac *OSSAccessControlService) IsDisabled() bool {
|
||||
if ac.Cfg == nil {
|
||||
if ac.features == nil {
|
||||
return true
|
||||
}
|
||||
return !ac.Cfg.FeatureToggles["accesscontrol"]
|
||||
return !ac.features.IsAccesscontrolEnabled()
|
||||
}
|
||||
|
||||
func (ac *OSSAccessControlService) registerUsageMetrics() {
|
||||
|
@ -12,17 +12,14 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/usagestats"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
)
|
||||
|
||||
func setupTestEnv(t testing.TB) *OSSAccessControlService {
|
||||
t.Helper()
|
||||
|
||||
cfg := setting.NewCfg()
|
||||
cfg.FeatureToggles = map[string]bool{"accesscontrol": true}
|
||||
|
||||
ac := &OSSAccessControlService{
|
||||
Cfg: cfg,
|
||||
features: featuremgmt.WithToggles("accesscontrol"),
|
||||
UsageStats: &usagestats.UsageStatsMock{T: t},
|
||||
Log: log.New("accesscontrol"),
|
||||
registrations: accesscontrol.RegistrationList{},
|
||||
@ -148,12 +145,9 @@ func TestUsageMetrics(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := setting.NewCfg()
|
||||
if tt.enabled {
|
||||
cfg.FeatureToggles = map[string]bool{"accesscontrol": true}
|
||||
}
|
||||
features := featuremgmt.WithToggles("accesscontrol", tt.enabled)
|
||||
|
||||
s := ProvideService(cfg, &usagestats.UsageStatsMock{T: t})
|
||||
s := ProvideService(features, &usagestats.UsageStatsMock{T: t})
|
||||
report, err := s.UsageStats.GetUsageReport(context.Background())
|
||||
assert.Nil(t, err)
|
||||
|
||||
@ -267,7 +261,7 @@ func TestOSSAccessControlService_RegisterFixedRole(t *testing.T) {
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ac := &OSSAccessControlService{
|
||||
Cfg: setting.NewCfg(),
|
||||
features: featuremgmt.WithToggles(),
|
||||
UsageStats: &usagestats.UsageStatsMock{T: t},
|
||||
Log: log.New("accesscontrol-test"),
|
||||
}
|
||||
@ -386,12 +380,11 @@ func TestOSSAccessControlService_DeclareFixedRoles(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ac := &OSSAccessControlService{
|
||||
Cfg: setting.NewCfg(),
|
||||
features: featuremgmt.WithToggles("accesscontrol"),
|
||||
UsageStats: &usagestats.UsageStatsMock{T: t},
|
||||
Log: log.New("accesscontrol-test"),
|
||||
registrations: accesscontrol.RegistrationList{},
|
||||
}
|
||||
ac.Cfg.FeatureToggles = map[string]bool{"accesscontrol": true}
|
||||
|
||||
// Test
|
||||
err := ac.DeclareFixedRoles(tt.registrations...)
|
||||
@ -459,9 +452,6 @@ func TestOSSAccessControlService_RegisterFixedRoles(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
cfg := setting.NewCfg()
|
||||
cfg.FeatureToggles = map[string]bool{"accesscontrol": true}
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Remove any inserted role after the test case has been run
|
||||
t.Cleanup(func() {
|
||||
@ -472,12 +462,11 @@ func TestOSSAccessControlService_RegisterFixedRoles(t *testing.T) {
|
||||
|
||||
// Setup
|
||||
ac := &OSSAccessControlService{
|
||||
Cfg: setting.NewCfg(),
|
||||
features: featuremgmt.WithToggles("accesscontrol"),
|
||||
UsageStats: &usagestats.UsageStatsMock{T: t},
|
||||
Log: log.New("accesscontrol-test"),
|
||||
registrations: accesscontrol.RegistrationList{},
|
||||
}
|
||||
ac.Cfg.FeatureToggles = map[string]bool{"accesscontrol": true}
|
||||
ac.registrations.Append(tt.registrations...)
|
||||
|
||||
// Test
|
||||
@ -552,7 +541,7 @@ func TestOSSAccessControlService_GetUserPermissions(t *testing.T) {
|
||||
|
||||
// Setup
|
||||
ac := setupTestEnv(t)
|
||||
ac.Cfg.FeatureToggles = map[string]bool{"accesscontrol": true}
|
||||
ac.features = featuremgmt.WithToggles("accesscontrol")
|
||||
|
||||
registration.Role.Permissions = []accesscontrol.Permission{tt.rawPerm}
|
||||
err := ac.DeclareFixedRoles(registration)
|
||||
@ -638,7 +627,6 @@ func TestOSSAccessControlService_Evaluate(t *testing.T) {
|
||||
|
||||
// Setup
|
||||
ac := setupTestEnv(t)
|
||||
ac.Cfg.FeatureToggles = map[string]bool{"accesscontrol": true}
|
||||
ac.RegisterAttributeScopeResolver("users:login:", userLoginScopeSolver)
|
||||
|
||||
registration.Role.Permissions = []accesscontrol.Permission{tt.rawPerm}
|
||||
|
95
pkg/services/featuremgmt/features.go
Normal file
95
pkg/services/featuremgmt/features.go
Normal file
@ -0,0 +1,95 @@
|
||||
package featuremgmt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// FeatureToggleState indicates the quality level
|
||||
type FeatureToggleState int
|
||||
|
||||
const (
|
||||
// FeatureStateUnknown indicates that no state is specified
|
||||
FeatureStateUnknown FeatureToggleState = iota
|
||||
|
||||
// FeatureStateAlpha the feature is in active development and may change at any time
|
||||
FeatureStateAlpha
|
||||
|
||||
// FeatureStateBeta the feature is still in development, but settings will have migrations
|
||||
FeatureStateBeta
|
||||
|
||||
// FeatureStateStable this is a stable feature
|
||||
FeatureStateStable
|
||||
|
||||
// FeatureStateDeprecated the feature will be removed in the future
|
||||
FeatureStateDeprecated
|
||||
)
|
||||
|
||||
func (s FeatureToggleState) String() string {
|
||||
switch s {
|
||||
case FeatureStateAlpha:
|
||||
return "alpha"
|
||||
case FeatureStateBeta:
|
||||
return "beta"
|
||||
case FeatureStateStable:
|
||||
return "stable"
|
||||
case FeatureStateDeprecated:
|
||||
return "deprecated"
|
||||
case FeatureStateUnknown:
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// MarshalJSON marshals the enum as a quoted json string
|
||||
func (s FeatureToggleState) MarshalJSON() ([]byte, error) {
|
||||
buffer := bytes.NewBufferString(`"`)
|
||||
buffer.WriteString(s.String())
|
||||
buffer.WriteString(`"`)
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON unmarshals a quoted json string to the enum value
|
||||
func (s *FeatureToggleState) UnmarshalJSON(b []byte) error {
|
||||
var j string
|
||||
err := json.Unmarshal(b, &j)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch j {
|
||||
case "alpha":
|
||||
*s = FeatureStateAlpha
|
||||
|
||||
case "beta":
|
||||
*s = FeatureStateBeta
|
||||
|
||||
case "stable":
|
||||
*s = FeatureStateStable
|
||||
|
||||
case "deprecated":
|
||||
*s = FeatureStateDeprecated
|
||||
|
||||
default:
|
||||
*s = FeatureStateUnknown
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type FeatureFlag struct {
|
||||
Name string `json:"name" yaml:"name"` // Unique name
|
||||
Description string `json:"description"`
|
||||
State FeatureToggleState `json:"state,omitempty"`
|
||||
DocsURL string `json:"docsURL,omitempty"`
|
||||
|
||||
// CEL-GO expression. Using the value "true" will mean this is on by default
|
||||
Expression string `json:"expression,omitempty"`
|
||||
|
||||
// Special behavior flags
|
||||
RequiresDevMode bool `json:"requiresDevMode,omitempty"` // can not be enabled in production
|
||||
RequiresRestart bool `json:"requiresRestart,omitempty"` // The server must be initialized with the value
|
||||
RequiresLicense bool `json:"requiresLicense,omitempty"` // Must be enabled in the license
|
||||
FrontendOnly bool `json:"frontend,omitempty"` // change is only seen in the frontend
|
||||
|
||||
// Internal properties
|
||||
// expr string `json:-`
|
||||
}
|
195
pkg/services/featuremgmt/manager.go
Normal file
195
pkg/services/featuremgmt/manager.go
Normal file
@ -0,0 +1,195 @@
|
||||
package featuremgmt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
type FeatureManager struct {
|
||||
isDevMod bool
|
||||
licensing models.Licensing
|
||||
flags map[string]*FeatureFlag
|
||||
enabled map[string]bool // only the "on" values
|
||||
toggles *FeatureToggles
|
||||
config string // path to config file
|
||||
vars map[string]interface{}
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
// This will merge the flags with the current configuration
|
||||
func (fm *FeatureManager) registerFlags(flags ...FeatureFlag) {
|
||||
for idx, add := range flags {
|
||||
if add.Name == "" {
|
||||
continue // skip it with warning?
|
||||
}
|
||||
flag, ok := fm.flags[add.Name]
|
||||
if !ok {
|
||||
fm.flags[add.Name] = &flags[idx]
|
||||
continue
|
||||
}
|
||||
|
||||
// Selectively update properties
|
||||
if add.Description != "" {
|
||||
flag.Description = add.Description
|
||||
}
|
||||
if add.DocsURL != "" {
|
||||
flag.DocsURL = add.DocsURL
|
||||
}
|
||||
if add.Expression != "" {
|
||||
flag.Expression = add.Expression
|
||||
}
|
||||
|
||||
// The most recently defined state
|
||||
if add.State != FeatureStateUnknown {
|
||||
flag.State = add.State
|
||||
}
|
||||
|
||||
// Only gets more restrictive
|
||||
if add.RequiresDevMode {
|
||||
flag.RequiresDevMode = true
|
||||
}
|
||||
|
||||
if add.RequiresLicense {
|
||||
flag.RequiresLicense = true
|
||||
}
|
||||
|
||||
if add.RequiresRestart {
|
||||
flag.RequiresRestart = true
|
||||
}
|
||||
}
|
||||
|
||||
// This will evaluate all flags
|
||||
fm.update()
|
||||
}
|
||||
|
||||
func (fm *FeatureManager) evaluate(ff *FeatureFlag) bool {
|
||||
if ff.RequiresDevMode && !fm.isDevMod {
|
||||
return false
|
||||
}
|
||||
|
||||
if ff.RequiresLicense && (fm.licensing == nil || !fm.licensing.FeatureEnabled(ff.Name)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// TODO: CEL - expression
|
||||
return ff.Expression == "true"
|
||||
}
|
||||
|
||||
// Update
|
||||
func (fm *FeatureManager) update() {
|
||||
enabled := make(map[string]bool)
|
||||
for _, flag := range fm.flags {
|
||||
val := fm.evaluate(flag)
|
||||
|
||||
// Update the registry
|
||||
track := 0.0
|
||||
if val {
|
||||
track = 1
|
||||
enabled[flag.Name] = true
|
||||
}
|
||||
|
||||
// Register value with prometheus metric
|
||||
featureToggleInfo.WithLabelValues(flag.Name).Set(track)
|
||||
}
|
||||
fm.enabled = enabled
|
||||
}
|
||||
|
||||
// Run is called by background services
|
||||
func (fm *FeatureManager) readFile() error {
|
||||
if fm.config == "" {
|
||||
return nil // not configured
|
||||
}
|
||||
|
||||
cfg, err := readConfigFile(fm.config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fm.registerFlags(cfg.Flags...)
|
||||
fm.vars = cfg.Vars
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsEnabled checks if a feature is enabled
|
||||
func (fm *FeatureManager) IsEnabled(flag string) bool {
|
||||
return fm.enabled[flag]
|
||||
}
|
||||
|
||||
// GetEnabled returns a map contaning only the features that are enabled
|
||||
func (fm *FeatureManager) GetEnabled(ctx context.Context) map[string]bool {
|
||||
enabled := make(map[string]bool, len(fm.enabled))
|
||||
for key, val := range fm.enabled {
|
||||
if val {
|
||||
enabled[key] = true
|
||||
}
|
||||
}
|
||||
return enabled
|
||||
}
|
||||
|
||||
// Toggles returns FeatureToggles.
|
||||
func (fm *FeatureManager) Toggles() *FeatureToggles {
|
||||
if fm.toggles == nil {
|
||||
fm.toggles = &FeatureToggles{manager: fm}
|
||||
}
|
||||
return fm.toggles
|
||||
}
|
||||
|
||||
// GetFlags returns all flag definitions
|
||||
func (fm *FeatureManager) GetFlags() []FeatureFlag {
|
||||
v := make([]FeatureFlag, 0, len(fm.flags))
|
||||
for _, value := range fm.flags {
|
||||
v = append(v, *value)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func (fm *FeatureManager) HandleGetSettings(c *models.ReqContext) {
|
||||
res := make(map[string]interface{}, 3)
|
||||
res["enabled"] = fm.GetEnabled(c.Req.Context())
|
||||
|
||||
vv := make([]*FeatureFlag, 0, len(fm.flags))
|
||||
for _, v := range fm.flags {
|
||||
vv = append(vv, v)
|
||||
}
|
||||
|
||||
res["info"] = vv
|
||||
|
||||
response.JSON(200, res).WriteTo(c)
|
||||
}
|
||||
|
||||
// WithFeatures is used to define feature toggles for testing.
|
||||
// The arguments are a list of strings that are optionally followed by a boolean value
|
||||
func WithFeatures(spec ...interface{}) *FeatureManager {
|
||||
count := len(spec)
|
||||
enabled := make(map[string]bool, count)
|
||||
|
||||
idx := 0
|
||||
for idx < count {
|
||||
key := fmt.Sprintf("%v", spec[idx])
|
||||
val := true
|
||||
idx++
|
||||
if idx < count && reflect.TypeOf(spec[idx]).Kind() == reflect.Bool {
|
||||
val = spec[idx].(bool)
|
||||
idx++
|
||||
}
|
||||
|
||||
if val {
|
||||
enabled[key] = true
|
||||
}
|
||||
}
|
||||
|
||||
return &FeatureManager{enabled: enabled}
|
||||
}
|
||||
|
||||
func WithToggles(spec ...interface{}) *FeatureToggles {
|
||||
return &FeatureToggles{
|
||||
manager: WithFeatures(spec...),
|
||||
}
|
||||
}
|
77
pkg/services/featuremgmt/manager_test.go
Normal file
77
pkg/services/featuremgmt/manager_test.go
Normal 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)
|
||||
})
|
||||
}
|
163
pkg/services/featuremgmt/registry.go
Normal file
163
pkg/services/featuremgmt/registry.go
Normal file
@ -0,0 +1,163 @@
|
||||
package featuremgmt
|
||||
|
||||
import "github.com/grafana/grafana/pkg/services/secrets"
|
||||
|
||||
var (
|
||||
FLAG_database_metrics = "database_metrics"
|
||||
FLAG_live_config = "live-config"
|
||||
FLAG_recordedQueries = "recordedQueries"
|
||||
|
||||
// Register each toggle here
|
||||
standardFeatureFlags = []FeatureFlag{
|
||||
{
|
||||
Name: FLAG_recordedQueries,
|
||||
Description: "Supports saving queries that can be scraped by prometheus",
|
||||
State: FeatureStateBeta,
|
||||
RequiresLicense: true,
|
||||
},
|
||||
{
|
||||
Name: "teamsync",
|
||||
Description: "Team sync lets you set up synchronization between your auth providers teams and teams in Grafana",
|
||||
State: FeatureStateStable,
|
||||
DocsURL: "https://grafana.com/docs/grafana/latest/enterprise/team-sync/",
|
||||
RequiresLicense: true,
|
||||
},
|
||||
{
|
||||
Name: "ldapsync",
|
||||
Description: "Enhanced LDAP integration",
|
||||
State: FeatureStateStable,
|
||||
DocsURL: "https://grafana.com/docs/grafana/latest/enterprise/enhanced_ldap/",
|
||||
RequiresLicense: true,
|
||||
},
|
||||
{
|
||||
Name: "caching",
|
||||
Description: "Temporarily store data source query results.",
|
||||
State: FeatureStateStable,
|
||||
DocsURL: "https://grafana.com/docs/grafana/latest/enterprise/query-caching/",
|
||||
RequiresLicense: true,
|
||||
},
|
||||
{
|
||||
Name: "dspermissions",
|
||||
Description: "Data source permissions",
|
||||
State: FeatureStateStable,
|
||||
DocsURL: "https://grafana.com/docs/grafana/latest/enterprise/datasource_permissions/",
|
||||
RequiresLicense: true,
|
||||
},
|
||||
{
|
||||
Name: "analytics",
|
||||
Description: "Analytics",
|
||||
State: FeatureStateStable,
|
||||
RequiresLicense: true,
|
||||
},
|
||||
{
|
||||
Name: "enterprise.plugins",
|
||||
Description: "Enterprise plugins",
|
||||
State: FeatureStateStable,
|
||||
DocsURL: "https://grafana.com/grafana/plugins/?enterprise=1",
|
||||
RequiresLicense: true,
|
||||
},
|
||||
{
|
||||
Name: "trimDefaults",
|
||||
Description: "Use cue schema to remove values that will be applied automatically",
|
||||
State: FeatureStateBeta,
|
||||
},
|
||||
{
|
||||
Name: secrets.EnvelopeEncryptionFeatureToggle,
|
||||
Description: "encrypt secrets",
|
||||
State: FeatureStateBeta,
|
||||
},
|
||||
|
||||
{
|
||||
Name: "httpclientprovider_azure_auth",
|
||||
State: FeatureStateBeta,
|
||||
},
|
||||
{
|
||||
Name: "service-accounts",
|
||||
Description: "support service accounts",
|
||||
State: FeatureStateBeta,
|
||||
RequiresLicense: true,
|
||||
},
|
||||
|
||||
{
|
||||
Name: FLAG_database_metrics,
|
||||
Description: "Add prometheus metrics for database tables",
|
||||
State: FeatureStateStable,
|
||||
},
|
||||
{
|
||||
Name: "dashboardPreviews",
|
||||
Description: "Create and show thumbnails for dashboard search results",
|
||||
State: FeatureStateAlpha,
|
||||
},
|
||||
{
|
||||
Name: FLAG_live_config,
|
||||
Description: "Save grafana live configuration in SQL tables",
|
||||
State: FeatureStateAlpha,
|
||||
},
|
||||
{
|
||||
Name: "live-pipeline",
|
||||
Description: "enable a generic live processing pipeline",
|
||||
State: FeatureStateAlpha,
|
||||
},
|
||||
{
|
||||
Name: "live-service-web-worker",
|
||||
Description: "This will use a webworker thread to processes events rather than the main thread",
|
||||
State: FeatureStateAlpha,
|
||||
FrontendOnly: true,
|
||||
},
|
||||
{
|
||||
Name: "queryOverLive",
|
||||
Description: "Use grafana live websocket to execute backend queries",
|
||||
State: FeatureStateAlpha,
|
||||
FrontendOnly: true,
|
||||
},
|
||||
{
|
||||
Name: "tempoSearch",
|
||||
Description: "Enable searching in tempo datasources",
|
||||
State: FeatureStateBeta,
|
||||
FrontendOnly: true,
|
||||
},
|
||||
{
|
||||
Name: "tempoBackendSearch",
|
||||
Description: "Use backend for tempo search",
|
||||
State: FeatureStateBeta,
|
||||
},
|
||||
{
|
||||
Name: "tempoServiceGraph",
|
||||
Description: "show service",
|
||||
State: FeatureStateBeta,
|
||||
FrontendOnly: true,
|
||||
},
|
||||
{
|
||||
Name: "fullRangeLogsVolume",
|
||||
Description: "Show full range logs volume in expore",
|
||||
State: FeatureStateBeta,
|
||||
FrontendOnly: true,
|
||||
},
|
||||
{
|
||||
Name: "accesscontrol",
|
||||
Description: "Support robust access control",
|
||||
State: FeatureStateBeta,
|
||||
RequiresLicense: true,
|
||||
},
|
||||
{
|
||||
Name: "prometheus_azure_auth",
|
||||
Description: "Use azure authentication for prometheus datasource",
|
||||
State: FeatureStateBeta,
|
||||
},
|
||||
{
|
||||
Name: "newNavigation",
|
||||
Description: "Try the next gen naviation model",
|
||||
State: FeatureStateAlpha,
|
||||
},
|
||||
{
|
||||
Name: "showFeatureFlagsInUI",
|
||||
Description: "Show feature flags in the settings UI",
|
||||
State: FeatureStateAlpha,
|
||||
RequiresDevMode: true,
|
||||
},
|
||||
{
|
||||
Name: "disable_http_request_histogram",
|
||||
State: FeatureStateAlpha,
|
||||
},
|
||||
}
|
||||
)
|
78
pkg/services/featuremgmt/service.go
Normal file
78
pkg/services/featuremgmt/service.go
Normal file
@ -0,0 +1,78 @@
|
||||
package featuremgmt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
|
||||
var (
|
||||
// The values are updated each time
|
||||
featureToggleInfo = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Name: "feature_toggles_info",
|
||||
Help: "info metric that exposes what feature toggles are enabled or not",
|
||||
Namespace: "grafana",
|
||||
}, []string{"name"})
|
||||
)
|
||||
|
||||
func ProvideManagerService(cfg *setting.Cfg, licensing models.Licensing) (*FeatureManager, error) {
|
||||
mgmt := &FeatureManager{
|
||||
isDevMod: setting.Env != setting.Prod,
|
||||
licensing: licensing,
|
||||
flags: make(map[string]*FeatureFlag, 30),
|
||||
enabled: make(map[string]bool),
|
||||
log: log.New("featuremgmt"),
|
||||
}
|
||||
|
||||
// Register the standard flags
|
||||
mgmt.registerFlags(standardFeatureFlags...)
|
||||
|
||||
// Load the flags from `custom.ini` files
|
||||
flags, err := setting.ReadFeatureTogglesFromInitFile(cfg.Raw.Section("feature_toggles"))
|
||||
if err != nil {
|
||||
return mgmt, err
|
||||
}
|
||||
for key, val := range flags {
|
||||
flag, ok := mgmt.flags[key]
|
||||
if !ok {
|
||||
flag = &FeatureFlag{
|
||||
Name: key,
|
||||
State: FeatureStateUnknown,
|
||||
}
|
||||
mgmt.flags[key] = flag
|
||||
}
|
||||
flag.Expression = fmt.Sprintf("%t", val) // true | false
|
||||
}
|
||||
|
||||
// Load config settings
|
||||
configfile := filepath.Join(cfg.HomePath, "conf", "features.yaml")
|
||||
if _, err := os.Stat(configfile); err == nil {
|
||||
mgmt.log.Info("[experimental] loading features from config file", "path", configfile)
|
||||
mgmt.config = configfile
|
||||
err = mgmt.readFile()
|
||||
if err != nil {
|
||||
return mgmt, err
|
||||
}
|
||||
}
|
||||
|
||||
// update the values
|
||||
mgmt.update()
|
||||
|
||||
// Minimum approach to avoid circular dependency
|
||||
cfg.IsFeatureToggleEnabled = mgmt.IsEnabled
|
||||
return mgmt, nil
|
||||
}
|
||||
|
||||
// ProvideToggles allows read-only access to the feature state
|
||||
func ProvideToggles(mgmt *FeatureManager) *FeatureToggles {
|
||||
return &FeatureToggles{
|
||||
manager: mgmt,
|
||||
}
|
||||
}
|
34
pkg/services/featuremgmt/settings.go
Normal file
34
pkg/services/featuremgmt/settings.go
Normal 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
|
||||
}
|
25
pkg/services/featuremgmt/settings_test.go
Normal file
25
pkg/services/featuremgmt/settings_test.go
Normal 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))
|
||||
}
|
33
pkg/services/featuremgmt/testdata/features.yaml
vendored
Normal file
33
pkg/services/featuremgmt/testdata/features.yaml
vendored
Normal 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")
|
13
pkg/services/featuremgmt/testdata/included.yaml
vendored
Normal file
13
pkg/services/featuremgmt/testdata/included.yaml
vendored
Normal 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
|
10
pkg/services/featuremgmt/toggles.go
Normal file
10
pkg/services/featuremgmt/toggles.go
Normal file
@ -0,0 +1,10 @@
|
||||
package featuremgmt
|
||||
|
||||
type FeatureToggles struct {
|
||||
manager *FeatureManager
|
||||
}
|
||||
|
||||
// IsEnabled checks if a feature is enabled
|
||||
func (ft *FeatureToggles) IsEnabled(flag string) bool {
|
||||
return ft.manager.IsEnabled(flag)
|
||||
}
|
157
pkg/services/featuremgmt/toggles_gen.go
Normal file
157
pkg/services/featuremgmt/toggles_gen.go
Normal file
@ -0,0 +1,157 @@
|
||||
// NOTE: This file is autogenerated
|
||||
|
||||
package featuremgmt
|
||||
|
||||
// IsRecordedQueriesEnabled checks for the flag: recordedQueries
|
||||
// Supports saving queries that can be scraped by prometheus
|
||||
func (ft *FeatureToggles) IsRecordedQueriesEnabled() bool {
|
||||
return ft.manager.IsEnabled("recordedQueries")
|
||||
}
|
||||
|
||||
// IsTeamsyncEnabled checks for the flag: teamsync
|
||||
// Team sync lets you set up synchronization between your auth providers teams and teams in Grafana
|
||||
func (ft *FeatureToggles) IsTeamsyncEnabled() bool {
|
||||
return ft.manager.IsEnabled("teamsync")
|
||||
}
|
||||
|
||||
// IsLdapsyncEnabled checks for the flag: ldapsync
|
||||
// Enhanced LDAP integration
|
||||
func (ft *FeatureToggles) IsLdapsyncEnabled() bool {
|
||||
return ft.manager.IsEnabled("ldapsync")
|
||||
}
|
||||
|
||||
// IsCachingEnabled checks for the flag: caching
|
||||
// Temporarily store data source query results.
|
||||
func (ft *FeatureToggles) IsCachingEnabled() bool {
|
||||
return ft.manager.IsEnabled("caching")
|
||||
}
|
||||
|
||||
// IsDspermissionsEnabled checks for the flag: dspermissions
|
||||
// Data source permissions
|
||||
func (ft *FeatureToggles) IsDspermissionsEnabled() bool {
|
||||
return ft.manager.IsEnabled("dspermissions")
|
||||
}
|
||||
|
||||
// IsAnalyticsEnabled checks for the flag: analytics
|
||||
// Analytics
|
||||
func (ft *FeatureToggles) IsAnalyticsEnabled() bool {
|
||||
return ft.manager.IsEnabled("analytics")
|
||||
}
|
||||
|
||||
// IsEnterprisePluginsEnabled checks for the flag: enterprise.plugins
|
||||
// Enterprise plugins
|
||||
func (ft *FeatureToggles) IsEnterprisePluginsEnabled() bool {
|
||||
return ft.manager.IsEnabled("enterprise.plugins")
|
||||
}
|
||||
|
||||
// IsTrimDefaultsEnabled checks for the flag: trimDefaults
|
||||
// Use cue schema to remove values that will be applied automatically
|
||||
func (ft *FeatureToggles) IsTrimDefaultsEnabled() bool {
|
||||
return ft.manager.IsEnabled("trimDefaults")
|
||||
}
|
||||
|
||||
// IsEnvelopeEncryptionEnabled checks for the flag: envelopeEncryption
|
||||
// encrypt secrets
|
||||
func (ft *FeatureToggles) IsEnvelopeEncryptionEnabled() bool {
|
||||
return ft.manager.IsEnabled("envelopeEncryption")
|
||||
}
|
||||
|
||||
// IsHttpclientproviderAzureAuthEnabled checks for the flag: httpclientprovider_azure_auth
|
||||
func (ft *FeatureToggles) IsHttpclientproviderAzureAuthEnabled() bool {
|
||||
return ft.manager.IsEnabled("httpclientprovider_azure_auth")
|
||||
}
|
||||
|
||||
// IsServiceAccountsEnabled checks for the flag: service-accounts
|
||||
// support service accounts
|
||||
func (ft *FeatureToggles) IsServiceAccountsEnabled() bool {
|
||||
return ft.manager.IsEnabled("service-accounts")
|
||||
}
|
||||
|
||||
// IsDatabaseMetricsEnabled checks for the flag: database_metrics
|
||||
// Add prometheus metrics for database tables
|
||||
func (ft *FeatureToggles) IsDatabaseMetricsEnabled() bool {
|
||||
return ft.manager.IsEnabled("database_metrics")
|
||||
}
|
||||
|
||||
// IsDashboardPreviewsEnabled checks for the flag: dashboardPreviews
|
||||
// Create and show thumbnails for dashboard search results
|
||||
func (ft *FeatureToggles) IsDashboardPreviewsEnabled() bool {
|
||||
return ft.manager.IsEnabled("dashboardPreviews")
|
||||
}
|
||||
|
||||
// IsLiveConfigEnabled checks for the flag: live-config
|
||||
// Save grafana live configuration in SQL tables
|
||||
func (ft *FeatureToggles) IsLiveConfigEnabled() bool {
|
||||
return ft.manager.IsEnabled("live-config")
|
||||
}
|
||||
|
||||
// IsLivePipelineEnabled checks for the flag: live-pipeline
|
||||
// enable a generic live processing pipeline
|
||||
func (ft *FeatureToggles) IsLivePipelineEnabled() bool {
|
||||
return ft.manager.IsEnabled("live-pipeline")
|
||||
}
|
||||
|
||||
// IsLiveServiceWebWorkerEnabled checks for the flag: live-service-web-worker
|
||||
// This will use a webworker thread to processes events rather than the main thread
|
||||
func (ft *FeatureToggles) IsLiveServiceWebWorkerEnabled() bool {
|
||||
return ft.manager.IsEnabled("live-service-web-worker")
|
||||
}
|
||||
|
||||
// IsQueryOverLiveEnabled checks for the flag: queryOverLive
|
||||
// Use grafana live websocket to execute backend queries
|
||||
func (ft *FeatureToggles) IsQueryOverLiveEnabled() bool {
|
||||
return ft.manager.IsEnabled("queryOverLive")
|
||||
}
|
||||
|
||||
// IsTempoSearchEnabled checks for the flag: tempoSearch
|
||||
// Enable searching in tempo datasources
|
||||
func (ft *FeatureToggles) IsTempoSearchEnabled() bool {
|
||||
return ft.manager.IsEnabled("tempoSearch")
|
||||
}
|
||||
|
||||
// IsTempoBackendSearchEnabled checks for the flag: tempoBackendSearch
|
||||
// Use backend for tempo search
|
||||
func (ft *FeatureToggles) IsTempoBackendSearchEnabled() bool {
|
||||
return ft.manager.IsEnabled("tempoBackendSearch")
|
||||
}
|
||||
|
||||
// IsTempoServiceGraphEnabled checks for the flag: tempoServiceGraph
|
||||
// show service
|
||||
func (ft *FeatureToggles) IsTempoServiceGraphEnabled() bool {
|
||||
return ft.manager.IsEnabled("tempoServiceGraph")
|
||||
}
|
||||
|
||||
// IsFullRangeLogsVolumeEnabled checks for the flag: fullRangeLogsVolume
|
||||
// Show full range logs volume in expore
|
||||
func (ft *FeatureToggles) IsFullRangeLogsVolumeEnabled() bool {
|
||||
return ft.manager.IsEnabled("fullRangeLogsVolume")
|
||||
}
|
||||
|
||||
// IsAccesscontrolEnabled checks for the flag: accesscontrol
|
||||
// Support robust access control
|
||||
func (ft *FeatureToggles) IsAccesscontrolEnabled() bool {
|
||||
return ft.manager.IsEnabled("accesscontrol")
|
||||
}
|
||||
|
||||
// IsPrometheusAzureAuthEnabled checks for the flag: prometheus_azure_auth
|
||||
// Use azure authentication for prometheus datasource
|
||||
func (ft *FeatureToggles) IsPrometheusAzureAuthEnabled() bool {
|
||||
return ft.manager.IsEnabled("prometheus_azure_auth")
|
||||
}
|
||||
|
||||
// IsNewNavigationEnabled checks for the flag: newNavigation
|
||||
// Try the next gen naviation model
|
||||
func (ft *FeatureToggles) IsNewNavigationEnabled() bool {
|
||||
return ft.manager.IsEnabled("newNavigation")
|
||||
}
|
||||
|
||||
// IsShowFeatureFlagsInUIEnabled checks for the flag: showFeatureFlagsInUI
|
||||
// Show feature flags in the settings UI
|
||||
func (ft *FeatureToggles) IsShowFeatureFlagsInUIEnabled() bool {
|
||||
return ft.manager.IsEnabled("showFeatureFlagsInUI")
|
||||
}
|
||||
|
||||
// IsDisableHttpRequestHistogramEnabled checks for the flag: disable_http_request_histogram
|
||||
func (ft *FeatureToggles) IsDisableHttpRequestHistogramEnabled() bool {
|
||||
return ft.manager.IsEnabled("disable_http_request_histogram")
|
||||
}
|
140
pkg/services/featuremgmt/toggles_gen_test.go
Normal file
140
pkg/services/featuremgmt/toggles_gen_test.go
Normal file
@ -0,0 +1,140 @@
|
||||
package featuremgmt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestFeatureToggleFiles(t *testing.T) {
|
||||
// Typescript files
|
||||
verifyAndGenerateFile(t,
|
||||
"../../../packages/grafana-data/src/types/featureToggles.gen.ts",
|
||||
generateTypeScript(),
|
||||
)
|
||||
|
||||
// Golang files
|
||||
verifyAndGenerateFile(t,
|
||||
"toggles_gen.go",
|
||||
generateRegistry(t),
|
||||
)
|
||||
}
|
||||
|
||||
func verifyAndGenerateFile(t *testing.T, fpath string, gen string) {
|
||||
// nolint:gosec
|
||||
// We can ignore the gosec G304 warning since this is a test and the function is only called explicitly above
|
||||
body, err := ioutil.ReadFile(fpath)
|
||||
if err == nil {
|
||||
if diff := cmp.Diff(gen, string(body)); diff != "" {
|
||||
str := fmt.Sprintf("body mismatch (-want +got):\n%s\n", diff)
|
||||
err = fmt.Errorf(str)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
e2 := os.WriteFile(fpath, []byte(gen), 0644)
|
||||
if e2 != nil {
|
||||
t.Errorf("error writing file: %s", e2.Error())
|
||||
}
|
||||
abs, _ := filepath.Abs(fpath)
|
||||
t.Errorf("feature toggle do not match: %s (%s)", err.Error(), abs)
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func generateTypeScript() string {
|
||||
buf := `// NOTE: This file was auto generated. DO NOT EDIT DIRECTLY!
|
||||
// To change feature flags, edit:
|
||||
// pkg/services/featuremgmt/registry.go
|
||||
// Then run tests in:
|
||||
// pkg/services/featuremgmt/toggles_gen_test.go
|
||||
|
||||
/**
|
||||
* Describes available feature toggles in Grafana. These can be configured via
|
||||
* conf/custom.ini to enable features under development or not yet available in
|
||||
* stable version.
|
||||
*
|
||||
* Only enabled values will be returned in this interface
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface FeatureToggles {
|
||||
[name: string]: boolean | undefined; // support any string value
|
||||
|
||||
`
|
||||
for _, flag := range standardFeatureFlags {
|
||||
buf += " " + getTypeScriptKey(flag.Name) + "?: boolean;\n"
|
||||
}
|
||||
|
||||
buf += "}\n"
|
||||
return buf
|
||||
}
|
||||
|
||||
func getTypeScriptKey(key string) string {
|
||||
if strings.Contains(key, "-") || strings.Contains(key, ".") {
|
||||
return "['" + key + "']"
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
func isLetterOrNumber(c rune) bool {
|
||||
return !unicode.IsLetter(c) && !unicode.IsNumber(c)
|
||||
}
|
||||
|
||||
func asCamelCase(key string) string {
|
||||
parts := strings.FieldsFunc(key, isLetterOrNumber)
|
||||
for idx, part := range parts {
|
||||
parts[idx] = strings.Title(part)
|
||||
}
|
||||
return strings.Join(parts, "")
|
||||
}
|
||||
|
||||
func generateRegistry(t *testing.T) string {
|
||||
tmpl, err := template.New("fn").Parse(`
|
||||
// Is{{.CamleCase}}Enabled checks for the flag: {{.Flag.Name}}{{.Ext}}
|
||||
func (ft *FeatureToggles) Is{{.CamleCase}}Enabled() bool {
|
||||
return ft.manager.IsEnabled("{{.Flag.Name}}")
|
||||
}
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatal("error reading template", "error", err.Error())
|
||||
return ""
|
||||
}
|
||||
|
||||
data := struct {
|
||||
CamleCase string
|
||||
Flag FeatureFlag
|
||||
Ext string
|
||||
}{
|
||||
CamleCase: "?",
|
||||
}
|
||||
|
||||
var buff bytes.Buffer
|
||||
|
||||
buff.WriteString(`// NOTE: This file is autogenerated
|
||||
|
||||
package featuremgmt
|
||||
`)
|
||||
|
||||
for _, flag := range standardFeatureFlags {
|
||||
data.CamleCase = asCamelCase(flag.Name)
|
||||
data.Flag = flag
|
||||
data.Ext = ""
|
||||
|
||||
if flag.Description != "" {
|
||||
data.Ext += "\n// " + flag.Description
|
||||
}
|
||||
|
||||
_ = tmpl.Execute(&buff, data)
|
||||
}
|
||||
|
||||
return buff.String()
|
||||
}
|
@ -15,6 +15,7 @@ import (
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/query"
|
||||
|
||||
"github.com/centrifugal/centrifuge"
|
||||
@ -67,9 +68,10 @@ type CoreGrafanaScope struct {
|
||||
func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, routeRegister routing.RouteRegister,
|
||||
pluginStore plugins.Store, cacheService *localcache.CacheService,
|
||||
dataSourceCache datasources.CacheService, sqlStore *sqlstore.SQLStore, secretsService secrets.Service,
|
||||
usageStatsService usagestats.Service, queryDataService *query.Service) (*GrafanaLive, error) {
|
||||
usageStatsService usagestats.Service, queryDataService *query.Service, toggles *featuremgmt.FeatureToggles) (*GrafanaLive, error) {
|
||||
g := &GrafanaLive{
|
||||
Cfg: cfg,
|
||||
Features: toggles,
|
||||
PluginContextProvider: plugCtxProvider,
|
||||
RouteRegister: routeRegister,
|
||||
pluginStore: pluginStore,
|
||||
@ -174,7 +176,7 @@ func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, r
|
||||
}
|
||||
|
||||
g.ManagedStreamRunner = managedStreamRunner
|
||||
if enabled := g.Cfg.FeatureToggles["live-pipeline"]; enabled {
|
||||
if g.Features.IsLivePipelineEnabled() {
|
||||
var builder pipeline.RuleBuilder
|
||||
if os.Getenv("GF_LIVE_DEV_BUILDER") != "" {
|
||||
builder = &pipeline.DevRuleBuilder{
|
||||
@ -391,6 +393,7 @@ func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, r
|
||||
type GrafanaLive struct {
|
||||
PluginContextProvider *plugincontext.Provider
|
||||
Cfg *setting.Cfg
|
||||
Features *featuremgmt.FeatureToggles
|
||||
RouteRegister routing.RouteRegister
|
||||
CacheService *localcache.CacheService
|
||||
DataSourceCache datasources.CacheService
|
||||
|
@ -8,9 +8,9 @@ import (
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/schema"
|
||||
"github.com/grafana/grafana/pkg/schema/load"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
const ServiceName = "SchemaLoader"
|
||||
@ -26,13 +26,13 @@ type RenderUser struct {
|
||||
OrgRole string
|
||||
}
|
||||
|
||||
func ProvideService(cfg *setting.Cfg) (*SchemaLoaderService, error) {
|
||||
func ProvideService(features *featuremgmt.FeatureToggles) (*SchemaLoaderService, error) {
|
||||
dashFam, err := load.BaseDashboardFamily(baseLoadPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load dashboard cue schema from path %q: %w", baseLoadPath, err)
|
||||
}
|
||||
s := &SchemaLoaderService{
|
||||
Cfg: cfg,
|
||||
features: features,
|
||||
DashFamily: dashFam,
|
||||
log: log.New("schemaloader"),
|
||||
}
|
||||
@ -42,14 +42,14 @@ func ProvideService(cfg *setting.Cfg) (*SchemaLoaderService, error) {
|
||||
type SchemaLoaderService struct {
|
||||
log log.Logger
|
||||
DashFamily schema.VersionedCueSchema
|
||||
Cfg *setting.Cfg
|
||||
features *featuremgmt.FeatureToggles
|
||||
}
|
||||
|
||||
func (rs *SchemaLoaderService) IsDisabled() bool {
|
||||
if rs.Cfg == nil {
|
||||
if rs.features == nil {
|
||||
return true
|
||||
}
|
||||
return !rs.Cfg.IsTrimDefaultsEnabled()
|
||||
return !rs.features.IsTrimDefaultsEnabled()
|
||||
}
|
||||
|
||||
func (rs *SchemaLoaderService) DashboardApplyDefaults(input *simplejson.Json) (*simplejson.Json, error) {
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/usagestats"
|
||||
"github.com/grafana/grafana/pkg/services/encryption/ossencryption"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/kmsproviders/osskmsproviders"
|
||||
"github.com/grafana/grafana/pkg/services/secrets"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@ -23,10 +24,15 @@ func SetupTestService(tb testing.TB, store secrets.Store) *SecretsService {
|
||||
[security]
|
||||
secret_key = ` + defaultKey))
|
||||
require.NoError(tb, err)
|
||||
|
||||
features := featuremgmt.WithToggles("envelopeEncryption")
|
||||
|
||||
cfg := &setting.Cfg{Raw: raw}
|
||||
cfg.FeatureToggles = map[string]bool{secrets.EnvelopeEncryptionFeatureToggle: true}
|
||||
cfg.IsFeatureToggleEnabled = features.IsEnabled
|
||||
|
||||
settings := &setting.OSSImpl{Cfg: cfg}
|
||||
assert.True(tb, settings.IsFeatureToggleEnabled(secrets.EnvelopeEncryptionFeatureToggle))
|
||||
assert.True(tb, settings.IsFeatureToggleEnabled("envelopeEncryption"))
|
||||
assert.True(tb, features.IsEnvelopeEncryptionEnabled())
|
||||
|
||||
encryption := ossencryption.ProvideService()
|
||||
secretsService, err := ProvideSecretsService(
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/usagestats"
|
||||
"github.com/grafana/grafana/pkg/services/encryption/ossencryption"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/kmsproviders/osskmsproviders"
|
||||
"github.com/grafana/grafana/pkg/services/secrets"
|
||||
"github.com/grafana/grafana/pkg/services/secrets/database"
|
||||
@ -180,8 +181,8 @@ func TestSecretsService_UseCurrentProvider(t *testing.T) {
|
||||
providerID := secrets.ProviderID("fakeProvider.v1")
|
||||
settings := &setting.OSSImpl{
|
||||
Cfg: &setting.Cfg{
|
||||
Raw: raw,
|
||||
FeatureToggles: map[string]bool{secrets.EnvelopeEncryptionFeatureToggle: true},
|
||||
Raw: raw,
|
||||
IsFeatureToggleEnabled: featuremgmt.WithToggles(secrets.EnvelopeEncryptionFeatureToggle).IsEnabled,
|
||||
},
|
||||
}
|
||||
encr := ossencryption.ProvideService()
|
||||
|
@ -13,8 +13,8 @@ import (
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
acmiddleware "github.com/grafana/grafana/pkg/services/accesscontrol/middleware"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
@ -40,9 +40,9 @@ func NewServiceAccountsAPI(
|
||||
}
|
||||
|
||||
func (api *ServiceAccountsAPI) RegisterAPIEndpoints(
|
||||
cfg *setting.Cfg,
|
||||
features *featuremgmt.FeatureToggles,
|
||||
) {
|
||||
if !cfg.FeatureToggles["service-accounts"] {
|
||||
if !features.IsServiceAccountsEnabled() {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -13,10 +13,10 @@ import (
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
||||
"github.com/grafana/grafana/pkg/services/serviceaccounts/tests"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
@ -97,7 +97,7 @@ func serviceAccountRequestScenario(t *testing.T, httpMethod string, endpoint str
|
||||
|
||||
func setupTestServer(t *testing.T, svc *tests.ServiceAccountMock, routerRegister routing.RouteRegister, acmock *accesscontrolmock.Mock, sqlStore *sqlstore.SQLStore) *web.Mux {
|
||||
a := NewServiceAccountsAPI(svc, acmock, routerRegister, database.NewServiceAccountsStore(sqlStore))
|
||||
a.RegisterAPIEndpoints(&setting.Cfg{FeatureToggles: map[string]bool{"service-accounts": true}})
|
||||
a.RegisterAPIEndpoints(featuremgmt.WithToggles("service-accounts"))
|
||||
|
||||
m := web.New()
|
||||
signedUser := &models.SignedInUser{
|
||||
|
@ -7,11 +7,11 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
||||
"github.com/grafana/grafana/pkg/services/serviceaccounts/api"
|
||||
"github.com/grafana/grafana/pkg/services/serviceaccounts/database"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -19,21 +19,21 @@ var (
|
||||
)
|
||||
|
||||
type ServiceAccountsService struct {
|
||||
store serviceaccounts.Store
|
||||
cfg *setting.Cfg
|
||||
log log.Logger
|
||||
store serviceaccounts.Store
|
||||
features *featuremgmt.FeatureToggles
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func ProvideServiceAccountsService(
|
||||
cfg *setting.Cfg,
|
||||
features *featuremgmt.FeatureToggles,
|
||||
store *sqlstore.SQLStore,
|
||||
ac accesscontrol.AccessControl,
|
||||
routeRegister routing.RouteRegister,
|
||||
) (*ServiceAccountsService, error) {
|
||||
s := &ServiceAccountsService{
|
||||
cfg: cfg,
|
||||
store: database.NewServiceAccountsStore(store),
|
||||
log: log.New("serviceaccounts"),
|
||||
features: features,
|
||||
store: database.NewServiceAccountsStore(store),
|
||||
log: log.New("serviceaccounts"),
|
||||
}
|
||||
|
||||
if err := RegisterRoles(ac); err != nil {
|
||||
@ -41,13 +41,13 @@ func ProvideServiceAccountsService(
|
||||
}
|
||||
|
||||
serviceaccountsAPI := api.NewServiceAccountsAPI(s, ac, routeRegister, s.store)
|
||||
serviceaccountsAPI.RegisterAPIEndpoints(cfg)
|
||||
serviceaccountsAPI.RegisterAPIEndpoints(features)
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (sa *ServiceAccountsService) CreateServiceAccount(ctx context.Context, saForm *serviceaccounts.CreateServiceaccountForm) (*models.User, error) {
|
||||
if !sa.cfg.FeatureToggles["service-accounts"] {
|
||||
if !sa.features.IsServiceAccountsEnabled() {
|
||||
sa.log.Debug(ServiceAccountFeatureToggleNotFound)
|
||||
return nil, nil
|
||||
}
|
||||
@ -55,7 +55,7 @@ func (sa *ServiceAccountsService) CreateServiceAccount(ctx context.Context, saFo
|
||||
}
|
||||
|
||||
func (sa *ServiceAccountsService) DeleteServiceAccount(ctx context.Context, orgID, serviceAccountID int64) error {
|
||||
if !sa.cfg.FeatureToggles["service-accounts"] {
|
||||
if !sa.features.IsServiceAccountsEnabled() {
|
||||
sa.log.Debug(ServiceAccountFeatureToggleNotFound)
|
||||
return nil
|
||||
}
|
||||
|
@ -5,31 +5,29 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/serviceaccounts/tests"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestProvideServiceAccount_DeleteServiceAccount(t *testing.T) {
|
||||
t.Run("feature toggle present, should call store function", func(t *testing.T) {
|
||||
cfg := setting.NewCfg()
|
||||
storeMock := &tests.ServiceAccountsStoreMock{Calls: tests.Calls{}}
|
||||
cfg.FeatureToggles = map[string]bool{"service-accounts": true}
|
||||
svc := ServiceAccountsService{cfg: cfg, store: storeMock}
|
||||
svc := ServiceAccountsService{
|
||||
features: featuremgmt.WithToggles("service-accounts", true),
|
||||
store: storeMock}
|
||||
err := svc.DeleteServiceAccount(context.Background(), 1, 1)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, storeMock.Calls.DeleteServiceAccount, 1)
|
||||
})
|
||||
|
||||
t.Run("no feature toggle present, should not call store function", func(t *testing.T) {
|
||||
cfg := setting.NewCfg()
|
||||
svcMock := &tests.ServiceAccountsStoreMock{Calls: tests.Calls{}}
|
||||
cfg.FeatureToggles = map[string]bool{"service-accounts": false}
|
||||
svc := ServiceAccountsService{
|
||||
cfg: cfg,
|
||||
store: svcMock,
|
||||
log: log.New("serviceaccounts-manager-test"),
|
||||
features: featuremgmt.WithToggles("service-accounts", false),
|
||||
store: svcMock,
|
||||
log: log.New("serviceaccounts-manager-test"),
|
||||
}
|
||||
err := svc.DeleteServiceAccount(context.Background(), 1, 1)
|
||||
require.NoError(t, err)
|
||||
|
@ -3,6 +3,7 @@ package migrations
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrations/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrations/ualert"
|
||||
. "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
@ -56,8 +57,8 @@ func (*OSSMigrations) AddMigration(mg *Migrator) {
|
||||
ualert.AddTablesMigrations(mg)
|
||||
ualert.AddDashAlertMigration(mg)
|
||||
addLibraryElementsMigrations(mg)
|
||||
if mg.Cfg != nil {
|
||||
if mg.Cfg.IsLiveConfigEnabled() {
|
||||
if mg.Cfg.IsFeatureToggleEnabled != nil {
|
||||
if mg.Cfg.IsFeatureToggleEnabled(featuremgmt.FLAG_live_config) {
|
||||
addLiveChannelMigrations(mg)
|
||||
}
|
||||
}
|
||||
|
@ -117,7 +117,7 @@ func (ss *SQLStore) GetOrgUsers(ctx context.Context, query *models.GetOrgUsersQu
|
||||
// service accounts table in the modelling
|
||||
whereConditions = append(whereConditions, fmt.Sprintf("%s.is_service_account = %t", x.Dialect().Quote("user"), query.IsServiceAccount))
|
||||
|
||||
if ss.Cfg.FeatureToggles["accesscontrol"] {
|
||||
if ss.Cfg.IsFeatureToggleEnabled("accesscontrol") {
|
||||
q, args, err := accesscontrol.Filter(ctx, ss.Dialect, "org_user.user_id", "users", "org.users:read", query.User)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -180,7 +180,7 @@ func (ss *SQLStore) SearchOrgUsers(ctx context.Context, query *models.SearchOrgU
|
||||
// service accounts table in the modelling
|
||||
whereConditions = append(whereConditions, fmt.Sprintf("%s.is_service_account = %t", x.Dialect().Quote("user"), query.IsServiceAccount))
|
||||
|
||||
if ss.Cfg.FeatureToggles["accesscontrol"] {
|
||||
if ss.Cfg.IsFeatureToggleEnabled("accesscontrol") {
|
||||
q, args, err := accesscontrol.Filter(ctx, ss.Dialect, "org_user.user_id", "users", "org.users:read", query.User)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
)
|
||||
|
||||
type getOrgUsersTestCase struct {
|
||||
@ -61,7 +62,7 @@ func TestSQLStore_GetOrgUsers(t *testing.T) {
|
||||
}
|
||||
|
||||
store := InitTestDB(t)
|
||||
store.Cfg.FeatureToggles = map[string]bool{"accesscontrol": true}
|
||||
store.Cfg.IsFeatureToggleEnabled = featuremgmt.WithToggles("accesscontrol").IsEnabled
|
||||
seedOrgUsers(t, store, 10)
|
||||
|
||||
for _, tt := range tests {
|
||||
@ -127,7 +128,7 @@ func TestSQLStore_SearchOrgUsers(t *testing.T) {
|
||||
}
|
||||
|
||||
store := InitTestDB(t)
|
||||
store.Cfg.FeatureToggles = map[string]bool{"accesscontrol": true}
|
||||
store.Cfg.IsFeatureToggleEnabled = featuremgmt.WithToggles("accesscontrol").IsEnabled
|
||||
seedOrgUsers(t, store, 10)
|
||||
|
||||
for _, tt := range tests {
|
||||
|
@ -23,6 +23,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/services/annotations"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrations"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
|
||||
@ -326,7 +327,7 @@ func (ss *SQLStore) initEngine(engine *xorm.Engine) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if ss.Cfg.IsDatabaseMetricsEnabled() {
|
||||
if ss.Cfg.IsFeatureToggleEnabled(featuremgmt.FLAG_database_metrics) {
|
||||
ss.dbCfg.Type = WrapDatabaseDriverWithHooks(ss.dbCfg.Type, ss.tracer)
|
||||
}
|
||||
|
||||
@ -492,6 +493,7 @@ func initTestDB(migration registry.DatabaseMigrator, opts ...InitTestDBOpt) (*SQ
|
||||
|
||||
// set test db config
|
||||
cfg := setting.NewCfg()
|
||||
cfg.IsFeatureToggleEnabled = func(key string) bool { return false }
|
||||
sec, err := cfg.Raw.NewSection("database")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/guardian"
|
||||
"github.com/grafana/grafana/pkg/services/live"
|
||||
"github.com/grafana/grafana/pkg/services/rendering"
|
||||
@ -39,8 +40,8 @@ type Service interface {
|
||||
CrawlerStatus(c *models.ReqContext) response.Response
|
||||
}
|
||||
|
||||
func ProvideService(cfg *setting.Cfg, renderService rendering.Service, gl *live.GrafanaLive) Service {
|
||||
if !cfg.IsDashboardPreviesEnabled() {
|
||||
func ProvideService(cfg *setting.Cfg, features *featuremgmt.FeatureToggles, renderService rendering.Service, gl *live.GrafanaLive) Service {
|
||||
if !features.IsDashboardPreviewsEnabled() {
|
||||
return &dummyService{}
|
||||
}
|
||||
|
||||
|
@ -132,7 +132,7 @@ func (o *OSSImpl) Section(section string) Section {
|
||||
func (OSSImpl) RegisterReloadHandler(string, ReloadHandler) {}
|
||||
|
||||
func (o OSSImpl) IsFeatureToggleEnabled(name string) bool {
|
||||
return o.Cfg.FeatureToggles[name]
|
||||
return o.Cfg.IsFeatureToggleEnabled(name)
|
||||
}
|
||||
|
||||
type keyValImpl struct {
|
||||
|
@ -342,8 +342,10 @@ type Cfg struct {
|
||||
|
||||
ApiKeyMaxSecondsToLive int64
|
||||
|
||||
// Use to enable new features which may still be in alpha/beta stage.
|
||||
FeatureToggles map[string]bool
|
||||
// Check if a feature toggle is enabled
|
||||
// @deprecated
|
||||
IsFeatureToggleEnabled func(key string) bool // filled in dynamically
|
||||
|
||||
AnonymousEnabled bool
|
||||
AnonymousOrgName string
|
||||
AnonymousOrgRole string
|
||||
@ -429,41 +431,6 @@ type Cfg struct {
|
||||
UnifiedAlerting UnifiedAlertingSettings
|
||||
}
|
||||
|
||||
// IsLiveConfigEnabled returns true if live should be able to save configs to SQL tables
|
||||
func (cfg Cfg) IsLiveConfigEnabled() bool {
|
||||
return cfg.FeatureToggles["live-config"]
|
||||
}
|
||||
|
||||
// IsLiveConfigEnabled returns true if live should be able to save configs to SQL tables
|
||||
func (cfg Cfg) IsDashboardPreviesEnabled() bool {
|
||||
return cfg.FeatureToggles["dashboardPreviews"]
|
||||
}
|
||||
|
||||
// IsTrimDefaultsEnabled returns whether the standalone trim dashboard default feature is enabled.
|
||||
func (cfg Cfg) IsTrimDefaultsEnabled() bool {
|
||||
return cfg.FeatureToggles["trimDefaults"]
|
||||
}
|
||||
|
||||
// IsDatabaseMetricsEnabled returns whether the database instrumentation feature is enabled.
|
||||
func (cfg Cfg) IsDatabaseMetricsEnabled() bool {
|
||||
return cfg.FeatureToggles["database_metrics"]
|
||||
}
|
||||
|
||||
// IsHTTPRequestHistogramDisabled returns whether the request historgrams is disabled.
|
||||
// This feature toggle will be removed in Grafana 8.x but gives the operator
|
||||
// some graceperiod to update all the monitoring tools.
|
||||
func (cfg Cfg) IsHTTPRequestHistogramDisabled() bool {
|
||||
return cfg.FeatureToggles["disable_http_request_histogram"]
|
||||
}
|
||||
|
||||
func (cfg Cfg) IsNewNavigationEnabled() bool {
|
||||
return cfg.FeatureToggles["newNavigation"]
|
||||
}
|
||||
|
||||
func (cfg Cfg) IsServiceAccountEnabled() bool {
|
||||
return cfg.FeatureToggles["service-accounts"]
|
||||
}
|
||||
|
||||
type CommandLineArgs struct {
|
||||
Config string
|
||||
HomePath string
|
||||
|
@ -3,42 +3,23 @@ package setting
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
var (
|
||||
featureToggleInfo = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Name: "feature_toggles_info",
|
||||
Help: "info metric that exposes what feature toggles are enabled or not",
|
||||
Namespace: "grafana",
|
||||
}, []string{"name"})
|
||||
|
||||
defaultFeatureToggles = map[string]bool{
|
||||
"recordedQueries": false,
|
||||
"accesscontrol": false,
|
||||
"service-accounts": false,
|
||||
"httpclientprovider_azure_auth": false,
|
||||
}
|
||||
)
|
||||
|
||||
// @deprecated -- should use `featuremgmt.FeatureToggles`
|
||||
func (cfg *Cfg) readFeatureToggles(iniFile *ini.File) error {
|
||||
toggles, err := overrideDefaultWithConfiguration(iniFile, defaultFeatureToggles)
|
||||
section := iniFile.Section("feature_toggles")
|
||||
toggles, err := ReadFeatureTogglesFromInitFile(section)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg.FeatureToggles = toggles
|
||||
|
||||
cfg.IsFeatureToggleEnabled = func(key string) bool { return toggles[key] }
|
||||
return nil
|
||||
}
|
||||
|
||||
func overrideDefaultWithConfiguration(iniFile *ini.File, featureToggles map[string]bool) (map[string]bool, error) {
|
||||
// Read and populate feature toggles list
|
||||
featureTogglesSection := iniFile.Section("feature_toggles")
|
||||
func ReadFeatureTogglesFromInitFile(featureTogglesSection *ini.Section) (map[string]bool, error) {
|
||||
featureToggles := make(map[string]bool, 10)
|
||||
|
||||
// parse the comma separated list in `enable`.
|
||||
featuresTogglesStr := valueAsString(featureTogglesSection, "enable", "")
|
||||
@ -60,15 +41,5 @@ func overrideDefaultWithConfiguration(iniFile *ini.File, featureToggles map[stri
|
||||
|
||||
featureToggles[v.Name()] = b
|
||||
}
|
||||
|
||||
// track if feature toggles are enabled or not using an info metric
|
||||
for k, v := range featureToggles {
|
||||
if v {
|
||||
featureToggleInfo.WithLabelValues(k).Set(1)
|
||||
} else {
|
||||
featureToggleInfo.WithLabelValues(k).Set(0)
|
||||
}
|
||||
}
|
||||
|
||||
return featureToggles, nil
|
||||
}
|
||||
|
@ -14,7 +14,6 @@ func TestFeatureToggles(t *testing.T) {
|
||||
conf map[string]string
|
||||
err error
|
||||
expectedToggles map[string]bool
|
||||
defaultToggles map[string]bool
|
||||
}{
|
||||
{
|
||||
name: "can parse feature toggles passed in the `enable` array",
|
||||
@ -58,18 +57,6 @@ func TestFeatureToggles(t *testing.T) {
|
||||
expectedToggles: map[string]bool{},
|
||||
err: strconv.ErrSyntax,
|
||||
},
|
||||
{
|
||||
name: "should override default feature toggles",
|
||||
defaultToggles: map[string]bool{
|
||||
"feature1": true,
|
||||
},
|
||||
conf: map[string]string{
|
||||
"feature1": "false",
|
||||
},
|
||||
expectedToggles: map[string]bool{
|
||||
"feature1": false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
@ -81,12 +68,7 @@ func TestFeatureToggles(t *testing.T) {
|
||||
require.ErrorIs(t, err, nil)
|
||||
}
|
||||
|
||||
dt := map[string]bool{}
|
||||
if len(tc.defaultToggles) > 0 {
|
||||
dt = tc.defaultToggles
|
||||
}
|
||||
|
||||
featureToggles, err := overrideDefaultWithConfiguration(f, dt)
|
||||
featureToggles, err := ReadFeatureTogglesFromInitFile(toggles)
|
||||
require.ErrorIs(t, err, tc.err)
|
||||
|
||||
if err == nil {
|
||||
|
@ -79,7 +79,7 @@ func (cfg *Cfg) readUnifiedAlertingEnabledSetting(section *ini.Section) (*bool,
|
||||
// the unified alerting is not enabled by default. First, check the feature flag
|
||||
if err != nil {
|
||||
// TODO: Remove in Grafana v9
|
||||
if cfg.FeatureToggles["ngalert"] {
|
||||
if cfg.IsFeatureToggleEnabled("ngalert") {
|
||||
cfg.Logger.Warn("ngalert feature flag is deprecated: use unified alerting enabled setting instead")
|
||||
enabled = true
|
||||
// feature flag overrides the legacy alerting setting.
|
||||
|
@ -143,6 +143,7 @@ func TestUnifiedAlertingSettings(t *testing.T) {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
f := ini.Empty()
|
||||
cfg := NewCfg()
|
||||
cfg.IsFeatureToggleEnabled = func(key string) bool { return false }
|
||||
unifiedAlertingSec, err := f.NewSection("unified_alerting")
|
||||
require.NoError(t, err)
|
||||
for k, v := range tc.unifiedAlertingOptions {
|
||||
|
@ -37,7 +37,7 @@ func (s *Service) SubscribeStream(_ context.Context, req *backend.SubscribeStrea
|
||||
}
|
||||
}
|
||||
|
||||
if s.cfg.FeatureToggles["live-pipeline"] {
|
||||
if s.features.IsLivePipelineEnabled() {
|
||||
// While developing Live pipeline avoid sending initial data.
|
||||
initialData = nil
|
||||
}
|
||||
@ -126,7 +126,7 @@ func (s *Service) runTestStream(ctx context.Context, path string, conf testStrea
|
||||
}
|
||||
|
||||
mode := data.IncludeDataOnly
|
||||
if s.cfg.FeatureToggles["live-pipeline"] {
|
||||
if s.features.IsLivePipelineEnabled() {
|
||||
mode = data.IncludeAll
|
||||
}
|
||||
|
||||
|
@ -10,11 +10,13 @@ import (
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
func ProvideService(cfg *setting.Cfg) *Service {
|
||||
func ProvideService(cfg *setting.Cfg, features *featuremgmt.FeatureToggles) *Service {
|
||||
s := &Service{
|
||||
features: features,
|
||||
queryMux: datasource.NewQueryTypeMux(),
|
||||
scenarios: map[string]*Scenario{},
|
||||
frame: data.NewFrame("testdata",
|
||||
@ -46,6 +48,7 @@ type Service struct {
|
||||
labelFrame *data.Frame
|
||||
queryMux *datasource.QueryTypeMux
|
||||
resourceHandler backend.CallResourceHandler
|
||||
features *featuremgmt.FeatureToggles
|
||||
}
|
||||
|
||||
func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||
|
@ -83,13 +83,13 @@ export class ContextSrv {
|
||||
}
|
||||
|
||||
accessControlEnabled(): boolean {
|
||||
return featureEnabled('accesscontrol') && Boolean(config.featureToggles['accesscontrol']);
|
||||
return featureEnabled(config.featureToggles.accesscontrol);
|
||||
}
|
||||
|
||||
// Checks whether user has required permission
|
||||
hasPermissionInMetadata(action: AccessControlAction | string, object: WithAccessControlMetadata): boolean {
|
||||
// Fallback if access control disabled
|
||||
if (!config.featureToggles['accesscontrol']) {
|
||||
if (!config.featureToggles.accesscontrol) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -99,7 +99,7 @@ export class ContextSrv {
|
||||
// Checks whether user has required permission
|
||||
hasPermission(action: AccessControlAction | string): boolean {
|
||||
// Fallback if access control disabled
|
||||
if (!config.featureToggles['accesscontrol']) {
|
||||
if (!config.featureToggles.accesscontrol) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -126,14 +126,14 @@ export class ContextSrv {
|
||||
}
|
||||
|
||||
hasAccessToExplore() {
|
||||
if (config.featureToggles['accesscontrol']) {
|
||||
if (config.featureToggles.accesscontrol) {
|
||||
return this.hasPermission(AccessControlAction.DataSourcesExplore);
|
||||
}
|
||||
return (this.isEditor || config.viewersCanEdit) && config.exploreEnabled;
|
||||
}
|
||||
|
||||
hasAccess(action: string, fallBack: boolean) {
|
||||
if (!config.featureToggles['accesscontrol']) {
|
||||
if (!config.featureToggles.accesscontrol) {
|
||||
return fallBack;
|
||||
}
|
||||
return this.hasPermission(action);
|
||||
@ -141,7 +141,7 @@ export class ContextSrv {
|
||||
|
||||
// evaluates access control permissions, granting access if the user has any of them; uses fallback if access control is disabled
|
||||
evaluatePermission(fallback: () => string[], actions: string[]) {
|
||||
if (!config.featureToggles['accesscontrol']) {
|
||||
if (!config.featureToggles.accesscontrol) {
|
||||
return fallback();
|
||||
}
|
||||
if (actions.some((action) => this.hasPermission(action))) {
|
||||
|
@ -2,7 +2,7 @@ import config from '../../core/config';
|
||||
|
||||
// accessControlQueryParam adds an additional accesscontrol=true param to params when accesscontrol is enabled
|
||||
export function accessControlQueryParam(params = {}) {
|
||||
if (!config.featureToggles['accesscontrol']) {
|
||||
if (!config.featureToggles.accesscontrol) {
|
||||
return params;
|
||||
}
|
||||
return { ...params, accesscontrol: true };
|
||||
|
@ -49,14 +49,14 @@ describe('PluginListItemBadges', () => {
|
||||
});
|
||||
|
||||
it('renders an enterprise badge (when a license is valid)', () => {
|
||||
config.licenseInfo.enabledFeatures = { 'enterprise.plugins': true };
|
||||
config.featureToggles = { 'enterprise.plugins': true };
|
||||
render(<PluginListItemBadges plugin={{ ...plugin, isEnterprise: true }} />);
|
||||
expect(screen.getByText(/enterprise/i)).toBeVisible();
|
||||
expect(screen.queryByRole('button', { name: /learn more/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders an enterprise badge with icon and link (when a license is invalid)', () => {
|
||||
config.licenseInfo.enabledFeatures = {};
|
||||
config.featureToggles = {};
|
||||
render(<PluginListItemBadges plugin={{ ...plugin, isEnterprise: true }} />);
|
||||
expect(screen.getByText(/enterprise/i)).toBeVisible();
|
||||
expect(screen.getByLabelText(/lock icon/i)).toBeInTheDocument();
|
||||
|
@ -90,7 +90,7 @@ describe('Plugin details page', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
config.pluginAdminExternalManageEnabled = false;
|
||||
config.licenseInfo.enabledFeatures = {};
|
||||
config.featureToggles = {};
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
@ -325,7 +325,7 @@ describe('Plugin details page', () => {
|
||||
});
|
||||
|
||||
it('should display an install button for enterprise plugins if license is valid', async () => {
|
||||
config.licenseInfo.enabledFeatures = { 'enterprise.plugins': true };
|
||||
config.featureToggles = { 'enterprise.plugins': true };
|
||||
|
||||
const { queryByRole } = renderPluginDetails({ id, isInstalled: false, isEnterprise: true });
|
||||
|
||||
@ -333,7 +333,7 @@ describe('Plugin details page', () => {
|
||||
});
|
||||
|
||||
it('should not display install button for enterprise plugins if license is invalid', async () => {
|
||||
config.licenseInfo.enabledFeatures = {};
|
||||
config.featureToggles = {};
|
||||
|
||||
const { queryByRole, queryByText } = renderPluginDetails({ id, isInstalled: true, isEnterprise: true });
|
||||
|
||||
@ -772,7 +772,7 @@ describe('Plugin details page', () => {
|
||||
});
|
||||
|
||||
it('should not display an install button for enterprise plugins if license is valid', async () => {
|
||||
config.licenseInfo.enabledFeatures = { 'enterprise.plugins': true };
|
||||
config.featureToggles = { 'enterprise.plugins': true };
|
||||
const { queryByRole, queryByText } = renderPluginDetails({ id, isInstalled: false, isEnterprise: true });
|
||||
|
||||
await waitFor(() => expect(queryByText(PluginTabLabels.OVERVIEW)).toBeInTheDocument());
|
||||
|
@ -10,9 +10,7 @@ import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps
|
||||
jest.mock('@grafana/runtime/src/config', () => ({
|
||||
...((jest.requireActual('@grafana/runtime/src/config') as unknown) as object),
|
||||
config: {
|
||||
licenseInfo: {
|
||||
enabledFeatures: { teamsync: true },
|
||||
},
|
||||
featureToggles: { teamsync: true },
|
||||
},
|
||||
}));
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user