FeatureFlags: define features outside settings.Cfg (take 3) (#44443)

This commit is contained in:
Ryan McKinley
2022-01-26 09:44:20 -08:00
committed by GitHub
parent 84a5910e56
commit 5d66194ec5
64 changed files with 1193 additions and 248 deletions
@@ -9,13 +9,13 @@ import (
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/prometheus/client_golang/prometheus"
)
func ProvideService(cfg *setting.Cfg, usageStats usagestats.Service) *OSSAccessControlService {
func ProvideService(features featuremgmt.FeatureToggles, usageStats usagestats.Service) *OSSAccessControlService {
s := &OSSAccessControlService{
Cfg: cfg,
features: features,
UsageStats: usageStats,
Log: log.New("accesscontrol"),
ScopeResolver: accesscontrol.NewScopeResolver(),
@@ -26,7 +26,7 @@ func ProvideService(cfg *setting.Cfg, usageStats usagestats.Service) *OSSAccessC
// OSSAccessControlService is the service implementing role based access control.
type OSSAccessControlService struct {
Cfg *setting.Cfg
features featuremgmt.FeatureToggles
UsageStats usagestats.Service
Log log.Logger
registrations accesscontrol.RegistrationList
@@ -34,10 +34,10 @@ type OSSAccessControlService struct {
}
func (ac *OSSAccessControlService) IsDisabled() bool {
if ac.Cfg == nil {
if ac.features == nil {
return true
}
return !ac.Cfg.FeatureToggles["accesscontrol"]
return !ac.features.IsEnabled(featuremgmt.FlagAccesscontrol)
}
func (ac *OSSAccessControlService) registerUsageMetrics() {
@@ -12,17 +12,14 @@ import (
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/services/featuremgmt"
)
func setupTestEnv(t testing.TB) *OSSAccessControlService {
t.Helper()
cfg := setting.NewCfg()
cfg.FeatureToggles = map[string]bool{"accesscontrol": true}
ac := &OSSAccessControlService{
Cfg: cfg,
features: featuremgmt.WithFeatures(featuremgmt.FlagAccesscontrol),
UsageStats: &usagestats.UsageStatsMock{T: t},
Log: log.New("accesscontrol"),
registrations: accesscontrol.RegistrationList{},
@@ -148,12 +145,9 @@ func TestUsageMetrics(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := setting.NewCfg()
if tt.enabled {
cfg.FeatureToggles = map[string]bool{"accesscontrol": true}
}
features := featuremgmt.WithFeatures("accesscontrol", tt.enabled)
s := ProvideService(cfg, &usagestats.UsageStatsMock{T: t})
s := ProvideService(features, &usagestats.UsageStatsMock{T: t})
report, err := s.UsageStats.GetUsageReport(context.Background())
assert.Nil(t, err)
@@ -267,7 +261,7 @@ func TestOSSAccessControlService_RegisterFixedRole(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
ac := &OSSAccessControlService{
Cfg: setting.NewCfg(),
features: featuremgmt.WithFeatures(),
UsageStats: &usagestats.UsageStatsMock{T: t},
Log: log.New("accesscontrol-test"),
}
@@ -386,12 +380,11 @@ func TestOSSAccessControlService_DeclareFixedRoles(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ac := &OSSAccessControlService{
Cfg: setting.NewCfg(),
features: featuremgmt.WithFeatures(featuremgmt.FlagAccesscontrol),
UsageStats: &usagestats.UsageStatsMock{T: t},
Log: log.New("accesscontrol-test"),
registrations: accesscontrol.RegistrationList{},
}
ac.Cfg.FeatureToggles = map[string]bool{"accesscontrol": true}
// Test
err := ac.DeclareFixedRoles(tt.registrations...)
@@ -459,9 +452,6 @@ func TestOSSAccessControlService_RegisterFixedRoles(t *testing.T) {
}
for _, tt := range tests {
cfg := setting.NewCfg()
cfg.FeatureToggles = map[string]bool{"accesscontrol": true}
t.Run(tt.name, func(t *testing.T) {
// Remove any inserted role after the test case has been run
t.Cleanup(func() {
@@ -472,12 +462,11 @@ func TestOSSAccessControlService_RegisterFixedRoles(t *testing.T) {
// Setup
ac := &OSSAccessControlService{
Cfg: setting.NewCfg(),
features: featuremgmt.WithFeatures(featuremgmt.FlagAccesscontrol),
UsageStats: &usagestats.UsageStatsMock{T: t},
Log: log.New("accesscontrol-test"),
registrations: accesscontrol.RegistrationList{},
}
ac.Cfg.FeatureToggles = map[string]bool{"accesscontrol": true}
ac.registrations.Append(tt.registrations...)
// Test
@@ -552,7 +541,7 @@ func TestOSSAccessControlService_GetUserPermissions(t *testing.T) {
// Setup
ac := setupTestEnv(t)
ac.Cfg.FeatureToggles = map[string]bool{"accesscontrol": true}
ac.features = featuremgmt.WithFeatures(featuremgmt.FlagAccesscontrol)
registration.Role.Permissions = []accesscontrol.Permission{tt.rawPerm}
err := ac.DeclareFixedRoles(registration)
@@ -638,7 +627,6 @@ func TestOSSAccessControlService_Evaluate(t *testing.T) {
// Setup
ac := setupTestEnv(t)
ac.Cfg.FeatureToggles = map[string]bool{"accesscontrol": true}
ac.RegisterAttributeScopeResolver("users:login:", userLoginScopeSolver)
registration.Role.Permissions = []accesscontrol.Permission{tt.rawPerm}
+96
View File
@@ -0,0 +1,96 @@
package featuremgmt
import (
"bytes"
"encoding/json"
)
type FeatureToggles interface {
IsEnabled(flag string) bool
}
// FeatureFlagState indicates the quality level
type FeatureFlagState int
const (
// FeatureStateUnknown indicates that no state is specified
FeatureStateUnknown FeatureFlagState = iota
// FeatureStateAlpha the feature is in active development and may change at any time
FeatureStateAlpha
// FeatureStateBeta the feature is still in development, but settings will have migrations
FeatureStateBeta
// FeatureStateStable this is a stable feature
FeatureStateStable
// FeatureStateDeprecated the feature will be removed in the future
FeatureStateDeprecated
)
func (s FeatureFlagState) String() string {
switch s {
case FeatureStateAlpha:
return "alpha"
case FeatureStateBeta:
return "beta"
case FeatureStateStable:
return "stable"
case FeatureStateDeprecated:
return "deprecated"
case FeatureStateUnknown:
}
return "unknown"
}
// MarshalJSON marshals the enum as a quoted json string
func (s FeatureFlagState) MarshalJSON() ([]byte, error) {
buffer := bytes.NewBufferString(`"`)
buffer.WriteString(s.String())
buffer.WriteString(`"`)
return buffer.Bytes(), nil
}
// UnmarshalJSON unmarshals a quoted json string to the enum value
func (s *FeatureFlagState) UnmarshalJSON(b []byte) error {
var j string
err := json.Unmarshal(b, &j)
if err != nil {
return err
}
switch j {
case "alpha":
*s = FeatureStateAlpha
case "beta":
*s = FeatureStateBeta
case "stable":
*s = FeatureStateStable
case "deprecated":
*s = FeatureStateDeprecated
default:
*s = FeatureStateUnknown
}
return nil
}
type FeatureFlag struct {
Name string `json:"name" yaml:"name"` // Unique name
Description string `json:"description"`
State FeatureFlagState `json:"state,omitempty"`
DocsURL string `json:"docsURL,omitempty"`
// CEL-GO expression. Using the value "true" will mean this is on by default
Expression string `json:"expression,omitempty"`
// Special behavior flags
RequiresDevMode bool `json:"requiresDevMode,omitempty"` // can not be enabled in production
RequiresRestart bool `json:"requiresRestart,omitempty"` // The server must be initialized with the value
RequiresLicense bool `json:"requiresLicense,omitempty"` // Must be enabled in the license
FrontendOnly bool `json:"frontend,omitempty"` // change is only seen in the frontend
}
+185
View File
@@ -0,0 +1,185 @@
package featuremgmt
import (
"context"
"fmt"
"reflect"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/models"
)
var (
_ FeatureToggles = (*FeatureManager)(nil)
)
type FeatureManager struct {
isDevMod bool
licensing models.Licensing
flags map[string]*FeatureFlag
enabled map[string]bool // only the "on" values
config string // path to config file
vars map[string]interface{}
log log.Logger
}
// This will merge the flags with the current configuration
func (fm *FeatureManager) registerFlags(flags ...FeatureFlag) {
for _, add := range flags {
if add.Name == "" {
continue // skip it with warning?
}
flag, ok := fm.flags[add.Name]
if !ok {
f := add // make a copy
fm.flags[add.Name] = &f
continue
}
// Selectively update properties
if add.Description != "" {
flag.Description = add.Description
}
if add.DocsURL != "" {
flag.DocsURL = add.DocsURL
}
if add.Expression != "" {
flag.Expression = add.Expression
}
// The most recently defined state
if add.State != FeatureStateUnknown {
flag.State = add.State
}
// Only gets more restrictive
if add.RequiresDevMode {
flag.RequiresDevMode = true
}
if add.RequiresLicense {
flag.RequiresLicense = true
}
if add.RequiresRestart {
flag.RequiresRestart = true
}
}
// This will evaluate all flags
fm.update()
}
func (fm *FeatureManager) evaluate(ff *FeatureFlag) bool {
if ff.RequiresDevMode && !fm.isDevMod {
return false
}
if ff.RequiresLicense && (fm.licensing == nil || !fm.licensing.FeatureEnabled(ff.Name)) {
return false
}
// TODO: CEL - expression
return ff.Expression == "true"
}
// Update
func (fm *FeatureManager) update() {
enabled := make(map[string]bool)
for _, flag := range fm.flags {
val := fm.evaluate(flag)
// Update the registry
track := 0.0
if val {
track = 1
enabled[flag.Name] = true
}
// Register value with prometheus metric
featureToggleInfo.WithLabelValues(flag.Name).Set(track)
}
fm.enabled = enabled
}
// Run is called by background services
func (fm *FeatureManager) readFile() error {
if fm.config == "" {
return nil // not configured
}
cfg, err := readConfigFile(fm.config)
if err != nil {
return err
}
fm.registerFlags(cfg.Flags...)
fm.vars = cfg.Vars
return nil
}
// IsEnabled checks if a feature is enabled
func (fm *FeatureManager) IsEnabled(flag string) bool {
return fm.enabled[flag]
}
// GetEnabled returns a map contaning only the features that are enabled
func (fm *FeatureManager) GetEnabled(ctx context.Context) map[string]bool {
enabled := make(map[string]bool, len(fm.enabled))
for key, val := range fm.enabled {
if val {
enabled[key] = true
}
}
return enabled
}
// GetFlags returns all flag definitions
func (fm *FeatureManager) GetFlags() []FeatureFlag {
v := make([]FeatureFlag, 0, len(fm.flags))
for _, value := range fm.flags {
v = append(v, *value)
}
return v
}
func (fm *FeatureManager) HandleGetSettings(c *models.ReqContext) {
res := make(map[string]interface{}, 3)
res["enabled"] = fm.GetEnabled(c.Req.Context())
vv := make([]*FeatureFlag, 0, len(fm.flags))
for _, v := range fm.flags {
vv = append(vv, v)
}
res["info"] = vv
response.JSON(200, res).WriteTo(c)
}
// WithFeatures is used to define feature toggles for testing.
// The arguments are a list of strings that are optionally followed by a boolean value
func WithFeatures(spec ...interface{}) *FeatureManager {
count := len(spec)
enabled := make(map[string]bool, count)
idx := 0
for idx < count {
key := fmt.Sprintf("%v", spec[idx])
val := true
idx++
if idx < count && reflect.TypeOf(spec[idx]).Kind() == reflect.Bool {
val = spec[idx].(bool)
idx++
}
if val {
enabled[key] = true
}
}
return &FeatureManager{enabled: enabled}
}
+77
View File
@@ -0,0 +1,77 @@
package featuremgmt
import (
"context"
"testing"
"github.com/stretchr/testify/require"
)
func TestFeatureManager(t *testing.T) {
t.Run("check testing stubs", func(t *testing.T) {
ft := WithFeatures("a", "b", "c")
require.True(t, ft.IsEnabled("a"))
require.True(t, ft.IsEnabled("b"))
require.True(t, ft.IsEnabled("c"))
require.False(t, ft.IsEnabled("d"))
require.Equal(t, map[string]bool{"a": true, "b": true, "c": true}, ft.GetEnabled(context.Background()))
// Explicit values
ft = WithFeatures("a", true, "b", false)
require.True(t, ft.IsEnabled("a"))
require.False(t, ft.IsEnabled("b"))
require.Equal(t, map[string]bool{"a": true}, ft.GetEnabled(context.Background()))
})
t.Run("check license validation", func(t *testing.T) {
ft := FeatureManager{
flags: map[string]*FeatureFlag{},
}
ft.registerFlags(FeatureFlag{
Name: "a",
RequiresLicense: true,
RequiresDevMode: true,
Expression: "true",
}, FeatureFlag{
Name: "b",
Expression: "true",
})
require.False(t, ft.IsEnabled("a"))
require.True(t, ft.IsEnabled("b"))
require.False(t, ft.IsEnabled("c")) // uknown flag
// Try changing "requires license"
ft.registerFlags(FeatureFlag{
Name: "a",
RequiresLicense: false, // shuld still require license!
}, FeatureFlag{
Name: "b",
RequiresLicense: true, // expression is still "true"
})
require.False(t, ft.IsEnabled("a"))
require.False(t, ft.IsEnabled("b"))
require.False(t, ft.IsEnabled("c"))
})
t.Run("check description and docs configs", func(t *testing.T) {
ft := FeatureManager{
flags: map[string]*FeatureFlag{},
}
ft.registerFlags(FeatureFlag{
Name: "a",
Description: "first",
}, FeatureFlag{
Name: "a",
Description: "second",
}, FeatureFlag{
Name: "a",
DocsURL: "http://something",
}, FeatureFlag{
Name: "a",
})
flag := ft.flags["a"]
require.Equal(t, "second", flag.Description)
require.Equal(t, "http://something", flag.DocsURL)
})
}
+118
View File
@@ -0,0 +1,118 @@
package featuremgmt
var (
// Register each toggle here
standardFeatureFlags = []FeatureFlag{
{
Name: "trimDefaults",
Description: "Use cue schema to remove values that will be applied automatically",
State: FeatureStateBeta,
},
{
Name: "envelopeEncryption",
Description: "encrypt secrets",
State: FeatureStateBeta,
},
{
Name: "httpclientprovider_azure_auth",
Description: "use http client for azure auth",
State: FeatureStateBeta,
},
{
Name: "service-accounts",
Description: "support service accounts",
State: FeatureStateBeta,
RequiresLicense: true,
},
{
Name: "database_metrics",
Description: "Add prometheus metrics for database tables",
State: FeatureStateStable,
},
{
Name: "dashboardPreviews",
Description: "Create and show thumbnails for dashboard search results",
State: FeatureStateAlpha,
},
{
Name: "live-config",
Description: "Save grafana live configuration in SQL tables",
State: FeatureStateAlpha,
},
{
Name: "live-pipeline",
Description: "enable a generic live processing pipeline",
State: FeatureStateAlpha,
},
{
Name: "live-service-web-worker",
Description: "This will use a webworker thread to processes events rather than the main thread",
State: FeatureStateAlpha,
FrontendOnly: true,
},
{
Name: "queryOverLive",
Description: "Use grafana live websocket to execute backend queries",
State: FeatureStateAlpha,
FrontendOnly: true,
},
{
Name: "tempoSearch",
Description: "Enable searching in tempo datasources",
State: FeatureStateBeta,
FrontendOnly: true,
},
{
Name: "tempoBackendSearch",
Description: "Use backend for tempo search",
State: FeatureStateBeta,
},
{
Name: "tempoServiceGraph",
Description: "show service",
State: FeatureStateBeta,
FrontendOnly: true,
},
{
Name: "fullRangeLogsVolume",
Description: "Show full range logs volume in explore",
State: FeatureStateBeta,
FrontendOnly: true,
},
{
Name: "accesscontrol",
Description: "Support robust access control",
State: FeatureStateBeta,
RequiresLicense: true,
},
{
Name: "prometheus_azure_auth",
Description: "Use azure authentication for prometheus datasource",
State: FeatureStateBeta,
},
{
Name: "newNavigation",
Description: "Try the next gen navigation model",
State: FeatureStateAlpha,
},
{
Name: "showFeatureFlagsInUI",
Description: "Show feature flags in the settings UI",
State: FeatureStateAlpha,
RequiresDevMode: true,
},
{
Name: "disable_http_request_histogram",
Description: "Do not create histograms for http requests",
State: FeatureStateAlpha,
},
{
Name: "validatedQueries",
Description: "only execute the query saved in a panel",
State: FeatureStateAlpha,
RequiresDevMode: true,
},
}
)
+76
View File
@@ -0,0 +1,76 @@
package featuremgmt
import (
"fmt"
"os"
"path/filepath"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var (
// The values are updated each time
featureToggleInfo = promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "feature_toggles_info",
Help: "info metric that exposes what feature toggles are enabled or not",
Namespace: "grafana",
}, []string{"name"})
)
func ProvideManagerService(cfg *setting.Cfg, licensing models.Licensing) (*FeatureManager, error) {
mgmt := &FeatureManager{
isDevMod: setting.Env != setting.Prod,
licensing: licensing,
flags: make(map[string]*FeatureFlag, 30),
enabled: make(map[string]bool),
log: log.New("featuremgmt"),
}
// Register the standard flags
mgmt.registerFlags(standardFeatureFlags...)
// Load the flags from `custom.ini` files
flags, err := setting.ReadFeatureTogglesFromInitFile(cfg.Raw.Section("feature_toggles"))
if err != nil {
return mgmt, err
}
for key, val := range flags {
flag, ok := mgmt.flags[key]
if !ok {
flag = &FeatureFlag{
Name: key,
State: FeatureStateUnknown,
}
mgmt.flags[key] = flag
}
flag.Expression = fmt.Sprintf("%t", val) // true | false
}
// Load config settings
configfile := filepath.Join(cfg.HomePath, "conf", "features.yaml")
if _, err := os.Stat(configfile); err == nil {
mgmt.log.Info("[experimental] loading features from config file", "path", configfile)
mgmt.config = configfile
err = mgmt.readFile()
if err != nil {
return mgmt, err
}
}
// update the values
mgmt.update()
// Minimum approach to avoid circular dependency
cfg.IsFeatureToggleEnabled = mgmt.IsEnabled
return mgmt, nil
}
// ProvideToggles allows read-only access to the feature state
func ProvideToggles(mgmt *FeatureManager) FeatureToggles {
return mgmt
}
+84
View File
@@ -0,0 +1,84 @@
package featuremgmt
import (
"testing"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/require"
)
func TestFeatureService(t *testing.T) {
license := stubLicenseServier{
flags: []FeatureFlag{
{
Name: "a.yes.default",
RequiresLicense: true,
Expression: "true",
},
{
Name: "a.yes",
RequiresLicense: true,
Expression: "",
},
{
Name: "b.no",
RequiresLicense: true,
},
},
enabled: map[string]bool{
"a.yes.default": true,
"a.yes": true,
},
}
require.False(t, license.FeatureEnabled("unknown"))
require.False(t, license.FeatureEnabled("b.no"))
require.True(t, license.FeatureEnabled("a.yes"))
require.True(t, license.FeatureEnabled("a.yes.default"))
cfg := setting.NewCfg()
mgmt, err := ProvideManagerService(cfg, license)
require.NoError(t, err)
require.NotNil(t, mgmt)
// Enterprise features do not fall though automatically
require.False(t, mgmt.IsEnabled("a.yes.default"))
require.False(t, mgmt.IsEnabled("a.yes")) // licensed, but not enabled
}
var (
_ models.Licensing = (*stubLicenseServier)(nil)
)
type stubLicenseServier struct {
flags []FeatureFlag
enabled map[string]bool
}
func (s stubLicenseServier) Expiry() int64 {
return 100
}
func (s stubLicenseServier) Edition() string {
return "test"
}
func (s stubLicenseServier) ContentDeliveryPrefix() string {
return ""
}
func (s stubLicenseServier) LicenseURL(showAdminLicensingPage bool) string {
return "http://??"
}
func (s stubLicenseServier) StateInfo() string {
return "ok"
}
func (s stubLicenseServier) EnabledFeatures() map[string]bool {
return map[string]bool{}
}
func (s stubLicenseServier) FeatureEnabled(feature string) bool {
return s.enabled[feature]
}
+34
View File
@@ -0,0 +1,34 @@
package featuremgmt
import (
"io/ioutil"
"gopkg.in/yaml.v2"
)
type configBody struct {
// define variables that can be used in expressions
Vars map[string]interface{} `yaml:"vars"`
// Define and override feature flag properties
Flags []FeatureFlag `yaml:"flags"`
// keep track of where the fie was loaded from
filename string
}
// will read a single configfile
func readConfigFile(filename string) (*configBody, error) {
cfg := &configBody{}
// Can ignore gosec G304 because the file path is forced within config subfolder
//nolint:gosec
yamlFile, err := ioutil.ReadFile(filename)
if err != nil {
return cfg, err
}
err = yaml.Unmarshal(yamlFile, cfg)
cfg.filename = filename
return cfg, err
}
+25
View File
@@ -0,0 +1,25 @@
package featuremgmt
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)
func TestReadingFeatureSettings(t *testing.T) {
config, err := readConfigFile("testdata/features.yaml")
require.NoError(t, err, "No error when reading feature configs")
assert.Equal(t, map[string]interface{}{
"level": "free",
"stack": "something",
"valA": "value from features.yaml",
}, config.Vars)
out, err := yaml.Marshal(config)
require.NoError(t, err)
fmt.Printf("%s", string(out))
}
+33
View File
@@ -0,0 +1,33 @@
include:
- included.yaml # not yet supported
vars:
stack: something
level: free
valA: value from features.yaml
flags:
- name: feature1
description: feature1
expression: "false"
- name: feature3
description: feature3
expression: "true"
- name: feature3
description: feature3
expression: env.level == 'free'
- name: displaySwedishTheme
description: enable swedish background theme
expression: |
// restrict to users allowing swedish language
req.locale.contains("sv")
- name: displayFrenchFlag
description: sho background theme
expression: |
// only admins
user.id == 1
// show to users allowing french language
&& req.locale.contains("fr")
+13
View File
@@ -0,0 +1,13 @@
include:
- features.yaml # make sure we avoid recusion!
# variables that can be used in expressions
vars:
stack: something
deep: 1
valA: value from included.yaml
flags:
- name: featureFromIncludedFile
description: an inlcuded file
expression: invalid expression string here
+85
View File
@@ -0,0 +1,85 @@
// NOTE: This file is autogenerated
package featuremgmt
const (
// FlagTrimDefaults
// Use cue schema to remove values that will be applied automatically
FlagTrimDefaults = "trimDefaults"
// FlagEnvelopeEncryption
// encrypt secrets
FlagEnvelopeEncryption = "envelopeEncryption"
// FlagHttpclientproviderAzureAuth
// use http client for azure auth
FlagHttpclientproviderAzureAuth = "httpclientprovider_azure_auth"
// FlagServiceAccounts
// support service accounts
FlagServiceAccounts = "service-accounts"
// FlagDatabaseMetrics
// Add prometheus metrics for database tables
FlagDatabaseMetrics = "database_metrics"
// FlagDashboardPreviews
// Create and show thumbnails for dashboard search results
FlagDashboardPreviews = "dashboardPreviews"
// FlagLiveConfig
// Save grafana live configuration in SQL tables
FlagLiveConfig = "live-config"
// FlagLivePipeline
// enable a generic live processing pipeline
FlagLivePipeline = "live-pipeline"
// FlagLiveServiceWebWorker
// This will use a webworker thread to processes events rather than the main thread
FlagLiveServiceWebWorker = "live-service-web-worker"
// FlagQueryOverLive
// Use grafana live websocket to execute backend queries
FlagQueryOverLive = "queryOverLive"
// FlagTempoSearch
// Enable searching in tempo datasources
FlagTempoSearch = "tempoSearch"
// FlagTempoBackendSearch
// Use backend for tempo search
FlagTempoBackendSearch = "tempoBackendSearch"
// FlagTempoServiceGraph
// show service
FlagTempoServiceGraph = "tempoServiceGraph"
// FlagFullRangeLogsVolume
// Show full range logs volume in explore
FlagFullRangeLogsVolume = "fullRangeLogsVolume"
// FlagAccesscontrol
// Support robust access control
FlagAccesscontrol = "accesscontrol"
// FlagPrometheusAzureAuth
// Use azure authentication for prometheus datasource
FlagPrometheusAzureAuth = "prometheus_azure_auth"
// FlagNewNavigation
// Try the next gen navigation model
FlagNewNavigation = "newNavigation"
// FlagShowFeatureFlagsInUI
// Show feature flags in the settings UI
FlagShowFeatureFlagsInUI = "showFeatureFlagsInUI"
// FlagDisableHttpRequestHistogram
// Do not create histograms for http requests
FlagDisableHttpRequestHistogram = "disable_http_request_histogram"
// FlagValidatedQueries
// only execute the query saved in a panel
FlagValidatedQueries = "validatedQueries"
)
@@ -0,0 +1,140 @@
package featuremgmt
import (
"bytes"
"fmt"
"html/template"
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
"unicode"
"github.com/google/go-cmp/cmp"
)
func TestFeatureToggleFiles(t *testing.T) {
// Typescript files
verifyAndGenerateFile(t,
"../../../packages/grafana-data/src/types/featureToggles.gen.ts",
generateTypeScript(),
)
// Golang files
verifyAndGenerateFile(t,
"toggles_gen.go",
generateRegistry(t),
)
}
func verifyAndGenerateFile(t *testing.T, fpath string, gen string) {
// nolint:gosec
// We can ignore the gosec G304 warning since this is a test and the function is only called explicitly above
body, err := ioutil.ReadFile(fpath)
if err == nil {
if diff := cmp.Diff(gen, string(body)); diff != "" {
str := fmt.Sprintf("body mismatch (-want +got):\n%s\n", diff)
err = fmt.Errorf(str)
}
}
if err != nil {
e2 := os.WriteFile(fpath, []byte(gen), 0644)
if e2 != nil {
t.Errorf("error writing file: %s", e2.Error())
}
abs, _ := filepath.Abs(fpath)
t.Errorf("feature toggle do not match: %s (%s)", err.Error(), abs)
t.Fail()
}
}
func generateTypeScript() string {
buf := `// NOTE: This file was auto generated. DO NOT EDIT DIRECTLY!
// To change feature flags, edit:
// pkg/services/featuremgmt/registry.go
// Then run tests in:
// pkg/services/featuremgmt/toggles_gen_test.go
/**
* Describes available feature toggles in Grafana. These can be configured via
* conf/custom.ini to enable features under development or not yet available in
* stable version.
*
* Only enabled values will be returned in this interface
*
* @public
*/
export interface FeatureToggles {
[name: string]: boolean | undefined; // support any string value
`
for _, flag := range standardFeatureFlags {
buf += " " + getTypeScriptKey(flag.Name) + "?: boolean;\n"
}
buf += "}\n"
return buf
}
func getTypeScriptKey(key string) string {
if strings.Contains(key, "-") || strings.Contains(key, ".") {
return "['" + key + "']"
}
return key
}
func isLetterOrNumber(c rune) bool {
return !unicode.IsLetter(c) && !unicode.IsNumber(c)
}
func asCamelCase(key string) string {
parts := strings.FieldsFunc(key, isLetterOrNumber)
for idx, part := range parts {
parts[idx] = strings.Title(part)
}
return strings.Join(parts, "")
}
func generateRegistry(t *testing.T) string {
tmpl, err := template.New("fn").Parse(`
{{"\t"}}// Flag{{.CamelCase}}{{.Ext}}
{{"\t"}}Flag{{.CamelCase}} = "{{.Flag.Name}}"
`)
if err != nil {
t.Fatal("error reading template", "error", err.Error())
return ""
}
data := struct {
CamelCase string
Flag FeatureFlag
Ext string
}{
CamelCase: "?",
}
var buff bytes.Buffer
buff.WriteString(`// NOTE: This file is autogenerated
package featuremgmt
const (`)
for _, flag := range standardFeatureFlags {
data.CamelCase = asCamelCase(flag.Name)
data.Flag = flag
data.Ext = ""
if flag.Description != "" {
data.Ext += "\n\t// " + flag.Description
}
_ = tmpl.Execute(&buff, data)
}
buff.WriteString(")\n")
return buff.String()
}
@@ -2,6 +2,7 @@ package osskmsproviders
import (
"github.com/grafana/grafana/pkg/services/encryption"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/kmsproviders"
grafana "github.com/grafana/grafana/pkg/services/kmsproviders/defaultprovider"
"github.com/grafana/grafana/pkg/services/secrets"
@@ -21,7 +22,7 @@ func ProvideService(enc encryption.Internal, settings setting.Provider) Service
}
func (s Service) Provide() (map[secrets.ProviderID]secrets.Provider, error) {
if !s.settings.IsFeatureToggleEnabled(secrets.EnvelopeEncryptionFeatureToggle) {
if !s.settings.IsFeatureToggleEnabled(featuremgmt.FlagEnvelopeEncryption) {
return nil, nil
}
+5 -2
View File
@@ -15,6 +15,7 @@ import (
jsoniter "github.com/json-iterator/go"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/query"
"github.com/centrifugal/centrifuge"
@@ -67,9 +68,10 @@ type CoreGrafanaScope struct {
func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, routeRegister routing.RouteRegister,
pluginStore plugins.Store, cacheService *localcache.CacheService,
dataSourceCache datasources.CacheService, sqlStore *sqlstore.SQLStore, secretsService secrets.Service,
usageStatsService usagestats.Service, queryDataService *query.Service) (*GrafanaLive, error) {
usageStatsService usagestats.Service, queryDataService *query.Service, toggles featuremgmt.FeatureToggles) (*GrafanaLive, error) {
g := &GrafanaLive{
Cfg: cfg,
Features: toggles,
PluginContextProvider: plugCtxProvider,
RouteRegister: routeRegister,
pluginStore: pluginStore,
@@ -174,7 +176,7 @@ func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, r
}
g.ManagedStreamRunner = managedStreamRunner
if enabled := g.Cfg.FeatureToggles["live-pipeline"]; enabled {
if g.Features.IsEnabled(featuremgmt.FlagLivePipeline) {
var builder pipeline.RuleBuilder
if os.Getenv("GF_LIVE_DEV_BUILDER") != "" {
builder = &pipeline.DevRuleBuilder{
@@ -391,6 +393,7 @@ func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, r
type GrafanaLive struct {
PluginContextProvider *plugincontext.Provider
Cfg *setting.Cfg
Features featuremgmt.FeatureToggles
RouteRegister routing.RouteRegister
CacheService *localcache.CacheService
DataSourceCache datasources.CacheService
+6 -6
View File
@@ -8,9 +8,9 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/schema"
"github.com/grafana/grafana/pkg/schema/load"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/setting"
)
const ServiceName = "SchemaLoader"
@@ -26,13 +26,13 @@ type RenderUser struct {
OrgRole string
}
func ProvideService(cfg *setting.Cfg) (*SchemaLoaderService, error) {
func ProvideService(features featuremgmt.FeatureToggles) (*SchemaLoaderService, error) {
dashFam, err := load.BaseDashboardFamily(baseLoadPath)
if err != nil {
return nil, fmt.Errorf("failed to load dashboard cue schema from path %q: %w", baseLoadPath, err)
}
s := &SchemaLoaderService{
Cfg: cfg,
features: features,
DashFamily: dashFam,
log: log.New("schemaloader"),
}
@@ -42,14 +42,14 @@ func ProvideService(cfg *setting.Cfg) (*SchemaLoaderService, error) {
type SchemaLoaderService struct {
log log.Logger
DashFamily schema.VersionedCueSchema
Cfg *setting.Cfg
features featuremgmt.FeatureToggles
}
func (rs *SchemaLoaderService) IsDisabled() bool {
if rs.Cfg == nil {
if rs.features == nil {
return true
}
return !rs.Cfg.IsTrimDefaultsEnabled()
return !rs.features.IsEnabled(featuremgmt.FlagTrimDefaults)
}
func (rs *SchemaLoaderService) DashboardApplyDefaults(input *simplejson.Json) (*simplejson.Json, error) {
+8 -2
View File
@@ -5,6 +5,7 @@ import (
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/services/encryption/ossencryption"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/kmsproviders/osskmsproviders"
"github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/setting"
@@ -23,10 +24,15 @@ func SetupTestService(tb testing.TB, store secrets.Store) *SecretsService {
[security]
secret_key = ` + defaultKey))
require.NoError(tb, err)
features := featuremgmt.WithFeatures(featuremgmt.FlagEnvelopeEncryption)
cfg := &setting.Cfg{Raw: raw}
cfg.FeatureToggles = map[string]bool{secrets.EnvelopeEncryptionFeatureToggle: true}
cfg.IsFeatureToggleEnabled = features.IsEnabled
settings := &setting.OSSImpl{Cfg: cfg}
assert.True(tb, settings.IsFeatureToggleEnabled(secrets.EnvelopeEncryptionFeatureToggle))
assert.True(tb, settings.IsFeatureToggleEnabled(featuremgmt.FlagEnvelopeEncryption))
assert.True(tb, features.IsEnabled(featuremgmt.FlagEnvelopeEncryption))
encryption := ossencryption.ProvideService()
secretsService, err := ProvideSecretsService(
+8 -7
View File
@@ -12,6 +12,7 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/services/encryption"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/kmsproviders"
"github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/setting"
@@ -43,7 +44,7 @@ func ProvideSecretsService(
}
logger := log.New("secrets")
enabled := settings.IsFeatureToggleEnabled(secrets.EnvelopeEncryptionFeatureToggle)
enabled := settings.IsFeatureToggleEnabled(featuremgmt.FlagEnvelopeEncryption)
currentProviderID := readCurrentProviderID(settings)
if _, ok := providers[currentProviderID]; enabled && !ok {
@@ -87,7 +88,7 @@ func (s *SecretsService) registerUsageMetrics() {
// Enabled / disabled
usageMetrics["stats.encryption.envelope_encryption_enabled.count"] = 0
if s.settings.IsFeatureToggleEnabled(secrets.EnvelopeEncryptionFeatureToggle) {
if s.settings.IsFeatureToggleEnabled(featuremgmt.FlagEnvelopeEncryption) {
usageMetrics["stats.encryption.envelope_encryption_enabled.count"] = 1
}
@@ -130,11 +131,11 @@ func (s *SecretsService) Encrypt(ctx context.Context, payload []byte, opt secret
func (s *SecretsService) EncryptWithDBSession(ctx context.Context, payload []byte, opt secrets.EncryptionOptions, sess *xorm.Session) ([]byte, error) {
// Use legacy encryption service if envelopeEncryptionFeatureToggle toggle is off
if !s.settings.IsFeatureToggleEnabled(secrets.EnvelopeEncryptionFeatureToggle) {
if !s.settings.IsFeatureToggleEnabled(featuremgmt.FlagEnvelopeEncryption) {
return s.enc.Encrypt(ctx, payload, setting.SecretKey)
}
// If encryption secrets.EnvelopeEncryptionFeatureToggle toggle is on, use envelope encryption
// If encryption featuremgmt.FlagEnvelopeEncryption toggle is on, use envelope encryption
scope := opt()
keyName := s.keyName(scope)
@@ -172,12 +173,12 @@ func (s *SecretsService) keyName(scope string) string {
}
func (s *SecretsService) Decrypt(ctx context.Context, payload []byte) ([]byte, error) {
// Use legacy encryption service if secrets.EnvelopeEncryptionFeatureToggle toggle is off
if !s.settings.IsFeatureToggleEnabled(secrets.EnvelopeEncryptionFeatureToggle) {
// Use legacy encryption service if featuremgmt.FlagEnvelopeEncryption toggle is off
if !s.settings.IsFeatureToggleEnabled(featuremgmt.FlagEnvelopeEncryption) {
return s.enc.Decrypt(ctx, payload, setting.SecretKey)
}
// If encryption secrets.EnvelopeEncryptionFeatureToggle toggle is on, use envelope encryption
// If encryption featuremgmt.FlagEnvelopeEncryption toggle is on, use envelope encryption
if len(payload) == 0 {
return nil, fmt.Errorf("unable to decrypt empty payload")
}
+3 -2
View File
@@ -7,6 +7,7 @@ import (
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/services/encryption/ossencryption"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/kmsproviders/osskmsproviders"
"github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/services/secrets/database"
@@ -180,8 +181,8 @@ func TestSecretsService_UseCurrentProvider(t *testing.T) {
providerID := secrets.ProviderID("fakeProvider.v1")
settings := &setting.OSSImpl{
Cfg: &setting.Cfg{
Raw: raw,
FeatureToggles: map[string]bool{secrets.EnvelopeEncryptionFeatureToggle: true},
Raw: raw,
IsFeatureToggleEnabled: featuremgmt.WithFeatures(featuremgmt.FlagEnvelopeEncryption).IsEnabled,
},
}
encr := ossencryption.ProvideService()
-4
View File
@@ -8,10 +8,6 @@ import (
"xorm.io/xorm"
)
const (
EnvelopeEncryptionFeatureToggle = "envelopeEncryption"
)
// Service is an envelope encryption service in charge of encrypting/decrypting secrets.
// It is a replacement for encryption.Service
type Service interface {
+3 -3
View File
@@ -13,8 +13,8 @@ import (
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
acmiddleware "github.com/grafana/grafana/pkg/services/accesscontrol/middleware"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/serviceaccounts"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web"
)
@@ -40,9 +40,9 @@ func NewServiceAccountsAPI(
}
func (api *ServiceAccountsAPI) RegisterAPIEndpoints(
cfg *setting.Cfg,
features featuremgmt.FeatureToggles,
) {
if !cfg.FeatureToggles["service-accounts"] {
if !features.IsEnabled(featuremgmt.FlagServiceAccounts) {
return
}
+2 -2
View File
@@ -13,10 +13,10 @@ import (
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/serviceaccounts"
"github.com/grafana/grafana/pkg/services/serviceaccounts/tests"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web"
"github.com/stretchr/testify/require"
@@ -97,7 +97,7 @@ func serviceAccountRequestScenario(t *testing.T, httpMethod string, endpoint str
func setupTestServer(t *testing.T, svc *tests.ServiceAccountMock, routerRegister routing.RouteRegister, acmock *accesscontrolmock.Mock, sqlStore *sqlstore.SQLStore) *web.Mux {
a := NewServiceAccountsAPI(svc, acmock, routerRegister, database.NewServiceAccountsStore(sqlStore))
a.RegisterAPIEndpoints(&setting.Cfg{FeatureToggles: map[string]bool{"service-accounts": true}})
a.RegisterAPIEndpoints(featuremgmt.WithFeatures(featuremgmt.FlagServiceAccounts))
m := web.New()
signedUser := &models.SignedInUser{
+11 -11
View File
@@ -7,11 +7,11 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/serviceaccounts"
"github.com/grafana/grafana/pkg/services/serviceaccounts/api"
"github.com/grafana/grafana/pkg/services/serviceaccounts/database"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
)
var (
@@ -19,21 +19,21 @@ var (
)
type ServiceAccountsService struct {
store serviceaccounts.Store
cfg *setting.Cfg
log log.Logger
store serviceaccounts.Store
features featuremgmt.FeatureToggles
log log.Logger
}
func ProvideServiceAccountsService(
cfg *setting.Cfg,
features featuremgmt.FeatureToggles,
store *sqlstore.SQLStore,
ac accesscontrol.AccessControl,
routeRegister routing.RouteRegister,
) (*ServiceAccountsService, error) {
s := &ServiceAccountsService{
cfg: cfg,
store: database.NewServiceAccountsStore(store),
log: log.New("serviceaccounts"),
features: features,
store: database.NewServiceAccountsStore(store),
log: log.New("serviceaccounts"),
}
if err := RegisterRoles(ac); err != nil {
@@ -41,13 +41,13 @@ func ProvideServiceAccountsService(
}
serviceaccountsAPI := api.NewServiceAccountsAPI(s, ac, routeRegister, s.store)
serviceaccountsAPI.RegisterAPIEndpoints(cfg)
serviceaccountsAPI.RegisterAPIEndpoints(features)
return s, nil
}
func (sa *ServiceAccountsService) CreateServiceAccount(ctx context.Context, saForm *serviceaccounts.CreateServiceaccountForm) (*models.User, error) {
if !sa.cfg.FeatureToggles["service-accounts"] {
if !sa.features.IsEnabled(featuremgmt.FlagServiceAccounts) {
sa.log.Debug(ServiceAccountFeatureToggleNotFound)
return nil, nil
}
@@ -55,7 +55,7 @@ func (sa *ServiceAccountsService) CreateServiceAccount(ctx context.Context, saFo
}
func (sa *ServiceAccountsService) DeleteServiceAccount(ctx context.Context, orgID, serviceAccountID int64) error {
if !sa.cfg.FeatureToggles["service-accounts"] {
if !sa.features.IsEnabled(featuremgmt.FlagServiceAccounts) {
sa.log.Debug(ServiceAccountFeatureToggleNotFound)
return nil
}
@@ -5,31 +5,29 @@ import (
"testing"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/serviceaccounts/tests"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestProvideServiceAccount_DeleteServiceAccount(t *testing.T) {
t.Run("feature toggle present, should call store function", func(t *testing.T) {
cfg := setting.NewCfg()
storeMock := &tests.ServiceAccountsStoreMock{Calls: tests.Calls{}}
cfg.FeatureToggles = map[string]bool{"service-accounts": true}
svc := ServiceAccountsService{cfg: cfg, store: storeMock}
svc := ServiceAccountsService{
features: featuremgmt.WithFeatures("service-accounts", true),
store: storeMock}
err := svc.DeleteServiceAccount(context.Background(), 1, 1)
require.NoError(t, err)
assert.Len(t, storeMock.Calls.DeleteServiceAccount, 1)
})
t.Run("no feature toggle present, should not call store function", func(t *testing.T) {
cfg := setting.NewCfg()
svcMock := &tests.ServiceAccountsStoreMock{Calls: tests.Calls{}}
cfg.FeatureToggles = map[string]bool{"service-accounts": false}
svc := ServiceAccountsService{
cfg: cfg,
store: svcMock,
log: log.New("serviceaccounts-manager-test"),
features: featuremgmt.WithFeatures("service-accounts", false),
store: svcMock,
log: log.New("serviceaccounts-manager-test"),
}
err := svc.DeleteServiceAccount(context.Background(), 1, 1)
require.NoError(t, err)
@@ -3,6 +3,7 @@ package migrations
import (
"os"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/sqlstore/migrations/accesscontrol"
"github.com/grafana/grafana/pkg/services/sqlstore/migrations/ualert"
. "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
@@ -56,8 +57,8 @@ func (*OSSMigrations) AddMigration(mg *Migrator) {
ualert.AddTablesMigrations(mg)
ualert.AddDashAlertMigration(mg)
addLibraryElementsMigrations(mg)
if mg.Cfg != nil {
if mg.Cfg.IsLiveConfigEnabled() {
if mg.Cfg != nil && mg.Cfg.IsFeatureToggleEnabled != nil {
if mg.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagLiveConfig) {
addLiveChannelMigrations(mg)
}
}
+3 -2
View File
@@ -9,6 +9,7 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/util"
)
@@ -117,7 +118,7 @@ func (ss *SQLStore) GetOrgUsers(ctx context.Context, query *models.GetOrgUsersQu
// service accounts table in the modelling
whereConditions = append(whereConditions, fmt.Sprintf("%s.is_service_account = %t", x.Dialect().Quote("user"), query.IsServiceAccount))
if ss.Cfg.FeatureToggles["accesscontrol"] {
if ss.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagAccesscontrol) {
q, args, err := accesscontrol.Filter(ctx, ss.Dialect, "org_user.user_id", "users", "org.users:read", query.User)
if err != nil {
return err
@@ -180,7 +181,7 @@ func (ss *SQLStore) SearchOrgUsers(ctx context.Context, query *models.SearchOrgU
// service accounts table in the modelling
whereConditions = append(whereConditions, fmt.Sprintf("%s.is_service_account = %t", x.Dialect().Quote("user"), query.IsServiceAccount))
if ss.Cfg.FeatureToggles["accesscontrol"] {
if ss.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagAccesscontrol) {
q, args, err := accesscontrol.Filter(ctx, ss.Dialect, "org_user.user_id", "users", "org.users:read", query.User)
if err != nil {
return err
+3 -2
View File
@@ -11,6 +11,7 @@ import (
"github.com/grafana/grafana/pkg/models"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/featuremgmt"
)
type getOrgUsersTestCase struct {
@@ -61,7 +62,7 @@ func TestSQLStore_GetOrgUsers(t *testing.T) {
}
store := InitTestDB(t)
store.Cfg.FeatureToggles = map[string]bool{"accesscontrol": true}
store.Cfg.IsFeatureToggleEnabled = featuremgmt.WithFeatures(featuremgmt.FlagAccesscontrol).IsEnabled
seedOrgUsers(t, store, 10)
for _, tt := range tests {
@@ -127,7 +128,7 @@ func TestSQLStore_SearchOrgUsers(t *testing.T) {
}
store := InitTestDB(t)
store.Cfg.FeatureToggles = map[string]bool{"accesscontrol": true}
store.Cfg.IsFeatureToggleEnabled = featuremgmt.WithFeatures(featuremgmt.FlagAccesscontrol).IsEnabled
seedOrgUsers(t, store, 10)
for _, tt := range tests {
+3 -1
View File
@@ -23,6 +23,7 @@ import (
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/annotations"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/sqlstore/migrations"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
@@ -330,7 +331,7 @@ func (ss *SQLStore) initEngine(engine *xorm.Engine) error {
return err
}
if ss.Cfg.IsDatabaseMetricsEnabled() {
if ss.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagDatabaseMetrics) {
ss.dbCfg.Type = WrapDatabaseDriverWithHooks(ss.dbCfg.Type, ss.tracer)
}
@@ -496,6 +497,7 @@ func initTestDB(migration registry.DatabaseMigrator, opts ...InitTestDBOpt) (*SQ
// set test db config
cfg := setting.NewCfg()
cfg.IsFeatureToggleEnabled = func(key string) bool { return false }
sec, err := cfg.Raw.NewSection("database")
if err != nil {
return nil, err
+3 -2
View File
@@ -14,6 +14,7 @@ import (
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/live"
"github.com/grafana/grafana/pkg/services/rendering"
@@ -39,8 +40,8 @@ type Service interface {
CrawlerStatus(c *models.ReqContext) response.Response
}
func ProvideService(cfg *setting.Cfg, renderService rendering.Service, gl *live.GrafanaLive) Service {
if !cfg.IsDashboardPreviesEnabled() {
func ProvideService(cfg *setting.Cfg, features featuremgmt.FeatureToggles, renderService rendering.Service, gl *live.GrafanaLive) Service {
if !features.IsEnabled(featuremgmt.FlagDashboardPreviews) {
return &dummyService{}
}