mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
FeatureFlags: define features outside settings.Cfg (take 3) (#44443)
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -156,6 +156,7 @@ compilation-stats.json
|
|||||||
|
|
||||||
# auto generated Go files
|
# auto generated Go files
|
||||||
*_gen.go
|
*_gen.go
|
||||||
|
!pkg/services/featuremgmt/toggles_gen.go
|
||||||
|
|
||||||
# Auto-generated localisation files
|
# Auto-generated localisation files
|
||||||
public/locales/_build/
|
public/locales/_build/
|
||||||
|
@@ -1,3 +1,9 @@
|
|||||||
|
// NOTE: This file was auto generated. DO NOT EDIT DIRECTLY!
|
||||||
|
// To change feature flags, edit:
|
||||||
|
// pkg/services/featuremgmt/registry.go
|
||||||
|
// Then run tests in:
|
||||||
|
// pkg/services/featuremgmt/toggles_gen_test.go
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Describes available feature toggles in Grafana. These can be configured via
|
* Describes available feature toggles in Grafana. These can be configured via
|
||||||
* conf/custom.ini to enable features under development or not yet available in
|
* conf/custom.ini to enable features under development or not yet available in
|
||||||
@@ -10,13 +16,6 @@
|
|||||||
export interface FeatureToggles {
|
export interface FeatureToggles {
|
||||||
[name: string]: boolean | undefined; // support any string value
|
[name: string]: boolean | undefined; // support any string value
|
||||||
|
|
||||||
recordedQueries?: boolean;
|
|
||||||
teamsync?: boolean;
|
|
||||||
ldapsync?: boolean;
|
|
||||||
caching?: boolean;
|
|
||||||
dspermissions?: boolean;
|
|
||||||
analytics?: boolean;
|
|
||||||
['enterprise.plugins']?: boolean;
|
|
||||||
trimDefaults?: boolean;
|
trimDefaults?: boolean;
|
||||||
envelopeEncryption?: boolean;
|
envelopeEncryption?: boolean;
|
||||||
httpclientprovider_azure_auth?: boolean;
|
httpclientprovider_azure_auth?: boolean;
|
||||||
@@ -36,4 +35,5 @@ export interface FeatureToggles {
|
|||||||
newNavigation?: boolean;
|
newNavigation?: boolean;
|
||||||
showFeatureFlagsInUI?: boolean;
|
showFeatureFlagsInUI?: boolean;
|
||||||
disable_http_request_histogram?: boolean;
|
disable_http_request_histogram?: boolean;
|
||||||
|
validatedQueries?: boolean;
|
||||||
}
|
}
|
||||||
|
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
acmiddleware "github.com/grafana/grafana/pkg/services/accesscontrol/middleware"
|
acmiddleware "github.com/grafana/grafana/pkg/services/accesscontrol/middleware"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
sa "github.com/grafana/grafana/pkg/services/serviceaccounts/manager"
|
sa "github.com/grafana/grafana/pkg/services/serviceaccounts/manager"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -437,7 +438,7 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
// Some channels may have info
|
// Some channels may have info
|
||||||
liveRoute.Get("/info/*", routing.Wrap(hs.Live.HandleInfoHTTP))
|
liveRoute.Get("/info/*", routing.Wrap(hs.Live.HandleInfoHTTP))
|
||||||
|
|
||||||
if hs.Cfg.FeatureToggles["live-pipeline"] {
|
if hs.Features.IsEnabled(featuremgmt.FlagLivePipeline) {
|
||||||
// POST Live data to be processed according to channel rules.
|
// POST Live data to be processed according to channel rules.
|
||||||
liveRoute.Post("/pipeline/push/*", hs.LivePushGateway.HandlePipelinePush)
|
liveRoute.Post("/pipeline/push/*", hs.LivePushGateway.HandlePipelinePush)
|
||||||
liveRoute.Post("/pipeline-convert-test", routing.Wrap(hs.Live.HandlePipelineConvertTestHTTP), reqOrgAdmin)
|
liveRoute.Post("/pipeline-convert-test", routing.Wrap(hs.Live.HandlePipelineConvertTestHTTP), reqOrgAdmin)
|
||||||
@@ -460,6 +461,9 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
// admin api
|
// admin api
|
||||||
r.Group("/api/admin", func(adminRoute routing.RouteRegister) {
|
r.Group("/api/admin", func(adminRoute routing.RouteRegister) {
|
||||||
adminRoute.Get("/settings", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionSettingsRead)), routing.Wrap(hs.AdminGetSettings))
|
adminRoute.Get("/settings", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionSettingsRead)), routing.Wrap(hs.AdminGetSettings))
|
||||||
|
if hs.Features.IsEnabled(featuremgmt.FlagShowFeatureFlagsInUI) {
|
||||||
|
adminRoute.Get("/settings/features", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionSettingsRead)), hs.Features.HandleGetSettings)
|
||||||
|
}
|
||||||
adminRoute.Get("/stats", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionServerStatsRead)), routing.Wrap(AdminGetStats))
|
adminRoute.Get("/stats", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionServerStatsRead)), routing.Wrap(AdminGetStats))
|
||||||
adminRoute.Post("/pause-all-alerts", reqGrafanaAdmin, routing.Wrap(PauseAllAlerts))
|
adminRoute.Post("/pause-all-alerts", reqGrafanaAdmin, routing.Wrap(PauseAllAlerts))
|
||||||
|
|
||||||
|
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
"github.com/grafana/grafana/pkg/components/apikeygen"
|
"github.com/grafana/grafana/pkg/components/apikeygen"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/web"
|
"github.com/grafana/grafana/pkg/web"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -83,7 +84,7 @@ func (hs *HTTPServer) AddAPIKey(c *models.ReqContext) response.Response {
|
|||||||
}
|
}
|
||||||
cmd.OrgId = c.OrgId
|
cmd.OrgId = c.OrgId
|
||||||
var err error
|
var err error
|
||||||
if hs.Cfg.FeatureToggles["service-accounts"] {
|
if hs.Features.IsEnabled(featuremgmt.FlagServiceAccounts) {
|
||||||
// Api keys should now be created with addadditionalapikey endpoint
|
// Api keys should now be created with addadditionalapikey endpoint
|
||||||
return response.Error(400, "API keys should now be added via the AdditionalAPIKey endpoint.", err)
|
return response.Error(400, "API keys should now be added via the AdditionalAPIKey endpoint.", err)
|
||||||
}
|
}
|
||||||
@@ -120,7 +121,7 @@ func (hs *HTTPServer) AdditionalAPIKey(c *models.ReqContext) response.Response {
|
|||||||
if err := web.Bind(c.Req, &cmd); err != nil {
|
if err := web.Bind(c.Req, &cmd); err != nil {
|
||||||
return response.Error(http.StatusBadRequest, "bad request data", err)
|
return response.Error(http.StatusBadRequest, "bad request data", err)
|
||||||
}
|
}
|
||||||
if !hs.Cfg.FeatureToggles["service-accounts"] {
|
if !hs.Features.IsEnabled(featuremgmt.FlagServiceAccounts) {
|
||||||
return response.Error(500, "Requires services-accounts feature", errors.New("feature missing"))
|
return response.Error(500, "Requires services-accounts feature", errors.New("feature missing"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -30,6 +30,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/accesscontrol/resourceservices"
|
"github.com/grafana/grafana/pkg/services/accesscontrol/resourceservices"
|
||||||
"github.com/grafana/grafana/pkg/services/auth"
|
"github.com/grafana/grafana/pkg/services/auth"
|
||||||
"github.com/grafana/grafana/pkg/services/contexthandler"
|
"github.com/grafana/grafana/pkg/services/contexthandler"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/quota"
|
"github.com/grafana/grafana/pkg/services/quota"
|
||||||
"github.com/grafana/grafana/pkg/services/rendering"
|
"github.com/grafana/grafana/pkg/services/rendering"
|
||||||
"github.com/grafana/grafana/pkg/services/searchusers"
|
"github.com/grafana/grafana/pkg/services/searchusers"
|
||||||
@@ -215,8 +216,8 @@ func (s *fakeRenderService) Init() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func setupAccessControlScenarioContext(t *testing.T, cfg *setting.Cfg, url string, permissions []*accesscontrol.Permission) (*scenarioContext, *HTTPServer) {
|
func setupAccessControlScenarioContext(t *testing.T, cfg *setting.Cfg, url string, permissions []*accesscontrol.Permission) (*scenarioContext, *HTTPServer) {
|
||||||
cfg.FeatureToggles = make(map[string]bool)
|
features := featuremgmt.WithFeatures(featuremgmt.FlagAccesscontrol)
|
||||||
cfg.FeatureToggles["accesscontrol"] = true
|
cfg.IsFeatureToggleEnabled = features.IsEnabled
|
||||||
cfg.Quota.Enabled = false
|
cfg.Quota.Enabled = false
|
||||||
|
|
||||||
bus := bus.GetBus()
|
bus := bus.GetBus()
|
||||||
@@ -224,6 +225,7 @@ func setupAccessControlScenarioContext(t *testing.T, cfg *setting.Cfg, url strin
|
|||||||
Cfg: cfg,
|
Cfg: cfg,
|
||||||
Bus: bus,
|
Bus: bus,
|
||||||
Live: newTestLive(t),
|
Live: newTestLive(t),
|
||||||
|
Features: features,
|
||||||
QuotaService: "a.QuotaService{Cfg: cfg},
|
QuotaService: "a.QuotaService{Cfg: cfg},
|
||||||
RouteRegister: routing.NewRouteRegister(),
|
RouteRegister: routing.NewRouteRegister(),
|
||||||
AccessControl: accesscontrolmock.New().WithPermissions(permissions),
|
AccessControl: accesscontrolmock.New().WithPermissions(permissions),
|
||||||
@@ -298,13 +300,25 @@ func setInitCtxSignedInOrgAdmin(initCtx *models.ReqContext) {
|
|||||||
initCtx.SignedInUser = &models.SignedInUser{UserId: testUserID, OrgId: 1, OrgRole: models.ROLE_ADMIN, Login: testUserLogin}
|
initCtx.SignedInUser = &models.SignedInUser{UserId: testUserID, OrgId: 1, OrgRole: models.ROLE_ADMIN, Login: testUserLogin}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setupSimpleHTTPServer(features *featuremgmt.FeatureManager) *HTTPServer {
|
||||||
|
if features == nil {
|
||||||
|
features = featuremgmt.WithFeatures()
|
||||||
|
}
|
||||||
|
cfg := setting.NewCfg()
|
||||||
|
cfg.IsFeatureToggleEnabled = features.IsEnabled
|
||||||
|
|
||||||
|
return &HTTPServer{
|
||||||
|
Cfg: cfg,
|
||||||
|
Features: features,
|
||||||
|
Bus: bus.GetBus(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func setupHTTPServer(t *testing.T, useFakeAccessControl bool, enableAccessControl bool) accessControlScenarioContext {
|
func setupHTTPServer(t *testing.T, useFakeAccessControl bool, enableAccessControl bool) accessControlScenarioContext {
|
||||||
// Use a new conf
|
// Use a new conf
|
||||||
|
features := featuremgmt.WithFeatures("accesscontrol", enableAccessControl)
|
||||||
cfg := setting.NewCfg()
|
cfg := setting.NewCfg()
|
||||||
cfg.FeatureToggles = make(map[string]bool)
|
cfg.IsFeatureToggleEnabled = features.IsEnabled
|
||||||
if enableAccessControl {
|
|
||||||
cfg.FeatureToggles["accesscontrol"] = enableAccessControl
|
|
||||||
}
|
|
||||||
|
|
||||||
return setupHTTPServerWithCfg(t, useFakeAccessControl, enableAccessControl, cfg)
|
return setupHTTPServerWithCfg(t, useFakeAccessControl, enableAccessControl, cfg)
|
||||||
}
|
}
|
||||||
@@ -312,6 +326,9 @@ func setupHTTPServer(t *testing.T, useFakeAccessControl bool, enableAccessContro
|
|||||||
func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessControl bool, cfg *setting.Cfg) accessControlScenarioContext {
|
func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessControl bool, cfg *setting.Cfg) accessControlScenarioContext {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
features := featuremgmt.WithFeatures("accesscontrol", enableAccessControl)
|
||||||
|
cfg.IsFeatureToggleEnabled = features.IsEnabled
|
||||||
|
|
||||||
var acmock *accesscontrolmock.Mock
|
var acmock *accesscontrolmock.Mock
|
||||||
var ac *ossaccesscontrol.OSSAccessControlService
|
var ac *ossaccesscontrol.OSSAccessControlService
|
||||||
|
|
||||||
@@ -325,6 +342,7 @@ func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessCont
|
|||||||
// Create minimal HTTP Server
|
// Create minimal HTTP Server
|
||||||
hs := &HTTPServer{
|
hs := &HTTPServer{
|
||||||
Cfg: cfg,
|
Cfg: cfg,
|
||||||
|
Features: features,
|
||||||
Bus: bus,
|
Bus: bus,
|
||||||
Live: newTestLive(t),
|
Live: newTestLive(t),
|
||||||
QuotaService: "a.QuotaService{Cfg: cfg},
|
QuotaService: "a.QuotaService{Cfg: cfg},
|
||||||
@@ -344,7 +362,7 @@ func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessCont
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
hs.TeamPermissionsService = teamPermissionService
|
hs.TeamPermissionsService = teamPermissionService
|
||||||
} else {
|
} else {
|
||||||
ac = ossaccesscontrol.ProvideService(cfg, &usagestats.UsageStatsMock{T: t})
|
ac = ossaccesscontrol.ProvideService(hs.Features, &usagestats.UsageStatsMock{T: t})
|
||||||
hs.AccessControl = ac
|
hs.AccessControl = ac
|
||||||
// Perform role registration
|
// Perform role registration
|
||||||
err := hs.declareFixedRoles()
|
err := hs.declareFixedRoles()
|
||||||
|
@@ -20,6 +20,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/services/alerting"
|
"github.com/grafana/grafana/pkg/services/alerting"
|
||||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/libraryelements"
|
"github.com/grafana/grafana/pkg/services/libraryelements"
|
||||||
"github.com/grafana/grafana/pkg/services/live"
|
"github.com/grafana/grafana/pkg/services/live"
|
||||||
"github.com/grafana/grafana/pkg/services/provisioning"
|
"github.com/grafana/grafana/pkg/services/provisioning"
|
||||||
@@ -88,8 +89,17 @@ type testState struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newTestLive(t *testing.T) *live.GrafanaLive {
|
func newTestLive(t *testing.T) *live.GrafanaLive {
|
||||||
|
features := featuremgmt.WithFeatures()
|
||||||
cfg := &setting.Cfg{AppURL: "http://localhost:3000/"}
|
cfg := &setting.Cfg{AppURL: "http://localhost:3000/"}
|
||||||
gLive, err := live.ProvideService(nil, cfg, routing.NewRouteRegister(), nil, nil, nil, sqlstore.InitTestDB(t), nil, &usagestats.UsageStatsMock{T: t}, nil)
|
cfg.IsFeatureToggleEnabled = features.IsEnabled
|
||||||
|
gLive, err := live.ProvideService(nil, cfg,
|
||||||
|
routing.NewRouteRegister(),
|
||||||
|
nil, nil, nil,
|
||||||
|
sqlstore.InitTestDB(t),
|
||||||
|
nil,
|
||||||
|
&usagestats.UsageStatsMock{T: t},
|
||||||
|
nil,
|
||||||
|
features)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
return gLive
|
return gLive
|
||||||
}
|
}
|
||||||
|
@@ -249,7 +249,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i
|
|||||||
"edition": hs.License.Edition(),
|
"edition": hs.License.Edition(),
|
||||||
"enabledFeatures": hs.License.EnabledFeatures(),
|
"enabledFeatures": hs.License.EnabledFeatures(),
|
||||||
},
|
},
|
||||||
"featureToggles": hs.Cfg.FeatureToggles,
|
"featureToggles": hs.Features.GetEnabled(c.Req.Context()),
|
||||||
"rendererAvailable": hs.RenderService.IsAvailable(),
|
"rendererAvailable": hs.RenderService.IsAvailable(),
|
||||||
"rendererVersion": hs.RenderService.Version(),
|
"rendererVersion": hs.RenderService.Version(),
|
||||||
"http2Enabled": hs.Cfg.Protocol == setting.HTTP2Scheme,
|
"http2Enabled": hs.Cfg.Protocol == setting.HTTP2Scheme,
|
||||||
|
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/licensing"
|
"github.com/grafana/grafana/pkg/services/licensing"
|
||||||
"github.com/grafana/grafana/pkg/services/rendering"
|
"github.com/grafana/grafana/pkg/services/rendering"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
@@ -19,9 +20,10 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func setupTestEnvironment(t *testing.T, cfg *setting.Cfg) (*web.Mux, *HTTPServer) {
|
func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features *featuremgmt.FeatureManager) (*web.Mux, *HTTPServer) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
sqlstore.InitTestDB(t)
|
sqlstore.InitTestDB(t)
|
||||||
|
cfg.IsFeatureToggleEnabled = features.IsEnabled
|
||||||
|
|
||||||
{
|
{
|
||||||
oldVersion := setting.BuildVersion
|
oldVersion := setting.BuildVersion
|
||||||
@@ -37,9 +39,10 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg) (*web.Mux, *HTTPServer
|
|||||||
sqlStore := sqlstore.InitTestDB(t)
|
sqlStore := sqlstore.InitTestDB(t)
|
||||||
|
|
||||||
hs := &HTTPServer{
|
hs := &HTTPServer{
|
||||||
Cfg: cfg,
|
Cfg: cfg,
|
||||||
Bus: bus.GetBus(),
|
Features: features,
|
||||||
License: &licensing.OSSLicensingService{Cfg: cfg},
|
Bus: bus.GetBus(),
|
||||||
|
License: &licensing.OSSLicensingService{Cfg: cfg},
|
||||||
RenderService: &rendering.RenderingService{
|
RenderService: &rendering.RenderingService{
|
||||||
Cfg: cfg,
|
Cfg: cfg,
|
||||||
RendererPluginManager: &fakeRendererManager{},
|
RendererPluginManager: &fakeRendererManager{},
|
||||||
@@ -73,7 +76,8 @@ func TestHTTPServer_GetFrontendSettings_hideVersionAnonymous(t *testing.T) {
|
|||||||
cfg.Env = "testing"
|
cfg.Env = "testing"
|
||||||
cfg.BuildVersion = "7.8.9"
|
cfg.BuildVersion = "7.8.9"
|
||||||
cfg.BuildCommit = "01234567"
|
cfg.BuildCommit = "01234567"
|
||||||
m, hs := setupTestEnvironment(t, cfg)
|
|
||||||
|
m, hs := setupTestEnvironment(t, cfg, featuremgmt.WithFeatures())
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/api/frontend/settings", nil)
|
req := httptest.NewRequest(http.MethodGet, "/api/frontend/settings", nil)
|
||||||
|
|
||||||
|
@@ -36,6 +36,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/datasourceproxy"
|
"github.com/grafana/grafana/pkg/services/datasourceproxy"
|
||||||
"github.com/grafana/grafana/pkg/services/datasources"
|
"github.com/grafana/grafana/pkg/services/datasources"
|
||||||
"github.com/grafana/grafana/pkg/services/encryption"
|
"github.com/grafana/grafana/pkg/services/encryption"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/hooks"
|
"github.com/grafana/grafana/pkg/services/hooks"
|
||||||
"github.com/grafana/grafana/pkg/services/libraryelements"
|
"github.com/grafana/grafana/pkg/services/libraryelements"
|
||||||
"github.com/grafana/grafana/pkg/services/librarypanels"
|
"github.com/grafana/grafana/pkg/services/librarypanels"
|
||||||
@@ -77,6 +78,7 @@ type HTTPServer struct {
|
|||||||
Bus bus.Bus
|
Bus bus.Bus
|
||||||
RenderService rendering.Service
|
RenderService rendering.Service
|
||||||
Cfg *setting.Cfg
|
Cfg *setting.Cfg
|
||||||
|
Features *featuremgmt.FeatureManager
|
||||||
SettingsProvider setting.Provider
|
SettingsProvider setting.Provider
|
||||||
HooksService *hooks.HooksService
|
HooksService *hooks.HooksService
|
||||||
CacheService *localcache.CacheService
|
CacheService *localcache.CacheService
|
||||||
@@ -138,7 +140,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
|
|||||||
loginService login.Service, accessControl accesscontrol.AccessControl,
|
loginService login.Service, accessControl accesscontrol.AccessControl,
|
||||||
dataSourceProxy *datasourceproxy.DataSourceProxyService, searchService *search.SearchService,
|
dataSourceProxy *datasourceproxy.DataSourceProxyService, searchService *search.SearchService,
|
||||||
live *live.GrafanaLive, livePushGateway *pushhttp.Gateway, plugCtxProvider *plugincontext.Provider,
|
live *live.GrafanaLive, livePushGateway *pushhttp.Gateway, plugCtxProvider *plugincontext.Provider,
|
||||||
contextHandler *contexthandler.ContextHandler,
|
contextHandler *contexthandler.ContextHandler, features *featuremgmt.FeatureManager,
|
||||||
schemaService *schemaloader.SchemaLoaderService, alertNG *ngalert.AlertNG,
|
schemaService *schemaloader.SchemaLoaderService, alertNG *ngalert.AlertNG,
|
||||||
libraryPanelService librarypanels.Service, libraryElementService libraryelements.Service,
|
libraryPanelService librarypanels.Service, libraryElementService libraryelements.Service,
|
||||||
quotaService *quota.QuotaService, socialService social.Service, tracer tracing.Tracer,
|
quotaService *quota.QuotaService, socialService social.Service, tracer tracing.Tracer,
|
||||||
@@ -171,6 +173,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
|
|||||||
AuthTokenService: userTokenService,
|
AuthTokenService: userTokenService,
|
||||||
cleanUpService: cleanUpService,
|
cleanUpService: cleanUpService,
|
||||||
ShortURLService: shortURLService,
|
ShortURLService: shortURLService,
|
||||||
|
Features: features,
|
||||||
ThumbService: thumbService,
|
ThumbService: thumbService,
|
||||||
RemoteCacheService: remoteCache,
|
RemoteCacheService: remoteCache,
|
||||||
ProvisioningService: provisioningService,
|
ProvisioningService: provisioningService,
|
||||||
|
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/plugins"
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -85,7 +86,7 @@ func (hs *HTTPServer) getAppLinks(c *models.ReqContext) ([]*dtos.NavLink, error)
|
|||||||
SortWeight: dtos.WeightPlugin,
|
SortWeight: dtos.WeightPlugin,
|
||||||
}
|
}
|
||||||
|
|
||||||
if hs.Cfg.IsNewNavigationEnabled() {
|
if hs.Features.IsEnabled(featuremgmt.FlagNewNavigation) {
|
||||||
appLink.Section = dtos.NavSectionPlugin
|
appLink.Section = dtos.NavSectionPlugin
|
||||||
} else {
|
} else {
|
||||||
appLink.Section = dtos.NavSectionCore
|
appLink.Section = dtos.NavSectionCore
|
||||||
@@ -143,7 +144,9 @@ func (hs *HTTPServer) getAppLinks(c *models.ReqContext) ([]*dtos.NavLink, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func enableServiceAccount(hs *HTTPServer, c *models.ReqContext) bool {
|
func enableServiceAccount(hs *HTTPServer, c *models.ReqContext) bool {
|
||||||
return c.OrgRole == models.ROLE_ADMIN && hs.Cfg.IsServiceAccountEnabled() && hs.serviceAccountsService.Migrated(c.Req.Context(), c.OrgId)
|
return c.OrgRole == models.ROLE_ADMIN &&
|
||||||
|
hs.Features.IsEnabled(featuremgmt.FlagServiceAccounts) &&
|
||||||
|
hs.serviceAccountsService.Migrated(c.Req.Context(), c.OrgId)
|
||||||
}
|
}
|
||||||
|
|
||||||
func enableTeams(hs *HTTPServer, c *models.ReqContext) bool {
|
func enableTeams(hs *HTTPServer, c *models.ReqContext) bool {
|
||||||
@@ -154,7 +157,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
|
|||||||
hasAccess := ac.HasAccess(hs.AccessControl, c)
|
hasAccess := ac.HasAccess(hs.AccessControl, c)
|
||||||
navTree := []*dtos.NavLink{}
|
navTree := []*dtos.NavLink{}
|
||||||
|
|
||||||
if hs.Cfg.IsNewNavigationEnabled() {
|
if hs.Features.IsEnabled(featuremgmt.FlagNewNavigation) {
|
||||||
navTree = append(navTree, &dtos.NavLink{
|
navTree = append(navTree, &dtos.NavLink{
|
||||||
Text: "Home",
|
Text: "Home",
|
||||||
Id: "home",
|
Id: "home",
|
||||||
@@ -165,7 +168,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasEditPerm && !hs.Cfg.IsNewNavigationEnabled() {
|
if hasEditPerm && !hs.Features.IsEnabled(featuremgmt.FlagNewNavigation) {
|
||||||
children := hs.buildCreateNavLinks(c)
|
children := hs.buildCreateNavLinks(c)
|
||||||
navTree = append(navTree, &dtos.NavLink{
|
navTree = append(navTree, &dtos.NavLink{
|
||||||
Text: "Create",
|
Text: "Create",
|
||||||
@@ -181,7 +184,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
|
|||||||
dashboardChildLinks := hs.buildDashboardNavLinks(c, hasEditPerm)
|
dashboardChildLinks := hs.buildDashboardNavLinks(c, hasEditPerm)
|
||||||
|
|
||||||
dashboardsUrl := "/"
|
dashboardsUrl := "/"
|
||||||
if hs.Cfg.IsNewNavigationEnabled() {
|
if hs.Features.IsEnabled(featuremgmt.FlagNewNavigation) {
|
||||||
dashboardsUrl = "/dashboards"
|
dashboardsUrl = "/dashboards"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,7 +315,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if hs.Cfg.FeatureToggles["live-pipeline"] {
|
if hs.Features.IsEnabled(featuremgmt.FlagLivePipeline) {
|
||||||
liveNavLinks := []*dtos.NavLink{}
|
liveNavLinks := []*dtos.NavLink{}
|
||||||
|
|
||||||
liveNavLinks = append(liveNavLinks, &dtos.NavLink{
|
liveNavLinks = append(liveNavLinks, &dtos.NavLink{
|
||||||
@@ -346,7 +349,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
|
|||||||
SortWeight: dtos.WeightConfig,
|
SortWeight: dtos.WeightConfig,
|
||||||
Children: configNodes,
|
Children: configNodes,
|
||||||
}
|
}
|
||||||
if hs.Cfg.IsNewNavigationEnabled() {
|
if hs.Features.IsEnabled(featuremgmt.FlagNewNavigation) {
|
||||||
configNode.Section = dtos.NavSectionConfig
|
configNode.Section = dtos.NavSectionConfig
|
||||||
} else {
|
} else {
|
||||||
configNode.Section = dtos.NavSectionCore
|
configNode.Section = dtos.NavSectionCore
|
||||||
@@ -358,7 +361,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
|
|||||||
|
|
||||||
if len(adminNavLinks) > 0 {
|
if len(adminNavLinks) > 0 {
|
||||||
navSection := dtos.NavSectionCore
|
navSection := dtos.NavSectionCore
|
||||||
if hs.Cfg.IsNewNavigationEnabled() {
|
if hs.Features.IsEnabled(featuremgmt.FlagNewNavigation) {
|
||||||
navSection = dtos.NavSectionConfig
|
navSection = dtos.NavSectionConfig
|
||||||
}
|
}
|
||||||
serverAdminNode := navlinks.GetServerAdminNode(adminNavLinks, navSection)
|
serverAdminNode := navlinks.GetServerAdminNode(adminNavLinks, navSection)
|
||||||
@@ -386,7 +389,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
|
|||||||
|
|
||||||
func (hs *HTTPServer) buildDashboardNavLinks(c *models.ReqContext, hasEditPerm bool) []*dtos.NavLink {
|
func (hs *HTTPServer) buildDashboardNavLinks(c *models.ReqContext, hasEditPerm bool) []*dtos.NavLink {
|
||||||
dashboardChildNavs := []*dtos.NavLink{}
|
dashboardChildNavs := []*dtos.NavLink{}
|
||||||
if !hs.Cfg.IsNewNavigationEnabled() {
|
if !hs.Features.IsEnabled(featuremgmt.FlagNewNavigation) {
|
||||||
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{
|
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{
|
||||||
Text: "Home", Id: "home", Url: hs.Cfg.AppSubURL + "/", Icon: "home-alt", HideFromTabs: true,
|
Text: "Home", Id: "home", Url: hs.Cfg.AppSubURL + "/", Icon: "home-alt", HideFromTabs: true,
|
||||||
})
|
})
|
||||||
@@ -417,7 +420,7 @@ func (hs *HTTPServer) buildDashboardNavLinks(c *models.ReqContext, hasEditPerm b
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasEditPerm && hs.Cfg.IsNewNavigationEnabled() {
|
if hasEditPerm && hs.Features.IsEnabled(featuremgmt.FlagNewNavigation) {
|
||||||
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{
|
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{
|
||||||
Text: "Divider", Divider: true, Id: "divider", HideFromTabs: true,
|
Text: "Divider", Divider: true, Id: "divider", HideFromTabs: true,
|
||||||
})
|
})
|
||||||
@@ -622,7 +625,7 @@ func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewDat
|
|||||||
LoadingLogo: "public/img/grafana_icon.svg",
|
LoadingLogo: "public/img/grafana_icon.svg",
|
||||||
}
|
}
|
||||||
|
|
||||||
if hs.Cfg.FeatureToggles["accesscontrol"] {
|
if hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol) {
|
||||||
userPermissions, err := hs.AccessControl.GetUserPermissions(c.Req.Context(), c.SignedInUser)
|
userPermissions, err := hs.AccessControl.GetUserPermissions(c.Req.Context(), c.SignedInUser)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/api/response"
|
"github.com/grafana/grafana/pkg/api/response"
|
||||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
@@ -89,7 +90,7 @@ func (hs *HTTPServer) CreateOrg(c *models.ReqContext) response.Response {
|
|||||||
if err := web.Bind(c.Req, &cmd); err != nil {
|
if err := web.Bind(c.Req, &cmd); err != nil {
|
||||||
return response.Error(http.StatusBadRequest, "bad request data", err)
|
return response.Error(http.StatusBadRequest, "bad request data", err)
|
||||||
}
|
}
|
||||||
acEnabled := hs.Cfg.FeatureToggles["accesscontrol"]
|
acEnabled := hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol)
|
||||||
if !acEnabled && !(setting.AllowUserOrgCreate || c.IsGrafanaAdmin) {
|
if !acEnabled && !(setting.AllowUserOrgCreate || c.IsGrafanaAdmin) {
|
||||||
return response.Error(403, "Access denied", nil)
|
return response.Error(403, "Access denied", nil)
|
||||||
}
|
}
|
||||||
|
@@ -16,6 +16,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
@@ -34,8 +35,8 @@ func setUpGetOrgUsersDB(t *testing.T, sqlStore *sqlstore.SQLStore) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestOrgUsersAPIEndpoint_userLoggedIn(t *testing.T) {
|
func TestOrgUsersAPIEndpoint_userLoggedIn(t *testing.T) {
|
||||||
settings := setting.NewCfg()
|
hs := setupSimpleHTTPServer(featuremgmt.WithFeatures())
|
||||||
hs := &HTTPServer{Cfg: settings}
|
settings := hs.Cfg
|
||||||
|
|
||||||
sqlStore := sqlstore.InitTestDB(t)
|
sqlStore := sqlstore.InitTestDB(t)
|
||||||
sqlStore.Cfg = settings
|
sqlStore.Cfg = settings
|
||||||
|
@@ -5,7 +5,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/middleware"
|
"github.com/grafana/grafana/pkg/middleware"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/web"
|
"github.com/grafana/grafana/pkg/web"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -52,8 +52,8 @@ type RouteRegister interface {
|
|||||||
|
|
||||||
type RegisterNamedMiddleware func(name string) web.Handler
|
type RegisterNamedMiddleware func(name string) web.Handler
|
||||||
|
|
||||||
func ProvideRegister(cfg *setting.Cfg) *RouteRegisterImpl {
|
func ProvideRegister(features featuremgmt.FeatureToggles) *RouteRegisterImpl {
|
||||||
return NewRouteRegister(middleware.ProvideRouteOperationName, middleware.RequestMetrics(cfg))
|
return NewRouteRegister(middleware.ProvideRouteOperationName, middleware.RequestMetrics(features))
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRouteRegister creates a new RouteRegister with all middlewares sent as params
|
// NewRouteRegister creates a new RouteRegister with all middlewares sent as params
|
||||||
|
@@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/api/dtos"
|
"github.com/grafana/grafana/pkg/api/dtos"
|
||||||
"github.com/grafana/grafana/pkg/api/response"
|
"github.com/grafana/grafana/pkg/api/response"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
"github.com/grafana/grafana/pkg/web"
|
"github.com/grafana/grafana/pkg/web"
|
||||||
@@ -19,7 +20,7 @@ func (hs *HTTPServer) CreateTeam(c *models.ReqContext) response.Response {
|
|||||||
if err := web.Bind(c.Req, &cmd); err != nil {
|
if err := web.Bind(c.Req, &cmd); err != nil {
|
||||||
return response.Error(http.StatusBadRequest, "bad request data", err)
|
return response.Error(http.StatusBadRequest, "bad request data", err)
|
||||||
}
|
}
|
||||||
accessControlEnabled := hs.Cfg.FeatureToggles["accesscontrol"]
|
accessControlEnabled := hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol)
|
||||||
if !accessControlEnabled && c.OrgRole == models.ROLE_VIEWER {
|
if !accessControlEnabled && c.OrgRole == models.ROLE_VIEWER {
|
||||||
return response.Error(403, "Not allowed to create team.", nil)
|
return response.Error(403, "Not allowed to create team.", nil)
|
||||||
}
|
}
|
||||||
|
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/api/response"
|
"github.com/grafana/grafana/pkg/api/response"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
|
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
"github.com/grafana/grafana/pkg/web"
|
"github.com/grafana/grafana/pkg/web"
|
||||||
)
|
)
|
||||||
@@ -61,7 +62,7 @@ func (hs *HTTPServer) AddTeamMember(c *models.ReqContext) response.Response {
|
|||||||
return response.Error(http.StatusBadRequest, "teamId is invalid", err)
|
return response.Error(http.StatusBadRequest, "teamId is invalid", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !hs.Cfg.FeatureToggles["accesscontrol"] {
|
if !hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol) {
|
||||||
if err := hs.teamGuardian.CanAdmin(c.Req.Context(), cmd.OrgId, cmd.TeamId, c.SignedInUser); err != nil {
|
if err := hs.teamGuardian.CanAdmin(c.Req.Context(), cmd.OrgId, cmd.TeamId, c.SignedInUser); err != nil {
|
||||||
return response.Error(403, "Not allowed to add team member", err)
|
return response.Error(403, "Not allowed to add team member", err)
|
||||||
}
|
}
|
||||||
@@ -101,7 +102,7 @@ func (hs *HTTPServer) UpdateTeamMember(c *models.ReqContext) response.Response {
|
|||||||
}
|
}
|
||||||
orgId := c.OrgId
|
orgId := c.OrgId
|
||||||
|
|
||||||
if !hs.Cfg.FeatureToggles["accesscontrol"] {
|
if !hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol) {
|
||||||
if err := hs.teamGuardian.CanAdmin(c.Req.Context(), orgId, teamId, c.SignedInUser); err != nil {
|
if err := hs.teamGuardian.CanAdmin(c.Req.Context(), orgId, teamId, c.SignedInUser); err != nil {
|
||||||
return response.Error(403, "Not allowed to update team member", err)
|
return response.Error(403, "Not allowed to update team member", err)
|
||||||
}
|
}
|
||||||
@@ -144,7 +145,7 @@ func (hs *HTTPServer) RemoveTeamMember(c *models.ReqContext) response.Response {
|
|||||||
return response.Error(http.StatusBadRequest, "userId is invalid", err)
|
return response.Error(http.StatusBadRequest, "userId is invalid", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !hs.Cfg.FeatureToggles["accesscontrol"] {
|
if !hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol) {
|
||||||
if err := hs.teamGuardian.CanAdmin(c.Req.Context(), orgId, teamId, c.SignedInUser); err != nil {
|
if err := hs.teamGuardian.CanAdmin(c.Req.Context(), orgId, teamId, c.SignedInUser); err != nil {
|
||||||
return response.Error(403, "Not allowed to remove team member", err)
|
return response.Error(403, "Not allowed to remove team member", err)
|
||||||
}
|
}
|
||||||
|
@@ -32,9 +32,7 @@ func (stub *testLogger) Warn(testMessage string, ctx ...interface{}) {
|
|||||||
|
|
||||||
func TestTeamAPIEndpoint(t *testing.T) {
|
func TestTeamAPIEndpoint(t *testing.T) {
|
||||||
t.Run("Given two teams", func(t *testing.T) {
|
t.Run("Given two teams", func(t *testing.T) {
|
||||||
hs := &HTTPServer{
|
hs := setupSimpleHTTPServer(nil)
|
||||||
Cfg: setting.NewCfg(),
|
|
||||||
}
|
|
||||||
hs.SQLStore = sqlstore.InitTestDB(t)
|
hs.SQLStore = sqlstore.InitTestDB(t)
|
||||||
|
|
||||||
loggedInUserScenario(t, "When calling GET on", "/api/teams/search", "/api/teams/search", func(sc *scenarioContext) {
|
loggedInUserScenario(t, "When calling GET on", "/api/teams/search", "/api/teams/search", func(sc *scenarioContext) {
|
||||||
@@ -73,9 +71,7 @@ func TestTeamAPIEndpoint(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("When creating team with API key", func(t *testing.T) {
|
t.Run("When creating team with API key", func(t *testing.T) {
|
||||||
hs := &HTTPServer{
|
hs := setupSimpleHTTPServer(nil)
|
||||||
Cfg: setting.NewCfg(),
|
|
||||||
}
|
|
||||||
hs.Cfg.EditorsCanAdmin = true
|
hs.Cfg.EditorsCanAdmin = true
|
||||||
|
|
||||||
teamName := "team foo"
|
teamName := "team foo"
|
||||||
|
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
|
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
|
||||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/runner"
|
"github.com/grafana/grafana/pkg/cmd/grafana-cli/runner"
|
||||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/utils"
|
"github.com/grafana/grafana/pkg/cmd/grafana-cli/utils"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/notifier"
|
"github.com/grafana/grafana/pkg/services/ngalert/notifier"
|
||||||
"github.com/grafana/grafana/pkg/services/secrets"
|
"github.com/grafana/grafana/pkg/services/secrets"
|
||||||
"github.com/grafana/grafana/pkg/services/secrets/manager"
|
"github.com/grafana/grafana/pkg/services/secrets/manager"
|
||||||
@@ -178,7 +179,7 @@ func (s alertingSecret) reencrypt(secretsSrv *manager.SecretsService, sess *xorm
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ReEncryptSecrets(_ utils.CommandLine, runner runner.Runner) error {
|
func ReEncryptSecrets(_ utils.CommandLine, runner runner.Runner) error {
|
||||||
if !runner.SettingsProvider.IsFeatureToggleEnabled(secrets.EnvelopeEncryptionFeatureToggle) {
|
if !runner.SettingsProvider.IsFeatureToggleEnabled(featuremgmt.FlagEnvelopeEncryption) {
|
||||||
logger.Warn("Envelope encryption is not enabled, quitting...")
|
logger.Warn("Envelope encryption is not enabled, quitting...")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/infra/localcache"
|
"github.com/grafana/grafana/pkg/infra/localcache"
|
||||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||||
"github.com/grafana/grafana/pkg/infra/usagestats"
|
"github.com/grafana/grafana/pkg/infra/usagestats"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/secrets"
|
"github.com/grafana/grafana/pkg/services/secrets"
|
||||||
secretsDatabase "github.com/grafana/grafana/pkg/services/secrets/database"
|
secretsDatabase "github.com/grafana/grafana/pkg/services/secrets/database"
|
||||||
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
|
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
|
||||||
@@ -25,6 +26,8 @@ var wireSet = wire.NewSet(
|
|||||||
localcache.ProvideService,
|
localcache.ProvideService,
|
||||||
tracing.ProvideService,
|
tracing.ProvideService,
|
||||||
bus.ProvideBus,
|
bus.ProvideBus,
|
||||||
|
featuremgmt.ProvideManagerService,
|
||||||
|
featuremgmt.ProvideToggles,
|
||||||
wire.Bind(new(bus.Bus), new(*bus.InProcBus)),
|
wire.Bind(new(bus.Bus), new(*bus.InProcBus)),
|
||||||
sqlstore.ProvideService,
|
sqlstore.ProvideService,
|
||||||
wire.InterfaceValue(new(usagestats.Service), noOpUsageStats{}),
|
wire.InterfaceValue(new(usagestats.Service), noOpUsageStats{}),
|
||||||
|
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/infra/metrics/metricutil"
|
"github.com/grafana/grafana/pkg/infra/metrics/metricutil"
|
||||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/mwitkow/go-conntrack"
|
"github.com/mwitkow/go-conntrack"
|
||||||
)
|
)
|
||||||
@@ -16,7 +17,7 @@ import (
|
|||||||
var newProviderFunc = sdkhttpclient.NewProvider
|
var newProviderFunc = sdkhttpclient.NewProvider
|
||||||
|
|
||||||
// New creates a new HTTP client provider with pre-configured middlewares.
|
// New creates a new HTTP client provider with pre-configured middlewares.
|
||||||
func New(cfg *setting.Cfg, tracer tracing.Tracer) *sdkhttpclient.Provider {
|
func New(cfg *setting.Cfg, tracer tracing.Tracer, features featuremgmt.FeatureToggles) *sdkhttpclient.Provider {
|
||||||
logger := log.New("httpclient")
|
logger := log.New("httpclient")
|
||||||
userAgent := fmt.Sprintf("Grafana/%s", cfg.BuildVersion)
|
userAgent := fmt.Sprintf("Grafana/%s", cfg.BuildVersion)
|
||||||
|
|
||||||
@@ -35,7 +36,7 @@ func New(cfg *setting.Cfg, tracer tracing.Tracer) *sdkhttpclient.Provider {
|
|||||||
|
|
||||||
setDefaultTimeoutOptions(cfg)
|
setDefaultTimeoutOptions(cfg)
|
||||||
|
|
||||||
if cfg.FeatureToggles["httpclientprovider_azure_auth"] {
|
if features.IsEnabled(featuremgmt.FlagHttpclientproviderAzureAuth) {
|
||||||
middlewares = append(middlewares, AzureMiddleware(cfg))
|
middlewares = append(middlewares, AzureMiddleware(cfg))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
|
sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
|
||||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@@ -22,7 +23,7 @@ func TestHTTPClientProvider(t *testing.T) {
|
|||||||
})
|
})
|
||||||
tracer, err := tracing.InitializeTracerForTest()
|
tracer, err := tracing.InitializeTracerForTest()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
_ = New(&setting.Cfg{SigV4AuthEnabled: false}, tracer)
|
_ = New(&setting.Cfg{SigV4AuthEnabled: false}, tracer, featuremgmt.WithFeatures())
|
||||||
require.Len(t, providerOpts, 1)
|
require.Len(t, providerOpts, 1)
|
||||||
o := providerOpts[0]
|
o := providerOpts[0]
|
||||||
require.Len(t, o.Middlewares, 6)
|
require.Len(t, o.Middlewares, 6)
|
||||||
@@ -46,7 +47,7 @@ func TestHTTPClientProvider(t *testing.T) {
|
|||||||
})
|
})
|
||||||
tracer, err := tracing.InitializeTracerForTest()
|
tracer, err := tracing.InitializeTracerForTest()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
_ = New(&setting.Cfg{SigV4AuthEnabled: true}, tracer)
|
_ = New(&setting.Cfg{SigV4AuthEnabled: true}, tracer, featuremgmt.WithFeatures())
|
||||||
require.Len(t, providerOpts, 1)
|
require.Len(t, providerOpts, 1)
|
||||||
o := providerOpts[0]
|
o := providerOpts[0]
|
||||||
require.Len(t, o.Middlewares, 7)
|
require.Len(t, o.Middlewares, 7)
|
||||||
|
@@ -7,7 +7,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/web"
|
"github.com/grafana/grafana/pkg/web"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
cw "github.com/weaveworks/common/tracing"
|
cw "github.com/weaveworks/common/tracing"
|
||||||
@@ -45,7 +45,7 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RequestMetrics is a middleware handler that instruments the request.
|
// RequestMetrics is a middleware handler that instruments the request.
|
||||||
func RequestMetrics(cfg *setting.Cfg) func(handler string) web.Handler {
|
func RequestMetrics(features featuremgmt.FeatureToggles) func(handler string) web.Handler {
|
||||||
return func(handler string) web.Handler {
|
return func(handler string) web.Handler {
|
||||||
return func(res http.ResponseWriter, req *http.Request, c *web.Context) {
|
return func(res http.ResponseWriter, req *http.Request, c *web.Context) {
|
||||||
rw := res.(web.ResponseWriter)
|
rw := res.(web.ResponseWriter)
|
||||||
@@ -60,7 +60,7 @@ func RequestMetrics(cfg *setting.Cfg) func(handler string) web.Handler {
|
|||||||
method := sanitizeMethod(req.Method)
|
method := sanitizeMethod(req.Method)
|
||||||
|
|
||||||
// enable histogram and disable summaries + counters for http requests.
|
// enable histogram and disable summaries + counters for http requests.
|
||||||
if cfg.IsHTTPRequestHistogramDisabled() {
|
if features.IsEnabled(featuremgmt.FlagDisableHttpRequestHistogram) {
|
||||||
duration := time.Since(now).Nanoseconds() / int64(time.Millisecond)
|
duration := time.Since(now).Nanoseconds() / int64(time.Millisecond)
|
||||||
metrics.MHttpRequestTotal.WithLabelValues(handler, code, method).Inc()
|
metrics.MHttpRequestTotal.WithLabelValues(handler, code, method).Inc()
|
||||||
metrics.MHttpRequestSummary.WithLabelValues(handler, code, method).Observe(float64(duration))
|
metrics.MHttpRequestSummary.WithLabelValues(handler, code, method).Observe(float64(duration))
|
||||||
|
@@ -85,7 +85,6 @@ func pluginScenario(t *testing.T, desc string, fn func(*testing.T, *PluginManage
|
|||||||
|
|
||||||
t.Run("Given a plugin", func(t *testing.T) {
|
t.Run("Given a plugin", func(t *testing.T) {
|
||||||
cfg := &setting.Cfg{
|
cfg := &setting.Cfg{
|
||||||
FeatureToggles: map[string]bool{},
|
|
||||||
PluginSettings: setting.PluginSettings{
|
PluginSettings: setting.PluginSettings{
|
||||||
"test-app": map[string]string{
|
"test-app": map[string]string{
|
||||||
"path": "testdata/test-app",
|
"path": "testdata/test-app",
|
||||||
|
@@ -18,7 +18,6 @@ import (
|
|||||||
|
|
||||||
func TestGetPluginDashboards(t *testing.T) {
|
func TestGetPluginDashboards(t *testing.T) {
|
||||||
cfg := &setting.Cfg{
|
cfg := &setting.Cfg{
|
||||||
FeatureToggles: map[string]bool{},
|
|
||||||
PluginSettings: setting.PluginSettings{
|
PluginSettings: setting.PluginSettings{
|
||||||
"test-app": map[string]string{
|
"test-app": map[string]string{
|
||||||
"path": "testdata/test-app",
|
"path": "testdata/test-app",
|
||||||
|
@@ -17,6 +17,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/plugins/backendplugin/provider"
|
"github.com/grafana/grafana/pkg/plugins/backendplugin/provider"
|
||||||
"github.com/grafana/grafana/pkg/plugins/manager/loader"
|
"github.com/grafana/grafana/pkg/plugins/manager/loader"
|
||||||
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/licensing"
|
"github.com/grafana/grafana/pkg/services/licensing"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/tsdb/azuremonitor"
|
"github.com/grafana/grafana/pkg/tsdb/azuremonitor"
|
||||||
@@ -50,11 +51,13 @@ func TestPluginManager_int_init(t *testing.T) {
|
|||||||
bundledPluginsPath, err := filepath.Abs("../../../plugins-bundled/internal")
|
bundledPluginsPath, err := filepath.Abs("../../../plugins-bundled/internal")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
features := featuremgmt.WithFeatures()
|
||||||
cfg := &setting.Cfg{
|
cfg := &setting.Cfg{
|
||||||
Raw: ini.Empty(),
|
Raw: ini.Empty(),
|
||||||
Env: setting.Prod,
|
Env: setting.Prod,
|
||||||
StaticRootPath: staticRootPath,
|
StaticRootPath: staticRootPath,
|
||||||
BundledPluginsPath: bundledPluginsPath,
|
BundledPluginsPath: bundledPluginsPath,
|
||||||
|
IsFeatureToggleEnabled: features.IsEnabled,
|
||||||
PluginSettings: map[string]map[string]string{
|
PluginSettings: map[string]map[string]string{
|
||||||
"plugin.datasource-id": {
|
"plugin.datasource-id": {
|
||||||
"path": "testdata/test-app",
|
"path": "testdata/test-app",
|
||||||
@@ -79,7 +82,7 @@ func TestPluginManager_int_init(t *testing.T) {
|
|||||||
otsdb := opentsdb.ProvideService(hcp)
|
otsdb := opentsdb.ProvideService(hcp)
|
||||||
pr := prometheus.ProvideService(hcp, tracer)
|
pr := prometheus.ProvideService(hcp, tracer)
|
||||||
tmpo := tempo.ProvideService(hcp)
|
tmpo := tempo.ProvideService(hcp)
|
||||||
td := testdatasource.ProvideService(cfg)
|
td := testdatasource.ProvideService(cfg, features)
|
||||||
pg := postgres.ProvideService(cfg)
|
pg := postgres.ProvideService(cfg)
|
||||||
my := mysql.ProvideService(cfg, hcp)
|
my := mysql.ProvideService(cfg, hcp)
|
||||||
ms := mssql.ProvideService(cfg)
|
ms := mssql.ProvideService(cfg)
|
||||||
|
@@ -36,6 +36,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
|
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
|
||||||
"github.com/grafana/grafana/pkg/services/datasourceproxy"
|
"github.com/grafana/grafana/pkg/services/datasourceproxy"
|
||||||
"github.com/grafana/grafana/pkg/services/datasources"
|
"github.com/grafana/grafana/pkg/services/datasources"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/hooks"
|
"github.com/grafana/grafana/pkg/services/hooks"
|
||||||
"github.com/grafana/grafana/pkg/services/libraryelements"
|
"github.com/grafana/grafana/pkg/services/libraryelements"
|
||||||
"github.com/grafana/grafana/pkg/services/librarypanels"
|
"github.com/grafana/grafana/pkg/services/librarypanels"
|
||||||
@@ -183,6 +184,8 @@ var wireBasicSet = wire.NewSet(
|
|||||||
wire.Bind(new(teamguardian.Store), new(*teamguardianDatabase.TeamGuardianStoreImpl)),
|
wire.Bind(new(teamguardian.Store), new(*teamguardianDatabase.TeamGuardianStoreImpl)),
|
||||||
teamguardianManager.ProvideService,
|
teamguardianManager.ProvideService,
|
||||||
wire.Bind(new(teamguardian.TeamGuardian), new(*teamguardianManager.Service)),
|
wire.Bind(new(teamguardian.TeamGuardian), new(*teamguardianManager.Service)),
|
||||||
|
featuremgmt.ProvideManagerService,
|
||||||
|
featuremgmt.ProvideToggles,
|
||||||
resourceservices.ProvideResourceServices,
|
resourceservices.ProvideResourceServices,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -9,13 +9,13 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/infra/usagestats"
|
"github.com/grafana/grafana/pkg/infra/usagestats"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ProvideService(cfg *setting.Cfg, usageStats usagestats.Service) *OSSAccessControlService {
|
func ProvideService(features featuremgmt.FeatureToggles, usageStats usagestats.Service) *OSSAccessControlService {
|
||||||
s := &OSSAccessControlService{
|
s := &OSSAccessControlService{
|
||||||
Cfg: cfg,
|
features: features,
|
||||||
UsageStats: usageStats,
|
UsageStats: usageStats,
|
||||||
Log: log.New("accesscontrol"),
|
Log: log.New("accesscontrol"),
|
||||||
ScopeResolver: accesscontrol.NewScopeResolver(),
|
ScopeResolver: accesscontrol.NewScopeResolver(),
|
||||||
@@ -26,7 +26,7 @@ func ProvideService(cfg *setting.Cfg, usageStats usagestats.Service) *OSSAccessC
|
|||||||
|
|
||||||
// OSSAccessControlService is the service implementing role based access control.
|
// OSSAccessControlService is the service implementing role based access control.
|
||||||
type OSSAccessControlService struct {
|
type OSSAccessControlService struct {
|
||||||
Cfg *setting.Cfg
|
features featuremgmt.FeatureToggles
|
||||||
UsageStats usagestats.Service
|
UsageStats usagestats.Service
|
||||||
Log log.Logger
|
Log log.Logger
|
||||||
registrations accesscontrol.RegistrationList
|
registrations accesscontrol.RegistrationList
|
||||||
@@ -34,10 +34,10 @@ type OSSAccessControlService struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ac *OSSAccessControlService) IsDisabled() bool {
|
func (ac *OSSAccessControlService) IsDisabled() bool {
|
||||||
if ac.Cfg == nil {
|
if ac.features == nil {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return !ac.Cfg.FeatureToggles["accesscontrol"]
|
return !ac.features.IsEnabled(featuremgmt.FlagAccesscontrol)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ac *OSSAccessControlService) registerUsageMetrics() {
|
func (ac *OSSAccessControlService) registerUsageMetrics() {
|
||||||
|
@@ -12,17 +12,14 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/infra/usagestats"
|
"github.com/grafana/grafana/pkg/infra/usagestats"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
func setupTestEnv(t testing.TB) *OSSAccessControlService {
|
func setupTestEnv(t testing.TB) *OSSAccessControlService {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
cfg := setting.NewCfg()
|
|
||||||
cfg.FeatureToggles = map[string]bool{"accesscontrol": true}
|
|
||||||
|
|
||||||
ac := &OSSAccessControlService{
|
ac := &OSSAccessControlService{
|
||||||
Cfg: cfg,
|
features: featuremgmt.WithFeatures(featuremgmt.FlagAccesscontrol),
|
||||||
UsageStats: &usagestats.UsageStatsMock{T: t},
|
UsageStats: &usagestats.UsageStatsMock{T: t},
|
||||||
Log: log.New("accesscontrol"),
|
Log: log.New("accesscontrol"),
|
||||||
registrations: accesscontrol.RegistrationList{},
|
registrations: accesscontrol.RegistrationList{},
|
||||||
@@ -148,12 +145,9 @@ func TestUsageMetrics(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
cfg := setting.NewCfg()
|
features := featuremgmt.WithFeatures("accesscontrol", tt.enabled)
|
||||||
if tt.enabled {
|
|
||||||
cfg.FeatureToggles = map[string]bool{"accesscontrol": true}
|
|
||||||
}
|
|
||||||
|
|
||||||
s := ProvideService(cfg, &usagestats.UsageStatsMock{T: t})
|
s := ProvideService(features, &usagestats.UsageStatsMock{T: t})
|
||||||
report, err := s.UsageStats.GetUsageReport(context.Background())
|
report, err := s.UsageStats.GetUsageReport(context.Background())
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
@@ -267,7 +261,7 @@ func TestOSSAccessControlService_RegisterFixedRole(t *testing.T) {
|
|||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
ac := &OSSAccessControlService{
|
ac := &OSSAccessControlService{
|
||||||
Cfg: setting.NewCfg(),
|
features: featuremgmt.WithFeatures(),
|
||||||
UsageStats: &usagestats.UsageStatsMock{T: t},
|
UsageStats: &usagestats.UsageStatsMock{T: t},
|
||||||
Log: log.New("accesscontrol-test"),
|
Log: log.New("accesscontrol-test"),
|
||||||
}
|
}
|
||||||
@@ -386,12 +380,11 @@ func TestOSSAccessControlService_DeclareFixedRoles(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
ac := &OSSAccessControlService{
|
ac := &OSSAccessControlService{
|
||||||
Cfg: setting.NewCfg(),
|
features: featuremgmt.WithFeatures(featuremgmt.FlagAccesscontrol),
|
||||||
UsageStats: &usagestats.UsageStatsMock{T: t},
|
UsageStats: &usagestats.UsageStatsMock{T: t},
|
||||||
Log: log.New("accesscontrol-test"),
|
Log: log.New("accesscontrol-test"),
|
||||||
registrations: accesscontrol.RegistrationList{},
|
registrations: accesscontrol.RegistrationList{},
|
||||||
}
|
}
|
||||||
ac.Cfg.FeatureToggles = map[string]bool{"accesscontrol": true}
|
|
||||||
|
|
||||||
// Test
|
// Test
|
||||||
err := ac.DeclareFixedRoles(tt.registrations...)
|
err := ac.DeclareFixedRoles(tt.registrations...)
|
||||||
@@ -459,9 +452,6 @@ func TestOSSAccessControlService_RegisterFixedRoles(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
cfg := setting.NewCfg()
|
|
||||||
cfg.FeatureToggles = map[string]bool{"accesscontrol": true}
|
|
||||||
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
// Remove any inserted role after the test case has been run
|
// Remove any inserted role after the test case has been run
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
@@ -472,12 +462,11 @@ func TestOSSAccessControlService_RegisterFixedRoles(t *testing.T) {
|
|||||||
|
|
||||||
// Setup
|
// Setup
|
||||||
ac := &OSSAccessControlService{
|
ac := &OSSAccessControlService{
|
||||||
Cfg: setting.NewCfg(),
|
features: featuremgmt.WithFeatures(featuremgmt.FlagAccesscontrol),
|
||||||
UsageStats: &usagestats.UsageStatsMock{T: t},
|
UsageStats: &usagestats.UsageStatsMock{T: t},
|
||||||
Log: log.New("accesscontrol-test"),
|
Log: log.New("accesscontrol-test"),
|
||||||
registrations: accesscontrol.RegistrationList{},
|
registrations: accesscontrol.RegistrationList{},
|
||||||
}
|
}
|
||||||
ac.Cfg.FeatureToggles = map[string]bool{"accesscontrol": true}
|
|
||||||
ac.registrations.Append(tt.registrations...)
|
ac.registrations.Append(tt.registrations...)
|
||||||
|
|
||||||
// Test
|
// Test
|
||||||
@@ -552,7 +541,7 @@ func TestOSSAccessControlService_GetUserPermissions(t *testing.T) {
|
|||||||
|
|
||||||
// Setup
|
// Setup
|
||||||
ac := setupTestEnv(t)
|
ac := setupTestEnv(t)
|
||||||
ac.Cfg.FeatureToggles = map[string]bool{"accesscontrol": true}
|
ac.features = featuremgmt.WithFeatures(featuremgmt.FlagAccesscontrol)
|
||||||
|
|
||||||
registration.Role.Permissions = []accesscontrol.Permission{tt.rawPerm}
|
registration.Role.Permissions = []accesscontrol.Permission{tt.rawPerm}
|
||||||
err := ac.DeclareFixedRoles(registration)
|
err := ac.DeclareFixedRoles(registration)
|
||||||
@@ -638,7 +627,6 @@ func TestOSSAccessControlService_Evaluate(t *testing.T) {
|
|||||||
|
|
||||||
// Setup
|
// Setup
|
||||||
ac := setupTestEnv(t)
|
ac := setupTestEnv(t)
|
||||||
ac.Cfg.FeatureToggles = map[string]bool{"accesscontrol": true}
|
|
||||||
ac.RegisterAttributeScopeResolver("users:login:", userLoginScopeSolver)
|
ac.RegisterAttributeScopeResolver("users:login:", userLoginScopeSolver)
|
||||||
|
|
||||||
registration.Role.Permissions = []accesscontrol.Permission{tt.rawPerm}
|
registration.Role.Permissions = []accesscontrol.Permission{tt.rawPerm}
|
||||||
|
96
pkg/services/featuremgmt/features.go
Normal file
96
pkg/services/featuremgmt/features.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
package featuremgmt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FeatureToggles interface {
|
||||||
|
IsEnabled(flag string) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// FeatureFlagState indicates the quality level
|
||||||
|
type FeatureFlagState int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// FeatureStateUnknown indicates that no state is specified
|
||||||
|
FeatureStateUnknown FeatureFlagState = iota
|
||||||
|
|
||||||
|
// FeatureStateAlpha the feature is in active development and may change at any time
|
||||||
|
FeatureStateAlpha
|
||||||
|
|
||||||
|
// FeatureStateBeta the feature is still in development, but settings will have migrations
|
||||||
|
FeatureStateBeta
|
||||||
|
|
||||||
|
// FeatureStateStable this is a stable feature
|
||||||
|
FeatureStateStable
|
||||||
|
|
||||||
|
// FeatureStateDeprecated the feature will be removed in the future
|
||||||
|
FeatureStateDeprecated
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s FeatureFlagState) String() string {
|
||||||
|
switch s {
|
||||||
|
case FeatureStateAlpha:
|
||||||
|
return "alpha"
|
||||||
|
case FeatureStateBeta:
|
||||||
|
return "beta"
|
||||||
|
case FeatureStateStable:
|
||||||
|
return "stable"
|
||||||
|
case FeatureStateDeprecated:
|
||||||
|
return "deprecated"
|
||||||
|
case FeatureStateUnknown:
|
||||||
|
}
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON marshals the enum as a quoted json string
|
||||||
|
func (s FeatureFlagState) MarshalJSON() ([]byte, error) {
|
||||||
|
buffer := bytes.NewBufferString(`"`)
|
||||||
|
buffer.WriteString(s.String())
|
||||||
|
buffer.WriteString(`"`)
|
||||||
|
return buffer.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON unmarshals a quoted json string to the enum value
|
||||||
|
func (s *FeatureFlagState) UnmarshalJSON(b []byte) error {
|
||||||
|
var j string
|
||||||
|
err := json.Unmarshal(b, &j)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch j {
|
||||||
|
case "alpha":
|
||||||
|
*s = FeatureStateAlpha
|
||||||
|
|
||||||
|
case "beta":
|
||||||
|
*s = FeatureStateBeta
|
||||||
|
|
||||||
|
case "stable":
|
||||||
|
*s = FeatureStateStable
|
||||||
|
|
||||||
|
case "deprecated":
|
||||||
|
*s = FeatureStateDeprecated
|
||||||
|
|
||||||
|
default:
|
||||||
|
*s = FeatureStateUnknown
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type FeatureFlag struct {
|
||||||
|
Name string `json:"name" yaml:"name"` // Unique name
|
||||||
|
Description string `json:"description"`
|
||||||
|
State FeatureFlagState `json:"state,omitempty"`
|
||||||
|
DocsURL string `json:"docsURL,omitempty"`
|
||||||
|
|
||||||
|
// CEL-GO expression. Using the value "true" will mean this is on by default
|
||||||
|
Expression string `json:"expression,omitempty"`
|
||||||
|
|
||||||
|
// Special behavior flags
|
||||||
|
RequiresDevMode bool `json:"requiresDevMode,omitempty"` // can not be enabled in production
|
||||||
|
RequiresRestart bool `json:"requiresRestart,omitempty"` // The server must be initialized with the value
|
||||||
|
RequiresLicense bool `json:"requiresLicense,omitempty"` // Must be enabled in the license
|
||||||
|
FrontendOnly bool `json:"frontend,omitempty"` // change is only seen in the frontend
|
||||||
|
}
|
185
pkg/services/featuremgmt/manager.go
Normal file
185
pkg/services/featuremgmt/manager.go
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
package featuremgmt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/api/response"
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ FeatureToggles = (*FeatureManager)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
type FeatureManager struct {
|
||||||
|
isDevMod bool
|
||||||
|
licensing models.Licensing
|
||||||
|
flags map[string]*FeatureFlag
|
||||||
|
enabled map[string]bool // only the "on" values
|
||||||
|
config string // path to config file
|
||||||
|
vars map[string]interface{}
|
||||||
|
log log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// This will merge the flags with the current configuration
|
||||||
|
func (fm *FeatureManager) registerFlags(flags ...FeatureFlag) {
|
||||||
|
for _, add := range flags {
|
||||||
|
if add.Name == "" {
|
||||||
|
continue // skip it with warning?
|
||||||
|
}
|
||||||
|
flag, ok := fm.flags[add.Name]
|
||||||
|
if !ok {
|
||||||
|
f := add // make a copy
|
||||||
|
fm.flags[add.Name] = &f
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Selectively update properties
|
||||||
|
if add.Description != "" {
|
||||||
|
flag.Description = add.Description
|
||||||
|
}
|
||||||
|
if add.DocsURL != "" {
|
||||||
|
flag.DocsURL = add.DocsURL
|
||||||
|
}
|
||||||
|
if add.Expression != "" {
|
||||||
|
flag.Expression = add.Expression
|
||||||
|
}
|
||||||
|
|
||||||
|
// The most recently defined state
|
||||||
|
if add.State != FeatureStateUnknown {
|
||||||
|
flag.State = add.State
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only gets more restrictive
|
||||||
|
if add.RequiresDevMode {
|
||||||
|
flag.RequiresDevMode = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if add.RequiresLicense {
|
||||||
|
flag.RequiresLicense = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if add.RequiresRestart {
|
||||||
|
flag.RequiresRestart = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This will evaluate all flags
|
||||||
|
fm.update()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *FeatureManager) evaluate(ff *FeatureFlag) bool {
|
||||||
|
if ff.RequiresDevMode && !fm.isDevMod {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if ff.RequiresLicense && (fm.licensing == nil || !fm.licensing.FeatureEnabled(ff.Name)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: CEL - expression
|
||||||
|
return ff.Expression == "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update
|
||||||
|
func (fm *FeatureManager) update() {
|
||||||
|
enabled := make(map[string]bool)
|
||||||
|
for _, flag := range fm.flags {
|
||||||
|
val := fm.evaluate(flag)
|
||||||
|
|
||||||
|
// Update the registry
|
||||||
|
track := 0.0
|
||||||
|
if val {
|
||||||
|
track = 1
|
||||||
|
enabled[flag.Name] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register value with prometheus metric
|
||||||
|
featureToggleInfo.WithLabelValues(flag.Name).Set(track)
|
||||||
|
}
|
||||||
|
fm.enabled = enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run is called by background services
|
||||||
|
func (fm *FeatureManager) readFile() error {
|
||||||
|
if fm.config == "" {
|
||||||
|
return nil // not configured
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := readConfigFile(fm.config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fm.registerFlags(cfg.Flags...)
|
||||||
|
fm.vars = cfg.Vars
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEnabled checks if a feature is enabled
|
||||||
|
func (fm *FeatureManager) IsEnabled(flag string) bool {
|
||||||
|
return fm.enabled[flag]
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEnabled returns a map contaning only the features that are enabled
|
||||||
|
func (fm *FeatureManager) GetEnabled(ctx context.Context) map[string]bool {
|
||||||
|
enabled := make(map[string]bool, len(fm.enabled))
|
||||||
|
for key, val := range fm.enabled {
|
||||||
|
if val {
|
||||||
|
enabled[key] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFlags returns all flag definitions
|
||||||
|
func (fm *FeatureManager) GetFlags() []FeatureFlag {
|
||||||
|
v := make([]FeatureFlag, 0, len(fm.flags))
|
||||||
|
for _, value := range fm.flags {
|
||||||
|
v = append(v, *value)
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *FeatureManager) HandleGetSettings(c *models.ReqContext) {
|
||||||
|
res := make(map[string]interface{}, 3)
|
||||||
|
res["enabled"] = fm.GetEnabled(c.Req.Context())
|
||||||
|
|
||||||
|
vv := make([]*FeatureFlag, 0, len(fm.flags))
|
||||||
|
for _, v := range fm.flags {
|
||||||
|
vv = append(vv, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
res["info"] = vv
|
||||||
|
|
||||||
|
response.JSON(200, res).WriteTo(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithFeatures is used to define feature toggles for testing.
|
||||||
|
// The arguments are a list of strings that are optionally followed by a boolean value
|
||||||
|
func WithFeatures(spec ...interface{}) *FeatureManager {
|
||||||
|
count := len(spec)
|
||||||
|
enabled := make(map[string]bool, count)
|
||||||
|
|
||||||
|
idx := 0
|
||||||
|
for idx < count {
|
||||||
|
key := fmt.Sprintf("%v", spec[idx])
|
||||||
|
val := true
|
||||||
|
idx++
|
||||||
|
if idx < count && reflect.TypeOf(spec[idx]).Kind() == reflect.Bool {
|
||||||
|
val = spec[idx].(bool)
|
||||||
|
idx++
|
||||||
|
}
|
||||||
|
|
||||||
|
if val {
|
||||||
|
enabled[key] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &FeatureManager{enabled: enabled}
|
||||||
|
}
|
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)
|
||||||
|
})
|
||||||
|
}
|
118
pkg/services/featuremgmt/registry.go
Normal file
118
pkg/services/featuremgmt/registry.go
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
package featuremgmt
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Register each toggle here
|
||||||
|
standardFeatureFlags = []FeatureFlag{
|
||||||
|
{
|
||||||
|
Name: "trimDefaults",
|
||||||
|
Description: "Use cue schema to remove values that will be applied automatically",
|
||||||
|
State: FeatureStateBeta,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "envelopeEncryption",
|
||||||
|
Description: "encrypt secrets",
|
||||||
|
State: FeatureStateBeta,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
Name: "httpclientprovider_azure_auth",
|
||||||
|
Description: "use http client for azure auth",
|
||||||
|
State: FeatureStateBeta,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "service-accounts",
|
||||||
|
Description: "support service accounts",
|
||||||
|
State: FeatureStateBeta,
|
||||||
|
RequiresLicense: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
Name: "database_metrics",
|
||||||
|
Description: "Add prometheus metrics for database tables",
|
||||||
|
State: FeatureStateStable,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "dashboardPreviews",
|
||||||
|
Description: "Create and show thumbnails for dashboard search results",
|
||||||
|
State: FeatureStateAlpha,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "live-config",
|
||||||
|
Description: "Save grafana live configuration in SQL tables",
|
||||||
|
State: FeatureStateAlpha,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "live-pipeline",
|
||||||
|
Description: "enable a generic live processing pipeline",
|
||||||
|
State: FeatureStateAlpha,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "live-service-web-worker",
|
||||||
|
Description: "This will use a webworker thread to processes events rather than the main thread",
|
||||||
|
State: FeatureStateAlpha,
|
||||||
|
FrontendOnly: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "queryOverLive",
|
||||||
|
Description: "Use grafana live websocket to execute backend queries",
|
||||||
|
State: FeatureStateAlpha,
|
||||||
|
FrontendOnly: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "tempoSearch",
|
||||||
|
Description: "Enable searching in tempo datasources",
|
||||||
|
State: FeatureStateBeta,
|
||||||
|
FrontendOnly: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "tempoBackendSearch",
|
||||||
|
Description: "Use backend for tempo search",
|
||||||
|
State: FeatureStateBeta,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "tempoServiceGraph",
|
||||||
|
Description: "show service",
|
||||||
|
State: FeatureStateBeta,
|
||||||
|
FrontendOnly: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "fullRangeLogsVolume",
|
||||||
|
Description: "Show full range logs volume in explore",
|
||||||
|
State: FeatureStateBeta,
|
||||||
|
FrontendOnly: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "accesscontrol",
|
||||||
|
Description: "Support robust access control",
|
||||||
|
State: FeatureStateBeta,
|
||||||
|
RequiresLicense: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "prometheus_azure_auth",
|
||||||
|
Description: "Use azure authentication for prometheus datasource",
|
||||||
|
State: FeatureStateBeta,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "newNavigation",
|
||||||
|
Description: "Try the next gen navigation model",
|
||||||
|
State: FeatureStateAlpha,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "showFeatureFlagsInUI",
|
||||||
|
Description: "Show feature flags in the settings UI",
|
||||||
|
State: FeatureStateAlpha,
|
||||||
|
RequiresDevMode: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "disable_http_request_histogram",
|
||||||
|
Description: "Do not create histograms for http requests",
|
||||||
|
State: FeatureStateAlpha,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "validatedQueries",
|
||||||
|
Description: "only execute the query saved in a panel",
|
||||||
|
State: FeatureStateAlpha,
|
||||||
|
RequiresDevMode: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
76
pkg/services/featuremgmt/service.go
Normal file
76
pkg/services/featuremgmt/service.go
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
package featuremgmt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// The values are updated each time
|
||||||
|
featureToggleInfo = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||||
|
Name: "feature_toggles_info",
|
||||||
|
Help: "info metric that exposes what feature toggles are enabled or not",
|
||||||
|
Namespace: "grafana",
|
||||||
|
}, []string{"name"})
|
||||||
|
)
|
||||||
|
|
||||||
|
func ProvideManagerService(cfg *setting.Cfg, licensing models.Licensing) (*FeatureManager, error) {
|
||||||
|
mgmt := &FeatureManager{
|
||||||
|
isDevMod: setting.Env != setting.Prod,
|
||||||
|
licensing: licensing,
|
||||||
|
flags: make(map[string]*FeatureFlag, 30),
|
||||||
|
enabled: make(map[string]bool),
|
||||||
|
log: log.New("featuremgmt"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the standard flags
|
||||||
|
mgmt.registerFlags(standardFeatureFlags...)
|
||||||
|
|
||||||
|
// Load the flags from `custom.ini` files
|
||||||
|
flags, err := setting.ReadFeatureTogglesFromInitFile(cfg.Raw.Section("feature_toggles"))
|
||||||
|
if err != nil {
|
||||||
|
return mgmt, err
|
||||||
|
}
|
||||||
|
for key, val := range flags {
|
||||||
|
flag, ok := mgmt.flags[key]
|
||||||
|
if !ok {
|
||||||
|
flag = &FeatureFlag{
|
||||||
|
Name: key,
|
||||||
|
State: FeatureStateUnknown,
|
||||||
|
}
|
||||||
|
mgmt.flags[key] = flag
|
||||||
|
}
|
||||||
|
flag.Expression = fmt.Sprintf("%t", val) // true | false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load config settings
|
||||||
|
configfile := filepath.Join(cfg.HomePath, "conf", "features.yaml")
|
||||||
|
if _, err := os.Stat(configfile); err == nil {
|
||||||
|
mgmt.log.Info("[experimental] loading features from config file", "path", configfile)
|
||||||
|
mgmt.config = configfile
|
||||||
|
err = mgmt.readFile()
|
||||||
|
if err != nil {
|
||||||
|
return mgmt, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update the values
|
||||||
|
mgmt.update()
|
||||||
|
|
||||||
|
// Minimum approach to avoid circular dependency
|
||||||
|
cfg.IsFeatureToggleEnabled = mgmt.IsEnabled
|
||||||
|
return mgmt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProvideToggles allows read-only access to the feature state
|
||||||
|
func ProvideToggles(mgmt *FeatureManager) FeatureToggles {
|
||||||
|
return mgmt
|
||||||
|
}
|
84
pkg/services/featuremgmt/service_test.go
Normal file
84
pkg/services/featuremgmt/service_test.go
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
package featuremgmt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFeatureService(t *testing.T) {
|
||||||
|
license := stubLicenseServier{
|
||||||
|
flags: []FeatureFlag{
|
||||||
|
{
|
||||||
|
Name: "a.yes.default",
|
||||||
|
RequiresLicense: true,
|
||||||
|
Expression: "true",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "a.yes",
|
||||||
|
RequiresLicense: true,
|
||||||
|
Expression: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "b.no",
|
||||||
|
RequiresLicense: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
enabled: map[string]bool{
|
||||||
|
"a.yes.default": true,
|
||||||
|
"a.yes": true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
require.False(t, license.FeatureEnabled("unknown"))
|
||||||
|
require.False(t, license.FeatureEnabled("b.no"))
|
||||||
|
require.True(t, license.FeatureEnabled("a.yes"))
|
||||||
|
require.True(t, license.FeatureEnabled("a.yes.default"))
|
||||||
|
|
||||||
|
cfg := setting.NewCfg()
|
||||||
|
mgmt, err := ProvideManagerService(cfg, license)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, mgmt)
|
||||||
|
|
||||||
|
// Enterprise features do not fall though automatically
|
||||||
|
require.False(t, mgmt.IsEnabled("a.yes.default"))
|
||||||
|
require.False(t, mgmt.IsEnabled("a.yes")) // licensed, but not enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ models.Licensing = (*stubLicenseServier)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
type stubLicenseServier struct {
|
||||||
|
flags []FeatureFlag
|
||||||
|
enabled map[string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s stubLicenseServier) Expiry() int64 {
|
||||||
|
return 100
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s stubLicenseServier) Edition() string {
|
||||||
|
return "test"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s stubLicenseServier) ContentDeliveryPrefix() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s stubLicenseServier) LicenseURL(showAdminLicensingPage bool) string {
|
||||||
|
return "http://??"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s stubLicenseServier) StateInfo() string {
|
||||||
|
return "ok"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s stubLicenseServier) EnabledFeatures() map[string]bool {
|
||||||
|
return map[string]bool{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s stubLicenseServier) FeatureEnabled(feature string) bool {
|
||||||
|
return s.enabled[feature]
|
||||||
|
}
|
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
|
85
pkg/services/featuremgmt/toggles_gen.go
Normal file
85
pkg/services/featuremgmt/toggles_gen.go
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
// NOTE: This file is autogenerated
|
||||||
|
|
||||||
|
package featuremgmt
|
||||||
|
|
||||||
|
const (
|
||||||
|
// FlagTrimDefaults
|
||||||
|
// Use cue schema to remove values that will be applied automatically
|
||||||
|
FlagTrimDefaults = "trimDefaults"
|
||||||
|
|
||||||
|
// FlagEnvelopeEncryption
|
||||||
|
// encrypt secrets
|
||||||
|
FlagEnvelopeEncryption = "envelopeEncryption"
|
||||||
|
|
||||||
|
// FlagHttpclientproviderAzureAuth
|
||||||
|
// use http client for azure auth
|
||||||
|
FlagHttpclientproviderAzureAuth = "httpclientprovider_azure_auth"
|
||||||
|
|
||||||
|
// FlagServiceAccounts
|
||||||
|
// support service accounts
|
||||||
|
FlagServiceAccounts = "service-accounts"
|
||||||
|
|
||||||
|
// FlagDatabaseMetrics
|
||||||
|
// Add prometheus metrics for database tables
|
||||||
|
FlagDatabaseMetrics = "database_metrics"
|
||||||
|
|
||||||
|
// FlagDashboardPreviews
|
||||||
|
// Create and show thumbnails for dashboard search results
|
||||||
|
FlagDashboardPreviews = "dashboardPreviews"
|
||||||
|
|
||||||
|
// FlagLiveConfig
|
||||||
|
// Save grafana live configuration in SQL tables
|
||||||
|
FlagLiveConfig = "live-config"
|
||||||
|
|
||||||
|
// FlagLivePipeline
|
||||||
|
// enable a generic live processing pipeline
|
||||||
|
FlagLivePipeline = "live-pipeline"
|
||||||
|
|
||||||
|
// FlagLiveServiceWebWorker
|
||||||
|
// This will use a webworker thread to processes events rather than the main thread
|
||||||
|
FlagLiveServiceWebWorker = "live-service-web-worker"
|
||||||
|
|
||||||
|
// FlagQueryOverLive
|
||||||
|
// Use grafana live websocket to execute backend queries
|
||||||
|
FlagQueryOverLive = "queryOverLive"
|
||||||
|
|
||||||
|
// FlagTempoSearch
|
||||||
|
// Enable searching in tempo datasources
|
||||||
|
FlagTempoSearch = "tempoSearch"
|
||||||
|
|
||||||
|
// FlagTempoBackendSearch
|
||||||
|
// Use backend for tempo search
|
||||||
|
FlagTempoBackendSearch = "tempoBackendSearch"
|
||||||
|
|
||||||
|
// FlagTempoServiceGraph
|
||||||
|
// show service
|
||||||
|
FlagTempoServiceGraph = "tempoServiceGraph"
|
||||||
|
|
||||||
|
// FlagFullRangeLogsVolume
|
||||||
|
// Show full range logs volume in explore
|
||||||
|
FlagFullRangeLogsVolume = "fullRangeLogsVolume"
|
||||||
|
|
||||||
|
// FlagAccesscontrol
|
||||||
|
// Support robust access control
|
||||||
|
FlagAccesscontrol = "accesscontrol"
|
||||||
|
|
||||||
|
// FlagPrometheusAzureAuth
|
||||||
|
// Use azure authentication for prometheus datasource
|
||||||
|
FlagPrometheusAzureAuth = "prometheus_azure_auth"
|
||||||
|
|
||||||
|
// FlagNewNavigation
|
||||||
|
// Try the next gen navigation model
|
||||||
|
FlagNewNavigation = "newNavigation"
|
||||||
|
|
||||||
|
// FlagShowFeatureFlagsInUI
|
||||||
|
// Show feature flags in the settings UI
|
||||||
|
FlagShowFeatureFlagsInUI = "showFeatureFlagsInUI"
|
||||||
|
|
||||||
|
// FlagDisableHttpRequestHistogram
|
||||||
|
// Do not create histograms for http requests
|
||||||
|
FlagDisableHttpRequestHistogram = "disable_http_request_histogram"
|
||||||
|
|
||||||
|
// FlagValidatedQueries
|
||||||
|
// only execute the query saved in a panel
|
||||||
|
FlagValidatedQueries = "validatedQueries"
|
||||||
|
)
|
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(`
|
||||||
|
{{"\t"}}// Flag{{.CamelCase}}{{.Ext}}
|
||||||
|
{{"\t"}}Flag{{.CamelCase}} = "{{.Flag.Name}}"
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("error reading template", "error", err.Error())
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
data := struct {
|
||||||
|
CamelCase string
|
||||||
|
Flag FeatureFlag
|
||||||
|
Ext string
|
||||||
|
}{
|
||||||
|
CamelCase: "?",
|
||||||
|
}
|
||||||
|
|
||||||
|
var buff bytes.Buffer
|
||||||
|
|
||||||
|
buff.WriteString(`// NOTE: This file is autogenerated
|
||||||
|
|
||||||
|
package featuremgmt
|
||||||
|
|
||||||
|
const (`)
|
||||||
|
|
||||||
|
for _, flag := range standardFeatureFlags {
|
||||||
|
data.CamelCase = asCamelCase(flag.Name)
|
||||||
|
data.Flag = flag
|
||||||
|
data.Ext = ""
|
||||||
|
|
||||||
|
if flag.Description != "" {
|
||||||
|
data.Ext += "\n\t// " + flag.Description
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = tmpl.Execute(&buff, data)
|
||||||
|
}
|
||||||
|
buff.WriteString(")\n")
|
||||||
|
|
||||||
|
return buff.String()
|
||||||
|
}
|
@@ -2,6 +2,7 @@ package osskmsproviders
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/grafana/grafana/pkg/services/encryption"
|
"github.com/grafana/grafana/pkg/services/encryption"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/kmsproviders"
|
"github.com/grafana/grafana/pkg/services/kmsproviders"
|
||||||
grafana "github.com/grafana/grafana/pkg/services/kmsproviders/defaultprovider"
|
grafana "github.com/grafana/grafana/pkg/services/kmsproviders/defaultprovider"
|
||||||
"github.com/grafana/grafana/pkg/services/secrets"
|
"github.com/grafana/grafana/pkg/services/secrets"
|
||||||
@@ -21,7 +22,7 @@ func ProvideService(enc encryption.Internal, settings setting.Provider) Service
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s Service) Provide() (map[secrets.ProviderID]secrets.Provider, error) {
|
func (s Service) Provide() (map[secrets.ProviderID]secrets.Provider, error) {
|
||||||
if !s.settings.IsFeatureToggleEnabled(secrets.EnvelopeEncryptionFeatureToggle) {
|
if !s.settings.IsFeatureToggleEnabled(featuremgmt.FlagEnvelopeEncryption) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -15,6 +15,7 @@ import (
|
|||||||
|
|
||||||
jsoniter "github.com/json-iterator/go"
|
jsoniter "github.com/json-iterator/go"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/query"
|
"github.com/grafana/grafana/pkg/services/query"
|
||||||
|
|
||||||
"github.com/centrifugal/centrifuge"
|
"github.com/centrifugal/centrifuge"
|
||||||
@@ -67,9 +68,10 @@ type CoreGrafanaScope struct {
|
|||||||
func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, routeRegister routing.RouteRegister,
|
func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, routeRegister routing.RouteRegister,
|
||||||
pluginStore plugins.Store, cacheService *localcache.CacheService,
|
pluginStore plugins.Store, cacheService *localcache.CacheService,
|
||||||
dataSourceCache datasources.CacheService, sqlStore *sqlstore.SQLStore, secretsService secrets.Service,
|
dataSourceCache datasources.CacheService, sqlStore *sqlstore.SQLStore, secretsService secrets.Service,
|
||||||
usageStatsService usagestats.Service, queryDataService *query.Service) (*GrafanaLive, error) {
|
usageStatsService usagestats.Service, queryDataService *query.Service, toggles featuremgmt.FeatureToggles) (*GrafanaLive, error) {
|
||||||
g := &GrafanaLive{
|
g := &GrafanaLive{
|
||||||
Cfg: cfg,
|
Cfg: cfg,
|
||||||
|
Features: toggles,
|
||||||
PluginContextProvider: plugCtxProvider,
|
PluginContextProvider: plugCtxProvider,
|
||||||
RouteRegister: routeRegister,
|
RouteRegister: routeRegister,
|
||||||
pluginStore: pluginStore,
|
pluginStore: pluginStore,
|
||||||
@@ -174,7 +176,7 @@ func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, r
|
|||||||
}
|
}
|
||||||
|
|
||||||
g.ManagedStreamRunner = managedStreamRunner
|
g.ManagedStreamRunner = managedStreamRunner
|
||||||
if enabled := g.Cfg.FeatureToggles["live-pipeline"]; enabled {
|
if g.Features.IsEnabled(featuremgmt.FlagLivePipeline) {
|
||||||
var builder pipeline.RuleBuilder
|
var builder pipeline.RuleBuilder
|
||||||
if os.Getenv("GF_LIVE_DEV_BUILDER") != "" {
|
if os.Getenv("GF_LIVE_DEV_BUILDER") != "" {
|
||||||
builder = &pipeline.DevRuleBuilder{
|
builder = &pipeline.DevRuleBuilder{
|
||||||
@@ -391,6 +393,7 @@ func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, r
|
|||||||
type GrafanaLive struct {
|
type GrafanaLive struct {
|
||||||
PluginContextProvider *plugincontext.Provider
|
PluginContextProvider *plugincontext.Provider
|
||||||
Cfg *setting.Cfg
|
Cfg *setting.Cfg
|
||||||
|
Features featuremgmt.FeatureToggles
|
||||||
RouteRegister routing.RouteRegister
|
RouteRegister routing.RouteRegister
|
||||||
CacheService *localcache.CacheService
|
CacheService *localcache.CacheService
|
||||||
DataSourceCache datasources.CacheService
|
DataSourceCache datasources.CacheService
|
||||||
|
@@ -8,9 +8,9 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
"github.com/grafana/grafana/pkg/schema"
|
"github.com/grafana/grafana/pkg/schema"
|
||||||
"github.com/grafana/grafana/pkg/schema/load"
|
"github.com/grafana/grafana/pkg/schema/load"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const ServiceName = "SchemaLoader"
|
const ServiceName = "SchemaLoader"
|
||||||
@@ -26,13 +26,13 @@ type RenderUser struct {
|
|||||||
OrgRole string
|
OrgRole string
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProvideService(cfg *setting.Cfg) (*SchemaLoaderService, error) {
|
func ProvideService(features featuremgmt.FeatureToggles) (*SchemaLoaderService, error) {
|
||||||
dashFam, err := load.BaseDashboardFamily(baseLoadPath)
|
dashFam, err := load.BaseDashboardFamily(baseLoadPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to load dashboard cue schema from path %q: %w", baseLoadPath, err)
|
return nil, fmt.Errorf("failed to load dashboard cue schema from path %q: %w", baseLoadPath, err)
|
||||||
}
|
}
|
||||||
s := &SchemaLoaderService{
|
s := &SchemaLoaderService{
|
||||||
Cfg: cfg,
|
features: features,
|
||||||
DashFamily: dashFam,
|
DashFamily: dashFam,
|
||||||
log: log.New("schemaloader"),
|
log: log.New("schemaloader"),
|
||||||
}
|
}
|
||||||
@@ -42,14 +42,14 @@ func ProvideService(cfg *setting.Cfg) (*SchemaLoaderService, error) {
|
|||||||
type SchemaLoaderService struct {
|
type SchemaLoaderService struct {
|
||||||
log log.Logger
|
log log.Logger
|
||||||
DashFamily schema.VersionedCueSchema
|
DashFamily schema.VersionedCueSchema
|
||||||
Cfg *setting.Cfg
|
features featuremgmt.FeatureToggles
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rs *SchemaLoaderService) IsDisabled() bool {
|
func (rs *SchemaLoaderService) IsDisabled() bool {
|
||||||
if rs.Cfg == nil {
|
if rs.features == nil {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return !rs.Cfg.IsTrimDefaultsEnabled()
|
return !rs.features.IsEnabled(featuremgmt.FlagTrimDefaults)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rs *SchemaLoaderService) DashboardApplyDefaults(input *simplejson.Json) (*simplejson.Json, error) {
|
func (rs *SchemaLoaderService) DashboardApplyDefaults(input *simplejson.Json) (*simplejson.Json, error) {
|
||||||
|
@@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/usagestats"
|
"github.com/grafana/grafana/pkg/infra/usagestats"
|
||||||
"github.com/grafana/grafana/pkg/services/encryption/ossencryption"
|
"github.com/grafana/grafana/pkg/services/encryption/ossencryption"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/kmsproviders/osskmsproviders"
|
"github.com/grafana/grafana/pkg/services/kmsproviders/osskmsproviders"
|
||||||
"github.com/grafana/grafana/pkg/services/secrets"
|
"github.com/grafana/grafana/pkg/services/secrets"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
@@ -23,10 +24,15 @@ func SetupTestService(tb testing.TB, store secrets.Store) *SecretsService {
|
|||||||
[security]
|
[security]
|
||||||
secret_key = ` + defaultKey))
|
secret_key = ` + defaultKey))
|
||||||
require.NoError(tb, err)
|
require.NoError(tb, err)
|
||||||
|
|
||||||
|
features := featuremgmt.WithFeatures(featuremgmt.FlagEnvelopeEncryption)
|
||||||
|
|
||||||
cfg := &setting.Cfg{Raw: raw}
|
cfg := &setting.Cfg{Raw: raw}
|
||||||
cfg.FeatureToggles = map[string]bool{secrets.EnvelopeEncryptionFeatureToggle: true}
|
cfg.IsFeatureToggleEnabled = features.IsEnabled
|
||||||
|
|
||||||
settings := &setting.OSSImpl{Cfg: cfg}
|
settings := &setting.OSSImpl{Cfg: cfg}
|
||||||
assert.True(tb, settings.IsFeatureToggleEnabled(secrets.EnvelopeEncryptionFeatureToggle))
|
assert.True(tb, settings.IsFeatureToggleEnabled(featuremgmt.FlagEnvelopeEncryption))
|
||||||
|
assert.True(tb, features.IsEnabled(featuremgmt.FlagEnvelopeEncryption))
|
||||||
|
|
||||||
encryption := ossencryption.ProvideService()
|
encryption := ossencryption.ProvideService()
|
||||||
secretsService, err := ProvideSecretsService(
|
secretsService, err := ProvideSecretsService(
|
||||||
|
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/infra/usagestats"
|
"github.com/grafana/grafana/pkg/infra/usagestats"
|
||||||
"github.com/grafana/grafana/pkg/services/encryption"
|
"github.com/grafana/grafana/pkg/services/encryption"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/kmsproviders"
|
"github.com/grafana/grafana/pkg/services/kmsproviders"
|
||||||
"github.com/grafana/grafana/pkg/services/secrets"
|
"github.com/grafana/grafana/pkg/services/secrets"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
@@ -43,7 +44,7 @@ func ProvideSecretsService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger := log.New("secrets")
|
logger := log.New("secrets")
|
||||||
enabled := settings.IsFeatureToggleEnabled(secrets.EnvelopeEncryptionFeatureToggle)
|
enabled := settings.IsFeatureToggleEnabled(featuremgmt.FlagEnvelopeEncryption)
|
||||||
currentProviderID := readCurrentProviderID(settings)
|
currentProviderID := readCurrentProviderID(settings)
|
||||||
|
|
||||||
if _, ok := providers[currentProviderID]; enabled && !ok {
|
if _, ok := providers[currentProviderID]; enabled && !ok {
|
||||||
@@ -87,7 +88,7 @@ func (s *SecretsService) registerUsageMetrics() {
|
|||||||
|
|
||||||
// Enabled / disabled
|
// Enabled / disabled
|
||||||
usageMetrics["stats.encryption.envelope_encryption_enabled.count"] = 0
|
usageMetrics["stats.encryption.envelope_encryption_enabled.count"] = 0
|
||||||
if s.settings.IsFeatureToggleEnabled(secrets.EnvelopeEncryptionFeatureToggle) {
|
if s.settings.IsFeatureToggleEnabled(featuremgmt.FlagEnvelopeEncryption) {
|
||||||
usageMetrics["stats.encryption.envelope_encryption_enabled.count"] = 1
|
usageMetrics["stats.encryption.envelope_encryption_enabled.count"] = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,11 +131,11 @@ func (s *SecretsService) Encrypt(ctx context.Context, payload []byte, opt secret
|
|||||||
|
|
||||||
func (s *SecretsService) EncryptWithDBSession(ctx context.Context, payload []byte, opt secrets.EncryptionOptions, sess *xorm.Session) ([]byte, error) {
|
func (s *SecretsService) EncryptWithDBSession(ctx context.Context, payload []byte, opt secrets.EncryptionOptions, sess *xorm.Session) ([]byte, error) {
|
||||||
// Use legacy encryption service if envelopeEncryptionFeatureToggle toggle is off
|
// Use legacy encryption service if envelopeEncryptionFeatureToggle toggle is off
|
||||||
if !s.settings.IsFeatureToggleEnabled(secrets.EnvelopeEncryptionFeatureToggle) {
|
if !s.settings.IsFeatureToggleEnabled(featuremgmt.FlagEnvelopeEncryption) {
|
||||||
return s.enc.Encrypt(ctx, payload, setting.SecretKey)
|
return s.enc.Encrypt(ctx, payload, setting.SecretKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If encryption secrets.EnvelopeEncryptionFeatureToggle toggle is on, use envelope encryption
|
// If encryption featuremgmt.FlagEnvelopeEncryption toggle is on, use envelope encryption
|
||||||
scope := opt()
|
scope := opt()
|
||||||
keyName := s.keyName(scope)
|
keyName := s.keyName(scope)
|
||||||
|
|
||||||
@@ -172,12 +173,12 @@ func (s *SecretsService) keyName(scope string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *SecretsService) Decrypt(ctx context.Context, payload []byte) ([]byte, error) {
|
func (s *SecretsService) Decrypt(ctx context.Context, payload []byte) ([]byte, error) {
|
||||||
// Use legacy encryption service if secrets.EnvelopeEncryptionFeatureToggle toggle is off
|
// Use legacy encryption service if featuremgmt.FlagEnvelopeEncryption toggle is off
|
||||||
if !s.settings.IsFeatureToggleEnabled(secrets.EnvelopeEncryptionFeatureToggle) {
|
if !s.settings.IsFeatureToggleEnabled(featuremgmt.FlagEnvelopeEncryption) {
|
||||||
return s.enc.Decrypt(ctx, payload, setting.SecretKey)
|
return s.enc.Decrypt(ctx, payload, setting.SecretKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If encryption secrets.EnvelopeEncryptionFeatureToggle toggle is on, use envelope encryption
|
// If encryption featuremgmt.FlagEnvelopeEncryption toggle is on, use envelope encryption
|
||||||
if len(payload) == 0 {
|
if len(payload) == 0 {
|
||||||
return nil, fmt.Errorf("unable to decrypt empty payload")
|
return nil, fmt.Errorf("unable to decrypt empty payload")
|
||||||
}
|
}
|
||||||
|
@@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/usagestats"
|
"github.com/grafana/grafana/pkg/infra/usagestats"
|
||||||
"github.com/grafana/grafana/pkg/services/encryption/ossencryption"
|
"github.com/grafana/grafana/pkg/services/encryption/ossencryption"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/kmsproviders/osskmsproviders"
|
"github.com/grafana/grafana/pkg/services/kmsproviders/osskmsproviders"
|
||||||
"github.com/grafana/grafana/pkg/services/secrets"
|
"github.com/grafana/grafana/pkg/services/secrets"
|
||||||
"github.com/grafana/grafana/pkg/services/secrets/database"
|
"github.com/grafana/grafana/pkg/services/secrets/database"
|
||||||
@@ -180,8 +181,8 @@ func TestSecretsService_UseCurrentProvider(t *testing.T) {
|
|||||||
providerID := secrets.ProviderID("fakeProvider.v1")
|
providerID := secrets.ProviderID("fakeProvider.v1")
|
||||||
settings := &setting.OSSImpl{
|
settings := &setting.OSSImpl{
|
||||||
Cfg: &setting.Cfg{
|
Cfg: &setting.Cfg{
|
||||||
Raw: raw,
|
Raw: raw,
|
||||||
FeatureToggles: map[string]bool{secrets.EnvelopeEncryptionFeatureToggle: true},
|
IsFeatureToggleEnabled: featuremgmt.WithFeatures(featuremgmt.FlagEnvelopeEncryption).IsEnabled,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
encr := ossencryption.ProvideService()
|
encr := ossencryption.ProvideService()
|
||||||
|
@@ -8,10 +8,6 @@ import (
|
|||||||
"xorm.io/xorm"
|
"xorm.io/xorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
EnvelopeEncryptionFeatureToggle = "envelopeEncryption"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Service is an envelope encryption service in charge of encrypting/decrypting secrets.
|
// Service is an envelope encryption service in charge of encrypting/decrypting secrets.
|
||||||
// It is a replacement for encryption.Service
|
// It is a replacement for encryption.Service
|
||||||
type Service interface {
|
type Service interface {
|
||||||
|
@@ -13,8 +13,8 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
acmiddleware "github.com/grafana/grafana/pkg/services/accesscontrol/middleware"
|
acmiddleware "github.com/grafana/grafana/pkg/services/accesscontrol/middleware"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
|
||||||
"github.com/grafana/grafana/pkg/web"
|
"github.com/grafana/grafana/pkg/web"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -40,9 +40,9 @@ func NewServiceAccountsAPI(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (api *ServiceAccountsAPI) RegisterAPIEndpoints(
|
func (api *ServiceAccountsAPI) RegisterAPIEndpoints(
|
||||||
cfg *setting.Cfg,
|
features featuremgmt.FeatureToggles,
|
||||||
) {
|
) {
|
||||||
if !cfg.FeatureToggles["service-accounts"] {
|
if !features.IsEnabled(featuremgmt.FlagServiceAccounts) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -13,10 +13,10 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
||||||
"github.com/grafana/grafana/pkg/services/serviceaccounts/tests"
|
"github.com/grafana/grafana/pkg/services/serviceaccounts/tests"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
|
||||||
"github.com/grafana/grafana/pkg/web"
|
"github.com/grafana/grafana/pkg/web"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
@@ -97,7 +97,7 @@ func serviceAccountRequestScenario(t *testing.T, httpMethod string, endpoint str
|
|||||||
|
|
||||||
func setupTestServer(t *testing.T, svc *tests.ServiceAccountMock, routerRegister routing.RouteRegister, acmock *accesscontrolmock.Mock, sqlStore *sqlstore.SQLStore) *web.Mux {
|
func setupTestServer(t *testing.T, svc *tests.ServiceAccountMock, routerRegister routing.RouteRegister, acmock *accesscontrolmock.Mock, sqlStore *sqlstore.SQLStore) *web.Mux {
|
||||||
a := NewServiceAccountsAPI(svc, acmock, routerRegister, database.NewServiceAccountsStore(sqlStore))
|
a := NewServiceAccountsAPI(svc, acmock, routerRegister, database.NewServiceAccountsStore(sqlStore))
|
||||||
a.RegisterAPIEndpoints(&setting.Cfg{FeatureToggles: map[string]bool{"service-accounts": true}})
|
a.RegisterAPIEndpoints(featuremgmt.WithFeatures(featuremgmt.FlagServiceAccounts))
|
||||||
|
|
||||||
m := web.New()
|
m := web.New()
|
||||||
signedUser := &models.SignedInUser{
|
signedUser := &models.SignedInUser{
|
||||||
|
@@ -7,11 +7,11 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
||||||
"github.com/grafana/grafana/pkg/services/serviceaccounts/api"
|
"github.com/grafana/grafana/pkg/services/serviceaccounts/api"
|
||||||
"github.com/grafana/grafana/pkg/services/serviceaccounts/database"
|
"github.com/grafana/grafana/pkg/services/serviceaccounts/database"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -19,21 +19,21 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ServiceAccountsService struct {
|
type ServiceAccountsService struct {
|
||||||
store serviceaccounts.Store
|
store serviceaccounts.Store
|
||||||
cfg *setting.Cfg
|
features featuremgmt.FeatureToggles
|
||||||
log log.Logger
|
log log.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProvideServiceAccountsService(
|
func ProvideServiceAccountsService(
|
||||||
cfg *setting.Cfg,
|
features featuremgmt.FeatureToggles,
|
||||||
store *sqlstore.SQLStore,
|
store *sqlstore.SQLStore,
|
||||||
ac accesscontrol.AccessControl,
|
ac accesscontrol.AccessControl,
|
||||||
routeRegister routing.RouteRegister,
|
routeRegister routing.RouteRegister,
|
||||||
) (*ServiceAccountsService, error) {
|
) (*ServiceAccountsService, error) {
|
||||||
s := &ServiceAccountsService{
|
s := &ServiceAccountsService{
|
||||||
cfg: cfg,
|
features: features,
|
||||||
store: database.NewServiceAccountsStore(store),
|
store: database.NewServiceAccountsStore(store),
|
||||||
log: log.New("serviceaccounts"),
|
log: log.New("serviceaccounts"),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := RegisterRoles(ac); err != nil {
|
if err := RegisterRoles(ac); err != nil {
|
||||||
@@ -41,13 +41,13 @@ func ProvideServiceAccountsService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
serviceaccountsAPI := api.NewServiceAccountsAPI(s, ac, routeRegister, s.store)
|
serviceaccountsAPI := api.NewServiceAccountsAPI(s, ac, routeRegister, s.store)
|
||||||
serviceaccountsAPI.RegisterAPIEndpoints(cfg)
|
serviceaccountsAPI.RegisterAPIEndpoints(features)
|
||||||
|
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sa *ServiceAccountsService) CreateServiceAccount(ctx context.Context, saForm *serviceaccounts.CreateServiceaccountForm) (*models.User, error) {
|
func (sa *ServiceAccountsService) CreateServiceAccount(ctx context.Context, saForm *serviceaccounts.CreateServiceaccountForm) (*models.User, error) {
|
||||||
if !sa.cfg.FeatureToggles["service-accounts"] {
|
if !sa.features.IsEnabled(featuremgmt.FlagServiceAccounts) {
|
||||||
sa.log.Debug(ServiceAccountFeatureToggleNotFound)
|
sa.log.Debug(ServiceAccountFeatureToggleNotFound)
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
@@ -55,7 +55,7 @@ func (sa *ServiceAccountsService) CreateServiceAccount(ctx context.Context, saFo
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (sa *ServiceAccountsService) DeleteServiceAccount(ctx context.Context, orgID, serviceAccountID int64) error {
|
func (sa *ServiceAccountsService) DeleteServiceAccount(ctx context.Context, orgID, serviceAccountID int64) error {
|
||||||
if !sa.cfg.FeatureToggles["service-accounts"] {
|
if !sa.features.IsEnabled(featuremgmt.FlagServiceAccounts) {
|
||||||
sa.log.Debug(ServiceAccountFeatureToggleNotFound)
|
sa.log.Debug(ServiceAccountFeatureToggleNotFound)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@@ -5,31 +5,29 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/serviceaccounts/tests"
|
"github.com/grafana/grafana/pkg/services/serviceaccounts/tests"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestProvideServiceAccount_DeleteServiceAccount(t *testing.T) {
|
func TestProvideServiceAccount_DeleteServiceAccount(t *testing.T) {
|
||||||
t.Run("feature toggle present, should call store function", func(t *testing.T) {
|
t.Run("feature toggle present, should call store function", func(t *testing.T) {
|
||||||
cfg := setting.NewCfg()
|
|
||||||
storeMock := &tests.ServiceAccountsStoreMock{Calls: tests.Calls{}}
|
storeMock := &tests.ServiceAccountsStoreMock{Calls: tests.Calls{}}
|
||||||
cfg.FeatureToggles = map[string]bool{"service-accounts": true}
|
svc := ServiceAccountsService{
|
||||||
svc := ServiceAccountsService{cfg: cfg, store: storeMock}
|
features: featuremgmt.WithFeatures("service-accounts", true),
|
||||||
|
store: storeMock}
|
||||||
err := svc.DeleteServiceAccount(context.Background(), 1, 1)
|
err := svc.DeleteServiceAccount(context.Background(), 1, 1)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, storeMock.Calls.DeleteServiceAccount, 1)
|
assert.Len(t, storeMock.Calls.DeleteServiceAccount, 1)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("no feature toggle present, should not call store function", func(t *testing.T) {
|
t.Run("no feature toggle present, should not call store function", func(t *testing.T) {
|
||||||
cfg := setting.NewCfg()
|
|
||||||
svcMock := &tests.ServiceAccountsStoreMock{Calls: tests.Calls{}}
|
svcMock := &tests.ServiceAccountsStoreMock{Calls: tests.Calls{}}
|
||||||
cfg.FeatureToggles = map[string]bool{"service-accounts": false}
|
|
||||||
svc := ServiceAccountsService{
|
svc := ServiceAccountsService{
|
||||||
cfg: cfg,
|
features: featuremgmt.WithFeatures("service-accounts", false),
|
||||||
store: svcMock,
|
store: svcMock,
|
||||||
log: log.New("serviceaccounts-manager-test"),
|
log: log.New("serviceaccounts-manager-test"),
|
||||||
}
|
}
|
||||||
err := svc.DeleteServiceAccount(context.Background(), 1, 1)
|
err := svc.DeleteServiceAccount(context.Background(), 1, 1)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@@ -3,6 +3,7 @@ package migrations
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrations/accesscontrol"
|
"github.com/grafana/grafana/pkg/services/sqlstore/migrations/accesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrations/ualert"
|
"github.com/grafana/grafana/pkg/services/sqlstore/migrations/ualert"
|
||||||
. "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
. "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||||
@@ -56,8 +57,8 @@ func (*OSSMigrations) AddMigration(mg *Migrator) {
|
|||||||
ualert.AddTablesMigrations(mg)
|
ualert.AddTablesMigrations(mg)
|
||||||
ualert.AddDashAlertMigration(mg)
|
ualert.AddDashAlertMigration(mg)
|
||||||
addLibraryElementsMigrations(mg)
|
addLibraryElementsMigrations(mg)
|
||||||
if mg.Cfg != nil {
|
if mg.Cfg != nil && mg.Cfg.IsFeatureToggleEnabled != nil {
|
||||||
if mg.Cfg.IsLiveConfigEnabled() {
|
if mg.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagLiveConfig) {
|
||||||
addLiveChannelMigrations(mg)
|
addLiveChannelMigrations(mg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -117,7 +118,7 @@ func (ss *SQLStore) GetOrgUsers(ctx context.Context, query *models.GetOrgUsersQu
|
|||||||
// service accounts table in the modelling
|
// service accounts table in the modelling
|
||||||
whereConditions = append(whereConditions, fmt.Sprintf("%s.is_service_account = %t", x.Dialect().Quote("user"), query.IsServiceAccount))
|
whereConditions = append(whereConditions, fmt.Sprintf("%s.is_service_account = %t", x.Dialect().Quote("user"), query.IsServiceAccount))
|
||||||
|
|
||||||
if ss.Cfg.FeatureToggles["accesscontrol"] {
|
if ss.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagAccesscontrol) {
|
||||||
q, args, err := accesscontrol.Filter(ctx, ss.Dialect, "org_user.user_id", "users", "org.users:read", query.User)
|
q, args, err := accesscontrol.Filter(ctx, ss.Dialect, "org_user.user_id", "users", "org.users:read", query.User)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -180,7 +181,7 @@ func (ss *SQLStore) SearchOrgUsers(ctx context.Context, query *models.SearchOrgU
|
|||||||
// service accounts table in the modelling
|
// service accounts table in the modelling
|
||||||
whereConditions = append(whereConditions, fmt.Sprintf("%s.is_service_account = %t", x.Dialect().Quote("user"), query.IsServiceAccount))
|
whereConditions = append(whereConditions, fmt.Sprintf("%s.is_service_account = %t", x.Dialect().Quote("user"), query.IsServiceAccount))
|
||||||
|
|
||||||
if ss.Cfg.FeatureToggles["accesscontrol"] {
|
if ss.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagAccesscontrol) {
|
||||||
q, args, err := accesscontrol.Filter(ctx, ss.Dialect, "org_user.user_id", "users", "org.users:read", query.User)
|
q, args, err := accesscontrol.Filter(ctx, ss.Dialect, "org_user.user_id", "users", "org.users:read", query.User)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@@ -11,6 +11,7 @@ import (
|
|||||||
|
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
type getOrgUsersTestCase struct {
|
type getOrgUsersTestCase struct {
|
||||||
@@ -61,7 +62,7 @@ func TestSQLStore_GetOrgUsers(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
store := InitTestDB(t)
|
store := InitTestDB(t)
|
||||||
store.Cfg.FeatureToggles = map[string]bool{"accesscontrol": true}
|
store.Cfg.IsFeatureToggleEnabled = featuremgmt.WithFeatures(featuremgmt.FlagAccesscontrol).IsEnabled
|
||||||
seedOrgUsers(t, store, 10)
|
seedOrgUsers(t, store, 10)
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
@@ -127,7 +128,7 @@ func TestSQLStore_SearchOrgUsers(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
store := InitTestDB(t)
|
store := InitTestDB(t)
|
||||||
store.Cfg.FeatureToggles = map[string]bool{"accesscontrol": true}
|
store.Cfg.IsFeatureToggleEnabled = featuremgmt.WithFeatures(featuremgmt.FlagAccesscontrol).IsEnabled
|
||||||
seedOrgUsers(t, store, 10)
|
seedOrgUsers(t, store, 10)
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
@@ -23,6 +23,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/registry"
|
"github.com/grafana/grafana/pkg/registry"
|
||||||
"github.com/grafana/grafana/pkg/services/annotations"
|
"github.com/grafana/grafana/pkg/services/annotations"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrations"
|
"github.com/grafana/grafana/pkg/services/sqlstore/migrations"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
|
"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
|
||||||
@@ -330,7 +331,7 @@ func (ss *SQLStore) initEngine(engine *xorm.Engine) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if ss.Cfg.IsDatabaseMetricsEnabled() {
|
if ss.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagDatabaseMetrics) {
|
||||||
ss.dbCfg.Type = WrapDatabaseDriverWithHooks(ss.dbCfg.Type, ss.tracer)
|
ss.dbCfg.Type = WrapDatabaseDriverWithHooks(ss.dbCfg.Type, ss.tracer)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -496,6 +497,7 @@ func initTestDB(migration registry.DatabaseMigrator, opts ...InitTestDBOpt) (*SQ
|
|||||||
|
|
||||||
// set test db config
|
// set test db config
|
||||||
cfg := setting.NewCfg()
|
cfg := setting.NewCfg()
|
||||||
|
cfg.IsFeatureToggleEnabled = func(key string) bool { return false }
|
||||||
sec, err := cfg.Raw.NewSection("database")
|
sec, err := cfg.Raw.NewSection("database")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/api/response"
|
"github.com/grafana/grafana/pkg/api/response"
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/guardian"
|
"github.com/grafana/grafana/pkg/services/guardian"
|
||||||
"github.com/grafana/grafana/pkg/services/live"
|
"github.com/grafana/grafana/pkg/services/live"
|
||||||
"github.com/grafana/grafana/pkg/services/rendering"
|
"github.com/grafana/grafana/pkg/services/rendering"
|
||||||
@@ -39,8 +40,8 @@ type Service interface {
|
|||||||
CrawlerStatus(c *models.ReqContext) response.Response
|
CrawlerStatus(c *models.ReqContext) response.Response
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProvideService(cfg *setting.Cfg, renderService rendering.Service, gl *live.GrafanaLive) Service {
|
func ProvideService(cfg *setting.Cfg, features featuremgmt.FeatureToggles, renderService rendering.Service, gl *live.GrafanaLive) Service {
|
||||||
if !cfg.IsDashboardPreviesEnabled() {
|
if !features.IsEnabled(featuremgmt.FlagDashboardPreviews) {
|
||||||
return &dummyService{}
|
return &dummyService{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -132,7 +132,7 @@ func (o *OSSImpl) Section(section string) Section {
|
|||||||
func (OSSImpl) RegisterReloadHandler(string, ReloadHandler) {}
|
func (OSSImpl) RegisterReloadHandler(string, ReloadHandler) {}
|
||||||
|
|
||||||
func (o OSSImpl) IsFeatureToggleEnabled(name string) bool {
|
func (o OSSImpl) IsFeatureToggleEnabled(name string) bool {
|
||||||
return o.Cfg.FeatureToggles[name]
|
return o.Cfg.IsFeatureToggleEnabled(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
type keyValImpl struct {
|
type keyValImpl struct {
|
||||||
|
@@ -342,8 +342,10 @@ type Cfg struct {
|
|||||||
|
|
||||||
ApiKeyMaxSecondsToLive int64
|
ApiKeyMaxSecondsToLive int64
|
||||||
|
|
||||||
// Use to enable new features which may still be in alpha/beta stage.
|
// Check if a feature toggle is enabled
|
||||||
FeatureToggles map[string]bool
|
// @deprecated
|
||||||
|
IsFeatureToggleEnabled func(key string) bool // filled in dynamically
|
||||||
|
|
||||||
AnonymousEnabled bool
|
AnonymousEnabled bool
|
||||||
AnonymousOrgName string
|
AnonymousOrgName string
|
||||||
AnonymousOrgRole string
|
AnonymousOrgRole string
|
||||||
@@ -429,41 +431,6 @@ type Cfg struct {
|
|||||||
UnifiedAlerting UnifiedAlertingSettings
|
UnifiedAlerting UnifiedAlertingSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsLiveConfigEnabled returns true if live should be able to save configs to SQL tables
|
|
||||||
func (cfg Cfg) IsLiveConfigEnabled() bool {
|
|
||||||
return cfg.FeatureToggles["live-config"]
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsLiveConfigEnabled returns true if live should be able to save configs to SQL tables
|
|
||||||
func (cfg Cfg) IsDashboardPreviesEnabled() bool {
|
|
||||||
return cfg.FeatureToggles["dashboardPreviews"]
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsTrimDefaultsEnabled returns whether the standalone trim dashboard default feature is enabled.
|
|
||||||
func (cfg Cfg) IsTrimDefaultsEnabled() bool {
|
|
||||||
return cfg.FeatureToggles["trimDefaults"]
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsDatabaseMetricsEnabled returns whether the database instrumentation feature is enabled.
|
|
||||||
func (cfg Cfg) IsDatabaseMetricsEnabled() bool {
|
|
||||||
return cfg.FeatureToggles["database_metrics"]
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsHTTPRequestHistogramDisabled returns whether the request historgrams is disabled.
|
|
||||||
// This feature toggle will be removed in Grafana 8.x but gives the operator
|
|
||||||
// some graceperiod to update all the monitoring tools.
|
|
||||||
func (cfg Cfg) IsHTTPRequestHistogramDisabled() bool {
|
|
||||||
return cfg.FeatureToggles["disable_http_request_histogram"]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg Cfg) IsNewNavigationEnabled() bool {
|
|
||||||
return cfg.FeatureToggles["newNavigation"]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg Cfg) IsServiceAccountEnabled() bool {
|
|
||||||
return cfg.FeatureToggles["service-accounts"]
|
|
||||||
}
|
|
||||||
|
|
||||||
type CommandLineArgs struct {
|
type CommandLineArgs struct {
|
||||||
Config string
|
Config string
|
||||||
HomePath string
|
HomePath string
|
||||||
|
@@ -3,42 +3,23 @@ package setting
|
|||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
|
||||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
"gopkg.in/ini.v1"
|
"gopkg.in/ini.v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
// @deprecated -- should use `featuremgmt.FeatureToggles`
|
||||||
featureToggleInfo = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
|
||||||
Name: "feature_toggles_info",
|
|
||||||
Help: "info metric that exposes what feature toggles are enabled or not",
|
|
||||||
Namespace: "grafana",
|
|
||||||
}, []string{"name"})
|
|
||||||
|
|
||||||
defaultFeatureToggles = map[string]bool{
|
|
||||||
"recordedQueries": false,
|
|
||||||
"accesscontrol": false,
|
|
||||||
"service-accounts": false,
|
|
||||||
"httpclientprovider_azure_auth": false,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func (cfg *Cfg) readFeatureToggles(iniFile *ini.File) error {
|
func (cfg *Cfg) readFeatureToggles(iniFile *ini.File) error {
|
||||||
toggles, err := overrideDefaultWithConfiguration(iniFile, defaultFeatureToggles)
|
section := iniFile.Section("feature_toggles")
|
||||||
|
toggles, err := ReadFeatureTogglesFromInitFile(section)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
cfg.IsFeatureToggleEnabled = func(key string) bool { return toggles[key] }
|
||||||
cfg.FeatureToggles = toggles
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func overrideDefaultWithConfiguration(iniFile *ini.File, featureToggles map[string]bool) (map[string]bool, error) {
|
func ReadFeatureTogglesFromInitFile(featureTogglesSection *ini.Section) (map[string]bool, error) {
|
||||||
// Read and populate feature toggles list
|
featureToggles := make(map[string]bool, 10)
|
||||||
featureTogglesSection := iniFile.Section("feature_toggles")
|
|
||||||
|
|
||||||
// parse the comma separated list in `enable`.
|
// parse the comma separated list in `enable`.
|
||||||
featuresTogglesStr := valueAsString(featureTogglesSection, "enable", "")
|
featuresTogglesStr := valueAsString(featureTogglesSection, "enable", "")
|
||||||
@@ -60,15 +41,5 @@ func overrideDefaultWithConfiguration(iniFile *ini.File, featureToggles map[stri
|
|||||||
|
|
||||||
featureToggles[v.Name()] = b
|
featureToggles[v.Name()] = b
|
||||||
}
|
}
|
||||||
|
|
||||||
// track if feature toggles are enabled or not using an info metric
|
|
||||||
for k, v := range featureToggles {
|
|
||||||
if v {
|
|
||||||
featureToggleInfo.WithLabelValues(k).Set(1)
|
|
||||||
} else {
|
|
||||||
featureToggleInfo.WithLabelValues(k).Set(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return featureToggles, nil
|
return featureToggles, nil
|
||||||
}
|
}
|
||||||
|
@@ -14,7 +14,6 @@ func TestFeatureToggles(t *testing.T) {
|
|||||||
conf map[string]string
|
conf map[string]string
|
||||||
err error
|
err error
|
||||||
expectedToggles map[string]bool
|
expectedToggles map[string]bool
|
||||||
defaultToggles map[string]bool
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "can parse feature toggles passed in the `enable` array",
|
name: "can parse feature toggles passed in the `enable` array",
|
||||||
@@ -58,18 +57,6 @@ func TestFeatureToggles(t *testing.T) {
|
|||||||
expectedToggles: map[string]bool{},
|
expectedToggles: map[string]bool{},
|
||||||
err: strconv.ErrSyntax,
|
err: strconv.ErrSyntax,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "should override default feature toggles",
|
|
||||||
defaultToggles: map[string]bool{
|
|
||||||
"feature1": true,
|
|
||||||
},
|
|
||||||
conf: map[string]string{
|
|
||||||
"feature1": "false",
|
|
||||||
},
|
|
||||||
expectedToggles: map[string]bool{
|
|
||||||
"feature1": false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
@@ -81,12 +68,7 @@ func TestFeatureToggles(t *testing.T) {
|
|||||||
require.ErrorIs(t, err, nil)
|
require.ErrorIs(t, err, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
dt := map[string]bool{}
|
featureToggles, err := ReadFeatureTogglesFromInitFile(toggles)
|
||||||
if len(tc.defaultToggles) > 0 {
|
|
||||||
dt = tc.defaultToggles
|
|
||||||
}
|
|
||||||
|
|
||||||
featureToggles, err := overrideDefaultWithConfiguration(f, dt)
|
|
||||||
require.ErrorIs(t, err, tc.err)
|
require.ErrorIs(t, err, tc.err)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
@@ -79,7 +79,7 @@ func (cfg *Cfg) readUnifiedAlertingEnabledSetting(section *ini.Section) (*bool,
|
|||||||
// the unified alerting is not enabled by default. First, check the feature flag
|
// the unified alerting is not enabled by default. First, check the feature flag
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// TODO: Remove in Grafana v9
|
// TODO: Remove in Grafana v9
|
||||||
if cfg.FeatureToggles["ngalert"] {
|
if cfg.IsFeatureToggleEnabled("ngalert") {
|
||||||
cfg.Logger.Warn("ngalert feature flag is deprecated: use unified alerting enabled setting instead")
|
cfg.Logger.Warn("ngalert feature flag is deprecated: use unified alerting enabled setting instead")
|
||||||
enabled = true
|
enabled = true
|
||||||
// feature flag overrides the legacy alerting setting.
|
// feature flag overrides the legacy alerting setting.
|
||||||
|
@@ -143,6 +143,7 @@ func TestUnifiedAlertingSettings(t *testing.T) {
|
|||||||
t.Run(tc.desc, func(t *testing.T) {
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
f := ini.Empty()
|
f := ini.Empty()
|
||||||
cfg := NewCfg()
|
cfg := NewCfg()
|
||||||
|
cfg.IsFeatureToggleEnabled = func(key string) bool { return false }
|
||||||
unifiedAlertingSec, err := f.NewSection("unified_alerting")
|
unifiedAlertingSec, err := f.NewSection("unified_alerting")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
for k, v := range tc.unifiedAlertingOptions {
|
for k, v := range tc.unifiedAlertingOptions {
|
||||||
|
@@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
var random20HzStreamRegex = regexp.MustCompile(`random-20Hz-stream(-\d+)?`)
|
var random20HzStreamRegex = regexp.MustCompile(`random-20Hz-stream(-\d+)?`)
|
||||||
@@ -37,7 +38,7 @@ func (s *Service) SubscribeStream(_ context.Context, req *backend.SubscribeStrea
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.cfg.FeatureToggles["live-pipeline"] {
|
if s.features.IsEnabled(featuremgmt.FlagLivePipeline) {
|
||||||
// While developing Live pipeline avoid sending initial data.
|
// While developing Live pipeline avoid sending initial data.
|
||||||
initialData = nil
|
initialData = nil
|
||||||
}
|
}
|
||||||
@@ -126,7 +127,7 @@ func (s *Service) runTestStream(ctx context.Context, path string, conf testStrea
|
|||||||
}
|
}
|
||||||
|
|
||||||
mode := data.IncludeDataOnly
|
mode := data.IncludeDataOnly
|
||||||
if s.cfg.FeatureToggles["live-pipeline"] {
|
if s.features.IsEnabled(featuremgmt.FlagLivePipeline) {
|
||||||
mode = data.IncludeAll
|
mode = data.IncludeAll
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -10,11 +10,13 @@ import (
|
|||||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ProvideService(cfg *setting.Cfg) *Service {
|
func ProvideService(cfg *setting.Cfg, features featuremgmt.FeatureToggles) *Service {
|
||||||
s := &Service{
|
s := &Service{
|
||||||
|
features: features,
|
||||||
queryMux: datasource.NewQueryTypeMux(),
|
queryMux: datasource.NewQueryTypeMux(),
|
||||||
scenarios: map[string]*Scenario{},
|
scenarios: map[string]*Scenario{},
|
||||||
frame: data.NewFrame("testdata",
|
frame: data.NewFrame("testdata",
|
||||||
@@ -46,6 +48,7 @@ type Service struct {
|
|||||||
labelFrame *data.Frame
|
labelFrame *data.Frame
|
||||||
queryMux *datasource.QueryTypeMux
|
queryMux *datasource.QueryTypeMux
|
||||||
resourceHandler backend.CallResourceHandler
|
resourceHandler backend.CallResourceHandler
|
||||||
|
features featuremgmt.FeatureToggles
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||||
|
@@ -2,7 +2,7 @@ import config from '../../core/config';
|
|||||||
|
|
||||||
// accessControlQueryParam adds an additional accesscontrol=true param to params when accesscontrol is enabled
|
// accessControlQueryParam adds an additional accesscontrol=true param to params when accesscontrol is enabled
|
||||||
export function accessControlQueryParam(params = {}) {
|
export function accessControlQueryParam(params = {}) {
|
||||||
if (!config.featureToggles['accesscontrol']) {
|
if (!config.featureToggles.accesscontrol) {
|
||||||
return params;
|
return params;
|
||||||
}
|
}
|
||||||
return { ...params, accesscontrol: true };
|
return { ...params, accesscontrol: true };
|
||||||
|
Reference in New Issue
Block a user