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  }