internal/featureflag/featureflag.go (105 lines of code) (raw):
package featureflag
import (
"context"
"fmt"
"regexp"
"strconv"
"strings"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"gitlab.com/gitlab-org/gitaly/v16/internal/helper/env"
"google.golang.org/grpc/metadata"
)
var (
// EnableAllFeatureFlagsEnvVar will cause Gitaly to treat all feature flags as
// enabled in case its value is set to `true`. Only used for testing purposes.
EnableAllFeatureFlagsEnvVar = "GITALY_TESTING_ENABLE_ALL_FEATURE_FLAGS"
// featureFlagsOverride allows to enable all feature flags with a
// single environment variable. If the value of
// GITALY_TESTING_ENABLE_ALL_FEATURE_FLAGS is set to "true", then all
// feature flags will be enabled. This is only used for testing
// purposes such that we can run integration tests with feature flags.
featureFlagsOverride, _ = env.GetBool(EnableAllFeatureFlagsEnvVar, false)
flagChecks = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "gitaly_feature_flag_checks_total",
Help: "Number of enabled/disabled checks for Gitaly server side feature flags",
},
[]string{"flag", "enabled"},
)
// flagsByName is the set of defined feature flags mapped by their respective name.
flagsByName = map[string]FeatureFlag{}
)
// Feature flags must contain at least 2 characters. Can only contain lowercase letters,
// digits, and '_'. They must start with a letter, and cannot end with '_'.
// Feature flag name would be used to construct the corresponding metadata key, so:
// - Only characters allowed by grpc metadata keys can be used and uppercase letters
// would be normalized to lowercase, see
// https://pkg.go.dev/google.golang.org/grpc/metadata#New
// - It is critical that feature flags don't contain a dash, because the client converts
// dashes to underscores when converting a feature flag's name to the metadata key,
// and vice versa. The name wouldn't round-trip in case it had underscores and must
// thus use dashes instead.
var ffNameRegexp = regexp.MustCompile(`^[a-z][a-z0-9_]*[a-z0-9]$`)
const (
// ffPrefix is the prefix used for Gitaly-scoped feature flags.
ffPrefix = "gitaly-feature-"
)
// DefinedFlags returns the set of feature flags that have been explicitly defined.
func DefinedFlags() []FeatureFlag {
flags := make([]FeatureFlag, 0, len(flagsByName))
for _, flag := range flagsByName {
flags = append(flags, flag)
}
return flags
}
// FeatureFlag gates the implementation of new or changed functionality.
type FeatureFlag struct {
// Name is the name of the feature flag.
Name string `json:"name"`
// OnByDefault is the default value if the feature flag is not explicitly set in
// the incoming context.
OnByDefault bool `json:"on_by_default"`
}
// NewFeatureFlag creates a new feature flag and adds it to the array of all existing feature flags.
// The name must be of the format `some_feature_flag`. Accepts a version and rollout issue URL as
// input that are not used for anything but only for the sake of linking to the feature flag rollout
// issue in the Gitaly project.
func NewFeatureFlag(name, version, rolloutIssueURL string, onByDefault bool) FeatureFlag {
if !ffNameRegexp.MatchString(name) {
panic("invalid feature flag name.")
}
featureFlag := FeatureFlag{
Name: name,
OnByDefault: onByDefault,
}
flagsByName[name] = featureFlag
return featureFlag
}
// FromMetadataKey parses the given gRPC metadata key into a Gitaly feature flag and performs the
// necessary conversions. Returns an error in case the metadata does not refer to a feature flag.
//
// This function tries to look up the default value via our set of flag definitions. In case the
// flag definition is unknown to Gitaly it assumes a default value of `false`.
func FromMetadataKey(metadataKey string) (FeatureFlag, error) {
if !strings.HasPrefix(metadataKey, ffPrefix) {
return FeatureFlag{}, fmt.Errorf("not a feature flag: %q", metadataKey)
}
flagName := strings.TrimPrefix(metadataKey, ffPrefix)
flagName = strings.ReplaceAll(flagName, "-", "_")
flag, ok := flagsByName[flagName]
if !ok {
flag = FeatureFlag{
Name: flagName,
OnByDefault: false,
}
}
return flag, nil
}
// FormatWithValue converts the feature flag into a string with the given state. Note that this
// function uses the feature flag name and not the raw metadata key as used in gRPC metadata.
func (ff FeatureFlag) FormatWithValue(enabled bool) string {
return fmt.Sprintf("%s:%v", ff.Name, enabled)
}
// IsEnabled checks if the feature flag is enabled for the passed context.
// Only returns true if the metadata for the feature flag is set to "true"
func (ff FeatureFlag) IsEnabled(ctx context.Context) bool {
if featureFlagsOverride {
return true
}
val, ok := ff.valueFromContext(ctx)
if !ok {
if md, ok := metadata.FromIncomingContext(ctx); ok {
if _, ok := md[explicitFeatureFlagKey]; ok {
panic(fmt.Sprintf("checking for feature %q without use of feature sets", ff.Name))
}
}
return ff.OnByDefault
}
enabled := val == "true"
flagChecks.WithLabelValues(ff.Name, strconv.FormatBool(enabled)).Inc()
return enabled
}
// IsDisabled determines whether the feature flag is disabled in the incoming context.
func (ff FeatureFlag) IsDisabled(ctx context.Context) bool {
return !ff.IsEnabled(ctx)
}
// MetadataKey returns the key of the feature flag as it is present in the metadata map.
func (ff FeatureFlag) MetadataKey() string {
return ffPrefix + strings.ReplaceAll(ff.Name, "_", "-")
}
func (ff FeatureFlag) valueFromContext(ctx context.Context) (string, bool) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return "", false
}
val, ok := md[ff.MetadataKey()]
if !ok {
return "", false
}
if len(val) == 0 {
return "", false
}
return val[0], true
}