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