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{}