FeatureFlags: manage feature flags outside of settings.Cfg (#43692)

This commit is contained in:
Ryan McKinley
2022-01-20 13:42:05 -08:00
committed by GitHub
parent 7fbc7d019a
commit f94c0decbd
65 changed files with 1244 additions and 252 deletions
+95
View File
@@ -0,0 +1,95 @@
package featuremgmt
import (
"bytes"
"encoding/json"
)
// FeatureToggleState indicates the quality level
type FeatureToggleState int
const (
// FeatureStateUnknown indicates that no state is specified
FeatureStateUnknown FeatureToggleState = iota
// FeatureStateAlpha the feature is in active development and may change at any time
FeatureStateAlpha
// FeatureStateBeta the feature is still in development, but settings will have migrations
FeatureStateBeta
// FeatureStateStable this is a stable feature
FeatureStateStable
// FeatureStateDeprecated the feature will be removed in the future
FeatureStateDeprecated
)
func (s FeatureToggleState) String() string {
switch s {
case FeatureStateAlpha:
return "alpha"
case FeatureStateBeta:
return "beta"
case FeatureStateStable:
return "stable"
case FeatureStateDeprecated:
return "deprecated"
case FeatureStateUnknown:
}
return "unknown"
}
// MarshalJSON marshals the enum as a quoted json string
func (s FeatureToggleState) MarshalJSON() ([]byte, error) {
buffer := bytes.NewBufferString(`"`)
buffer.WriteString(s.String())
buffer.WriteString(`"`)
return buffer.Bytes(), nil
}
// UnmarshalJSON unmarshals a quoted json string to the enum value
func (s *FeatureToggleState) UnmarshalJSON(b []byte) error {
var j string
err := json.Unmarshal(b, &j)
if err != nil {
return err
}
switch j {
case "alpha":
*s = FeatureStateAlpha
case "beta":
*s = FeatureStateBeta
case "stable":
*s = FeatureStateStable
case "deprecated":
*s = FeatureStateDeprecated
default:
*s = FeatureStateUnknown
}
return nil
}
type FeatureFlag struct {
Name string `json:"name" yaml:"name"` // Unique name
Description string `json:"description"`
State FeatureToggleState `json:"state,omitempty"`
DocsURL string `json:"docsURL,omitempty"`
// CEL-GO expression. Using the value "true" will mean this is on by default
Expression string `json:"expression,omitempty"`
// Special behavior flags
RequiresDevMode bool `json:"requiresDevMode,omitempty"` // can not be enabled in production
RequiresRestart bool `json:"requiresRestart,omitempty"` // The server must be initialized with the value
RequiresLicense bool `json:"requiresLicense,omitempty"` // Must be enabled in the license
FrontendOnly bool `json:"frontend,omitempty"` // change is only seen in the frontend
// Internal properties
// expr string `json:-`
}
+195
View File
@@ -0,0 +1,195 @@
package featuremgmt
import (
"context"
"fmt"
"reflect"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/models"
)
type FeatureManager struct {
isDevMod bool
licensing models.Licensing
flags map[string]*FeatureFlag
enabled map[string]bool // only the "on" values
toggles *FeatureToggles
config string // path to config file
vars map[string]interface{}
log log.Logger
}
// This will merge the flags with the current configuration
func (fm *FeatureManager) registerFlags(flags ...FeatureFlag) {
for idx, add := range flags {
if add.Name == "" {
continue // skip it with warning?
}
flag, ok := fm.flags[add.Name]
if !ok {
fm.flags[add.Name] = &flags[idx]
continue
}
// Selectively update properties
if add.Description != "" {
flag.Description = add.Description
}
if add.DocsURL != "" {
flag.DocsURL = add.DocsURL
}
if add.Expression != "" {
flag.Expression = add.Expression
}
// The most recently defined state
if add.State != FeatureStateUnknown {
flag.State = add.State
}
// Only gets more restrictive
if add.RequiresDevMode {
flag.RequiresDevMode = true
}
if add.RequiresLicense {
flag.RequiresLicense = true
}
if add.RequiresRestart {
flag.RequiresRestart = true
}
}
// This will evaluate all flags
fm.update()
}
func (fm *FeatureManager) evaluate(ff *FeatureFlag) bool {
if ff.RequiresDevMode && !fm.isDevMod {
return false
}
if ff.RequiresLicense && (fm.licensing == nil || !fm.licensing.FeatureEnabled(ff.Name)) {
return false
}
// TODO: CEL - expression
return ff.Expression == "true"
}
// Update
func (fm *FeatureManager) update() {
enabled := make(map[string]bool)
for _, flag := range fm.flags {
val := fm.evaluate(flag)
// Update the registry
track := 0.0
if val {
track = 1
enabled[flag.Name] = true
}
// Register value with prometheus metric
featureToggleInfo.WithLabelValues(flag.Name).Set(track)
}
fm.enabled = enabled
}
// Run is called by background services
func (fm *FeatureManager) readFile() error {
if fm.config == "" {
return nil // not configured
}
cfg, err := readConfigFile(fm.config)
if err != nil {
return err
}
fm.registerFlags(cfg.Flags...)
fm.vars = cfg.Vars
return nil
}
// IsEnabled checks if a feature is enabled
func (fm *FeatureManager) IsEnabled(flag string) bool {
return fm.enabled[flag]
}
// GetEnabled returns a map contaning only the features that are enabled
func (fm *FeatureManager) GetEnabled(ctx context.Context) map[string]bool {
enabled := make(map[string]bool, len(fm.enabled))
for key, val := range fm.enabled {
if val {
enabled[key] = true
}
}
return enabled
}
// Toggles returns FeatureToggles.
func (fm *FeatureManager) Toggles() *FeatureToggles {
if fm.toggles == nil {
fm.toggles = &FeatureToggles{manager: fm}
}
return fm.toggles
}
// GetFlags returns all flag definitions
func (fm *FeatureManager) GetFlags() []FeatureFlag {
v := make([]FeatureFlag, 0, len(fm.flags))
for _, value := range fm.flags {
v = append(v, *value)
}
return v
}
func (fm *FeatureManager) HandleGetSettings(c *models.ReqContext) {
res := make(map[string]interface{}, 3)
res["enabled"] = fm.GetEnabled(c.Req.Context())
vv := make([]*FeatureFlag, 0, len(fm.flags))
for _, v := range fm.flags {
vv = append(vv, v)
}
res["info"] = vv
response.JSON(200, res).WriteTo(c)
}
// WithFeatures is used to define feature toggles for testing.
// The arguments are a list of strings that are optionally followed by a boolean value
func WithFeatures(spec ...interface{}) *FeatureManager {
count := len(spec)
enabled := make(map[string]bool, count)
idx := 0
for idx < count {
key := fmt.Sprintf("%v", spec[idx])
val := true
idx++
if idx < count && reflect.TypeOf(spec[idx]).Kind() == reflect.Bool {
val = spec[idx].(bool)
idx++
}
if val {
enabled[key] = true
}
}
return &FeatureManager{enabled: enabled}
}
func WithToggles(spec ...interface{}) *FeatureToggles {
return &FeatureToggles{
manager: WithFeatures(spec...),
}
}
+77
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)
})
}
+163
View File
@@ -0,0 +1,163 @@
package featuremgmt
import "github.com/grafana/grafana/pkg/services/secrets"
var (
FLAG_database_metrics = "database_metrics"
FLAG_live_config = "live-config"
FLAG_recordedQueries = "recordedQueries"
// Register each toggle here
standardFeatureFlags = []FeatureFlag{
{
Name: FLAG_recordedQueries,
Description: "Supports saving queries that can be scraped by prometheus",
State: FeatureStateBeta,
RequiresLicense: true,
},
{
Name: "teamsync",
Description: "Team sync lets you set up synchronization between your auth providers teams and teams in Grafana",
State: FeatureStateStable,
DocsURL: "https://grafana.com/docs/grafana/latest/enterprise/team-sync/",
RequiresLicense: true,
},
{
Name: "ldapsync",
Description: "Enhanced LDAP integration",
State: FeatureStateStable,
DocsURL: "https://grafana.com/docs/grafana/latest/enterprise/enhanced_ldap/",
RequiresLicense: true,
},
{
Name: "caching",
Description: "Temporarily store data source query results.",
State: FeatureStateStable,
DocsURL: "https://grafana.com/docs/grafana/latest/enterprise/query-caching/",
RequiresLicense: true,
},
{
Name: "dspermissions",
Description: "Data source permissions",
State: FeatureStateStable,
DocsURL: "https://grafana.com/docs/grafana/latest/enterprise/datasource_permissions/",
RequiresLicense: true,
},
{
Name: "analytics",
Description: "Analytics",
State: FeatureStateStable,
RequiresLicense: true,
},
{
Name: "enterprise.plugins",
Description: "Enterprise plugins",
State: FeatureStateStable,
DocsURL: "https://grafana.com/grafana/plugins/?enterprise=1",
RequiresLicense: true,
},
{
Name: "trimDefaults",
Description: "Use cue schema to remove values that will be applied automatically",
State: FeatureStateBeta,
},
{
Name: secrets.EnvelopeEncryptionFeatureToggle,
Description: "encrypt secrets",
State: FeatureStateBeta,
},
{
Name: "httpclientprovider_azure_auth",
State: FeatureStateBeta,
},
{
Name: "service-accounts",
Description: "support service accounts",
State: FeatureStateBeta,
RequiresLicense: true,
},
{
Name: FLAG_database_metrics,
Description: "Add prometheus metrics for database tables",
State: FeatureStateStable,
},
{
Name: "dashboardPreviews",
Description: "Create and show thumbnails for dashboard search results",
State: FeatureStateAlpha,
},
{
Name: FLAG_live_config,
Description: "Save grafana live configuration in SQL tables",
State: FeatureStateAlpha,
},
{
Name: "live-pipeline",
Description: "enable a generic live processing pipeline",
State: FeatureStateAlpha,
},
{
Name: "live-service-web-worker",
Description: "This will use a webworker thread to processes events rather than the main thread",
State: FeatureStateAlpha,
FrontendOnly: true,
},
{
Name: "queryOverLive",
Description: "Use grafana live websocket to execute backend queries",
State: FeatureStateAlpha,
FrontendOnly: true,
},
{
Name: "tempoSearch",
Description: "Enable searching in tempo datasources",
State: FeatureStateBeta,
FrontendOnly: true,
},
{
Name: "tempoBackendSearch",
Description: "Use backend for tempo search",
State: FeatureStateBeta,
},
{
Name: "tempoServiceGraph",
Description: "show service",
State: FeatureStateBeta,
FrontendOnly: true,
},
{
Name: "fullRangeLogsVolume",
Description: "Show full range logs volume in expore",
State: FeatureStateBeta,
FrontendOnly: true,
},
{
Name: "accesscontrol",
Description: "Support robust access control",
State: FeatureStateBeta,
RequiresLicense: true,
},
{
Name: "prometheus_azure_auth",
Description: "Use azure authentication for prometheus datasource",
State: FeatureStateBeta,
},
{
Name: "newNavigation",
Description: "Try the next gen naviation model",
State: FeatureStateAlpha,
},
{
Name: "showFeatureFlagsInUI",
Description: "Show feature flags in the settings UI",
State: FeatureStateAlpha,
RequiresDevMode: true,
},
{
Name: "disable_http_request_histogram",
State: FeatureStateAlpha,
},
}
)
+78
View File
@@ -0,0 +1,78 @@
package featuremgmt
import (
"fmt"
"os"
"path/filepath"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var (
// The values are updated each time
featureToggleInfo = promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "feature_toggles_info",
Help: "info metric that exposes what feature toggles are enabled or not",
Namespace: "grafana",
}, []string{"name"})
)
func ProvideManagerService(cfg *setting.Cfg, licensing models.Licensing) (*FeatureManager, error) {
mgmt := &FeatureManager{
isDevMod: setting.Env != setting.Prod,
licensing: licensing,
flags: make(map[string]*FeatureFlag, 30),
enabled: make(map[string]bool),
log: log.New("featuremgmt"),
}
// Register the standard flags
mgmt.registerFlags(standardFeatureFlags...)
// Load the flags from `custom.ini` files
flags, err := setting.ReadFeatureTogglesFromInitFile(cfg.Raw.Section("feature_toggles"))
if err != nil {
return mgmt, err
}
for key, val := range flags {
flag, ok := mgmt.flags[key]
if !ok {
flag = &FeatureFlag{
Name: key,
State: FeatureStateUnknown,
}
mgmt.flags[key] = flag
}
flag.Expression = fmt.Sprintf("%t", val) // true | false
}
// Load config settings
configfile := filepath.Join(cfg.HomePath, "conf", "features.yaml")
if _, err := os.Stat(configfile); err == nil {
mgmt.log.Info("[experimental] loading features from config file", "path", configfile)
mgmt.config = configfile
err = mgmt.readFile()
if err != nil {
return mgmt, err
}
}
// update the values
mgmt.update()
// Minimum approach to avoid circular dependency
cfg.IsFeatureToggleEnabled = mgmt.IsEnabled
return mgmt, nil
}
// ProvideToggles allows read-only access to the feature state
func ProvideToggles(mgmt *FeatureManager) *FeatureToggles {
return &FeatureToggles{
manager: mgmt,
}
}
+34
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
+10
View File
@@ -0,0 +1,10 @@
package featuremgmt
type FeatureToggles struct {
manager *FeatureManager
}
// IsEnabled checks if a feature is enabled
func (ft *FeatureToggles) IsEnabled(flag string) bool {
return ft.manager.IsEnabled(flag)
}
+157
View File
@@ -0,0 +1,157 @@
// NOTE: This file is autogenerated
package featuremgmt
// IsRecordedQueriesEnabled checks for the flag: recordedQueries
// Supports saving queries that can be scraped by prometheus
func (ft *FeatureToggles) IsRecordedQueriesEnabled() bool {
return ft.manager.IsEnabled("recordedQueries")
}
// IsTeamsyncEnabled checks for the flag: teamsync
// Team sync lets you set up synchronization between your auth providers teams and teams in Grafana
func (ft *FeatureToggles) IsTeamsyncEnabled() bool {
return ft.manager.IsEnabled("teamsync")
}
// IsLdapsyncEnabled checks for the flag: ldapsync
// Enhanced LDAP integration
func (ft *FeatureToggles) IsLdapsyncEnabled() bool {
return ft.manager.IsEnabled("ldapsync")
}
// IsCachingEnabled checks for the flag: caching
// Temporarily store data source query results.
func (ft *FeatureToggles) IsCachingEnabled() bool {
return ft.manager.IsEnabled("caching")
}
// IsDspermissionsEnabled checks for the flag: dspermissions
// Data source permissions
func (ft *FeatureToggles) IsDspermissionsEnabled() bool {
return ft.manager.IsEnabled("dspermissions")
}
// IsAnalyticsEnabled checks for the flag: analytics
// Analytics
func (ft *FeatureToggles) IsAnalyticsEnabled() bool {
return ft.manager.IsEnabled("analytics")
}
// IsEnterprisePluginsEnabled checks for the flag: enterprise.plugins
// Enterprise plugins
func (ft *FeatureToggles) IsEnterprisePluginsEnabled() bool {
return ft.manager.IsEnabled("enterprise.plugins")
}
// IsTrimDefaultsEnabled checks for the flag: trimDefaults
// Use cue schema to remove values that will be applied automatically
func (ft *FeatureToggles) IsTrimDefaultsEnabled() bool {
return ft.manager.IsEnabled("trimDefaults")
}
// IsEnvelopeEncryptionEnabled checks for the flag: envelopeEncryption
// encrypt secrets
func (ft *FeatureToggles) IsEnvelopeEncryptionEnabled() bool {
return ft.manager.IsEnabled("envelopeEncryption")
}
// IsHttpclientproviderAzureAuthEnabled checks for the flag: httpclientprovider_azure_auth
func (ft *FeatureToggles) IsHttpclientproviderAzureAuthEnabled() bool {
return ft.manager.IsEnabled("httpclientprovider_azure_auth")
}
// IsServiceAccountsEnabled checks for the flag: service-accounts
// support service accounts
func (ft *FeatureToggles) IsServiceAccountsEnabled() bool {
return ft.manager.IsEnabled("service-accounts")
}
// IsDatabaseMetricsEnabled checks for the flag: database_metrics
// Add prometheus metrics for database tables
func (ft *FeatureToggles) IsDatabaseMetricsEnabled() bool {
return ft.manager.IsEnabled("database_metrics")
}
// IsDashboardPreviewsEnabled checks for the flag: dashboardPreviews
// Create and show thumbnails for dashboard search results
func (ft *FeatureToggles) IsDashboardPreviewsEnabled() bool {
return ft.manager.IsEnabled("dashboardPreviews")
}
// IsLiveConfigEnabled checks for the flag: live-config
// Save grafana live configuration in SQL tables
func (ft *FeatureToggles) IsLiveConfigEnabled() bool {
return ft.manager.IsEnabled("live-config")
}
// IsLivePipelineEnabled checks for the flag: live-pipeline
// enable a generic live processing pipeline
func (ft *FeatureToggles) IsLivePipelineEnabled() bool {
return ft.manager.IsEnabled("live-pipeline")
}
// IsLiveServiceWebWorkerEnabled checks for the flag: live-service-web-worker
// This will use a webworker thread to processes events rather than the main thread
func (ft *FeatureToggles) IsLiveServiceWebWorkerEnabled() bool {
return ft.manager.IsEnabled("live-service-web-worker")
}
// IsQueryOverLiveEnabled checks for the flag: queryOverLive
// Use grafana live websocket to execute backend queries
func (ft *FeatureToggles) IsQueryOverLiveEnabled() bool {
return ft.manager.IsEnabled("queryOverLive")
}
// IsTempoSearchEnabled checks for the flag: tempoSearch
// Enable searching in tempo datasources
func (ft *FeatureToggles) IsTempoSearchEnabled() bool {
return ft.manager.IsEnabled("tempoSearch")
}
// IsTempoBackendSearchEnabled checks for the flag: tempoBackendSearch
// Use backend for tempo search
func (ft *FeatureToggles) IsTempoBackendSearchEnabled() bool {
return ft.manager.IsEnabled("tempoBackendSearch")
}
// IsTempoServiceGraphEnabled checks for the flag: tempoServiceGraph
// show service
func (ft *FeatureToggles) IsTempoServiceGraphEnabled() bool {
return ft.manager.IsEnabled("tempoServiceGraph")
}
// IsFullRangeLogsVolumeEnabled checks for the flag: fullRangeLogsVolume
// Show full range logs volume in expore
func (ft *FeatureToggles) IsFullRangeLogsVolumeEnabled() bool {
return ft.manager.IsEnabled("fullRangeLogsVolume")
}
// IsAccesscontrolEnabled checks for the flag: accesscontrol
// Support robust access control
func (ft *FeatureToggles) IsAccesscontrolEnabled() bool {
return ft.manager.IsEnabled("accesscontrol")
}
// IsPrometheusAzureAuthEnabled checks for the flag: prometheus_azure_auth
// Use azure authentication for prometheus datasource
func (ft *FeatureToggles) IsPrometheusAzureAuthEnabled() bool {
return ft.manager.IsEnabled("prometheus_azure_auth")
}
// IsNewNavigationEnabled checks for the flag: newNavigation
// Try the next gen naviation model
func (ft *FeatureToggles) IsNewNavigationEnabled() bool {
return ft.manager.IsEnabled("newNavigation")
}
// IsShowFeatureFlagsInUIEnabled checks for the flag: showFeatureFlagsInUI
// Show feature flags in the settings UI
func (ft *FeatureToggles) IsShowFeatureFlagsInUIEnabled() bool {
return ft.manager.IsEnabled("showFeatureFlagsInUI")
}
// IsDisableHttpRequestHistogramEnabled checks for the flag: disable_http_request_histogram
func (ft *FeatureToggles) IsDisableHttpRequestHistogramEnabled() bool {
return ft.manager.IsEnabled("disable_http_request_histogram")
}
@@ -0,0 +1,140 @@
package featuremgmt
import (
"bytes"
"fmt"
"html/template"
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
"unicode"
"github.com/google/go-cmp/cmp"
)
func TestFeatureToggleFiles(t *testing.T) {
// Typescript files
verifyAndGenerateFile(t,
"../../../packages/grafana-data/src/types/featureToggles.gen.ts",
generateTypeScript(),
)
// Golang files
verifyAndGenerateFile(t,
"toggles_gen.go",
generateRegistry(t),
)
}
func verifyAndGenerateFile(t *testing.T, fpath string, gen string) {
// nolint:gosec
// We can ignore the gosec G304 warning since this is a test and the function is only called explicitly above
body, err := ioutil.ReadFile(fpath)
if err == nil {
if diff := cmp.Diff(gen, string(body)); diff != "" {
str := fmt.Sprintf("body mismatch (-want +got):\n%s\n", diff)
err = fmt.Errorf(str)
}
}
if err != nil {
e2 := os.WriteFile(fpath, []byte(gen), 0644)
if e2 != nil {
t.Errorf("error writing file: %s", e2.Error())
}
abs, _ := filepath.Abs(fpath)
t.Errorf("feature toggle do not match: %s (%s)", err.Error(), abs)
t.Fail()
}
}
func generateTypeScript() string {
buf := `// NOTE: This file was auto generated. DO NOT EDIT DIRECTLY!
// To change feature flags, edit:
// pkg/services/featuremgmt/registry.go
// Then run tests in:
// pkg/services/featuremgmt/toggles_gen_test.go
/**
* Describes available feature toggles in Grafana. These can be configured via
* conf/custom.ini to enable features under development or not yet available in
* stable version.
*
* Only enabled values will be returned in this interface
*
* @public
*/
export interface FeatureToggles {
[name: string]: boolean | undefined; // support any string value
`
for _, flag := range standardFeatureFlags {
buf += " " + getTypeScriptKey(flag.Name) + "?: boolean;\n"
}
buf += "}\n"
return buf
}
func getTypeScriptKey(key string) string {
if strings.Contains(key, "-") || strings.Contains(key, ".") {
return "['" + key + "']"
}
return key
}
func isLetterOrNumber(c rune) bool {
return !unicode.IsLetter(c) && !unicode.IsNumber(c)
}
func asCamelCase(key string) string {
parts := strings.FieldsFunc(key, isLetterOrNumber)
for idx, part := range parts {
parts[idx] = strings.Title(part)
}
return strings.Join(parts, "")
}
func generateRegistry(t *testing.T) string {
tmpl, err := template.New("fn").Parse(`
// Is{{.CamleCase}}Enabled checks for the flag: {{.Flag.Name}}{{.Ext}}
func (ft *FeatureToggles) Is{{.CamleCase}}Enabled() bool {
return ft.manager.IsEnabled("{{.Flag.Name}}")
}
`)
if err != nil {
t.Fatal("error reading template", "error", err.Error())
return ""
}
data := struct {
CamleCase string
Flag FeatureFlag
Ext string
}{
CamleCase: "?",
}
var buff bytes.Buffer
buff.WriteString(`// NOTE: This file is autogenerated
package featuremgmt
`)
for _, flag := range standardFeatureFlags {
data.CamleCase = asCamelCase(flag.Name)
data.Flag = flag
data.Ext = ""
if flag.Description != "" {
data.Ext += "\n// " + flag.Description
}
_ = tmpl.Execute(&buff, data)
}
return buff.String()
}