-
Notifications
You must be signed in to change notification settings - Fork 7.2k
/
store.go
287 lines (223 loc) · 7.81 KB
/
store.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package config
import (
"bytes"
"encoding/json"
"strings"
"sync"
"github.com/mattermost/mattermost-server/v5/model"
"github.com/mattermost/mattermost-server/v5/utils/jsonutils"
"github.com/pkg/errors"
)
// Listener is a callback function invoked when the configuration changes.
type Listener func(oldConfig *model.Config, newConfig *model.Config)
type BackingStore interface {
// Set replaces the current configuration in its entirety and updates the backing store.
Set(*model.Config) error
// Load retrieves the configuration stored. If there is no configuration stored
// the io.ReadCloser will be nil
Load() ([]byte, error)
// GetFile fetches the contents of a previously persisted configuration file.
// If no such file exists, an empty byte array will be returned without error.
GetFile(name string) ([]byte, error)
// SetFile sets or replaces the contents of a configuration file.
SetFile(name string, data []byte) error
// HasFile returns true if the given file was previously persisted.
HasFile(name string) (bool, error)
// RemoveFile removes a previously persisted configuration file.
RemoveFile(name string) error
// String describes the backing store for the config.
String() string
Watch(callback func()) error
// Close cleans up resources associated with the store.
Close() error
}
// NewStore creates a database or file store given a data source name by which to connect.
func NewStore(dsn string, watch bool) (*Store, error) {
backingStore, err := getBackingStore(dsn, watch)
if err != nil {
return nil, err
}
return NewStoreFromBacking(backingStore)
}
func NewStoreFromBacking(backingStore BackingStore) (*Store, error) {
store := &Store{
backingStore: backingStore,
}
if err := store.Load(); err != nil {
return nil, errors.Wrap(err, "unable to load on store creation")
}
if err := backingStore.Watch(func() {
store.Load()
}); err != nil {
return nil, errors.Wrap(err, "failed to watch backing store")
}
return store, nil
}
func getBackingStore(dsn string, watch bool) (BackingStore, error) {
if strings.HasPrefix(dsn, "mysql://") || strings.HasPrefix(dsn, "postgres://") {
return NewDatabaseStore(dsn)
}
return NewFileStore(dsn, watch)
}
func NewTestMemoryStore() *Store {
memoryStore, err := NewMemoryStore()
if err != nil {
panic("failed to initialize memory store: " + err.Error())
}
configStore, err := NewStoreFromBacking(memoryStore)
if err != nil {
panic("failed to initialize config store: " + err.Error())
}
return configStore
}
type Store struct {
emitter
backingStore BackingStore
configLock sync.RWMutex
config *model.Config
configNoEnv *model.Config
persistFeatureFlags bool
}
// Get fetches the current, cached configuration.
func (s *Store) Get() *model.Config {
s.configLock.RLock()
defer s.configLock.RUnlock()
return s.config
}
// Get fetches the current, cached configuration without environment variable overrides.
func (s *Store) GetNoEnv() *model.Config {
s.configLock.RLock()
defer s.configLock.RUnlock()
return s.configNoEnv
}
// GetEnvironmentOverrides fetches the configuration fields overridden by environment variables.
func (s *Store) GetEnvironmentOverrides() map[string]interface{} {
return generateEnvironmentMap(GetEnvironment())
}
// RemoveEnvironmentOverrides returns a new config without the environment
// overrides
func (s *Store) RemoveEnvironmentOverrides(cfg *model.Config) *model.Config {
s.configLock.RLock()
defer s.configLock.RUnlock()
return removeEnvOverrides(cfg, s.configNoEnv, s.GetEnvironmentOverrides())
}
// PersistFeatures sets if the store should persist feature flags.
func (s *Store) PersistFeatures(persist bool) {
s.configLock.Lock()
defer s.configLock.Unlock()
s.persistFeatureFlags = persist
}
// Set replaces the current configuration in its entirety and updates the backing store.
func (s *Store) Set(newCfg *model.Config) (*model.Config, error) {
s.configLock.Lock()
var unlockOnce sync.Once
defer unlockOnce.Do(s.configLock.Unlock)
oldCfg := s.config.Clone()
// Really just for some tests we need to set defaults here
newCfg.SetDefaults()
// Sometimes the config is received with "fake" data in sensitive fields. Apply the real
// data from the existing config as necessary.
desanitize(oldCfg, newCfg)
if err := newCfg.IsValid(); err != nil {
return nil, errors.Wrap(err, "new configuration is invalid")
}
newCfg = removeEnvOverrides(newCfg, s.configNoEnv, s.GetEnvironmentOverrides())
// Don't persist feature flags unless we are on MM cloud
// MM cloud uses config in the DB as a cache of the feature flag
// settings in case the managment system is down when a pod starts.
if !s.persistFeatureFlags {
newCfg.FeatureFlags = nil
}
if err := s.backingStore.Set(newCfg); err != nil {
return nil, errors.Wrap(err, "failed to persist")
}
if err := s.loadLockedWithOld(oldCfg, &unlockOnce); err != nil {
return nil, errors.Wrap(err, "failed to load on save")
}
return oldCfg, nil
}
func (s *Store) loadLockedWithOld(oldCfg *model.Config, unlockOnce *sync.Once) error {
configBytes, err := s.backingStore.Load()
if err != nil {
return err
}
loadedConfig := &model.Config{}
if len(configBytes) != 0 {
if err = json.Unmarshal(configBytes, &loadedConfig); err != nil {
return jsonutils.HumanizeJsonError(err, configBytes)
}
}
loadedConfig.SetDefaults()
s.configNoEnv = loadedConfig.Clone()
fixConfig(s.configNoEnv)
loadedConfig = applyEnvironmentMap(loadedConfig, GetEnvironment())
fixConfig(loadedConfig)
if err := loadedConfig.IsValid(); err != nil {
return errors.Wrap(err, "invalid config")
}
// Apply changes that may have happened on load to the backing store.
oldCfgBytes, err := json.Marshal(oldCfg)
if err != nil {
return errors.Wrap(err, "failed to marshal old config")
}
newCfgBytes, err := json.Marshal(loadedConfig)
if err != nil {
return errors.Wrap(err, "failed to marshal loaded config")
}
if len(configBytes) == 0 || !bytes.Equal(oldCfgBytes, newCfgBytes) {
if err := s.backingStore.Set(s.configNoEnv); err != nil {
if !errors.Is(err, ErrReadOnlyConfiguration) {
return errors.Wrap(err, "failed to persist")
}
}
}
s.config = loadedConfig
unlockOnce.Do(s.configLock.Unlock)
s.invokeConfigListeners(oldCfg, loadedConfig)
return nil
}
// Load updates the current configuration from the backing store, possibly initializing.
func (s *Store) Load() error {
s.configLock.Lock()
var unlockOnce sync.Once
defer unlockOnce.Do(s.configLock.Unlock)
oldCfg := s.config.Clone()
return s.loadLockedWithOld(oldCfg, &unlockOnce)
}
// GetFile fetches the contents of a previously persisted configuration file.
// If no such file exists, an empty byte array will be returned without error.
func (s *Store) GetFile(name string) ([]byte, error) {
s.configLock.RLock()
defer s.configLock.RUnlock()
return s.backingStore.GetFile(name)
}
// SetFile sets or replaces the contents of a configuration file.
func (s *Store) SetFile(name string, data []byte) error {
s.configLock.Lock()
defer s.configLock.Unlock()
return s.backingStore.SetFile(name, data)
}
// HasFile returns true if the given file was previously persisted.
func (s *Store) HasFile(name string) (bool, error) {
s.configLock.RLock()
defer s.configLock.RUnlock()
return s.backingStore.HasFile(name)
}
// RemoveFile removes a previously persisted configuration file.
func (s *Store) RemoveFile(name string) error {
s.configLock.Lock()
defer s.configLock.Unlock()
return s.backingStore.RemoveFile(name)
}
// String describes the backing store for the config.
func (s *Store) String() string {
return s.backingStore.String()
}
// Close cleans up resources associated with the store.
func (s *Store) Close() error {
s.configLock.Lock()
defer s.configLock.Unlock()
return s.backingStore.Close()
}