Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitaly.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'internal/featureflag/featureflag.go')
-rw-r--r--internal/featureflag/featureflag.go175
1 files changed, 175 insertions, 0 deletions
diff --git a/internal/featureflag/featureflag.go b/internal/featureflag/featureflag.go
new file mode 100644
index 000000000..0ccf7ba31
--- /dev/null
+++ b/internal/featureflag/featureflag.go
@@ -0,0 +1,175 @@
+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
+}