FeatureToggles: Add context and and an explicit global check (#78081)

This commit is contained in:
Ryan McKinley 2023-11-14 12:50:27 -08:00 committed by GitHub
parent c887ef2c9a
commit f69fd3726b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
94 changed files with 228 additions and 208 deletions

View File

@ -108,13 +108,13 @@ func (hs *HTTPServer) registerRoutes() {
r.Get("/admin/orgs/edit/:id", authorizeInOrg(ac.UseGlobalOrg, ac.OrgsAccessEvaluator), hs.Index)
r.Get("/admin/stats", authorize(ac.EvalPermission(ac.ActionServerStatsRead)), hs.Index)
r.Get("/admin/authentication/ldap", authorize(ac.EvalPermission(ac.ActionLDAPStatusRead)), hs.Index)
if hs.Features.IsEnabled(featuremgmt.FlagStorage) {
if hs.Features.IsEnabledGlobally(featuremgmt.FlagStorage) {
r.Get("/admin/storage", reqSignedIn, hs.Index)
r.Get("/admin/storage/*", reqSignedIn, hs.Index)
}
// feature toggle admin page
if hs.Features.IsEnabled(featuremgmt.FlagFeatureToggleAdminPage) {
if hs.Features.IsEnabledGlobally(featuremgmt.FlagFeatureToggleAdminPage) {
r.Get("/admin/featuretoggles", authorize(ac.EvalPermission(ac.ActionFeatureManagementRead)), hs.Index)
}
@ -156,11 +156,11 @@ func (hs *HTTPServer) registerRoutes() {
r.Get("/dashboards/*", reqSignedIn, hs.Index)
r.Get("/goto/:uid", reqSignedIn, hs.redirectFromShortURL, hs.Index)
if hs.Features.IsEnabled(featuremgmt.FlagDashboardEmbed) {
if hs.Features.IsEnabledGlobally(featuremgmt.FlagDashboardEmbed) {
r.Get("/d-embed", reqSignedIn, middleware.AddAllowEmbeddingHeader(), hs.Index)
}
if hs.Features.IsEnabled(featuremgmt.FlagPublicDashboards) {
if hs.Features.IsEnabledGlobally(featuremgmt.FlagPublicDashboards) {
// list public dashboards
r.Get("/public-dashboards/list", reqSignedIn, hs.Index)
@ -216,7 +216,7 @@ func (hs *HTTPServer) registerRoutes() {
r.Get("/swagger-ui", swaggerUI)
r.Get("/openapi3", openapi3)
if hs.Features.IsEnabled(featuremgmt.FlagClientTokenRotation) {
if hs.Features.IsEnabledGlobally(featuremgmt.FlagClientTokenRotation) {
r.Post("/api/user/auth-tokens/rotate", routing.Wrap(hs.RotateUserAuthToken))
r.Get("/user/auth-tokens/rotate", routing.Wrap(hs.RotateUserAuthTokenRedirect))
}
@ -277,12 +277,12 @@ func (hs *HTTPServer) registerRoutes() {
orgRoute.Get("/quotas", authorize(ac.EvalPermission(ac.ActionOrgsQuotasRead)), routing.Wrap(hs.GetCurrentOrgQuotas))
})
if hs.Features.IsEnabled(featuremgmt.FlagStorage) {
if hs.Features.IsEnabledGlobally(featuremgmt.FlagStorage) {
// Will eventually be replaced with the 'object' route
apiRoute.Group("/storage", hs.StorageService.RegisterHTTPRoutes)
}
if hs.Features.IsEnabled(featuremgmt.FlagPanelTitleSearch) {
if hs.Features.IsEnabledGlobally(featuremgmt.FlagPanelTitleSearch) {
apiRoute.Group("/search-v2", hs.SearchV2HTTPService.RegisterHTTPRoutes)
}
@ -392,7 +392,7 @@ func (hs *HTTPServer) registerRoutes() {
apiRoute.Any("/plugin-proxy/:pluginId/*", requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), authorize(ac.EvalPermission(pluginaccesscontrol.ActionAppAccess, pluginIDScope)), hs.ProxyPluginRequest)
apiRoute.Any("/plugin-proxy/:pluginId", requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), authorize(ac.EvalPermission(pluginaccesscontrol.ActionAppAccess, pluginIDScope)), hs.ProxyPluginRequest)
if hs.Cfg.PluginAdminEnabled && (hs.Features.IsEnabled(featuremgmt.FlagManagedPluginsInstall) || !hs.Cfg.PluginAdminExternalManageEnabled) {
if hs.Cfg.PluginAdminEnabled && (hs.Features.IsEnabledGlobally(featuremgmt.FlagManagedPluginsInstall) || !hs.Cfg.PluginAdminExternalManageEnabled) {
apiRoute.Group("/plugins", func(pluginRoute routing.RouteRegister) {
pluginRoute.Post("/:pluginId/install", authorize(ac.EvalPermission(pluginaccesscontrol.ActionInstall)), routing.Wrap(hs.InstallPlugin))
pluginRoute.Post("/:pluginId/uninstall", authorize(ac.EvalPermission(pluginaccesscontrol.ActionInstall)), routing.Wrap(hs.UninstallPlugin))
@ -405,7 +405,7 @@ func (hs *HTTPServer) registerRoutes() {
pluginRoute.Get("/:pluginId/metrics", reqOrgAdmin, routing.Wrap(hs.CollectPluginMetrics))
})
if hs.Features.IsEnabled(featuremgmt.FlagFeatureToggleAdminPage) {
if hs.Features.IsEnabledGlobally(featuremgmt.FlagFeatureToggleAdminPage) {
apiRoute.Group("/featuremgmt", func(featuremgmtRoute routing.RouteRegister) {
featuremgmtRoute.Get("/state", authorize(ac.EvalPermission(ac.ActionFeatureManagementRead)), hs.GetFeatureMgmtState)
featuremgmtRoute.Get("/", authorize(ac.EvalPermission(ac.ActionFeatureManagementRead)), hs.GetFeatureToggles)

View File

@ -228,7 +228,7 @@ func setupSimpleHTTPServer(features *featuremgmt.FeatureManager) *HTTPServer {
features = featuremgmt.WithFeatures()
}
// nolint:staticcheck
cfg := setting.NewCfgWithFeatures(features.IsEnabled)
cfg := setting.NewCfgWithFeatures(features.IsEnabledGlobally)
return &HTTPServer{
Cfg: cfg,

View File

@ -94,7 +94,7 @@ func (hs *HTTPServer) GetDashboard(c *contextmodel.ReqContext) response.Response
// If public dashboards is enabled and we have a public dashboard, update meta
// values
if hs.Features.IsEnabled(featuremgmt.FlagPublicDashboards) {
if hs.Features.IsEnabledGlobally(featuremgmt.FlagPublicDashboards) {
publicDashboard, err := hs.PublicDashboardsApi.PublicDashboardService.FindByDashboardUid(c.Req.Context(), c.SignedInUser.GetOrgID(), dash.UID)
if err != nil && !errors.Is(err, publicdashboardModels.ErrPublicDashboardNotFound) {
return response.Error(http.StatusInternalServerError, "Error while retrieving public dashboards", err)

View File

@ -15,6 +15,7 @@ import (
"golang.org/x/exp/slices"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/api/datasource"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
@ -356,7 +357,7 @@ func validateJSONData(ctx context.Context, jsonData *simplejson.Json, cfg *setti
}
// Prevent adding a data source team header with a name that matches the auth proxy header name
if features.IsEnabled(featuremgmt.FlagTeamHttpHeaders) {
if features.IsEnabled(ctx, featuremgmt.FlagTeamHttpHeaders) {
err := validateTeamHTTPHeaderJSON(jsonData)
if err != nil {
return err

View File

@ -43,7 +43,7 @@ const REDACTED = "redacted"
func (hs *HTTPServer) GetFolders(c *contextmodel.ReqContext) response.Response {
var folders []*folder.Folder
var err error
if hs.Features.IsEnabled(featuremgmt.FlagNestedFolders) {
if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagNestedFolders) {
folders, err = hs.folderService.GetChildren(c.Req.Context(), &folder.GetChildrenQuery{
OrgID: c.SignedInUser.GetOrgID(),
Limit: c.QueryInt64("limit"),
@ -190,7 +190,7 @@ func (hs *HTTPServer) setDefaultFolderPermissions(ctx context.Context, orgID int
}
isNested := folder.ParentUID != ""
if !isNested || !hs.Features.IsEnabled(featuremgmt.FlagNestedFolders) {
if !isNested || !hs.Features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) {
permissions = append(permissions, []accesscontrol.SetResourcePermissionCommand{
{BuiltinRole: string(org.RoleEditor), Permission: dashboards.PERMISSION_EDIT.String()},
{BuiltinRole: string(org.RoleViewer), Permission: dashboards.PERMISSION_VIEW.String()},
@ -212,7 +212,7 @@ func (hs *HTTPServer) setDefaultFolderPermissions(ctx context.Context, orgID int
// 404: notFoundError
// 500: internalServerError
func (hs *HTTPServer) MoveFolder(c *contextmodel.ReqContext) response.Response {
if hs.Features.IsEnabled(featuremgmt.FlagNestedFolders) {
if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagNestedFolders) {
cmd := folder.MoveFolderCommand{}
if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
@ -390,7 +390,7 @@ func (hs *HTTPServer) newToFolderDto(c *contextmodel.ReqContext, f *folder.Folde
return dtos.Folder{}, err
}
if !hs.Features.IsEnabled(featuremgmt.FlagNestedFolders) {
if !hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagNestedFolders) {
return folderDTO, nil
}

View File

@ -66,7 +66,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
continue
}
if panel.ID == "datagrid" && !hs.Features.IsEnabled(featuremgmt.FlagEnableDatagridEditing) {
if panel.ID == "datagrid" && !hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagEnableDatagridEditing) {
continue
}

View File

@ -35,7 +35,7 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features *featuremgmt.
t.Helper()
db.InitTestDB(t)
// nolint:staticcheck
cfg.IsFeatureToggleEnabled = features.IsEnabled
cfg.IsFeatureToggleEnabled = features.IsEnabledGlobally
{
oldVersion := setting.BuildVersion

View File

@ -38,7 +38,7 @@ func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexV
return nil, err
}
if hs.Features.IsEnabled(featuremgmt.FlagIndividualCookiePreferences) {
if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagIndividualCookiePreferences) {
if !prefs.Cookies("analytics") {
settings.GoogleAnalytics4Id = ""
settings.GoogleAnalyticsId = ""
@ -166,7 +166,7 @@ func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexV
hs.HooksService.RunIndexDataHooks(&data, c)
data.NavTree.ApplyAdminIA(hs.Features.IsEnabled(featuremgmt.FlagNavAdminSubsections))
data.NavTree.ApplyAdminIA(hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagNavAdminSubsections))
data.NavTree.Sort()
return &data, nil
@ -244,7 +244,7 @@ func (hs *HTTPServer) getThemeForIndexData(themePrefId string, themeURLParam str
if pref.IsValidThemeID(themePrefId) {
theme := pref.GetThemeByID(themePrefId)
if !theme.IsExtra || hs.Features.IsEnabled(featuremgmt.FlagExtraThemes) {
if !theme.IsExtra || hs.Features.IsEnabledGlobally(featuremgmt.FlagExtraThemes) {
return theme
}
}

View File

@ -91,7 +91,7 @@ func (hs *HTTPServer) CookieOptionsFromCfg() cookies.CookieOptions {
}
func (hs *HTTPServer) LoginView(c *contextmodel.ReqContext) {
if hs.Features.IsEnabled(featuremgmt.FlagClientTokenRotation) {
if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagClientTokenRotation) {
if errors.Is(c.LookupTokenErr, authn.ErrTokenNeedsRotation) {
c.Redirect(hs.Cfg.AppSubURL + "/")
return
@ -334,7 +334,7 @@ func (hs *HTTPServer) RedirectResponseWithError(c *contextmodel.ReqContext, err
func (hs *HTTPServer) redirectURLWithErrorCookie(c *contextmodel.ReqContext, err error) string {
setCookie := true
if hs.Features.IsEnabled(featuremgmt.FlagIndividualCookiePreferences) {
if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagIndividualCookiePreferences) {
var userID int64
if c.SignedInUser != nil && !c.SignedInUser.IsNil() {
var errID error

View File

@ -63,7 +63,7 @@ func (hs *HTTPServer) QueryMetricsV2(c *contextmodel.ReqContext) response.Respon
func (hs *HTTPServer) toJsonStreamingResponse(ctx context.Context, qdr *backend.QueryDataResponse) response.Response {
statusWhenError := http.StatusBadRequest
if hs.Features.IsEnabled(featuremgmt.FlagDatasourceQueryMultiStatus) {
if hs.Features.IsEnabled(ctx, featuremgmt.FlagDatasourceQueryMultiStatus) {
statusWhenError = http.StatusMultiStatus
}

View File

@ -26,7 +26,7 @@ import (
func (hs *HTTPServer) registerPlaylistAPI(apiRoute routing.RouteRegister) {
// Register the actual handlers
apiRoute.Group("/playlists", func(playlistRoute routing.RouteRegister) {
if hs.Features.IsEnabled(featuremgmt.FlagKubernetesPlaylists) {
if hs.Features.IsEnabledGlobally(featuremgmt.FlagKubernetesPlaylists) {
// Use k8s client to implement legacy API
handler := newPlaylistK8sHandler(hs)
playlistRoute.Get("/", handler.searchPlaylists)

View File

@ -270,7 +270,7 @@ func (proxy *DataSourceProxy) director(req *http.Request) {
}
}
if proxy.features.IsEnabled(featuremgmt.FlagIdForwarding) && auth.IsIDForwardingEnabledForDataSource(proxy.ds) {
if proxy.features.IsEnabled(req.Context(), featuremgmt.FlagIdForwarding) && auth.IsIDForwardingEnabledForDataSource(proxy.ds) {
proxyutil.ApplyForwardIDHeader(req, proxy.ctx.SignedInUser)
}
}

View File

@ -7,6 +7,8 @@ import (
"net/http"
"net/url"
"go.opentelemetry.io/otel/attribute"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/plugins"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
@ -17,7 +19,6 @@ import (
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/util/proxyutil"
"github.com/grafana/grafana/pkg/web"
"go.opentelemetry.io/otel/attribute"
)
type PluginProxy struct {
@ -161,7 +162,7 @@ func (proxy PluginProxy) director(req *http.Request) {
proxyutil.ApplyUserHeader(proxy.cfg.SendUserHeader, req, proxy.ctx.SignedInUser)
if proxy.features.IsEnabled(featuremgmt.FlagIdForwarding) {
if proxy.features.IsEnabled(req.Context(), featuremgmt.FlagIdForwarding) {
proxyutil.ApplyForwardIDHeader(req, proxy.ctx.SignedInUser)
}

View File

@ -61,7 +61,7 @@ type DataPipeline []Node
func (dp *DataPipeline) execute(c context.Context, now time.Time, s *Service) (mathexp.Vars, error) {
vars := make(mathexp.Vars)
groupByDSFlag := s.features.IsEnabled(featuremgmt.FlagSseGroupByDatasource)
groupByDSFlag := s.features.IsEnabled(c, featuremgmt.FlagSseGroupByDatasource)
// Execute datasource nodes first, and grouped by datasource.
if groupByDSFlag {
dsNodes := []*DSNode{}
@ -227,7 +227,7 @@ func (s *Service) buildGraph(req *Request) (*simple.DirectedGraph, error) {
case TypeCMDNode:
node, err = buildCMDNode(rn, s.features)
case TypeMLNode:
if s.features.IsEnabled(featuremgmt.FlagMlExpressions) {
if s.features.IsEnabledGlobally(featuremgmt.FlagMlExpressions) {
node, err = s.buildMLNode(dp, rn, req)
if err != nil {
err = fmt.Errorf("fail to parse expression with refID %v: %w", rn.RefID, err)

View File

@ -388,7 +388,7 @@ func convertDataFramesToResults(ctx context.Context, frames data.Frames, datasou
}
var dt data.FrameType
dt, useDataplane, _ := shouldUseDataplane(frames, logger, s.features.IsEnabled(featuremgmt.FlagDisableSSEDataplane))
dt, useDataplane, _ := shouldUseDataplane(frames, logger, s.features.IsEnabled(ctx, featuremgmt.FlagDisableSSEDataplane))
if useDataplane {
logger.Debug("Handling SSE data source query through dataplane", "datatype", dt)
result, err := handleDataplaneFrames(ctx, s.tracer, dt, frames)

View File

@ -81,7 +81,7 @@ func UnmarshalThresholdCommand(rn *rawNode, features featuremgmt.FeatureToggles)
if err != nil {
return nil, fmt.Errorf("invalid condition: %w", err)
}
if firstCondition.UnloadEvaluator != nil && features.IsEnabled(featuremgmt.FlagRecoveryThreshold) {
if firstCondition.UnloadEvaluator != nil && features.IsEnabledGlobally(featuremgmt.FlagRecoveryThreshold) {
unloading, err := NewThresholdCommand(rn.RefID, referenceVar, firstCondition.UnloadEvaluator.Type, firstCondition.UnloadEvaluator.Params)
unloading.Invert = true
if err != nil {

View File

@ -134,7 +134,7 @@ func (s *SocialGoogle) extractFromAPI(ctx context.Context, client *http.Client)
}
func (s *SocialGoogle) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string {
if s.features.IsEnabled(featuremgmt.FlagAccessTokenExpirationCheck) && s.useRefreshToken {
if s.features.IsEnabledGlobally(featuremgmt.FlagAccessTokenExpirationCheck) && s.useRefreshToken {
opts = append(opts, oauth2.AccessTypeOffline, oauth2.ApprovalForce)
}
return s.SocialBase.AuthCodeURL(state, opts...)

View File

@ -206,7 +206,7 @@ func ProvideService(cfg *setting.Cfg,
forceUseGraphAPI: sec.Key("force_use_graph_api").MustBool(false),
skipOrgRoleSync: cfg.AzureADSkipOrgRoleSync,
}
if info.UseRefreshToken && features.IsEnabled(featuremgmt.FlagAccessTokenExpirationCheck) {
if info.UseRefreshToken && features.IsEnabledGlobally(featuremgmt.FlagAccessTokenExpirationCheck) {
appendUniqueScope(&config, OfflineAccessScope)
}
}
@ -219,7 +219,7 @@ func ProvideService(cfg *setting.Cfg,
allowedGroups: util.SplitString(sec.Key("allowed_groups").String()),
skipOrgRoleSync: cfg.OktaSkipOrgRoleSync,
}
if info.UseRefreshToken && features.IsEnabled(featuremgmt.FlagAccessTokenExpirationCheck) {
if info.UseRefreshToken && features.IsEnabledGlobally(featuremgmt.FlagAccessTokenExpirationCheck) {
appendUniqueScope(&config, OfflineAccessScope)
}
}

View File

@ -64,7 +64,7 @@ func (l *loggerImpl) Middleware() web.Middleware {
// put the start time on context so we can measure it later.
r = r.WithContext(log.InitstartTime(r.Context(), time.Now()))
if l.flags.IsEnabled(featuremgmt.FlagUnifiedRequestLog) {
if l.flags.IsEnabled(r.Context(), featuremgmt.FlagUnifiedRequestLog) {
r = r.WithContext(errutil.SetUnifiedLogging(r.Context()))
}
@ -128,7 +128,7 @@ func (l *loggerImpl) prepareLogParams(c *contextmodel.ReqContext, duration time.
logParams = append(logParams, "handler", handler)
}
if l.flags.IsEnabled(featuremgmt.FlagRequestInstrumentationStatusSource) {
if l.flags.IsEnabled(r.Context(), featuremgmt.FlagRequestInstrumentationStatusSource) {
rmd := requestmeta.GetRequestMetaData(c.Req.Context())
logParams = append(logParams, "status_source", rmd.StatusSource)
}

View File

@ -37,7 +37,7 @@ func RequestMetrics(features featuremgmt.FeatureToggles, cfg *setting.Cfg, promR
histogramLabels := []string{"handler", "status_code", "method"}
if features.IsEnabled(featuremgmt.FlagRequestInstrumentationStatusSource) {
if features.IsEnabledGlobally(featuremgmt.FlagRequestInstrumentationStatusSource) {
histogramLabels = append(histogramLabels, "status_source")
}
@ -45,7 +45,7 @@ func RequestMetrics(features featuremgmt.FeatureToggles, cfg *setting.Cfg, promR
histogramLabels = append(histogramLabels, "grafana_team")
}
if features.IsEnabled(featuremgmt.FlagHttpSLOLevels) {
if features.IsEnabledGlobally(featuremgmt.FlagHttpSLOLevels) {
histogramLabels = append(histogramLabels, "slo_group")
}
@ -56,7 +56,7 @@ func RequestMetrics(features featuremgmt.FeatureToggles, cfg *setting.Cfg, promR
Buckets: defBuckets,
}
if features.IsEnabled(featuremgmt.FlagEnableNativeHTTPHistogram) {
if features.IsEnabledGlobally(featuremgmt.FlagEnableNativeHTTPHistogram) {
// the recommended default value from the prom_client
// https://github.com/prometheus/client_golang/blob/main/prometheus/histogram.go#L411
// Giving this variable an value means the client will expose the histograms as an
@ -95,7 +95,7 @@ func RequestMetrics(features featuremgmt.FeatureToggles, cfg *setting.Cfg, promR
handler = "notfound"
} else {
// log requests where we could not identify handler so we can register them.
if features.IsEnabled(featuremgmt.FlagLogRequestsInstrumentedAsUnknown) {
if features.IsEnabled(r.Context(), featuremgmt.FlagLogRequestsInstrumentedAsUnknown) {
log.Warn("request instrumented as unknown", "path", r.URL.Path, "status_code", status)
}
}
@ -104,7 +104,7 @@ func RequestMetrics(features featuremgmt.FeatureToggles, cfg *setting.Cfg, promR
labelValues := []string{handler, code, r.Method}
rmd := requestmeta.GetRequestMetaData(r.Context())
if features.IsEnabled(featuremgmt.FlagRequestInstrumentationStatusSource) {
if features.IsEnabled(r.Context(), featuremgmt.FlagRequestInstrumentationStatusSource) {
labelValues = append(labelValues, string(rmd.StatusSource))
}
@ -112,7 +112,7 @@ func RequestMetrics(features featuremgmt.FeatureToggles, cfg *setting.Cfg, promR
labelValues = append(labelValues, rmd.Team)
}
if features.IsEnabled(featuremgmt.FlagHttpSLOLevels) {
if features.IsEnabled(r.Context(), featuremgmt.FlagHttpSLOLevels) {
labelValues = append(labelValues, string(rmd.SLOGroup))
}

View File

@ -155,7 +155,7 @@ func (fn ClientMiddlewareFunc) CreateClientMiddleware(next Client) Client {
}
type FeatureToggles interface {
IsEnabled(flag string) bool
IsEnabledGlobally(flag string) bool
GetEnabled(ctx context.Context) map[string]bool
}

View File

@ -594,6 +594,6 @@ func (f *FakeFeatureToggles) GetEnabled(_ context.Context) map[string]bool {
return f.features
}
func (f *FakeFeatureToggles) IsEnabled(feature string) bool {
func (f *FakeFeatureToggles) IsEnabledGlobally(feature string) bool {
return f.features[feature]
}

View File

@ -59,7 +59,7 @@ func (s *Service) Base(n PluginInfo) (string, error) {
func (s *Service) Module(n PluginInfo) (string, error) {
if n.class == plugins.ClassCore {
if s.cfg.Features != nil &&
s.cfg.Features.IsEnabled(featuremgmt.FlagExternalCorePlugins) &&
s.cfg.Features.IsEnabledGlobally(featuremgmt.FlagExternalCorePlugins) &&
filepath.Base(n.dir) == "dist" {
// The core plugin has been built externally, use the module from the dist folder
} else {

View File

@ -37,7 +37,7 @@ type TestingAPIBuilder struct {
}
func RegisterAPIService(features featuremgmt.FeatureToggles, apiregistration grafanaapiserver.APIRegistrar) *TestingAPIBuilder {
if !features.IsEnabled(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) {
if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) {
return nil // skip registration unless opting into experimental apis
}
builder := &TestingAPIBuilder{

View File

@ -43,7 +43,7 @@ func ProvideService(cfg *setting.Cfg, db db.DB, routeRegister routing.RouteRegis
return nil, err
}
if features.IsEnabled(featuremgmt.FlagSplitScopes) {
if features.IsEnabledGlobally(featuremgmt.FlagSplitScopes) {
// Migrating scopes that haven't been split yet to have kind, attribute and identifier in the DB
// This will be removed once we've:
// 1) removed the feature toggle and
@ -213,9 +213,9 @@ func permissionCacheKey(user identity.Requester) string {
// DeclarePluginRoles allow the caller to declare, to the service, plugin roles and their assignments
// to organization roles ("Viewer", "Editor", "Admin") or "Grafana Admin"
func (s *Service) DeclarePluginRoles(_ context.Context, ID, name string, regs []plugins.RoleRegistration) error {
func (s *Service) DeclarePluginRoles(ctx context.Context, ID, name string, regs []plugins.RoleRegistration) error {
// Protect behind feature toggle
if !s.features.IsEnabled(featuremgmt.FlagAccessControlOnCall) {
if !s.features.IsEnabled(ctx, featuremgmt.FlagAccessControlOnCall) {
return nil
}
@ -397,7 +397,7 @@ func PermissionMatchesSearchOptions(permission accesscontrol.Permission, searchO
}
func (s *Service) SaveExternalServiceRole(ctx context.Context, cmd accesscontrol.SaveExternalServiceRoleCommand) error {
if !(s.features.IsEnabled(featuremgmt.FlagExternalServiceAuth) || s.features.IsEnabled(featuremgmt.FlagExternalServiceAccounts)) {
if !(s.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAuth) || s.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts)) {
s.log.Debug("Registering an external service role is behind a feature flag, enable it to use this feature.")
return nil
}
@ -410,7 +410,7 @@ func (s *Service) SaveExternalServiceRole(ctx context.Context, cmd accesscontrol
}
func (s *Service) DeleteExternalServiceRole(ctx context.Context, externalServiceID string) error {
if !(s.features.IsEnabled(featuremgmt.FlagExternalServiceAuth) || s.features.IsEnabled(featuremgmt.FlagExternalServiceAccounts)) {
if !(s.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAuth) || s.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts)) {
s.log.Debug("Deleting an external service role is behind a feature flag, enable it to use this feature.")
return nil
}

View File

@ -37,7 +37,7 @@ func (api *AccessControlAPI) RegisterAPIEndpoints() {
api.RouteRegister.Group("/api/access-control", func(rr routing.RouteRegister) {
rr.Get("/user/actions", middleware.ReqSignedIn, routing.Wrap(api.getUserActions))
rr.Get("/user/permissions", middleware.ReqSignedIn, routing.Wrap(api.getUserPermissions))
if api.features.IsEnabled(featuremgmt.FlagAccessControlOnCall) {
if api.features.IsEnabledGlobally(featuremgmt.FlagAccessControlOnCall) {
userIDScope := ac.Scope("users", "id", ac.Parameter(":userID"))
rr.Get("/users/permissions/search", authorize(ac.EvalPermission(ac.ActionUsersPermissionsRead)), routing.Wrap(api.searchUsersPermissions))
rr.Get("/user/:userID/permissions/search", authorize(ac.EvalPermission(ac.ActionUsersPermissionsRead, userIDScope)), routing.Wrap(api.searchUserPermissions))

View File

@ -655,7 +655,7 @@ func (s *store) createPermissions(sess *db.Session, roleID int64, resource, reso
p.RoleID = roleID
p.Created = time.Now()
p.Updated = time.Now()
if s.features.IsEnabled(featuremgmt.FlagSplitScopes) {
if s.features.IsEnabledGlobally(featuremgmt.FlagSplitScopes) {
p.Kind, p.Attribute, p.Identifier = p.SplitScope()
}
permissions = append(permissions, p)

View File

@ -32,7 +32,7 @@ func ProvideService(
) *Service {
s := &Service{cfg: cfg, logger: log.New("id-service"), signer: signer, cache: cache, metrics: newMetrics(reg)}
if features.IsEnabled(featuremgmt.FlagIdForwarding) {
if features.IsEnabledGlobally(featuremgmt.FlagIdForwarding) {
authnService.RegisterPostAuthHook(s.hook, 140)
}

View File

@ -28,7 +28,7 @@ type LocalSigner struct {
}
func (s *LocalSigner) SignIDToken(ctx context.Context, claims *auth.IDClaims) (string, error) {
if !s.features.IsEnabled(featuremgmt.FlagIdForwarding) {
if !s.features.IsEnabled(ctx, featuremgmt.FlagIdForwarding) {
return "", nil
}

View File

@ -132,7 +132,7 @@ func ProvideService(
s.RegisterClient(clients.ProvideJWT(jwtService, cfg))
}
if s.cfg.ExtendedJWTAuthEnabled && features.IsEnabled(featuremgmt.FlagExternalServiceAuth) {
if s.cfg.ExtendedJWTAuthEnabled && features.IsEnabledGlobally(featuremgmt.FlagExternalServiceAuth) {
s.RegisterClient(clients.ProvideExtendedJWT(userService, cfg, signingKeysService, oauthServer))
}
@ -159,7 +159,7 @@ func ProvideService(
s.RegisterPostAuthHook(orgUserSyncService.SyncOrgRolesHook, 30)
s.RegisterPostAuthHook(userSyncService.SyncLastSeenHook, 120)
if features.IsEnabled(featuremgmt.FlagAccessTokenExpirationCheck) {
if features.IsEnabledGlobally(featuremgmt.FlagAccessTokenExpirationCheck) {
s.RegisterPostAuthHook(sync.ProvideOAuthTokenSync(oauthTokenService, sessionService, socialService).SyncOauthTokenHook, 60)
}

View File

@ -55,7 +55,7 @@ func (s *Session) Authenticate(ctx context.Context, r *authn.Request) (*authn.Id
return nil, err
}
if s.features.IsEnabled(featuremgmt.FlagClientTokenRotation) {
if s.features.IsEnabled(ctx, featuremgmt.FlagClientTokenRotation) {
if token.NeedsRotation(time.Duration(s.cfg.TokenRotationIntervalMinutes) * time.Minute) {
return nil, authn.ErrTokenNeedsRotation.Errorf("token needs to be rotated")
}
@ -88,7 +88,7 @@ func (s *Session) Priority() uint {
}
func (s *Session) Hook(ctx context.Context, identity *authn.Identity, r *authn.Request) error {
if identity.SessionToken == nil || s.features.IsEnabled(featuremgmt.FlagClientTokenRotation) {
if identity.SessionToken == nil || s.features.IsEnabled(ctx, featuremgmt.FlagClientTokenRotation) {
return nil
}

View File

@ -6,6 +6,9 @@ import (
"errors"
"net/http"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
@ -18,8 +21,6 @@ import (
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
func ProvideService(cfg *setting.Cfg, tracer tracing.Tracer, features *featuremgmt.FeatureManager, authnService authn.Service,
@ -140,7 +141,7 @@ func (h *ContextHandler) Middleware(next http.Handler) http.Handler {
func (h *ContextHandler) deleteInvalidCookieEndOfRequestFunc(reqContext *contextmodel.ReqContext) web.BeforeFunc {
return func(w web.ResponseWriter) {
if h.features.IsEnabled(featuremgmt.FlagClientTokenRotation) {
if h.features.IsEnabled(reqContext.Req.Context(), featuremgmt.FlagClientTokenRotation) {
return
}

View File

@ -66,7 +66,7 @@ func ProvideDashboardStore(sqlStore db.DB, cfg *setting.Cfg, features featuremgm
}
func (d *dashboardStore) emitEntityEvent() bool {
return d.features != nil && d.features.IsEnabled(featuremgmt.FlagPanelTitleSearch)
return d.features != nil && d.features.IsEnabledGlobally(featuremgmt.FlagPanelTitleSearch)
}
func (d *dashboardStore) ValidateDashboardBeforeSave(ctx context.Context, dashboard *dashboards.Dashboard, overwrite bool) (bool, error) {
@ -1010,7 +1010,12 @@ func (d *dashboardStore) FindDashboards(ctx context.Context, query *dashboards.F
}
if len(query.FolderUIDs) > 0 {
filters = append(filters, searchstore.FolderUIDFilter{Dialect: d.store.GetDialect(), OrgID: orgID, UIDs: query.FolderUIDs, NestedFoldersEnabled: d.features.IsEnabled(featuremgmt.FlagNestedFolders)})
filters = append(filters, searchstore.FolderUIDFilter{
Dialect: d.store.GetDialect(),
OrgID: orgID,
UIDs: query.FolderUIDs,
NestedFoldersEnabled: d.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders),
})
}
var res []dashboards.DashboardSearchProjection

View File

@ -198,7 +198,7 @@ func (s *Service) AddDataSource(ctx context.Context, cmd *datasources.AddDataSou
var err error
cmd.EncryptedSecureJsonData = make(map[string][]byte)
if !s.features.IsEnabled(featuremgmt.FlagDisableSecretsCompatibility) {
if !s.features.IsEnabled(ctx, featuremgmt.FlagDisableSecretsCompatibility) {
cmd.EncryptedSecureJsonData, err = s.SecretsService.EncryptJsonData(ctx, cmd.SecureJsonData, secrets.WithoutScope())
if err != nil {
return err
@ -689,7 +689,7 @@ func (s *Service) fillWithSecureJSONData(ctx context.Context, cmd *datasources.U
}
cmd.EncryptedSecureJsonData = make(map[string][]byte)
if !s.features.IsEnabled(featuremgmt.FlagDisableSecretsCompatibility) {
if !s.features.IsEnabled(ctx, featuremgmt.FlagDisableSecretsCompatibility) {
cmd.EncryptedSecureJsonData, err = s.SecretsService.EncryptJsonData(ctx, cmd.SecureJsonData, secrets.WithoutScope())
if err != nil {
return err

View File

@ -65,7 +65,7 @@ type OAuth2ServiceImpl struct {
func ProvideService(router routing.RouteRegister, bus bus.Bus, db db.DB, cfg *setting.Cfg,
extSvcAccSvc serviceaccounts.ExtSvcAccountsService, accessControl ac.AccessControl, acSvc ac.Service, userSvc user.Service,
teamSvc team.Service, keySvc signingkeys.Service, fmgmt *featuremgmt.FeatureManager) (*OAuth2ServiceImpl, error) {
if !fmgmt.IsEnabled(featuremgmt.FlagExternalServiceAuth) {
if !fmgmt.IsEnabledGlobally(featuremgmt.FlagExternalServiceAuth) {
return nil, nil
}
config := &fosite.Config{

View File

@ -51,14 +51,14 @@ func (r *Registry) RemoveExternalService(ctx context.Context, name string) error
switch provider {
case extsvcauth.ServiceAccounts:
if !r.features.IsEnabled(featuremgmt.FlagExternalServiceAccounts) {
if !r.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts) {
r.logger.Debug("Skipping External Service removal, flag disabled", "service", name, "flag", featuremgmt.FlagExternalServiceAccounts)
return nil
}
r.logger.Debug("Routing External Service removal to the External Service Account service", "service", name)
return r.saReg.RemoveExternalService(ctx, name)
case extsvcauth.OAuth2Server:
if !r.features.IsEnabled(featuremgmt.FlagExternalServiceAuth) {
if !r.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAuth) {
r.logger.Debug("Skipping External Service removal, flag disabled", "service", name, "flag", featuremgmt.FlagExternalServiceAccounts)
return nil
}
@ -80,14 +80,14 @@ func (r *Registry) SaveExternalService(ctx context.Context, cmd *extsvcauth.Exte
switch cmd.AuthProvider {
case extsvcauth.ServiceAccounts:
if !r.features.IsEnabled(featuremgmt.FlagExternalServiceAccounts) {
if !r.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts) {
r.logger.Warn("Skipping External Service authentication, flag disabled", "service", cmd.Name, "flag", featuremgmt.FlagExternalServiceAccounts)
return nil, nil
}
r.logger.Debug("Routing the External Service registration to the External Service Account service", "service", cmd.Name)
return r.saReg.SaveExternalService(ctx, cmd)
case extsvcauth.OAuth2Server:
if !r.features.IsEnabled(featuremgmt.FlagExternalServiceAuth) {
if !r.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAuth) {
r.logger.Warn("Skipping External Service authentication, flag disabled", "service", cmd.Name, "flag", featuremgmt.FlagExternalServiceAuth)
return nil, nil
}

View File

@ -126,7 +126,12 @@ func (fm *FeatureManager) readFile() error {
}
// IsEnabled checks if a feature is enabled
func (fm *FeatureManager) IsEnabled(flag string) bool {
func (fm *FeatureManager) IsEnabled(ctx context.Context, flag string) bool {
return fm.enabled[flag]
}
// IsEnabledGlobally checks if a feature is for all tenants
func (fm *FeatureManager) IsEnabledGlobally(flag string) bool {
return fm.enabled[flag]
}

View File

@ -10,17 +10,17 @@ import (
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.True(t, ft.IsEnabledGlobally("a"))
require.True(t, ft.IsEnabledGlobally("b"))
require.True(t, ft.IsEnabledGlobally("c"))
require.False(t, ft.IsEnabledGlobally("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.True(t, ft.IsEnabledGlobally("a"))
require.False(t, ft.IsEnabledGlobally("b"))
require.Equal(t, map[string]bool{"a": true}, ft.GetEnabled(context.Background()))
})
@ -37,9 +37,9 @@ func TestFeatureManager(t *testing.T) {
Name: "b",
Expression: "true",
})
require.False(t, ft.IsEnabled("a"))
require.True(t, ft.IsEnabled("b"))
require.False(t, ft.IsEnabled("c")) // uknown flag
require.False(t, ft.IsEnabledGlobally("a"))
require.True(t, ft.IsEnabledGlobally("b"))
require.False(t, ft.IsEnabledGlobally("c")) // uknown flag
// Try changing "requires license"
ft.registerFlags(FeatureFlag{
@ -49,9 +49,9 @@ func TestFeatureManager(t *testing.T) {
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"))
require.False(t, ft.IsEnabledGlobally("a"))
require.False(t, ft.IsEnabledGlobally("b"))
require.False(t, ft.IsEnabledGlobally("c"))
})
t.Run("check description and docs configs", func(t *testing.T) {

View File

@ -2,11 +2,21 @@ package featuremgmt
import (
"bytes"
"context"
"encoding/json"
)
type FeatureToggles interface {
IsEnabled(flag string) bool
// Check if a feature is enabled for a given context.
// The settings may be per user, tenant, or globally set in the cloud
IsEnabled(ctx context.Context, flag string) bool
// Check if a flag is configured globally. For now, this is the same
// as the function above, however it will move to only checking flags that
// are configured by the operator and shared across all tenants.
// 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
}
// FeatureFlagStage indicates the quality level

View File

@ -74,7 +74,7 @@ func ProvideManagerService(cfg *setting.Cfg, licensing licensing.Licensing) (*Fe
// Minimum approach to avoid circular dependency
// nolint:staticcheck
cfg.IsFeatureToggleEnabled = mgmt.IsEnabled
cfg.IsFeatureToggleEnabled = mgmt.IsEnabledGlobally
return mgmt, nil
}

View File

@ -43,8 +43,8 @@ func TestFeatureService(t *testing.T) {
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
require.False(t, mgmt.IsEnabledGlobally("a.yes.default"))
require.False(t, mgmt.IsEnabledGlobally("a.yes")) // licensed, but not enabled
}
var (

View File

@ -156,7 +156,7 @@ func (s *Service) Get(ctx context.Context, cmd *folder.GetFolderQuery) (*folder.
return nil, dashboards.ErrFolderAccessDenied
}
if !s.features.IsEnabled(featuremgmt.FlagNestedFolders) {
if !s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) {
return dashFolder, nil
}
@ -328,7 +328,7 @@ func (s *Service) deduplicateAvailableFolders(ctx context.Context, folders []*fo
}
func (s *Service) GetParents(ctx context.Context, q folder.GetParentsQuery) ([]*folder.Folder, error) {
if !s.features.IsEnabled(featuremgmt.FlagNestedFolders) {
if !s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) {
return nil, nil
}
return s.store.GetParents(ctx, q)
@ -360,7 +360,7 @@ func (s *Service) Create(ctx context.Context, cmd *folder.CreateFolderCommand) (
dashFolder := dashboards.NewDashboardFolder(cmd.Title)
dashFolder.OrgID = cmd.OrgID
if s.features.IsEnabled(featuremgmt.FlagNestedFolders) && cmd.ParentUID != "" {
if s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) && cmd.ParentUID != "" {
// Check that the user is allowed to create a subfolder in this folder
evaluator := accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, dashboards.ScopeFoldersProvider.GetResourceScopeUID(cmd.ParentUID))
hasAccess, evalErr := s.accessControl.Evaluate(ctx, cmd.SignedInUser, evaluator)
@ -801,7 +801,7 @@ func (s *Service) GetDescendantCounts(ctx context.Context, cmd *folder.GetDescen
result := []string{*cmd.UID}
countsMap := make(folder.DescendantCounts, len(s.registry)+1)
if s.features.IsEnabled(featuremgmt.FlagNestedFolders) {
if s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) {
subfolders, err := s.getNestedFolders(ctx, cmd.OrgID, *cmd.UID)
if err != nil {
logger.Error("failed to get subfolders", "error", err)

View File

@ -46,7 +46,7 @@ func newConfig(cfg *setting.Cfg, features featuremgmt.FeatureToggles) *config {
host := fmt.Sprintf("%s:%d", ip, port)
return &config{
enabled: features.IsEnabled(featuremgmt.FlagGrafanaAPIServer),
enabled: features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServer),
devMode: cfg.Env == setting.Dev,
dataPath: filepath.Join(cfg.DataPath, "grafana-apiserver"),
ip: ip,

View File

@ -45,7 +45,7 @@ func ProvideService(cfg *setting.Cfg, features featuremgmt.FeatureToggles, authe
s := &gPRCServerService{
cfg: cfg,
logger: log.New("grpc-server"),
enabled: features.IsEnabled(featuremgmt.FlagGrpcServer),
enabled: features.IsEnabledGlobally(featuremgmt.FlagGrpcServer),
}
// Register the metric here instead of an init() function so that we do

View File

@ -21,7 +21,7 @@ func (l *LibraryElementService) registerAPIEndpoints() {
l.RouteRegister.Group("/api/library-elements", func(entities routing.RouteRegister) {
uidScope := ScopeLibraryPanelsProvider.GetResourceScopeUID(ac.Parameter(":uid"))
if l.features.IsEnabled(featuremgmt.FlagLibraryPanelRBAC) {
if l.features.IsEnabledGlobally(featuremgmt.FlagLibraryPanelRBAC) {
entities.Post("/", authorize(ac.EvalPermission(ActionLibraryPanelsCreate)), routing.Wrap(l.createHandler))
entities.Delete("/:uid", authorize(ac.EvalPermission(ActionLibraryPanelsDelete, uidScope)), routing.Wrap(l.deleteHandler))
entities.Get("/", authorize(ac.EvalPermission(ActionLibraryPanelsRead)), routing.Wrap(l.getAllHandler))
@ -176,7 +176,7 @@ func (l *LibraryElementService) getAllHandler(c *contextmodel.ReqContext) respon
return toLibraryElementError(err, "Failed to get library elements")
}
if l.features.IsEnabled(featuremgmt.FlagLibraryPanelRBAC) {
if l.features.IsEnabled(c.Req.Context(), featuremgmt.FlagLibraryPanelRBAC) {
filteredPanels, err := l.filterLibraryPanelsByPermission(c, elementsResult.Elements)
if err != nil {
return toLibraryElementError(err, "Failed to evaluate permissions")
@ -278,7 +278,7 @@ func (l *LibraryElementService) getByNameHandler(c *contextmodel.ReqContext) res
return toLibraryElementError(err, "Failed to get library element")
}
if l.features.IsEnabled(featuremgmt.FlagLibraryPanelRBAC) {
if l.features.IsEnabled(c.Req.Context(), featuremgmt.FlagLibraryPanelRBAC) {
filteredElements, err := l.filterLibraryPanelsByPermission(c, elements)
if err != nil {
return toLibraryElementError(err, err.Error())

View File

@ -166,7 +166,7 @@ func (l *LibraryElementService) createLibraryElement(c context.Context, signedIn
}
err = l.SQLStore.WithTransactionalDbSession(c, func(session *db.Session) error {
if l.features.IsEnabled(featuremgmt.FlagLibraryPanelRBAC) {
if l.features.IsEnabled(c, featuremgmt.FlagLibraryPanelRBAC) {
allowed, err := l.AccessControl.Evaluate(c, signedInUser, ac.EvalPermission(ActionLibraryPanelsCreate, dashboards.ScopeFoldersProvider.GetResourceScopeUID(*cmd.FolderUID)))
if !allowed {
return fmt.Errorf("insufficient permissions for creating library panel in folder with UID %s", *cmd.FolderUID)

View File

@ -12,6 +12,7 @@ import (
func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink, error) {
var configNodes []*navtree.NavLink
ctx := c.Req.Context()
hasAccess := ac.HasAccess(s.accessControl, c)
hasGlobalAccess := ac.HasGlobalAccess(s.accessControl, s.accesscontrolService, c)
orgsAccessEvaluator := ac.EvalPermission(ac.ActionOrgsRead)
@ -55,7 +56,7 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink
})
}
disabled, err := s.apiKeyService.IsDisabled(c.Req.Context(), c.SignedInUser.GetOrgID())
disabled, err := s.apiKeyService.IsDisabled(ctx, c.SignedInUser.GetOrgID())
if err != nil {
return nil, err
}
@ -101,7 +102,7 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink
})
}
if s.features.IsEnabled(featuremgmt.FlagFeatureToggleAdminPage) && hasAccess(ac.EvalPermission(ac.ActionFeatureManagementRead)) {
if s.features.IsEnabled(ctx, featuremgmt.FlagFeatureToggleAdminPage) && hasAccess(ac.EvalPermission(ac.ActionFeatureManagementRead)) {
configNodes = append(configNodes, &navtree.NavLink{
Text: "Feature Toggles",
SubTitle: "View and edit feature toggles",
@ -111,7 +112,7 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink
})
}
if s.features.IsEnabled(featuremgmt.FlagCorrelations) && hasAccess(correlations.ConfigurationPageAccess) {
if s.features.IsEnabled(ctx, featuremgmt.FlagCorrelations) && hasAccess(correlations.ConfigurationPageAccess) {
configNodes = append(configNodes, &navtree.NavLink{
Text: "Correlations",
Icon: "gf-glue",
@ -121,7 +122,7 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink
})
}
if hasAccess(ac.EvalPermission(ac.ActionSettingsRead, ac.ScopeSettingsAll)) && s.features.IsEnabled(featuremgmt.FlagStorage) {
if hasAccess(ac.EvalPermission(ac.ActionSettingsRead, ac.ScopeSettingsAll)) && s.features.IsEnabled(ctx, featuremgmt.FlagStorage) {
storage := &navtree.NavLink{
Text: "Storage",
Id: "storage",

View File

@ -238,7 +238,7 @@ func (s *ServiceImpl) addPluginToSection(c *contextmodel.ReqContext, treeRoot *n
func (s *ServiceImpl) hasAccessToInclude(c *contextmodel.ReqContext, pluginID string) func(include *plugins.Includes) bool {
hasAccess := ac.HasAccess(s.accessControl, c)
return func(include *plugins.Includes) bool {
useRBAC := s.features.IsEnabled(featuremgmt.FlagAccessControlOnCall) && include.RequiresRBACAction()
useRBAC := s.features.IsEnabledGlobally(featuremgmt.FlagAccessControlOnCall) && include.RequiresRBACAction()
if useRBAC && !hasAccess(ac.EvalPermission(include.Action)) {
s.log.Debug("plugin include is covered by RBAC, user doesn't have access",
"plugin", pluginID,
@ -267,7 +267,7 @@ func (s *ServiceImpl) readNavigationSettings() {
"k6-app": {SectionID: navtree.NavIDRoot, SortWeight: navtree.WeightAlertsAndIncidents + 1, Text: "Performance testing", Icon: "k6"},
}
if s.features.IsEnabled(featuremgmt.FlagNavAdminSubsections) && s.features.IsEnabled(featuremgmt.FlagCostManagementUi) {
if s.features.IsEnabledGlobally(featuremgmt.FlagNavAdminSubsections) && s.features.IsEnabledGlobally(featuremgmt.FlagCostManagementUi) {
// if cost management is enabled we want to nest adaptive metrics and log volume explorer under that plugin
// in the admin section
s.navigationAppConfig["grafana-adaptive-metrics-app"] = NavigationAppConfig{SectionID: navtree.NavIDCfg}

View File

@ -358,7 +358,7 @@ func (s *ServiceImpl) buildDashboardNavLinks(c *contextmodel.ReqContext) []*navt
Icon: "library-panel",
})
if s.features.IsEnabled(featuremgmt.FlagPublicDashboards) {
if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagPublicDashboards) {
dashboardChildNavs = append(dashboardChildNavs, &navtree.NavLink{
Text: "Public dashboards",
Id: "dashboards/public",
@ -368,7 +368,7 @@ func (s *ServiceImpl) buildDashboardNavLinks(c *contextmodel.ReqContext) []*navt
}
}
if s.features.IsEnabled(featuremgmt.FlagScenes) {
if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagScenes) {
dashboardChildNavs = append(dashboardChildNavs, &navtree.NavLink{
Text: "Scenes",
Id: "scenes",

View File

@ -184,7 +184,7 @@ func (srv TestingApiSrv) RouteEvalQueries(c *contextmodel.ReqContext, cmd apimod
}
func (srv TestingApiSrv) BacktestAlertRule(c *contextmodel.ReqContext, cmd apimodels.BacktestConfig) response.Response {
if !srv.featureManager.IsEnabled(featuremgmt.FlagAlertingBacktesting) {
if !srv.featureManager.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingBacktesting) {
return ErrResp(http.StatusNotFound, nil, "Backgtesting API is not enabled")
}

View File

@ -41,6 +41,7 @@ func NewTestMigrationStore(t *testing.T, sqlStore *sqlstore.SQLStore, cfg *setti
cfg.UnifiedAlerting.BaseInterval = time.Second * 10
}
features := featuremgmt.WithFeatures()
cfg.IsFeatureToggleEnabled = features.IsEnabledGlobally
alertingStore := store.DBstore{
SQLStore: sqlStore,
Cfg: cfg.UnifiedAlerting,

View File

@ -246,9 +246,9 @@ func (ng *AlertNG) init() error {
Images: ng.ImageService,
Clock: clk,
Historian: history,
DoNotSaveNormalState: ng.FeatureToggles.IsEnabled(featuremgmt.FlagAlertingNoNormalState),
DoNotSaveNormalState: ng.FeatureToggles.IsEnabledGlobally(featuremgmt.FlagAlertingNoNormalState),
MaxStateSaveConcurrency: ng.Cfg.UnifiedAlerting.MaxStateSaveConcurrency,
ApplyNoDataAndErrorToAllStates: ng.FeatureToggles.IsEnabled(featuremgmt.FlagAlertingNoDataErrorExecution),
ApplyNoDataAndErrorToAllStates: ng.FeatureToggles.IsEnabledGlobally(featuremgmt.FlagAlertingNoDataErrorExecution),
Tracer: ng.tracer,
Log: log.New("ngalert.state.manager"),
}
@ -483,7 +483,7 @@ func applyStateHistoryFeatureToggles(cfg *setting.UnifiedAlertingStateHistorySet
// If all toggles are enabled, we listen to the state history config as written.
// If any of them are disabled, we ignore the configured backend and treat the toggles as an override.
// If multiple toggles are disabled, we go with the most "restrictive" one.
if !ft.IsEnabled(featuremgmt.FlagAlertStateHistoryLokiSecondary) {
if !ft.IsEnabledGlobally(featuremgmt.FlagAlertStateHistoryLokiSecondary) {
// If we cannot even treat Loki as a secondary, we must use annotations only.
if backend == historian.BackendTypeMultiple || backend == historian.BackendTypeLoki {
logger.Info("Forcing Annotation backend due to state history feature toggles")
@ -493,7 +493,7 @@ func applyStateHistoryFeatureToggles(cfg *setting.UnifiedAlertingStateHistorySet
}
return
}
if !ft.IsEnabled(featuremgmt.FlagAlertStateHistoryLokiPrimary) {
if !ft.IsEnabledGlobally(featuremgmt.FlagAlertStateHistoryLokiPrimary) {
// If we're using multiple backends, Loki must be the secondary.
if backend == historian.BackendTypeMultiple {
logger.Info("Coercing Loki to a secondary backend due to state history feature toggles")
@ -509,7 +509,7 @@ func applyStateHistoryFeatureToggles(cfg *setting.UnifiedAlertingStateHistorySet
}
return
}
if !ft.IsEnabled(featuremgmt.FlagAlertStateHistoryLokiOnly) {
if !ft.IsEnabledGlobally(featuremgmt.FlagAlertStateHistoryLokiOnly) {
// If we're not allowed to use Loki only, make it the primary but keep the annotation writes.
if backend == historian.BackendTypeLoki {
logger.Info("Forcing dual writes to Loki and Annotations due to state history feature toggles")

View File

@ -30,7 +30,7 @@ func (st DBstore) ListAlertInstances(ctx context.Context, cmd *models.ListAlertI
if cmd.RuleUID != "" {
addToQuery(` AND rule_uid = ?`, cmd.RuleUID)
}
if st.FeatureToggles.IsEnabled(featuremgmt.FlagAlertingNoNormalState) {
if st.FeatureToggles.IsEnabled(ctx, featuremgmt.FlagAlertingNoNormalState) {
s.WriteString(fmt.Sprintf(" AND NOT (current_state = '%s' AND current_reason = '')", models.InstanceStateNormal))
}
if err := sess.SQL(s.String(), params...).Find(&alertInstances); err != nil {

View File

@ -44,8 +44,6 @@ func SetupTestEnv(tb testing.TB, baseInterval time.Duration) (*ngalert.AlertNG,
tb.Helper()
cfg := setting.NewCfg()
// nolint:staticcheck
cfg.IsFeatureToggleEnabled = featuremgmt.WithFeatures().IsEnabled
cfg.UnifiedAlerting = setting.UnifiedAlertingSettings{
BaseInterval: setting.SchedulerBaseInterval,
}

View File

@ -220,7 +220,7 @@ func (d *Dynamic) setDetectorsFromCache(ctx context.Context) error {
// IsDisabled returns true if FlagPluginsDynamicAngularDetectionPatterns is not enabled.
func (d *Dynamic) IsDisabled() bool {
return !d.features.IsEnabled(featuremgmt.FlagPluginsDynamicAngularDetectionPatterns)
return !d.features.IsEnabledGlobally(featuremgmt.FlagPluginsDynamicAngularDetectionPatterns)
}
// randomSkew returns a random time.Duration between 0 and maxSkew.

View File

@ -16,7 +16,7 @@ func ProvideService(cfg *config.Cfg, dynamic *angulardetectorsprovider.Dynamic)
var detectorsProvider angulardetector.DetectorsProvider
var err error
static := angularinspector.NewDefaultStaticDetectorsProvider()
if cfg.Features != nil && cfg.Features.IsEnabled(featuremgmt.FlagPluginsDynamicAngularDetectionPatterns) {
if cfg.Features != nil && cfg.Features.IsEnabledGlobally(featuremgmt.FlagPluginsDynamicAngularDetectionPatterns) {
detectorsProvider = angulardetector.SequenceDetectorsProvider{dynamic, static}
} else {
detectorsProvider = static

View File

@ -7,12 +7,13 @@ import (
"github.com/grafana/grafana-aws-sdk/pkg/awsds"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/prometheus/client_golang/prometheus"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/caching"
"github.com/grafana/grafana/pkg/services/contexthandler"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/prometheus/client_golang/prometheus"
)
// needed to mock the function for testing
@ -93,7 +94,7 @@ func (m *CachingMiddleware) QueryData(ctx context.Context, req *backend.QueryDat
// Update the query cache with the result for this metrics request
if err == nil && cr.UpdateCacheFn != nil {
// If AWS async caching is not enabled, use the old code path
if m.features == nil || !m.features.IsEnabled(featuremgmt.FlagAwsAsyncQueryCaching) {
if m.features == nil || !m.features.IsEnabled(ctx, featuremgmt.FlagAwsAsyncQueryCaching) {
cr.UpdateCacheFn(ctx, resp)
} else {
// time how long shouldCacheQuery takes

View File

@ -50,7 +50,7 @@ func (m *LoggerMiddleware) logRequest(ctx context.Context, fn func(ctx context.C
if err != nil {
logParams = append(logParams, "error", err)
}
if m.features.IsEnabled(featuremgmt.FlagPluginsInstrumentationStatusSource) {
if m.features.IsEnabled(ctx, featuremgmt.FlagPluginsInstrumentationStatusSource) {
logParams = append(logParams, "statusSource", pluginrequestmeta.StatusSourceFromContext(ctx))
}
@ -82,7 +82,7 @@ func (m *LoggerMiddleware) QueryData(ctx context.Context, req *backend.QueryData
for refID, dr := range resp.Responses {
if dr.Error != nil {
logParams := []any{"refID", refID, "status", int(dr.Status), "error", dr.Error}
if m.features.IsEnabled(featuremgmt.FlagPluginsInstrumentationStatusSource) {
if m.features.IsEnabled(ctx, featuremgmt.FlagPluginsInstrumentationStatusSource) {
logParams = append(logParams, "statusSource", pluginrequestmeta.StatusSourceFromPluginErrorSource(dr.ErrorSource))
}
ctxLogger.Error("Partial data response error", logParams...)

View File

@ -33,7 +33,7 @@ type MetricsMiddleware struct {
func newMetricsMiddleware(promRegisterer prometheus.Registerer, pluginRegistry registry.Service, features featuremgmt.FeatureToggles) *MetricsMiddleware {
var additionalLabels []string
if features.IsEnabled(featuremgmt.FlagPluginsInstrumentationStatusSource) {
if features.IsEnabledGlobally(featuremgmt.FlagPluginsInstrumentationStatusSource) {
additionalLabels = []string{"status_source"}
}
pluginRequestCounter := prometheus.NewCounterVec(prometheus.CounterOpts{
@ -122,7 +122,7 @@ func (m *MetricsMiddleware) instrumentPluginRequest(ctx context.Context, pluginC
pluginRequestDurationLabels := []string{pluginCtx.PluginID, endpoint, target}
pluginRequestCounterLabels := []string{pluginCtx.PluginID, endpoint, status.String(), target}
pluginRequestDurationSecondsLabels := []string{"grafana-backend", pluginCtx.PluginID, endpoint, status.String(), target}
if m.features.IsEnabled(featuremgmt.FlagPluginsInstrumentationStatusSource) {
if m.features.IsEnabled(ctx, featuremgmt.FlagPluginsInstrumentationStatusSource) {
statusSource := pluginrequestmeta.StatusSourceFromContext(ctx)
pluginRequestDurationLabels = append(pluginRequestDurationLabels, string(statusSource))
pluginRequestCounterLabels = append(pluginRequestCounterLabels, string(statusSource))

View File

@ -175,7 +175,7 @@ func NewAsExternalStep(cfg *config.Cfg) *AsExternal {
// Filter will filter out any plugins that are marked to be disabled.
func (c *AsExternal) Filter(cl plugins.Class, bundles []*plugins.FoundBundle) ([]*plugins.FoundBundle, error) {
if c.cfg.Features == nil || !c.cfg.Features.IsEnabled(featuremgmt.FlagExternalCorePlugins) {
if c.cfg.Features == nil || !c.cfg.Features.IsEnabledGlobally(featuremgmt.FlagExternalCorePlugins) {
return bundles, nil
}

View File

@ -75,7 +75,7 @@ func TestIntegrationPluginManager(t *testing.T) {
Azure: &azsettings.AzureSettings{},
// nolint:staticcheck
IsFeatureToggleEnabled: features.IsEnabled,
IsFeatureToggleEnabled: features.IsEnabledGlobally,
}
tracer := tracing.InitializeTracerForTest()

View File

@ -152,7 +152,7 @@ func NewClientDecorator(
func CreateMiddlewares(cfg *setting.Cfg, oAuthTokenService oauthtoken.OAuthTokenService, tracer tracing.Tracer, cachingService caching.CachingService, features *featuremgmt.FeatureManager, promRegisterer prometheus.Registerer, registry registry.Service) []plugins.ClientMiddleware {
var middlewares []plugins.ClientMiddleware
if features.IsEnabled(featuremgmt.FlagPluginsInstrumentationStatusSource) {
if features.IsEnabledGlobally(featuremgmt.FlagPluginsInstrumentationStatusSource) {
middlewares = []plugins.ClientMiddleware{
clientmiddleware.NewPluginRequestMetaMiddleware(),
}
@ -172,11 +172,11 @@ func CreateMiddlewares(cfg *setting.Cfg, oAuthTokenService oauthtoken.OAuthToken
)
// Placing the new service implementation behind a feature flag until it is known to be stable
if features.IsEnabled(featuremgmt.FlagUseCachingService) {
if features.IsEnabledGlobally(featuremgmt.FlagUseCachingService) {
middlewares = append(middlewares, clientmiddleware.NewCachingMiddlewareWithFeatureManager(cachingService, features))
}
if features.IsEnabled(featuremgmt.FlagIdForwarding) {
if features.IsEnabledGlobally(featuremgmt.FlagIdForwarding) {
middlewares = append(middlewares, clientmiddleware.NewForwardIDMiddleware())
}
@ -186,7 +186,7 @@ func CreateMiddlewares(cfg *setting.Cfg, oAuthTokenService oauthtoken.OAuthToken
middlewares = append(middlewares, clientmiddleware.NewHTTPClientMiddleware())
if features.IsEnabled(featuremgmt.FlagPluginsInstrumentationStatusSource) {
if features.IsEnabledGlobally(featuremgmt.FlagPluginsInstrumentationStatusSource) {
// StatusSourceMiddleware should be at the very bottom, or any middlewares below it won't see the
// correct status source in their context.Context
middlewares = append(middlewares, clientmiddleware.NewStatusSourceMiddleware())

View File

@ -23,7 +23,7 @@ type Service struct {
func ProvideService(cfg *config.Cfg, reg extsvcauth.ExternalServiceRegistry, settingsSvc pluginsettings.Service) *Service {
s := &Service{
featureEnabled: cfg.Features.IsEnabled(featuremgmt.FlagExternalServiceAuth) || cfg.Features.IsEnabled(featuremgmt.FlagExternalServiceAccounts),
featureEnabled: cfg.Features.IsEnabledGlobally(featuremgmt.FlagExternalServiceAuth) || cfg.Features.IsEnabledGlobally(featuremgmt.FlagExternalServiceAccounts),
log: log.New("plugins.external.registration"),
reg: reg,
settingsSvc: settingsSvc,

View File

@ -1,9 +1,11 @@
package api
import (
"context"
"net/http"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/infra/log"
@ -41,7 +43,7 @@ func ProvideApi(
}
// attach api if PublicDashboards feature flag is enabled
if features.IsEnabled(featuremgmt.FlagPublicDashboards) {
if features.IsEnabledGlobally(featuremgmt.FlagPublicDashboards) {
api.RegisterAPIEndpoints()
}
@ -284,9 +286,9 @@ func (api *Api) DeletePublicDashboard(c *contextmodel.ReqContext) response.Respo
}
// Copied from pkg/api/metrics.go
func toJsonStreamingResponse(features *featuremgmt.FeatureManager, qdr *backend.QueryDataResponse) response.Response {
func toJsonStreamingResponse(ctx context.Context, features *featuremgmt.FeatureManager, qdr *backend.QueryDataResponse) response.Response {
statusWhenError := http.StatusBadRequest
if features.IsEnabled(featuremgmt.FlagDatasourceQueryMultiStatus) {
if features.IsEnabled(ctx, featuremgmt.FlagDatasourceQueryMultiStatus) {
statusWhenError = http.StatusMultiStatus
}

View File

@ -5,6 +5,7 @@ import (
"strconv"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
@ -71,7 +72,7 @@ func (api *Api) QueryPublicDashboard(c *contextmodel.ReqContext) response.Respon
return response.Err(err)
}
return toJsonStreamingResponse(api.Features, resp)
return toJsonStreamingResponse(c.Req.Context(), api.Features, resp)
}
// swagger:route GET /public/dashboards/{accessToken}/annotations dashboard_public getPublicAnnotations

View File

@ -37,7 +37,7 @@ func (rs *RenderingService) GetRenderUser(ctx context.Context, key string) (*Ren
var renderUser *RenderUser
if looksLikeJWT(key) && rs.features.IsEnabled(featuremgmt.FlagRenderAuthJWT) {
if looksLikeJWT(key) && rs.features.IsEnabled(ctx, featuremgmt.FlagRenderAuthJWT) {
from = "jwt"
renderUser = rs.getRenderUserFromJWT(key)
} else {

View File

@ -84,7 +84,7 @@ func ProvideService(cfg *setting.Cfg, features *featuremgmt.FeatureManager, remo
}
var renderKeyProvider renderKeyProvider
if features.IsEnabled(featuremgmt.FlagRenderAuthJWT) {
if features.IsEnabledGlobally(featuremgmt.FlagRenderAuthJWT) {
renderKeyProvider = &jwtRenderKeyProvider{
log: logger,
authToken: []byte(cfg.RendererAuthToken),

View File

@ -116,7 +116,7 @@ func ProvideService(cfg *setting.Cfg, sql db.DB, entityEventStore store.EntityEv
}
func (s *StandardSearchService) IsDisabled() bool {
return !s.features.IsEnabled(featuremgmt.FlagPanelTitleSearch)
return !s.features.IsEnabledGlobally(featuremgmt.FlagPanelTitleSearch)
}
func (s *StandardSearchService) Run(ctx context.Context) error {

View File

@ -44,7 +44,7 @@ func (s *DataSourceSecretMigrationService) Migrate(ctx context.Context) error {
}
logger.Debug(fmt.Sprint("secret migration status is ", migrationStatus))
// If this flag is true, delete secrets from the legacy secrets store as they are migrated
disableSecretsCompatibility := s.features.IsEnabled(featuremgmt.FlagDisableSecretsCompatibility)
disableSecretsCompatibility := s.features.IsEnabled(ctx, featuremgmt.FlagDisableSecretsCompatibility)
// If migration hasn't happened, migrate to unified secrets and keep copy in legacy
// If a complete migration happened and now backwards compatibility is enabled, copy secrets back to legacy
needCompatibility := migrationStatus != compatibleSecretMigrationValue && !disableSecretsCompatibility

View File

@ -48,7 +48,7 @@ func NewPluginSecretsKVStore(
secretsService: secretsService,
log: logger,
kvstore: kvstore,
backwardsCompatibilityDisabled: features.IsEnabled(featuremgmt.FlagDisableSecretsCompatibility),
backwardsCompatibilityDisabled: features.IsEnabledGlobally(featuremgmt.FlagDisableSecretsCompatibility),
fallbackStore: fallback,
}
}

View File

@ -147,7 +147,11 @@ func NewFakeFeatureToggles(t *testing.T, returnValue bool) featuremgmt.FeatureTo
}
}
func (f fakeFeatureToggles) IsEnabled(feature string) bool {
func (f fakeFeatureToggles) IsEnabledGlobally(feature string) bool {
return f.returnValue
}
func (f fakeFeatureToggles) IsEnabled(ctx context.Context, feature string) bool {
return f.returnValue
}

View File

@ -79,7 +79,7 @@ func ProvideSecretsService(
log: log.New("secrets"),
}
enabled := !features.IsEnabled(featuremgmt.FlagDisableEnvelopeEncryption)
enabled := !features.IsEnabledGlobally(featuremgmt.FlagDisableEnvelopeEncryption)
if enabled {
err := s.InitProviders()
@ -112,12 +112,12 @@ func (s *SecretsService) InitProviders() (err error) {
}
func (s *SecretsService) registerUsageMetrics() {
s.usageStats.RegisterMetricsFunc(func(context.Context) (map[string]any, error) {
s.usageStats.RegisterMetricsFunc(func(ctx context.Context) (map[string]any, error) {
usageMetrics := make(map[string]any)
// Enabled / disabled
usageMetrics["stats.encryption.envelope_encryption_enabled.count"] = 0
if !s.features.IsEnabled(featuremgmt.FlagDisableEnvelopeEncryption) {
if !s.features.IsEnabled(ctx, featuremgmt.FlagDisableEnvelopeEncryption) {
usageMetrics["stats.encryption.envelope_encryption_enabled.count"] = 1
}
@ -159,7 +159,7 @@ var b64 = base64.RawStdEncoding
func (s *SecretsService) Encrypt(ctx context.Context, payload []byte, opt secrets.EncryptionOptions) ([]byte, error) {
// Use legacy encryption service if featuremgmt.FlagDisableEnvelopeEncryption toggle is on
if s.features.IsEnabled(featuremgmt.FlagDisableEnvelopeEncryption) {
if s.features.IsEnabled(ctx, featuremgmt.FlagDisableEnvelopeEncryption) {
return s.enc.Encrypt(ctx, payload, setting.SecretKey)
}
@ -333,7 +333,7 @@ func (s *SecretsService) Decrypt(ctx context.Context, payload []byte) ([]byte, e
// If encrypted with envelope encryption, the feature is disabled and
// no provider is initialized, then we throw an error.
if s.encryptedWithEnvelopeEncryption(payload) &&
s.features.IsEnabled(featuremgmt.FlagDisableEnvelopeEncryption) &&
s.features.IsEnabled(ctx, featuremgmt.FlagDisableEnvelopeEncryption) &&
!s.providersInitialized() {
err = fmt.Errorf("failed to decrypt a secret encrypted with envelope encryption: envelope encryption is disabled")
return nil, err
@ -469,7 +469,7 @@ func (s *SecretsService) RotateDataKeys(ctx context.Context) error {
func (s *SecretsService) ReEncryptDataKeys(ctx context.Context) error {
s.log.Info("Data keys re-encryption triggered")
if s.features.IsEnabled(featuremgmt.FlagDisableEnvelopeEncryption) {
if s.features.IsEnabled(ctx, featuremgmt.FlagDisableEnvelopeEncryption) {
s.log.Info("Envelope encryption is not enabled but trying to init providers anyway...")
if err := s.InitProviders(); err != nil {

View File

@ -114,7 +114,7 @@ func (m *SecretsMigrator) RollBackSecrets(ctx context.Context) (bool, error) {
}
func (m *SecretsMigrator) initProvidersIfNeeded() error {
if m.features.IsEnabled(featuremgmt.FlagDisableEnvelopeEncryption) {
if m.features.IsEnabledGlobally(featuremgmt.FlagDisableEnvelopeEncryption) {
logger.Info("Envelope encryption is not enabled but trying to init providers anyway...")
if err := m.secretsSrv.InitProviders(); err != nil {

View File

@ -48,7 +48,7 @@ func NewServiceAccountsAPI(
RouterRegister: routerRegister,
log: log.New("serviceaccounts.api"),
permissionService: permissionService,
isExternalSAEnabled: features.IsEnabled(featuremgmt.FlagExternalServiceAccounts) || features.IsEnabled(featuremgmt.FlagExternalServiceAuth),
isExternalSAEnabled: features.IsEnabledGlobally(featuremgmt.FlagExternalServiceAccounts) || features.IsEnabledGlobally(featuremgmt.FlagExternalServiceAuth),
}
}

View File

@ -41,7 +41,7 @@ func ProvideExtSvcAccountsService(acSvc ac.Service, bus bus.Bus, db db.DB, featu
skvStore: kvstore.NewSQLSecretsKVStore(db, secretsSvc, logger), // Using SQL store to avoid a cyclic dependency
}
if features.IsEnabled(featuremgmt.FlagExternalServiceAccounts) || features.IsEnabled(featuremgmt.FlagExternalServiceAuth) {
if features.IsEnabledGlobally(featuremgmt.FlagExternalServiceAccounts) || features.IsEnabledGlobally(featuremgmt.FlagExternalServiceAuth) {
// Register the metrics
esa.metrics = newMetrics(reg, saSvc, logger)
@ -95,7 +95,7 @@ func (esa *ExtSvcAccountsService) RetrieveExtSvcAccount(ctx context.Context, org
// SaveExternalService creates, updates or delete a service account (and its token) with the requested permissions.
func (esa *ExtSvcAccountsService) SaveExternalService(ctx context.Context, cmd *extsvcauth.ExternalServiceRegistration) (*extsvcauth.ExternalService, error) {
// This is double proofing, we should never reach here anyway the flags have already been checked.
if !esa.features.IsEnabled(featuremgmt.FlagExternalServiceAccounts) && !esa.features.IsEnabled(featuremgmt.FlagExternalServiceAuth) {
if !esa.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts) && !esa.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAuth) {
esa.logger.Warn("This feature is behind a feature flag, please set it if you want to save external services")
return nil, nil
}
@ -140,7 +140,7 @@ func (esa *ExtSvcAccountsService) SaveExternalService(ctx context.Context, cmd *
func (esa *ExtSvcAccountsService) RemoveExternalService(ctx context.Context, name string) error {
// This is double proofing, we should never reach here anyway the flags have already been checked.
if !esa.features.IsEnabled(featuremgmt.FlagExternalServiceAccounts) && !esa.features.IsEnabled(featuremgmt.FlagExternalServiceAuth) {
if !esa.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts) && !esa.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAuth) {
esa.logger.Warn("This feature is behind a feature flag, please set it if you want to save external services")
return nil
}
@ -172,7 +172,7 @@ func (esa *ExtSvcAccountsService) RemoveExtSvcAccount(ctx context.Context, orgID
// ManageExtSvcAccount creates, updates or deletes the service account associated with an external service
func (esa *ExtSvcAccountsService) ManageExtSvcAccount(ctx context.Context, cmd *sa.ManageExtSvcAccountCmd) (int64, error) {
// This is double proofing, we should never reach here anyway the flags have already been checked.
if !esa.features.IsEnabled(featuremgmt.FlagExternalServiceAccounts) && !esa.features.IsEnabled(featuremgmt.FlagExternalServiceAuth) {
if !esa.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts) && !esa.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAuth) {
esa.logger.Warn("This feature is behind a feature flag, please set it if you want to save external services")
return 0, nil
}

View File

@ -38,7 +38,7 @@ func ProvideServiceAccountsProxy(
s := &ServiceAccountsProxy{
log: log.New("serviceaccounts.proxy"),
proxiedService: proxiedService,
isProxyEnabled: features.IsEnabled(featuremgmt.FlagExternalServiceAccounts) || features.IsEnabled(featuremgmt.FlagExternalServiceAuth),
isProxyEnabled: features.IsEnabledGlobally(featuremgmt.FlagExternalServiceAccounts) || features.IsEnabledGlobally(featuremgmt.FlagExternalServiceAuth),
}
serviceaccountsAPI := api.NewServiceAccountsAPI(cfg, s, ac, accesscontrolService, routeRegister, permissionService, features)

View File

@ -184,7 +184,7 @@ func TestMigrations(t *testing.T) {
{
desc: "without editors can admin",
// nolint:staticcheck
config: setting.NewCfgWithFeatures(featuremgmt.WithFeatures("accesscontrol").IsEnabled),
config: setting.NewCfgWithFeatures(featuremgmt.WithFeatures("accesscontrol").IsEnabledGlobally),
expectedRolePerms: map[string][]rawPermission{
"managed:users:1:permissions": {{Action: "teams:read", Scope: team1Scope}},
"managed:users:2:permissions": {{Action: "teams:read", Scope: team1Scope}},

View File

@ -84,7 +84,7 @@ func NewAccessControlDashboardPermissionFilter(user identity.Requester, permissi
}
var f PermissionsFilter
if features.IsEnabled(featuremgmt.FlagPermissionsFilterRemoveSubquery) {
if features.IsEnabledGlobally(featuremgmt.FlagPermissionsFilterRemoveSubquery) {
f = &accessControlDashboardPermissionFilterNoFolderSubquery{
accessControlDashboardPermissionFilter: accessControlDashboardPermissionFilter{
user: user, folderActions: folderActions, dashboardActions: dashboardActions, features: features,
@ -201,7 +201,7 @@ func (f *accessControlDashboardPermissionFilter) buildClauses() {
}
permSelector.WriteRune(')')
switch f.features.IsEnabled(featuremgmt.FlagNestedFolders) {
switch f.features.IsEnabledGlobally(featuremgmt.FlagNestedFolders) {
case true:
if len(permSelectorArgs) > 0 {
switch f.recursiveQueriesAreSupported {
@ -280,7 +280,7 @@ func (f *accessControlDashboardPermissionFilter) buildClauses() {
permSelector.WriteRune(')')
switch f.features.IsEnabled(featuremgmt.FlagNestedFolders) {
switch f.features.IsEnabledGlobally(featuremgmt.FlagNestedFolders) {
case true:
if len(permSelectorArgs) > 0 {
switch f.recursiveQueriesAreSupported {

View File

@ -116,7 +116,7 @@ func (f *accessControlDashboardPermissionFilterNoFolderSubquery) buildClauses()
permSelector.WriteRune(')')
switch f.features.IsEnabled(featuremgmt.FlagNestedFolders) {
switch f.features.IsEnabledGlobally(featuremgmt.FlagNestedFolders) {
case true:
if len(permSelectorArgs) > 0 {
switch f.recursiveQueriesAreSupported {
@ -193,7 +193,7 @@ func (f *accessControlDashboardPermissionFilterNoFolderSubquery) buildClauses()
}
permSelector.WriteRune(')')
switch f.features.IsEnabled(featuremgmt.FlagNestedFolders) {
switch f.features.IsEnabledGlobally(featuremgmt.FlagNestedFolders) {
case true:
if len(permSelectorArgs) > 0 {
switch f.recursiveQueriesAreSupported {

View File

@ -37,7 +37,7 @@ func (b *Builder) ToSQL(limit, page int64) (string, []any) {
INNER JOIN dashboard ON ids.id = dashboard.id`)
b.sql.WriteString("\n")
if b.Features.IsEnabled(featuremgmt.FlagNestedFolders) {
if b.Features.IsEnabledGlobally(featuremgmt.FlagNestedFolders) {
b.sql.WriteString(
`LEFT OUTER JOIN folder ON folder.uid = dashboard.folder_uid AND folder.org_id = dashboard.org_id`)
} else {
@ -67,7 +67,7 @@ func (b *Builder) buildSelect() {
dashboard.folder_id,
folder.uid AS folder_uid,
`)
if b.Features.IsEnabled(featuremgmt.FlagNestedFolders) {
if b.Features.IsEnabledGlobally(featuremgmt.FlagNestedFolders) {
b.sql.WriteString(`
folder.title AS folder_slug,`)
} else {

View File

@ -177,7 +177,7 @@ func makeSQLStoreTestConfig(t *testing.T, tc sqlStoreTest) *setting.Cfg {
tc.features = featuremgmt.WithFeatures()
}
// nolint:staticcheck
cfg := setting.NewCfgWithFeatures(tc.features.IsEnabled)
cfg := setting.NewCfgWithFeatures(tc.features.IsEnabledGlobally)
sec, err := cfg.Raw.NewSection("database")
require.NoError(t, err)

View File

@ -45,7 +45,7 @@ func ProvideService(cfg *setting.Cfg, sqlStore db.DB, ac ac.AccessControl,
fbStrategies: strategies,
}
if features.IsEnabled(featuremgmt.FlagSsoSettingsApi) {
if features.IsEnabledGlobally(featuremgmt.FlagSsoSettingsApi) {
ssoSettingsApi := api.ProvideApi(svc, routeRegister, ac)
ssoSettingsApi.RegisterAPIEndpoints()
}

View File

@ -14,7 +14,7 @@ import (
func MigrateEntityStore(xdb db.DB, features featuremgmt.FeatureToggles) error {
// Skip if feature flag is not enabled
if !features.IsEnabled(featuremgmt.FlagEntityStore) {
if !features.IsEnabledGlobally(featuremgmt.FlagEntityStore) {
return nil
}
@ -67,6 +67,6 @@ func MigrateEntityStore(xdb db.DB, features featuremgmt.FeatureToggles) error {
}
return mg.Start(
features.IsEnabled(featuremgmt.FlagMigrationLocking),
features.IsEnabledGlobally(featuremgmt.FlagMigrationLocking),
sql.GetMigrationLockAttemptTimeout())
}

View File

@ -70,7 +70,7 @@ type EntityEventsService interface {
}
func ProvideEntityEventsService(cfg *setting.Cfg, sqlStore db.DB, features featuremgmt.FeatureToggles) EntityEventsService {
if !features.IsEnabled(featuremgmt.FlagPanelTitleSearch) {
if !features.IsEnabledGlobally(featuremgmt.FlagPanelTitleSearch) {
return &dummyEntityEventsService{}
}

View File

@ -212,7 +212,7 @@ func (e *cloudWatchExecutor) executeStartQuery(ctx context.Context, logsClient c
QueryString: aws.String(modifiedQueryString),
}
if logsQuery.LogGroups != nil && len(logsQuery.LogGroups) > 0 && e.features.IsEnabled(featuremgmt.FlagCloudWatchCrossAccountQuerying) {
if logsQuery.LogGroups != nil && len(logsQuery.LogGroups) > 0 && e.features.IsEnabled(ctx, featuremgmt.FlagCloudWatchCrossAccountQuerying) {
var logGroupIdentifiers []string
for _, lg := range logsQuery.LogGroups {
arn := lg.Arn

View File

@ -7,8 +7,9 @@ import (
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
"github.com/stretchr/testify/mock"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
)
type LogsAPI struct {
@ -43,16 +44,6 @@ func (l *LogsService) GetLogGroupFieldsWithContext(ctx context.Context, request
return args.Get(0).([]resources.ResourceResponse[resources.LogGroupField]), args.Error(1)
}
type MockFeatures struct {
mock.Mock
}
func (f *MockFeatures) IsEnabled(feature string) bool {
args := f.Called(feature)
return args.Bool(0)
}
type MockLogEvents struct {
cloudwatchlogsiface.CloudWatchLogsAPI

View File

@ -10,6 +10,7 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/routes"
)
@ -28,7 +29,7 @@ func (e *cloudWatchExecutor) newResourceMux() *http.ServeMux {
mux.HandleFunc("/external-id", routes.ResourceRequestMiddleware(routes.ExternalIdHandler, logger, e.getRequestContext))
// feature is enabled by default, just putting behind a feature flag in case of unexpected bugs
if e.features.IsEnabled("cloudwatchNewRegionsHandler") {
if e.features.IsEnabledGlobally(featuremgmt.FlagCloudwatchNewRegionsHandler) {
mux.HandleFunc("/regions", routes.ResourceRequestMiddleware(routes.RegionsHandler, logger, e.getRequestContext))
} else {
mux.HandleFunc("/regions", handleResourceReq(e.handleGetRegions))

View File

@ -8,17 +8,19 @@ import (
"testing"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/mocks"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
func TestLogGroupFieldsRoute(t *testing.T) {
mockFeatures := mocks.MockFeatures{}
mockFeatures := featuremgmt.WithFeatures()
reqCtxFunc := func(_ context.Context, pluginCtx backend.PluginContext, region string) (reqCtx models.RequestContext, err error) {
return models.RequestContext{Features: &mockFeatures}, err
return models.RequestContext{Features: mockFeatures}, err
}
t.Run("returns 400 if an invalid LogGroupFieldsRequest is used", func(t *testing.T) {
rr := httptest.NewRecorder()

View File

@ -7,6 +7,7 @@ import (
"net/url"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
@ -46,5 +47,5 @@ var newLogGroupsService = func(ctx context.Context, pluginCtx backend.PluginCont
return nil, err
}
return services.NewLogGroupsService(reqCtx.LogsAPIProvider, reqCtx.Features.IsEnabled(featuremgmt.FlagCloudWatchCrossAccountQuerying)), nil
return services.NewLogGroupsService(reqCtx.LogsAPIProvider, reqCtx.Features.IsEnabled(ctx, featuremgmt.FlagCloudWatchCrossAccountQuerying)), nil
}

View File

@ -8,13 +8,14 @@ import (
"testing"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/mocks"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/utils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
func TestLogGroupsRoute(t *testing.T) {
@ -23,10 +24,9 @@ func TestLogGroupsRoute(t *testing.T) {
newLogGroupsService = origLogGroupsService
})
mockFeatures := mocks.MockFeatures{}
mockFeatures.On("IsEnabled", featuremgmt.FlagCloudWatchCrossAccountQuerying).Return(false)
mockFeatures := featuremgmt.WithFeatures()
reqCtxFunc := func(_ context.Context, pluginCtx backend.PluginContext, region string) (reqCtx models.RequestContext, err error) {
return models.RequestContext{Features: &mockFeatures}, err
return models.RequestContext{Features: mockFeatures}, err
}
t.Run("successfully returns 1 log group with account id", func(t *testing.T) {

View File

@ -37,7 +37,7 @@ func (e *cloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, logger
}
requestQueries, err := models.ParseMetricDataQueries(req.Queries, startTime, endTime, instance.Settings.Region, logger,
e.features.IsEnabled(featuremgmt.FlagCloudWatchCrossAccountQuerying))
e.features.IsEnabled(ctx, featuremgmt.FlagCloudWatchCrossAccountQuerying))
if err != nil {
return nil, err
}
@ -60,7 +60,7 @@ func (e *cloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, logger
region := r
batches := [][]*models.CloudWatchQuery{regionQueries}
if e.features.IsEnabled(featuremgmt.FlagCloudWatchBatchQueries) {
if e.features.IsEnabled(ctx, featuremgmt.FlagCloudWatchBatchQueries) {
batches = getMetricQueryBatches(regionQueries, logger)
}
@ -95,7 +95,7 @@ func (e *cloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, logger
return err
}
if e.features.IsEnabled(featuremgmt.FlagCloudWatchWildCardDimensionValues) {
if e.features.IsEnabled(ctx, featuremgmt.FlagCloudWatchWildCardDimensionValues) {
requestQueries, err = e.getDimensionValuesForWildcards(ctx, req.PluginContext, region, client, requestQueries, instance.tagValueCache, logger)
if err != nil {
return err

View File

@ -173,11 +173,11 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest)
}
responseOpts := ResponseOpts{
metricDataplane: s.features.IsEnabled(featuremgmt.FlagLokiMetricDataplane),
logsDataplane: s.features.IsEnabled(featuremgmt.FlagLokiLogsDataplane),
metricDataplane: s.features.IsEnabled(ctx, featuremgmt.FlagLokiMetricDataplane),
logsDataplane: s.features.IsEnabled(ctx, featuremgmt.FlagLokiLogsDataplane),
}
return queryData(ctx, req, dsInfo, responseOpts, s.tracer, logger, s.features.IsEnabled(featuremgmt.FlagLokiRunQueriesInParallel))
return queryData(ctx, req, dsInfo, responseOpts, s.tracer, logger, s.features.IsEnabled(ctx, featuremgmt.FlagLokiRunQueriesInParallel))
}
func queryData(ctx context.Context, req *backend.QueryDataRequest, dsInfo *datasourceInfo, responseOpts ResponseOpts, tracer tracing.Tracer, plog log.Logger, runInParallel bool) (*backend.QueryDataResponse, error) {

View File

@ -14,6 +14,7 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
"github.com/grafana/grafana-plugin-sdk-go/backend/tracing"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/tsdb/intervalv2"
"github.com/grafana/grafana/pkg/tsdb/prometheus/client"
@ -73,7 +74,7 @@ func New(
// standard deviation sampler is the default for backwards compatibility
exemplarSampler := exemplar.NewStandardDeviationSampler
if features.IsEnabled(featuremgmt.FlagDisablePrometheusExemplarSampling) {
if features.IsEnabledGlobally(featuremgmt.FlagDisablePrometheusExemplarSampling) {
exemplarSampler = exemplar.NewNoOpSampler
}
@ -85,7 +86,7 @@ func New(
TimeInterval: timeInterval,
ID: settings.ID,
URL: settings.URL,
enableDataplane: features.IsEnabled(featuremgmt.FlagPrometheusDataplane),
enableDataplane: features.IsEnabledGlobally(featuremgmt.FlagPrometheusDataplane),
exemplarSampler: exemplarSampler,
}, nil
}

View File

@ -15,6 +15,7 @@ import (
p "github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/tsdb/prometheus/kinds/dataquery"
"github.com/grafana/kindsys"
@ -23,6 +24,7 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb/prometheus/client"
"github.com/grafana/grafana/pkg/tsdb/prometheus/models"
@ -440,8 +442,7 @@ func setup() (*testContext, error) {
JSONData: json.RawMessage(`{"timeInterval": "15s"}`),
}
features := &fakeFeatureToggles{flags: map[string]bool{"prometheusBufferedClient": false}}
features := featuremgmt.WithFeatures()
opts, err := client.CreateTransportOptions(context.Background(), settings, &setting.Cfg{}, log.New())
if err != nil {
return nil, err
@ -460,14 +461,6 @@ func setup() (*testContext, error) {
}, nil
}
type fakeFeatureToggles struct {
flags map[string]bool
}
func (f *fakeFeatureToggles) IsEnabled(feature string) bool {
return f.flags[feature]
}
type fakeHttpClientProvider struct {
httpclient.Provider
opts httpclient.Options