github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/model/feature/flag.go (about)

     1  package feature
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"hash/crc32"
     8  	"net/url"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/cozy/cozy-stack/model/instance"
    13  	"github.com/cozy/cozy-stack/pkg/config/config"
    14  	"github.com/cozy/cozy-stack/pkg/consts"
    15  	"github.com/cozy/cozy-stack/pkg/couchdb"
    16  	"github.com/cozy/cozy-stack/pkg/prefixer"
    17  )
    18  
    19  // Flags is a struct for a set of feature flags.
    20  type Flags struct {
    21  	DocID   string
    22  	DocRev  string
    23  	M       map[string]interface{}
    24  	Sources []*Flags
    25  }
    26  
    27  // ID is part of the couchdb.Document interface
    28  func (f *Flags) ID() string { return f.DocID }
    29  
    30  // Rev is part of the couchdb.Document interface
    31  func (f *Flags) Rev() string { return f.DocRev }
    32  
    33  // DocType is part of the couchdb.Document interface
    34  func (f *Flags) DocType() string { return consts.Settings }
    35  
    36  // SetID is part of the couchdb.Document interface
    37  func (f *Flags) SetID(id string) { f.DocID = id }
    38  
    39  // SetRev is part of the couchdb.Document interface
    40  func (f *Flags) SetRev(rev string) { f.DocRev = rev }
    41  
    42  // Clone is part of the couchdb.Document interface
    43  func (f *Flags) Clone() couchdb.Doc {
    44  	clone := Flags{DocID: f.DocID, DocRev: f.DocRev}
    45  	clone.M = make(map[string]interface{})
    46  	for k, v := range f.M {
    47  		clone.M[k] = v
    48  	}
    49  	return &clone
    50  }
    51  
    52  // MarshalJSON is used for marshalling to JSON.
    53  func (f *Flags) MarshalJSON() ([]byte, error) {
    54  	return json.Marshal(f.M)
    55  }
    56  
    57  // UnmarshalJSON is used to parse JSON.
    58  func (f *Flags) UnmarshalJSON(bytes []byte) error {
    59  	err := json.Unmarshal(bytes, &f.M)
    60  	if err != nil {
    61  		return err
    62  	}
    63  	if id, ok := f.M["_id"].(string); ok {
    64  		f.SetID(id)
    65  		delete(f.M, "_id")
    66  	}
    67  	if rev, ok := f.M["_rev"].(string); ok {
    68  		f.SetRev(rev)
    69  		delete(f.M, "_rev")
    70  	}
    71  	return nil
    72  }
    73  
    74  func (f *Flags) GetList(name string) ([]interface{}, error) {
    75  	if f.M[name] == nil {
    76  		return []interface{}{}, nil
    77  	}
    78  
    79  	value, ok := f.M[name].(map[string]interface{})
    80  	if !ok {
    81  		return nil, fmt.Errorf("Flag %s is not a list flag", name)
    82  	}
    83  
    84  	list, ok := value["list"].([]interface{})
    85  	if !ok {
    86  		return nil, fmt.Errorf("Flag %s is not a list flag", name)
    87  	}
    88  
    89  	return list, nil
    90  }
    91  
    92  func (f *Flags) HasListItem(name, item string) (bool, error) {
    93  	list, err := f.GetList(name)
    94  	if err != nil {
    95  		return false, err
    96  	}
    97  
    98  	for _, i := range list {
    99  		if i == item {
   100  			return true, nil
   101  		}
   102  	}
   103  	return false, nil
   104  }
   105  
   106  // GetFlags returns the list of feature flags for the given instance.
   107  func GetFlags(inst *instance.Instance) (*Flags, error) {
   108  	sources := make([]*Flags, 0)
   109  	m := make(map[string]interface{})
   110  	flags := &Flags{
   111  		DocID:   consts.FlagsSettingsID,
   112  		M:       m,
   113  		Sources: sources,
   114  	}
   115  	flags.addInstanceFlags(inst)
   116  	if err := flags.addManager(inst); err != nil {
   117  		inst.Logger().WithNamespace("flags").
   118  			Warnf("Cannot get the flags from the manager: %s", err)
   119  	}
   120  	if err := flags.addConfig(inst); err != nil {
   121  		inst.Logger().WithNamespace("flags").
   122  			Warnf("Cannot get the flags from the config: %s", err)
   123  	}
   124  	if err := flags.addContext(inst); err != nil {
   125  		inst.Logger().WithNamespace("flags").
   126  			Warnf("Cannot get the flags from the context: %s", err)
   127  	}
   128  	if err := flags.addDefaults(inst); err != nil {
   129  		inst.Logger().WithNamespace("flags").
   130  			Warnf("Cannot get the flags from the defaults: %s", err)
   131  	}
   132  	return flags, nil
   133  }
   134  
   135  func (f *Flags) addInstanceFlags(inst *instance.Instance) {
   136  	if len(inst.FeatureFlags) == 0 {
   137  		return
   138  	}
   139  	m := make(map[string]interface{})
   140  	for k, v := range inst.FeatureFlags {
   141  		m[k] = v
   142  	}
   143  	flags := &Flags{
   144  		DocID: consts.InstanceFlagsSettingsID,
   145  		M:     m,
   146  	}
   147  	f.Sources = append(f.Sources, flags)
   148  	for k, v := range flags.M {
   149  		if _, ok := f.M[k]; !ok {
   150  			f.M[k] = v
   151  		}
   152  	}
   153  }
   154  
   155  func (f *Flags) addManager(inst *instance.Instance) error {
   156  	if len(inst.FeatureSets) == 0 {
   157  		return nil
   158  	}
   159  	m, err := getFlagsFromManager(inst)
   160  	if err != nil || len(m) == 0 {
   161  		return err
   162  	}
   163  	flags := &Flags{
   164  		DocID: consts.ManagerFlagsSettingsID,
   165  		M:     m,
   166  	}
   167  	f.Sources = append(f.Sources, flags)
   168  	for k, v := range flags.M {
   169  		if _, ok := f.M[k]; !ok {
   170  			f.M[k] = v
   171  		}
   172  	}
   173  	return nil
   174  }
   175  
   176  var (
   177  	cacheDuration      = 12 * time.Hour
   178  	errInvalidResponse = errors.New("Invalid response from the manager")
   179  )
   180  
   181  func getFlagsFromManager(inst *instance.Instance) (map[string]interface{}, error) {
   182  	cache := config.GetConfig().CacheStorage
   183  	cacheKey := fmt.Sprintf("flags:%s:%v", inst.ContextName, inst.FeatureSets)
   184  	var flags map[string]interface{}
   185  	if buf, ok := cache.Get(cacheKey); ok {
   186  		if err := json.Unmarshal(buf, &flags); err == nil {
   187  			return flags, nil
   188  		}
   189  	}
   190  
   191  	client := instance.APIManagerClient(inst)
   192  	if client == nil {
   193  		return flags, nil
   194  	}
   195  	query := url.Values{
   196  		"sets":    {strings.Join(inst.FeatureSets, ",")},
   197  		"context": {inst.ContextName},
   198  	}.Encode()
   199  	data, err := client.Get(fmt.Sprintf("/api/v1/features?%s", query))
   200  	if err != nil {
   201  		return nil, err
   202  	}
   203  	var ok bool
   204  	if flags, ok = data["flags"].(map[string]interface{}); !ok {
   205  		return nil, errInvalidResponse
   206  	}
   207  
   208  	if buf, err := json.Marshal(flags); err == nil {
   209  		cache.Set(cacheKey, buf, cacheDuration)
   210  	}
   211  	return flags, nil
   212  }
   213  
   214  func (f *Flags) addConfig(inst *instance.Instance) error {
   215  	ctx, ok := inst.SettingsContext()
   216  	if !ok {
   217  		return nil
   218  	}
   219  	normalized := make(map[string]interface{})
   220  	if m, ok := ctx["features"].(map[string]interface{}); ok {
   221  		for k, v := range m {
   222  			normalized[k] = v
   223  		}
   224  	} else if items, ok := ctx["features"].([]interface{}); ok {
   225  		for _, item := range items {
   226  			if m, ok := item.(map[string]interface{}); ok && len(m) == 1 {
   227  				for k, v := range m {
   228  					normalized[k] = v
   229  				}
   230  			} else {
   231  				normalized[fmt.Sprintf("%v", item)] = true
   232  			}
   233  		}
   234  	} else {
   235  		return nil
   236  	}
   237  	ctxFlags := &Flags{
   238  		DocID: consts.ConfigFlagsSettingsID,
   239  		M:     normalized,
   240  	}
   241  	f.Sources = append(f.Sources, ctxFlags)
   242  	for k, v := range ctxFlags.M {
   243  		if _, ok := f.M[k]; !ok {
   244  			f.M[k] = v
   245  		}
   246  	}
   247  	return nil
   248  }
   249  
   250  func (f *Flags) addContext(inst *instance.Instance) error {
   251  	id := fmt.Sprintf("%s.%s", consts.ContextFlagsSettingsID, inst.ContextName)
   252  	var context Flags
   253  	err := couchdb.GetDoc(prefixer.GlobalPrefixer, consts.Settings, id, &context)
   254  	if couchdb.IsNotFoundError(err) {
   255  		return nil
   256  	} else if err != nil {
   257  		return err
   258  	}
   259  	if len(context.M) == 0 {
   260  		return nil
   261  	}
   262  	context.SetID(consts.ContextFlagsSettingsID)
   263  	f.Sources = append(f.Sources, &context)
   264  	for k, v := range context.M {
   265  		if _, ok := f.M[k]; !ok {
   266  			if value := applyRatio(inst, k, v); value != nil {
   267  				f.M[k] = value
   268  			}
   269  		}
   270  	}
   271  	return nil
   272  }
   273  
   274  const maxUint32 = 1<<32 - 1
   275  
   276  func applyRatio(inst *instance.Instance, key string, data interface{}) interface{} {
   277  	items, ok := data.([]interface{})
   278  	if !ok || len(items) == 0 {
   279  		return nil
   280  	}
   281  	sum := crc32.ChecksumIEEE([]byte(fmt.Sprintf("%s:%s", inst.DocID, key)))
   282  	for i := range items {
   283  		item, ok := items[i].(map[string]interface{})
   284  		if !ok {
   285  			continue
   286  		}
   287  		ratio, ok := item["ratio"].(float64)
   288  		if !ok || ratio == 0.0 {
   289  			continue
   290  		}
   291  		if ratio == 1.0 {
   292  			return item["value"]
   293  		}
   294  		computed := uint32(ratio * maxUint32)
   295  		if computed >= sum {
   296  			return item["value"]
   297  		}
   298  		sum -= computed
   299  	}
   300  	return nil
   301  }
   302  
   303  func (f *Flags) addDefaults(inst *instance.Instance) error {
   304  	var defaults Flags
   305  	err := couchdb.GetDoc(prefixer.GlobalPrefixer, consts.Settings, consts.DefaultFlagsSettingsID, &defaults)
   306  	if couchdb.IsNotFoundError(err) {
   307  		return nil
   308  	} else if err != nil {
   309  		return err
   310  	}
   311  	if len(defaults.M) == 0 {
   312  		return nil
   313  	}
   314  	defaults.SetID(consts.DefaultFlagsSettingsID)
   315  	f.Sources = append(f.Sources, &defaults)
   316  	for k, v := range defaults.M {
   317  		if _, ok := f.M[k]; !ok {
   318  			f.M[k] = v
   319  		}
   320  	}
   321  	return nil
   322  }
   323  
   324  var _ couchdb.Doc = &Flags{}