github.com/masterhung0112/hk_server/v5@v5.0.0-20220302090640-ec71aef15e1c/config/store.go (about) 1 package config 2 3 import ( 4 "encoding/json" 5 "reflect" 6 "sync" 7 8 "github.com/pkg/errors" 9 10 "github.com/masterhung0112/hk_server/v5/model" 11 "github.com/masterhung0112/hk_server/v5/utils/jsonutils" 12 ) 13 14 var ( 15 // ErrReadOnlyStore is returned when an attempt to modify a read-only 16 // configuration store is made. 17 ErrReadOnlyStore = errors.New("configuration store is read-only") 18 ) 19 20 // Store is the higher level object that handles storing and retrieval of config data. 21 // To do so it relies on a variety of backing stores (e.g. file, database, memory). 22 type Store struct { 23 emitter 24 backingStore BackingStore 25 26 configLock sync.RWMutex 27 config *model.Config 28 configNoEnv *model.Config 29 configCustomDefaults *model.Config 30 31 readOnly bool 32 readOnlyFF bool 33 } 34 35 // BackingStore defines the behaviour exposed by the underlying store 36 // implementation (e.g. file, database). 37 type BackingStore interface { 38 // Set replaces the current configuration in its entirety and updates the backing store. 39 Set(*model.Config) error 40 41 // Load retrieves the configuration stored. If there is no configuration stored 42 // the io.ReadCloser will be nil 43 Load() ([]byte, error) 44 45 // GetFile fetches the contents of a previously persisted configuration file. 46 // If no such file exists, an empty byte array will be returned without error. 47 GetFile(name string) ([]byte, error) 48 49 // SetFile sets or replaces the contents of a configuration file. 50 SetFile(name string, data []byte) error 51 52 // HasFile returns true if the given file was previously persisted. 53 HasFile(name string) (bool, error) 54 55 // RemoveFile removes a previously persisted configuration file. 56 RemoveFile(name string) error 57 58 // String describes the backing store for the config. 59 String() string 60 61 Watch(callback func()) error 62 63 // Close cleans up resources associated with the store. 64 Close() error 65 } 66 67 // NewStoreFromBacking creates and returns a new config store given a backing store. 68 69 func NewStoreFromBacking(backingStore BackingStore, customDefaults *model.Config, readOnly bool) (*Store, error) { 70 store := &Store{ 71 backingStore: backingStore, 72 configCustomDefaults: customDefaults, 73 readOnly: readOnly, 74 readOnlyFF: true, 75 } 76 77 if err := store.Load(); err != nil { 78 return nil, errors.Wrap(err, "unable to load on store creation") 79 } 80 81 if err := backingStore.Watch(func() { 82 store.Load() 83 }); err != nil { 84 return nil, errors.Wrap(err, "failed to watch backing store") 85 } 86 87 return store, nil 88 } 89 90 // NewStoreFromDSN creates and returns a new config store backed by either a database or file store 91 // depending on the value of the given data source name string. 92 func NewStoreFromDSN(dsn string, watch, readOnly bool, customDefaults *model.Config) (*Store, error) { 93 var err error 94 var backingStore BackingStore 95 if IsDatabaseDSN(dsn) { 96 backingStore, err = NewDatabaseStore(dsn) 97 } else { 98 backingStore, err = NewFileStore(dsn, watch) 99 } 100 if err != nil { 101 return nil, err 102 } 103 104 store, err := NewStoreFromBacking(backingStore, customDefaults, readOnly) 105 if err != nil { 106 backingStore.Close() 107 return nil, errors.Wrap(err, "failed to create store") 108 } 109 110 return store, nil 111 } 112 113 // NewTestMemoryStore returns a new config store backed by a memory store 114 // to be used for testing purposes. 115 func NewTestMemoryStore() *Store { 116 memoryStore, err := NewMemoryStore() 117 if err != nil { 118 panic("failed to initialize memory store: " + err.Error()) 119 } 120 121 configStore, err := NewStoreFromBacking(memoryStore, nil, false) 122 if err != nil { 123 panic("failed to initialize config store: " + err.Error()) 124 } 125 126 return configStore 127 } 128 129 // Get fetches the current, cached configuration. 130 func (s *Store) Get() *model.Config { 131 s.configLock.RLock() 132 defer s.configLock.RUnlock() 133 return s.config 134 } 135 136 // GetNoEnv fetches the current cached configuration without environment variable overrides. 137 func (s *Store) GetNoEnv() *model.Config { 138 s.configLock.RLock() 139 defer s.configLock.RUnlock() 140 return s.configNoEnv 141 } 142 143 // GetEnvironmentOverrides fetches the configuration fields overridden by environment variables. 144 func (s *Store) GetEnvironmentOverrides() map[string]interface{} { 145 return generateEnvironmentMap(GetEnvironment(), nil) 146 } 147 148 // GetEnvironmentOverridesWithFilter fetches the configuration fields overridden by environment variables. 149 // If filter is not nil and returns false for a struct field, that field will be omitted. 150 func (s *Store) GetEnvironmentOverridesWithFilter(filter func(reflect.StructField) bool) map[string]interface{} { 151 return generateEnvironmentMap(GetEnvironment(), filter) 152 } 153 154 // RemoveEnvironmentOverrides returns a new config without the environment 155 // overrides. 156 func (s *Store) RemoveEnvironmentOverrides(cfg *model.Config) *model.Config { 157 s.configLock.RLock() 158 defer s.configLock.RUnlock() 159 return removeEnvOverrides(cfg, s.configNoEnv, s.GetEnvironmentOverrides()) 160 } 161 162 // SetReadOnlyFF sets whether feature flags should be written out to 163 // config or treated as read-only. 164 func (s *Store) SetReadOnlyFF(readOnly bool) { 165 s.configLock.Lock() 166 defer s.configLock.Unlock() 167 s.readOnlyFF = readOnly 168 } 169 170 // Set replaces the current configuration in its entirety and updates the backing store. 171 // It returns both old and new versions of the config. 172 func (s *Store) Set(newCfg *model.Config) (*model.Config, *model.Config, error) { 173 s.configLock.Lock() 174 defer s.configLock.Unlock() 175 176 if s.readOnly { 177 return nil, nil, ErrReadOnlyStore 178 } 179 180 newCfg = newCfg.Clone() 181 oldCfg := s.config.Clone() 182 oldCfgNoEnv := s.configNoEnv 183 184 // Setting defaults allows us to accept partial config objects. 185 newCfg.SetDefaults() 186 187 // Sometimes the config is received with "fake" data in sensitive fields. Apply the real 188 // data from the existing config as necessary. 189 desanitize(oldCfg, newCfg) 190 191 if err := newCfg.IsValid(); err != nil { 192 return nil, nil, errors.Wrap(err, "new configuration is invalid") 193 } 194 195 // We attempt to remove any environment override that may be present in the input config. 196 newCfgNoEnv := removeEnvOverrides(newCfg, oldCfgNoEnv, s.GetEnvironmentOverrides()) 197 198 // Don't store feature flags unless we are on MM cloud 199 // MM cloud uses config in the DB as a cache of the feature flag 200 // settings in case the management system is down when a pod starts. 201 // Backing up feature flags section in case we need to restore them later on. 202 oldCfgFF := oldCfg.FeatureFlags 203 oldCfgNoEnvFF := oldCfgNoEnv.FeatureFlags 204 // Clearing FF sections to avoid both comparing and persisting them. 205 if s.readOnlyFF { 206 oldCfg.FeatureFlags = nil 207 newCfg.FeatureFlags = nil 208 newCfgNoEnv.FeatureFlags = nil 209 } 210 211 if err := s.backingStore.Set(newCfgNoEnv); err != nil { 212 return nil, nil, errors.Wrap(err, "failed to persist") 213 } 214 215 // We apply back environment overrides since the input config may or 216 // may not have them applied. 217 newCfg = applyEnvironmentMap(newCfgNoEnv, GetEnvironment()) 218 fixConfig(newCfg) 219 if err := newCfg.IsValid(); err != nil { 220 return nil, nil, errors.Wrap(err, "new configuration is invalid") 221 } 222 223 hasChanged, err := equal(oldCfg, newCfg) 224 if err != nil { 225 return nil, nil, errors.Wrap(err, "failed to compare configs") 226 } 227 228 // We restore the previously cleared feature flags sections back. 229 if s.readOnlyFF { 230 oldCfg.FeatureFlags = oldCfgFF 231 newCfg.FeatureFlags = oldCfgFF 232 newCfgNoEnv.FeatureFlags = oldCfgNoEnvFF 233 } 234 235 s.configNoEnv = newCfgNoEnv 236 s.config = newCfg 237 238 newCfgCopy := newCfg.Clone() 239 240 if hasChanged { 241 s.configLock.Unlock() 242 s.invokeConfigListeners(oldCfg, newCfgCopy.Clone()) 243 s.configLock.Lock() 244 } 245 246 return oldCfg, newCfgCopy, nil 247 } 248 249 // Load updates the current configuration from the backing store, possibly initializing. 250 func (s *Store) Load() error { 251 s.configLock.Lock() 252 defer s.configLock.Unlock() 253 254 oldCfg := &model.Config{} 255 if s.config != nil { 256 oldCfg = s.config.Clone() 257 } 258 configBytes, err := s.backingStore.Load() 259 if err != nil { 260 return err 261 } 262 263 loadedCfg := &model.Config{} 264 if len(configBytes) != 0 { 265 if err = json.Unmarshal(configBytes, &loadedCfg); err != nil { 266 return jsonutils.HumanizeJSONError(err, configBytes) 267 } 268 } 269 270 // If we have custom defaults set, the initial config is merged on 271 // top of them and we delete them not to be used again in the 272 // configuration reloads 273 if s.configCustomDefaults != nil { 274 var mErr error 275 loadedCfg, mErr = Merge(s.configCustomDefaults, loadedCfg, nil) 276 if mErr != nil { 277 return errors.Wrap(mErr, "failed to merge custom config defaults") 278 } 279 s.configCustomDefaults = nil 280 } 281 282 // We set the SiteURL to empty (if nil) so that the following call to 283 // SetDefaults() will generate missing data. This avoids an additional write 284 // to the backing store. 285 if loadedCfg.ServiceSettings.SiteURL == nil { 286 loadedCfg.ServiceSettings.SiteURL = model.NewString("") 287 } 288 289 // Setting defaults allows us to accept partial config objects. 290 loadedCfg.SetDefaults() 291 292 // No need to clone here since the below call to applyEnvironmentMap 293 // already does that internally. 294 loadedCfgNoEnv := loadedCfg 295 fixConfig(loadedCfgNoEnv) 296 297 loadedCfg = applyEnvironmentMap(loadedCfg, GetEnvironment()) 298 fixConfig(loadedCfg) 299 if err := loadedCfg.IsValid(); err != nil { 300 return errors.Wrap(err, "invalid config") 301 } 302 303 // Backing up feature flags section in case we need to restore them later on. 304 oldCfgFF := oldCfg.FeatureFlags 305 loadedCfgFF := loadedCfg.FeatureFlags 306 loadedCfgNoEnvFF := loadedCfgNoEnv.FeatureFlags 307 // Clearing FF sections to avoid both comparing and persisting them. 308 if s.readOnlyFF { 309 oldCfg.FeatureFlags = nil 310 loadedCfg.FeatureFlags = nil 311 loadedCfgNoEnv.FeatureFlags = nil 312 } 313 // Check for changes that may have happened on load to the backing store. 314 hasChanged, err := equal(oldCfg, loadedCfg) 315 if err != nil { 316 return errors.Wrap(err, "failed to compare configs") 317 } 318 319 // We write back to the backing store only if the store is not read-only 320 // and the config has either changed or is missing. 321 if !s.readOnly && (hasChanged || len(configBytes) == 0) { 322 err := s.backingStore.Set(loadedCfgNoEnv) 323 if err != nil && !errors.Is(err, ErrReadOnlyConfiguration) { 324 return errors.Wrap(err, "failed to persist") 325 } 326 } 327 328 // We restore the previously cleared feature flags sections back. 329 if s.readOnlyFF { 330 oldCfg.FeatureFlags = oldCfgFF 331 loadedCfg.FeatureFlags = loadedCfgFF 332 loadedCfgNoEnv.FeatureFlags = loadedCfgNoEnvFF 333 } 334 335 s.config = loadedCfg 336 s.configNoEnv = loadedCfgNoEnv 337 338 loadedCfgCopy := loadedCfg.Clone() 339 340 if hasChanged { 341 s.configLock.Unlock() 342 s.invokeConfigListeners(oldCfg, loadedCfgCopy) 343 s.configLock.Lock() 344 } 345 346 return nil 347 } 348 349 // GetFile fetches the contents of a previously persisted configuration file. 350 // If no such file exists, an empty byte array will be returned without error. 351 func (s *Store) GetFile(name string) ([]byte, error) { 352 s.configLock.RLock() 353 defer s.configLock.RUnlock() 354 return s.backingStore.GetFile(name) 355 } 356 357 // SetFile sets or replaces the contents of a configuration file. 358 func (s *Store) SetFile(name string, data []byte) error { 359 s.configLock.Lock() 360 defer s.configLock.Unlock() 361 if s.readOnly { 362 return ErrReadOnlyStore 363 } 364 return s.backingStore.SetFile(name, data) 365 } 366 367 // HasFile returns true if the given file was previously persisted. 368 func (s *Store) HasFile(name string) (bool, error) { 369 s.configLock.RLock() 370 defer s.configLock.RUnlock() 371 return s.backingStore.HasFile(name) 372 } 373 374 // RemoveFile removes a previously persisted configuration file. 375 func (s *Store) RemoveFile(name string) error { 376 s.configLock.Lock() 377 defer s.configLock.Unlock() 378 if s.readOnly { 379 return ErrReadOnlyStore 380 } 381 return s.backingStore.RemoveFile(name) 382 } 383 384 // String describes the backing store for the config. 385 func (s *Store) String() string { 386 return s.backingStore.String() 387 } 388 389 // Close cleans up resources associated with the store. 390 func (s *Store) Close() error { 391 s.configLock.Lock() 392 defer s.configLock.Unlock() 393 return s.backingStore.Close() 394 } 395 396 // IsReadOnly returns whether or not the store is read-only. 397 func (s *Store) IsReadOnly() bool { 398 s.configLock.RLock() 399 defer s.configLock.RUnlock() 400 return s.readOnly 401 }