FeatureFlags: Use interface rather than manager (#80000)

This commit is contained in:
Ryan McKinley
2024-01-09 10:38:06 -08:00
committed by GitHub
parent e550829dae
commit 1caaa56de0
48 changed files with 125 additions and 143 deletions

View File

@ -600,7 +600,7 @@ func (hs *HTTPServer) GetAnnotationTags(c *contextmodel.ReqContext) response.Res
// where <type> is the type of annotation with id <id>.
// If annotationPermissionUpdate feature toggle is enabled, dashboard annotation scope will be resolved to the corresponding
// dashboard and folder scopes (eg, "dashboards:uid:<annotation_dashboard_uid>", "folders:uid:<parent_folder_uid>" etc).
func AnnotationTypeScopeResolver(annotationsRepo annotations.Repository, features *featuremgmt.FeatureManager, dashSvc dashboards.DashboardService, folderSvc folder.Service) (string, accesscontrol.ScopeAttributeResolver) {
func AnnotationTypeScopeResolver(annotationsRepo annotations.Repository, features featuremgmt.FeatureToggles, dashSvc dashboards.DashboardService, folderSvc folder.Service) (string, accesscontrol.ScopeAttributeResolver) {
prefix := accesscontrol.ScopeAnnotationsProvider.GetResourceScope("")
return prefix, accesscontrol.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, initialScope string) ([]string, error) {
scopeParts := strings.Split(initialScope, ":")

View File

@ -258,7 +258,7 @@ func userWithPermissions(orgID int64, permissions []accesscontrol.Permission) *u
return &user.SignedInUser{IsAnonymous: true, OrgID: orgID, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{orgID: accesscontrol.GroupScopesByAction(permissions)}}
}
func setupSimpleHTTPServer(features *featuremgmt.FeatureManager) *HTTPServer {
func setupSimpleHTTPServer(features featuremgmt.FeatureToggles) *HTTPServer {
if features == nil {
features = featuremgmt.WithFeatures()
}

View File

@ -339,7 +339,7 @@ func validateURL(cmdType string, url string) response.Response {
// validateJSONData prevents the user from adding a custom header with name that matches the auth proxy header name.
// This is done to prevent data source proxy from being used to circumvent auth proxy.
// For more context take a look at CVE-2022-35957
func validateJSONData(ctx context.Context, jsonData *simplejson.Json, cfg *setting.Cfg, features *featuremgmt.FeatureManager) error {
func validateJSONData(ctx context.Context, jsonData *simplejson.Json, cfg *setting.Cfg, features featuremgmt.FeatureToggles) error {
if jsonData == nil {
return nil
}

View File

@ -25,7 +25,7 @@ func (hs *HTTPServer) GetFeatureToggles(ctx *contextmodel.ReqContext) response.R
dtos := make([]featuremgmt.FeatureToggleDTO, 0)
// loop through features an add features that should be visible to dtos
for _, ft := range hs.Features.GetFlags() {
for _, ft := range hs.featureManager.GetFlags() {
if isFeatureHidden(ft, cfg.HiddenToggles) {
continue
}
@ -67,7 +67,7 @@ func (hs *HTTPServer) UpdateFeatureToggle(ctx *contextmodel.ReqContext) response
for _, t := range cmd.FeatureToggles {
// make sure flag exists, and only continue if flag is writeable
if f, ok := hs.Features.LookupFlag(t.Name); ok && isFeatureWriteable(f, hs.Cfg.FeatureManagement.ReadOnlyToggles) {
if f, ok := hs.featureManager.LookupFlag(t.Name); ok && isFeatureWriteable(f, hs.Cfg.FeatureManagement.ReadOnlyToggles) {
hs.log.Info("UpdateFeatureToggle: updating toggle", "toggle_name", t.Name, "enabled", t.Enabled, "username", ctx.SignedInUser.Login)
payload.FeatureToggles[t.Name] = strconv.FormatBool(t.Enabled)
} else {
@ -82,13 +82,13 @@ func (hs *HTTPServer) UpdateFeatureToggle(ctx *contextmodel.ReqContext) response
return response.Respond(http.StatusBadRequest, "Failed to perform webhook request")
}
hs.Features.SetRestartRequired()
hs.featureManager.SetRestartRequired()
return response.Respond(http.StatusOK, "feature toggles updated successfully")
}
func (hs *HTTPServer) GetFeatureMgmtState(ctx *contextmodel.ReqContext) response.Response {
fmState := hs.Features.GetState()
fmState := hs.featureManager.GetState()
return response.Respond(http.StatusOK, fmState)
}

View File

@ -8,6 +8,9 @@ import (
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/featuremgmt"
@ -16,8 +19,6 @@ import (
"github.com/grafana/grafana/pkg/services/user/usertest"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web/webtest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGetFeatureToggles(t *testing.T) {
@ -405,13 +406,16 @@ func runGetScenario(
cfg := setting.NewCfg()
cfg.FeatureManagement = settings
server := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = cfg
hs.Features = featuremgmt.WithFeatureFlags(append([]*featuremgmt.FeatureFlag{{
fm := featuremgmt.WithFeatureFlags(append([]*featuremgmt.FeatureFlag{{
Name: featuremgmt.FlagFeatureToggleAdminPage,
Enabled: true,
Stage: featuremgmt.FeatureStageGeneralAvailability,
}}, features...))
server := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = cfg
hs.Features = fm
hs.featureManager = fm
hs.orgService = orgtest.NewOrgServiceFake()
hs.userService = &usertest.FakeUserService{
ExpectedUser: &user.User{ID: 1},
@ -469,13 +473,16 @@ func runSetScenario(
cfg := setting.NewCfg()
cfg.FeatureManagement = settings
server := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = cfg
hs.Features = featuremgmt.WithFeatureFlags(append([]*featuremgmt.FeatureFlag{{
features := featuremgmt.WithFeatureFlags(append([]*featuremgmt.FeatureFlag{{
Name: featuremgmt.FlagFeatureToggleAdminPage,
Enabled: true,
Stage: featuremgmt.FeatureStageGeneralAvailability,
}}, serverFeatures...))
server := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = cfg
hs.Features = features
hs.featureManager = features
hs.orgService = orgtest.NewOrgServiceFake()
hs.userService = &usertest.FakeUserService{
ExpectedUser: &user.User{ID: 1},

View File

@ -95,7 +95,7 @@ func BenchmarkFolderListAndSearch(b *testing.B) {
desc string
url string
expectedLen int
features *featuremgmt.FeatureManager
features featuremgmt.FeatureToggles
}{
{
desc: "impl=default nested_folders=on get root folders",
@ -423,7 +423,7 @@ func setupDB(b testing.TB) benchScenario {
}
}
func setupServer(b testing.TB, sc benchScenario, features *featuremgmt.FeatureManager) *web.Macaron {
func setupServer(b testing.TB, sc benchScenario, features featuremgmt.FeatureToggles) *web.Macaron {
b.Helper()
m := web.New()

View File

@ -435,7 +435,7 @@ func TestFolderGetAPIEndpoint(t *testing.T) {
type testCase struct {
description string
URL string
features *featuremgmt.FeatureManager
features featuremgmt.FeatureToggles
expectedCode int
expectedParentUIDs []string
expectedParentOrgIDs []int64

View File

@ -32,7 +32,7 @@ import (
"github.com/grafana/grafana/pkg/web"
)
func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features *featuremgmt.FeatureManager, pstore pluginstore.Store, psettings pluginsettings.Service) (*web.Mux, *HTTPServer) {
func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features featuremgmt.FeatureToggles, pstore pluginstore.Store, psettings pluginsettings.Service) (*web.Mux, *HTTPServer) {
t.Helper()
db.InitTestDB(t)
// nolint:staticcheck

View File

@ -126,7 +126,8 @@ type HTTPServer struct {
RouteRegister routing.RouteRegister
RenderService rendering.Service
Cfg *setting.Cfg
Features *featuremgmt.FeatureManager
Features featuremgmt.FeatureToggles
featureManager *featuremgmt.FeatureManager
SettingsProvider setting.Provider
HooksService *hooks.HooksService
navTreeService navtree.Service
@ -290,7 +291,8 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
ShortURLService: shortURLService,
QueryHistoryService: queryHistoryService,
CorrelationsService: correlationsService,
Features: features,
Features: features, // a read only view of the managers state
featureManager: features,
StorageService: storageService,
RemoteCacheService: remoteCache,
ProvisioningService: provisioningService,

View File

@ -382,7 +382,7 @@ func createService(t testing.TB, cfg *setting.Cfg, store db.DB, statsService sta
store,
&mockSocial{},
&pluginstore.FakePluginStore{},
featuremgmt.WithFeatures("feature1", "feature2"),
featuremgmt.WithManager("feature1", "feature2"),
o.datasources,
httpclient.NewProvider(sdkhttpclient.ProviderOptions{Middlewares: []sdkhttpclient.Middleware{}}),
)

View File

@ -72,10 +72,10 @@ type keySetJWKS struct {
jose.JSONWebKeySet
}
func NewAzureADProvider(info *social.OAuthInfo, cfg *setting.Cfg, ssoSettings ssosettings.Service, features *featuremgmt.FeatureManager, cache remotecache.CacheStorage) *SocialAzureAD {
func NewAzureADProvider(info *social.OAuthInfo, cfg *setting.Cfg, ssoSettings ssosettings.Service, features featuremgmt.FeatureToggles, cache remotecache.CacheStorage) *SocialAzureAD {
config := createOAuthConfig(info, cfg, social.AzureADProviderName)
provider := &SocialAzureAD{
SocialBase: newSocialBase(social.AzureADProviderName, config, info, cfg.AutoAssignOrgRole, *features),
SocialBase: newSocialBase(social.AzureADProviderName, config, info, cfg.AutoAssignOrgRole, features),
cache: cache,
allowedOrganizations: util.SplitString(info.Extra[allowedOrganizationsKey]),
forceUseGraphAPI: MustBool(info.Extra[forceUseGraphAPIKey], false),

View File

@ -45,10 +45,10 @@ type SocialGenericOAuth struct {
teamIds []string
}
func NewGenericOAuthProvider(info *social.OAuthInfo, cfg *setting.Cfg, ssoSettings ssosettings.Service, features *featuremgmt.FeatureManager) *SocialGenericOAuth {
func NewGenericOAuthProvider(info *social.OAuthInfo, cfg *setting.Cfg, ssoSettings ssosettings.Service, features featuremgmt.FeatureToggles) *SocialGenericOAuth {
config := createOAuthConfig(info, cfg, social.GenericOAuthProviderName)
provider := &SocialGenericOAuth{
SocialBase: newSocialBase(social.GenericOAuthProviderName, config, info, cfg.AutoAssignOrgRole, *features),
SocialBase: newSocialBase(social.GenericOAuthProviderName, config, info, cfg.AutoAssignOrgRole, features),
teamsUrl: info.TeamsUrl,
emailAttributeName: info.EmailAttributeName,
emailAttributePath: info.EmailAttributePath,

View File

@ -53,13 +53,13 @@ var (
"User is not a member of one of the required organizations. Please contact identity provider administrator."))
)
func NewGitHubProvider(info *social.OAuthInfo, cfg *setting.Cfg, ssoSettings ssosettings.Service, features *featuremgmt.FeatureManager) *SocialGithub {
func NewGitHubProvider(info *social.OAuthInfo, cfg *setting.Cfg, ssoSettings ssosettings.Service, features featuremgmt.FeatureToggles) *SocialGithub {
teamIdsSplitted := util.SplitString(info.Extra[teamIdsKey])
teamIds := mustInts(teamIdsSplitted)
config := createOAuthConfig(info, cfg, social.GitHubProviderName)
provider := &SocialGithub{
SocialBase: newSocialBase(social.GitHubProviderName, config, info, cfg.AutoAssignOrgRole, *features),
SocialBase: newSocialBase(social.GitHubProviderName, config, info, cfg.AutoAssignOrgRole, features),
teamIds: teamIds,
allowedOrganizations: util.SplitString(info.Extra[allowedOrganizationsKey]),
}

View File

@ -52,10 +52,10 @@ type userData struct {
IsGrafanaAdmin *bool `json:"-"`
}
func NewGitLabProvider(info *social.OAuthInfo, cfg *setting.Cfg, ssoSettings ssosettings.Service, features *featuremgmt.FeatureManager) *SocialGitlab {
func NewGitLabProvider(info *social.OAuthInfo, cfg *setting.Cfg, ssoSettings ssosettings.Service, features featuremgmt.FeatureToggles) *SocialGitlab {
config := createOAuthConfig(info, cfg, social.GitlabProviderName)
provider := &SocialGitlab{
SocialBase: newSocialBase(social.GitlabProviderName, config, info, cfg.AutoAssignOrgRole, *features),
SocialBase: newSocialBase(social.GitlabProviderName, config, info, cfg.AutoAssignOrgRole, features),
}
if features.IsEnabledGlobally(featuremgmt.FlagSsoSettingsApi) {

View File

@ -38,10 +38,10 @@ type googleUserData struct {
rawJSON []byte `json:"-"`
}
func NewGoogleProvider(info *social.OAuthInfo, cfg *setting.Cfg, ssoSettings ssosettings.Service, features *featuremgmt.FeatureManager) *SocialGoogle {
func NewGoogleProvider(info *social.OAuthInfo, cfg *setting.Cfg, ssoSettings ssosettings.Service, features featuremgmt.FeatureToggles) *SocialGoogle {
config := createOAuthConfig(info, cfg, social.GoogleProviderName)
provider := &SocialGoogle{
SocialBase: newSocialBase(social.GoogleProviderName, config, info, cfg.AutoAssignOrgRole, *features),
SocialBase: newSocialBase(social.GoogleProviderName, config, info, cfg.AutoAssignOrgRole, features),
}
if strings.HasPrefix(info.ApiUrl, legacyAPIURL) {

View File

@ -33,7 +33,7 @@ type OrgRecord struct {
Login string `json:"login"`
}
func NewGrafanaComProvider(info *social.OAuthInfo, cfg *setting.Cfg, ssoSettings ssosettings.Service, features *featuremgmt.FeatureManager) *SocialGrafanaCom {
func NewGrafanaComProvider(info *social.OAuthInfo, cfg *setting.Cfg, ssoSettings ssosettings.Service, features featuremgmt.FeatureToggles) *SocialGrafanaCom {
// Override necessary settings
info.AuthUrl = cfg.GrafanaComURL + "/oauth2/authorize"
info.TokenUrl = cfg.GrafanaComURL + "/api/oauth2/token"
@ -41,7 +41,7 @@ func NewGrafanaComProvider(info *social.OAuthInfo, cfg *setting.Cfg, ssoSettings
config := createOAuthConfig(info, cfg, social.GrafanaComProviderName)
provider := &SocialGrafanaCom{
SocialBase: newSocialBase(social.GrafanaComProviderName, config, info, cfg.AutoAssignOrgRole, *features),
SocialBase: newSocialBase(social.GrafanaComProviderName, config, info, cfg.AutoAssignOrgRole, features),
url: cfg.GrafanaComURL,
allowedOrganizations: util.SplitString(info.Extra[allowedOrganizationsKey]),
}

View File

@ -44,10 +44,10 @@ type OktaClaims struct {
Name string `json:"name"`
}
func NewOktaProvider(info *social.OAuthInfo, cfg *setting.Cfg, ssoSettings ssosettings.Service, features *featuremgmt.FeatureManager) *SocialOkta {
func NewOktaProvider(info *social.OAuthInfo, cfg *setting.Cfg, ssoSettings ssosettings.Service, features featuremgmt.FeatureToggles) *SocialOkta {
config := createOAuthConfig(info, cfg, social.OktaProviderName)
provider := &SocialOkta{
SocialBase: newSocialBase(social.OktaProviderName, config, info, cfg.AutoAssignOrgRole, *features),
SocialBase: newSocialBase(social.OktaProviderName, config, info, cfg.AutoAssignOrgRole, features),
}
if info.UseRefreshToken {

View File

@ -26,14 +26,14 @@ type SocialBase struct {
info *social.OAuthInfo
log log.Logger
autoAssignOrgRole string
features featuremgmt.FeatureManager
features featuremgmt.FeatureToggles
}
func newSocialBase(name string,
config *oauth2.Config,
info *social.OAuthInfo,
autoAssignOrgRole string,
features featuremgmt.FeatureManager,
features featuremgmt.FeatureToggles,
) *SocialBase {
logger := log.New("oauth." + name)

View File

@ -37,7 +37,7 @@ type SocialService struct {
}
func ProvideService(cfg *setting.Cfg,
features *featuremgmt.FeatureManager,
features featuremgmt.FeatureToggles,
usageStats usagestats.Service,
bundleRegistry supportbundles.Service,
cache remotecache.CacheStorage,
@ -228,7 +228,7 @@ func (ss *SocialService) getUsageStats(ctx context.Context) (map[string]any, err
return m, nil
}
func createOAuthConnector(name string, info *social.OAuthInfo, cfg *setting.Cfg, ssoSettings ssosettings.Service, features *featuremgmt.FeatureManager, cache remotecache.CacheStorage) (social.SocialConnector, error) {
func createOAuthConnector(name string, info *social.OAuthInfo, cfg *setting.Cfg, ssoSettings ssosettings.Service, features featuremgmt.FeatureToggles, cache remotecache.CacheStorage) (social.SocialConnector, error) {
switch name {
case social.AzureADProviderName:
return connectors.NewAzureADProvider(info, cfg, ssoSettings, features, cache), nil

View File

@ -22,7 +22,7 @@ import (
func TestSocialService_ProvideService(t *testing.T) {
type testEnv struct {
features *featuremgmt.FeatureManager
features featuremgmt.FeatureToggles
}
testCases := []struct {
name string

View File

@ -3,8 +3,8 @@ package config
import (
"github.com/grafana/grafana-azure-sdk-go/azsettings"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/log"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
)
@ -44,7 +44,7 @@ type Cfg struct {
GrafanaAppURL string
GrafanaAppSubURL string
Features plugins.FeatureToggles
Features featuremgmt.FeatureToggles
AngularSupportEnabled bool
HideAngularDeprecation []string
@ -52,7 +52,7 @@ type Cfg struct {
func NewCfg(devMode bool, pluginsPath string, pluginSettings setting.PluginSettings, pluginsAllowUnsigned []string,
awsAllowedAuthProviders []string, awsAssumeRoleEnabled bool, awsExternalId string, azure *azsettings.AzureSettings, secureSocksDSProxy setting.SecureSocksDSProxySettings,
grafanaVersion string, logDatasourceRequests bool, pluginsCDNURLTemplate string, appURL string, appSubURL string, tracing Tracing, features plugins.FeatureToggles, angularSupportEnabled bool,
grafanaVersion string, logDatasourceRequests bool, pluginsCDNURLTemplate string, appURL string, appSubURL string, tracing Tracing, features featuremgmt.FeatureToggles, angularSupportEnabled bool,
grafanaComURL string, disablePlugins []string, hideAngularDeprecation []string, forwardHostEnvVars []string) *Cfg {
return &Cfg{
log: log.New("plugin.cfg"),

View File

@ -9,6 +9,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/grafana/grafana-azure-sdk-go/azsettings"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/auth"
"github.com/grafana/grafana/pkg/plugins/config"
@ -726,27 +727,27 @@ func TestService_GetConfigMap(t *testing.T) {
func TestService_GetConfigMap_featureToggles(t *testing.T) {
t.Run("Feature toggles list is deterministic", func(t *testing.T) {
tcs := []struct {
enabledFeatures []string
features featuremgmt.FeatureToggles
expectedConfig map[string]string
}{
{
enabledFeatures: nil,
features: nil,
expectedConfig: map[string]string{},
},
{
enabledFeatures: []string{},
features: featuremgmt.WithFeatures(),
expectedConfig: map[string]string{},
},
{
enabledFeatures: []string{"A", "B", "C"},
features: featuremgmt.WithFeatures("A", "B", "C"),
expectedConfig: map[string]string{"GF_INSTANCE_FEATURE_TOGGLES_ENABLE": "A,B,C"},
},
{
enabledFeatures: []string{"C", "B", "A"},
features: featuremgmt.WithFeatures("C", "B", "A"),
expectedConfig: map[string]string{"GF_INSTANCE_FEATURE_TOGGLES_ENABLE": "A,B,C"},
},
{
enabledFeatures: []string{"b", "a", "c", "d"},
features: featuremgmt.WithFeatures("b", "a", "c", "d"),
expectedConfig: map[string]string{"GF_INSTANCE_FEATURE_TOGGLES_ENABLE": "a,b,c,d"},
},
}
@ -754,7 +755,7 @@ func TestService_GetConfigMap_featureToggles(t *testing.T) {
for _, tc := range tcs {
s := &Service{
cfg: &config.Cfg{
Features: fakes.NewFakeFeatureToggles(tc.enabledFeatures...),
Features: tc.features,
},
}
require.Equal(t, tc.expectedConfig, s.GetConfigMap(context.Background(), "", nil))

View File

@ -149,11 +149,6 @@ func (fn ClientMiddlewareFunc) CreateClientMiddleware(next Client) Client {
return fn(next)
}
type FeatureToggles interface {
IsEnabledGlobally(flag string) bool
GetEnabled(ctx context.Context) map[string]bool
}
type SignatureCalculator interface {
Calculate(ctx context.Context, src PluginSource, plugin FoundPlugin) (Signature, error)
}

View File

@ -574,26 +574,3 @@ func (p *FakeBackendPlugin) Kill() {
defer p.mutex.Unlock()
p.Running = false
}
type FakeFeatureToggles struct {
features map[string]bool
}
func NewFakeFeatureToggles(features ...string) *FakeFeatureToggles {
m := make(map[string]bool)
for _, f := range features {
m[f] = true
}
return &FakeFeatureToggles{
features: m,
}
}
func (f *FakeFeatureToggles) GetEnabled(_ context.Context) map[string]bool {
return f.features
}
func (f *FakeFeatureToggles) IsEnabledGlobally(feature string) bool {
return f.features[feature]
}

View File

@ -26,10 +26,10 @@ var (
type Local struct {
log log.Logger
production bool
features plugins.FeatureToggles
features featuremgmt.FeatureToggles
}
func NewLocalFinder(devMode bool, features plugins.FeatureToggles) *Local {
func NewLocalFinder(devMode bool, features featuremgmt.FeatureToggles) *Local {
return &Local{
production: !devMode,
log: log.New("local.finder"),

View File

@ -41,7 +41,7 @@ var SharedWithMeFolderPermission = accesscontrol.Permission{
}
func ProvideService(cfg *setting.Cfg, db db.DB, routeRegister routing.RouteRegister, cache *localcache.CacheService,
accessControl accesscontrol.AccessControl, features *featuremgmt.FeatureManager) (*Service, error) {
accessControl accesscontrol.AccessControl, features featuremgmt.FeatureToggles) (*Service, error) {
service := ProvideOSSService(cfg, database.ProvideService(db), cache, features)
api.NewAccessControlAPI(routeRegister, accessControl, service, features).RegisterAPIEndpoints()
@ -62,7 +62,7 @@ func ProvideService(cfg *setting.Cfg, db db.DB, routeRegister routing.RouteRegis
return service, nil
}
func ProvideOSSService(cfg *setting.Cfg, store store, cache *localcache.CacheService, features *featuremgmt.FeatureManager) *Service {
func ProvideOSSService(cfg *setting.Cfg, store store, cache *localcache.CacheService, features featuremgmt.FeatureToggles) *Service {
s := &Service{
cfg: cfg,
store: store,
@ -93,7 +93,7 @@ type Service struct {
cache *localcache.CacheService
registrations accesscontrol.RegistrationList
roles map[string]*accesscontrol.RoleDTO
features *featuremgmt.FeatureManager
features featuremgmt.FeatureToggles
}
func (s *Service) GetUsageStats(_ context.Context) map[string]any {

View File

@ -15,7 +15,7 @@ import (
)
func NewAccessControlAPI(router routing.RouteRegister, accesscontrol ac.AccessControl, service ac.Service,
features *featuremgmt.FeatureManager) *AccessControlAPI {
features featuremgmt.FeatureToggles) *AccessControlAPI {
return &AccessControlAPI{
RouteRegister: router,
Service: service,
@ -28,7 +28,7 @@ type AccessControlAPI struct {
Service ac.Service
AccessControl ac.AccessControl
RouteRegister routing.RouteRegister
features *featuremgmt.FeatureManager
features featuremgmt.FeatureToggles
}
func (api *AccessControlAPI) RegisterAPIEndpoints() {

View File

@ -19,7 +19,7 @@ var _ authn.HookClient = new(Session)
var _ authn.ContextAwareClient = new(Session)
func ProvideSession(cfg *setting.Cfg, sessionService auth.UserTokenService,
features *featuremgmt.FeatureManager) *Session {
features featuremgmt.FeatureToggles) *Session {
return &Session{
cfg: cfg,
features: features,
@ -30,7 +30,7 @@ func ProvideSession(cfg *setting.Cfg, sessionService auth.UserTokenService,
type Session struct {
cfg *setting.Cfg
features *featuremgmt.FeatureManager
features featuremgmt.FeatureToggles
sessionService auth.UserTokenService
log log.Logger
}

View File

@ -64,7 +64,7 @@ func TestSession_Authenticate(t *testing.T) {
type fields struct {
sessionService auth.UserTokenService
features *featuremgmt.FeatureManager
features featuremgmt.FeatureToggles
}
type args struct {
r *authn.Request

View File

@ -25,7 +25,7 @@ import (
"github.com/grafana/grafana/pkg/web"
)
func ProvideService(cfg *setting.Cfg, tracer tracing.Tracer, features *featuremgmt.FeatureManager, authnService authn.Service,
func ProvideService(cfg *setting.Cfg, tracer tracing.Tracer, features featuremgmt.FeatureToggles, authnService authn.Service,
) *ContextHandler {
return &ContextHandler{
Cfg: cfg,
@ -39,7 +39,7 @@ func ProvideService(cfg *setting.Cfg, tracer tracing.Tracer, features *featuremg
type ContextHandler struct {
Cfg *setting.Cfg
tracer tracing.Tracer
features *featuremgmt.FeatureManager
features featuremgmt.FeatureToggles
authnService authn.Service
}

View File

@ -174,10 +174,14 @@ func (fm *FeatureManager) LookupFlag(name string) (FeatureFlag, bool) {
// ############# Test Functions #############
func WithFeatures(spec ...any) FeatureToggles {
return WithManager(spec...)
}
// WithFeatures is used to define feature toggles for testing.
// The arguments are a list of strings that are optionally followed by a boolean value for example:
// WithFeatures([]any{"my_feature", "other_feature"}) or WithFeatures([]any{"my_feature", true})
func WithFeatures(spec ...any) *FeatureManager {
func WithManager(spec ...any) *FeatureManager {
count := len(spec)
features := make(map[string]*FeatureFlag, count)
enabled := make(map[string]bool, count)

View File

@ -9,7 +9,7 @@ import (
func TestFeatureManager(t *testing.T) {
t.Run("check testing stubs", func(t *testing.T) {
ft := WithFeatures("a", "b", "c")
ft := WithManager("a", "b", "c")
require.True(t, ft.IsEnabledGlobally("a"))
require.True(t, ft.IsEnabledGlobally("b"))
require.True(t, ft.IsEnabledGlobally("c"))
@ -18,7 +18,7 @@ func TestFeatureManager(t *testing.T) {
require.Equal(t, map[string]bool{"a": true, "b": true, "c": true}, ft.GetEnabled(context.Background()))
// Explicit values
ft = WithFeatures("a", true, "b", false)
ft = WithManager("a", true, "b", false)
require.True(t, ft.IsEnabledGlobally("a"))
require.False(t, ft.IsEnabledGlobally("b"))
require.Equal(t, map[string]bool{"a": true}, ft.GetEnabled(context.Background()))

View File

@ -18,6 +18,10 @@ type FeatureToggles interface {
// Use of global feature flags should be limited and careful as they require
// a full server restart for a change to take place.
IsEnabledGlobally(flag string) bool
// Get the enabled flags -- this *may* also include disabled flags (with value false)
// but it is guaranteed to have the enabled ones listed
GetEnabled(ctx context.Context) map[string]bool
}
// FeatureFlagStage indicates the quality level

View File

@ -8,7 +8,7 @@ import (
)
func TestFeatureUsageStats(t *testing.T) {
featureManagerWithAllFeatures := WithFeatures(
featureManagerWithAllFeatures := WithManager(
"database_metrics",
"live-config",
"UPPER_SNAKE_CASE",

View File

@ -587,7 +587,7 @@ func TestIntegrationNestedFolderService(t *testing.T) {
testCases := []struct {
service *Service
featuresFlag *featuremgmt.FeatureManager
featuresFlag featuremgmt.FeatureToggles
prefix string
depth int
forceDelete bool

View File

@ -33,7 +33,7 @@ type ServiceImpl struct {
pluginStore pluginstore.Store
pluginSettings pluginsettings.Service
starService star.Service
features *featuremgmt.FeatureManager
features featuremgmt.FeatureToggles
dashboardService dashboards.DashboardService
accesscontrolService ac.Service
kvStore kvstore.KVStore
@ -52,7 +52,7 @@ type NavigationAppConfig struct {
Icon string
}
func ProvideService(cfg *setting.Cfg, accessControl ac.AccessControl, pluginStore pluginstore.Store, pluginSettings pluginsettings.Service, starService star.Service, features *featuremgmt.FeatureManager, dashboardService dashboards.DashboardService, accesscontrolService ac.Service, kvStore kvstore.KVStore, apiKeyService apikey.Service, license licensing.Licensing) navtree.Service {
func ProvideService(cfg *setting.Cfg, accessControl ac.AccessControl, pluginStore pluginstore.Store, pluginSettings pluginsettings.Service, starService star.Service, features featuremgmt.FeatureToggles, dashboardService dashboards.DashboardService, accesscontrolService ac.Service, kvStore kvstore.KVStore, apiKeyService apikey.Service, license licensing.Licensing) navtree.Service {
service := &ServiceImpl{
cfg: cfg,
log: log.New("navtree service"),

View File

@ -27,7 +27,7 @@ func NewCachingMiddleware(cachingService caching.CachingService) plugins.ClientM
// NewCachingMiddlewareWithFeatureManager creates a new plugins.ClientMiddleware that will
// attempt to read and write query results to the cache with a feature manager
func NewCachingMiddlewareWithFeatureManager(cachingService caching.CachingService, features *featuremgmt.FeatureManager) plugins.ClientMiddleware {
func NewCachingMiddlewareWithFeatureManager(cachingService caching.CachingService, features featuremgmt.FeatureToggles) plugins.ClientMiddleware {
log := log.New("caching_middleware")
if err := prometheus.Register(QueryCachingRequestHistogram); err != nil {
log.Error("Error registering prometheus collector 'QueryRequestHistogram'", "error", err)
@ -49,7 +49,7 @@ type CachingMiddleware struct {
next plugins.Client
caching caching.CachingService
log log.Logger
features *featuremgmt.FeatureManager
features featuremgmt.FeatureToggles
}
// QueryData receives a data request and attempts to access results already stored in the cache for that request.

View File

@ -9,7 +9,7 @@ import (
"github.com/grafana/grafana/pkg/setting"
)
func ProvideConfig(settingProvider setting.Provider, grafanaCfg *setting.Cfg, features *featuremgmt.FeatureManager) (*pCfg.Cfg, error) {
func ProvideConfig(settingProvider setting.Provider, grafanaCfg *setting.Cfg, features featuremgmt.FeatureToggles) (*pCfg.Cfg, error) {
plugins := settingProvider.Section("plugins")
allowedUnsigned := grafanaCfg.PluginsAllowUnsigned
if len(plugins.KeyValue("allow_loading_unsigned_plugins").Value()) > 0 {

View File

@ -507,7 +507,7 @@ func TestLoader_Load_ExternalRegistration(t *testing.T) {
t.Run("Load a plugin with oauth client registration", func(t *testing.T) {
cfg := &config.Cfg{
Features: fakes.NewFakeFeatureToggles(featuremgmt.FlagExternalServiceAuth),
Features: featuremgmt.WithFeatures(featuremgmt.FlagExternalServiceAuth),
PluginsAllowUnsigned: []string{"grafana-test-datasource"},
}
pluginPaths := []string{filepath.Join(testDataDir(t), "oauth-external-registration")}
@ -608,7 +608,7 @@ func TestLoader_Load_ExternalRegistration(t *testing.T) {
t.Run("Load a plugin with service account registration", func(t *testing.T) {
cfg := &config.Cfg{
Features: fakes.NewFakeFeatureToggles(featuremgmt.FlagExternalServiceAuth),
Features: featuremgmt.WithFeatures(featuremgmt.FlagExternalServiceAuth),
PluginsAllowUnsigned: []string{"grafana-test-datasource"},
}
pluginPaths := []string{filepath.Join(testDataDir(t), "external-registration")}

View File

@ -3,12 +3,13 @@ package pipeline
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/plugins/log"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/require"
)
func TestSkipPlugins(t *testing.T) {

View File

@ -27,7 +27,7 @@ type Api struct {
accessControl accesscontrol.AccessControl
cfg *setting.Cfg
features *featuremgmt.FeatureManager
features featuremgmt.FeatureToggles
log log.Logger
routeRegister routing.RouteRegister
}
@ -36,7 +36,7 @@ func ProvideApi(
pd publicdashboards.Service,
rr routing.RouteRegister,
ac accesscontrol.AccessControl,
features *featuremgmt.FeatureManager,
features featuremgmt.FeatureToggles,
md publicdashboards.Middleware,
cfg *setting.Cfg,
) *Api {
@ -297,7 +297,7 @@ func (api *Api) DeletePublicDashboard(c *contextmodel.ReqContext) response.Respo
}
// Copied from pkg/api/metrics.go
func toJsonStreamingResponse(ctx context.Context, features *featuremgmt.FeatureManager, qdr *backend.QueryDataResponse) response.Response {
func toJsonStreamingResponse(ctx context.Context, features featuremgmt.FeatureToggles, qdr *backend.QueryDataResponse) response.Response {
statusWhenError := http.StatusBadRequest
if features.IsEnabled(ctx, featuremgmt.FlagDatasourceQueryMultiStatus) {
statusWhenError = http.StatusMultiStatus

View File

@ -155,6 +155,10 @@ func (f fakeFeatureToggles) IsEnabled(ctx context.Context, feature string) bool
return f.returnValue
}
func (f fakeFeatureToggles) GetEnabled(ctx context.Context) map[string]bool {
return map[string]bool{}
}
// Fake grpc secrets plugin impl
type fakeGRPCSecretsPlugin struct {
kv map[Key]string

View File

@ -23,7 +23,7 @@ func SetupDisabledTestService(tb testing.TB, store secrets.Store) *SecretsServic
return setupTestService(tb, store, featuremgmt.WithFeatures(featuremgmt.FlagDisableEnvelopeEncryption))
}
func setupTestService(tb testing.TB, store secrets.Store, features *featuremgmt.FeatureManager) *SecretsService {
func setupTestService(tb testing.TB, store secrets.Store, features featuremgmt.FeatureToggles) *SecretsService {
tb.Helper()
defaultKey := "SdlklWklckeLS"
if len(setting.SecretKey) > 0 {

View File

@ -26,7 +26,7 @@ import (
type ExtSvcAccountsService struct {
acSvc ac.Service
features *featuremgmt.FeatureManager
features featuremgmt.FeatureToggles
logger log.Logger
metrics *metrics
saSvc sa.Service

View File

@ -4,6 +4,9 @@ import (
"context"
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/localcache"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
@ -18,8 +21,6 @@ import (
sa "github.com/grafana/grafana/pkg/services/serviceaccounts"
"github.com/grafana/grafana/pkg/services/serviceaccounts/tests"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
type TestEnv struct {

View File

@ -195,7 +195,7 @@ func TestIntegration_DashboardPermissionFilter(t *testing.T) {
usr := &user.SignedInUser{OrgID: 1, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByAction(tt.permissions)}}
for _, features := range []*featuremgmt.FeatureManager{featuremgmt.WithFeatures(), featuremgmt.WithFeatures(featuremgmt.FlagPermissionsFilterRemoveSubquery)} {
for _, features := range []featuremgmt.FeatureToggles{featuremgmt.WithFeatures(), featuremgmt.WithFeatures(featuremgmt.FlagPermissionsFilterRemoveSubquery)} {
m := features.GetEnabled(context.Background())
keys := make([]string, 0, len(m))
for k := range m {
@ -394,7 +394,7 @@ func TestIntegration_DashboardPermissionFilter_WithSelfContainedPermissions(t *t
usr := &user.SignedInUser{OrgID: 1, OrgRole: org.RoleViewer, AuthenticatedBy: login.ExtendedJWTModule, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByAction(tt.signedInUserPermissions)}}
for _, features := range []*featuremgmt.FeatureManager{featuremgmt.WithFeatures(), featuremgmt.WithFeatures(featuremgmt.FlagPermissionsFilterRemoveSubquery)} {
for _, features := range []featuremgmt.FeatureToggles{featuremgmt.WithFeatures(), featuremgmt.WithFeatures(featuremgmt.FlagPermissionsFilterRemoveSubquery)} {
m := features.GetEnabled(context.Background())
keys := make([]string, 0, len(m))
for k := range m {
@ -545,7 +545,7 @@ func TestIntegration_DashboardNestedPermissionFilter(t *testing.T) {
})
usr := &user.SignedInUser{OrgID: orgID, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{orgID: accesscontrol.GroupScopesByAction(tc.permissions)}}
for _, features := range []*featuremgmt.FeatureManager{featuremgmt.WithFeatures(tc.features...), featuremgmt.WithFeatures(append(tc.features, featuremgmt.FlagPermissionsFilterRemoveSubquery)...)} {
for _, features := range []featuremgmt.FeatureToggles{featuremgmt.WithFeatures(tc.features...), featuremgmt.WithFeatures(append(tc.features, featuremgmt.FlagPermissionsFilterRemoveSubquery)...)} {
m := features.GetEnabled(context.Background())
keys := make([]string, 0, len(m))
for k := range m {
@ -716,7 +716,7 @@ func TestIntegration_DashboardNestedPermissionFilter_WithSelfContainedPermission
}),
},
}
for _, features := range []*featuremgmt.FeatureManager{featuremgmt.WithFeatures(tc.features...), featuremgmt.WithFeatures(append(tc.features, featuremgmt.FlagPermissionsFilterRemoveSubquery)...)} {
for _, features := range []featuremgmt.FeatureToggles{featuremgmt.WithFeatures(tc.features...), featuremgmt.WithFeatures(append(tc.features, featuremgmt.FlagPermissionsFilterRemoveSubquery)...)} {
m := features.GetEnabled(context.Background())
keys := make([]string, 0, len(m))
for k := range m {

View File

@ -34,7 +34,7 @@ type SSOSettingsService struct {
}
func ProvideService(cfg *setting.Cfg, sqlStore db.DB, ac ac.AccessControl,
routeRegister routing.RouteRegister, features *featuremgmt.FeatureManager,
routeRegister routing.RouteRegister, features featuremgmt.FeatureToggles,
secrets secrets.Service) *SSOSettingsService {
strategies := []ssosettings.FallbackStrategy{
strategies.NewOAuthStrategy(cfg),

View File

@ -118,22 +118,8 @@ func GetMockService(version string, rt RoundTripper) *Service {
version: version,
fakeRoundTripper: rt,
},
features: &fakeFeatureToggles{
flags: map[string]bool{
featuremgmt.FlagInfluxqlStreamingParser: false,
},
},
// featuremgmt.FlagInfluxqlStreamingParser: false
features: featuremgmt.WithFeatures(),
}
}
type fakeFeatureToggles struct {
flags map[string]bool
}
func (f *fakeFeatureToggles) IsEnabledGlobally(flag string) bool {
return f.flags[flag]
}
func (f *fakeFeatureToggles) IsEnabled(ctx context.Context, flag string) bool {
return f.flags[flag]
}