github.com/haalcala/mattermost-server-change-repo@v0.0.0-20210713015153-16753fbeee5f/config/feature_flags.go (about)

     1  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
     2  // See LICENSE.txt for license information.
     3  
     4  package config
     5  
     6  import (
     7  	"math"
     8  	"reflect"
     9  	"strconv"
    10  	"strings"
    11  
    12  	"github.com/pkg/errors"
    13  	"github.com/splitio/go-client/v6/splitio/client"
    14  	"github.com/splitio/go-client/v6/splitio/conf"
    15  
    16  	"github.com/mattermost/mattermost-server/v5/mlog"
    17  	"github.com/mattermost/mattermost-server/v5/model"
    18  )
    19  
    20  type FeatureFlagSyncParams struct {
    21  	ServerID            string
    22  	SplitKey            string
    23  	SyncIntervalSeconds int
    24  	Log                 *mlog.Logger
    25  }
    26  
    27  type FeatureFlagSynchronizer struct {
    28  	FeatureFlagSyncParams
    29  
    30  	client  *client.SplitClient
    31  	stop    chan struct{}
    32  	stopped chan struct{}
    33  }
    34  
    35  var featureNames = getStructFields(model.FeatureFlags{})
    36  
    37  func NewFeatureFlagSynchronizer(params FeatureFlagSyncParams) (*FeatureFlagSynchronizer, error) {
    38  	cfg := conf.Default()
    39  	if params.Log != nil {
    40  		cfg.Logger = &splitLogger{wrappedLog: params.Log.With(mlog.String("service", "split"))}
    41  	} else {
    42  		cfg.LoggerConfig.LogLevel = math.MinInt32
    43  	}
    44  	factory, err := client.NewSplitFactory(params.SplitKey, cfg)
    45  	if err != nil {
    46  		return nil, errors.Wrap(err, "unable to create split factory")
    47  	}
    48  
    49  	return &FeatureFlagSynchronizer{
    50  		FeatureFlagSyncParams: params,
    51  		client:                factory.Client(),
    52  		stop:                  make(chan struct{}),
    53  		stopped:               make(chan struct{}),
    54  	}, nil
    55  }
    56  
    57  // ensureReady blocks until the syncronizer is ready to update feature flag values
    58  func (f *FeatureFlagSynchronizer) EnsureReady() error {
    59  	if err := f.client.BlockUntilReady(10); err != nil {
    60  		return errors.Wrap(err, "split.io client could not initialize")
    61  	}
    62  
    63  	return nil
    64  }
    65  
    66  func (f *FeatureFlagSynchronizer) UpdateFeatureFlagValues(base model.FeatureFlags) model.FeatureFlags {
    67  	featuresMap := f.client.Treatments(f.ServerID, featureNames, nil)
    68  	ffm := featureFlagsFromMap(featuresMap, base)
    69  	return ffm
    70  }
    71  
    72  func (f *FeatureFlagSynchronizer) Close() {
    73  	f.client.Destroy()
    74  }
    75  
    76  // featureFlagsFromMap sets the feature flags from a map[string]string.
    77  // It starts with baseFeatureFlags and only sets values that are
    78  // given by the upstream management system.
    79  // Makes the assumption that all feature flags are strings or booleans.
    80  // Strings are converted to booleans by considering case insensitive "on" or any value considered by strconv.ParseBool as true and any other value as false.
    81  func featureFlagsFromMap(featuresMap map[string]string, baseFeatureFlags model.FeatureFlags) model.FeatureFlags {
    82  	refStruct := reflect.ValueOf(&baseFeatureFlags).Elem()
    83  	for fieldName, fieldValue := range featuresMap {
    84  		refField := refStruct.FieldByName(fieldName)
    85  		// "control" is returned by split.io if the treatment is not found, in this case we should use the default value.
    86  		if !refField.IsValid() || !refField.CanSet() || fieldValue == "control" {
    87  			continue
    88  		}
    89  
    90  		switch refField.Type().Kind() {
    91  		case reflect.Bool:
    92  			parsedBoolValue, _ := strconv.ParseBool(fieldValue)
    93  			refField.Set(reflect.ValueOf(strings.ToLower(fieldValue) == "on" || parsedBoolValue))
    94  		default:
    95  			refField.Set(reflect.ValueOf(fieldValue))
    96  		}
    97  
    98  	}
    99  	return baseFeatureFlags
   100  }
   101  
   102  // featureFlagsToMap returns the feature flags as a map[string]string
   103  // Supports boolean and string feature flags.
   104  func featureFlagsToMap(featureFlags *model.FeatureFlags) map[string]string {
   105  	refStructVal := reflect.ValueOf(*featureFlags)
   106  	refStructType := reflect.TypeOf(*featureFlags)
   107  	ret := make(map[string]string)
   108  	for i := 0; i < refStructVal.NumField(); i++ {
   109  		refFieldVal := refStructVal.Field(i)
   110  		refFieldType := refStructType.Field(i)
   111  		if !refFieldVal.IsValid() {
   112  			continue
   113  		}
   114  		switch refFieldType.Type.Kind() {
   115  		case reflect.Bool:
   116  			ret[refFieldType.Name] = strconv.FormatBool(refFieldVal.Bool())
   117  		default:
   118  			ret[refFieldType.Name] = refFieldVal.String()
   119  		}
   120  	}
   121  
   122  	return ret
   123  }
   124  
   125  func getStructFields(s interface{}) []string {
   126  	structType := reflect.TypeOf(s)
   127  	fieldNames := make([]string, 0, structType.NumField())
   128  	for i := 0; i < structType.NumField(); i++ {
   129  		fieldNames = append(fieldNames, structType.Field(i).Name)
   130  	}
   131  
   132  	return fieldNames
   133  }