[MM-28692] Include config diffs in audit record for config changing API calls (#17623)

* Replace config generator

* Cleanup

* Some renaming and docs additions to add clarity

* Cleanup logging related methods

* Cleanup emitter

* Fix TestDefaultsGenerator

* Move feature flags synchronization logic out of config package

* Remove unnecessary util functions

* Simplify load/set logic

* Refine semantics and add some test to cover them

* Remove unnecessary deep copies

* Improve logic further

* Fix license header

* Review file store tests

* Fix test

* Fix test

* Avoid additional write during initialization

* More consistent naming

* Update app/feature_flags.go

Co-authored-by: Christopher Speller <crspeller@gmail.com>

* Update config/store.go

Co-authored-by: Christopher Speller <crspeller@gmail.com>

* Update config/store.go

Co-authored-by: Christopher Speller <crspeller@gmail.com>

* Update config/store.go

Co-authored-by: Ibrahim Serdar Acikgoz <serdaracikgoz86@gmail.com>

* Make ConfigStore.Set() return both old and new configs

* Implement config diff function

* Make app.SaveConfig return previous and current configs

* Add config diff to audit record

* Fix returned configs

* Include high level test

* Move FF synchronizer to its own package

* Remove unidiomatic use of sync.Once

* Add some comments

* Rename function

* More comment

* Save config diff in audit record for local endpoints

* Enable audit for config set/reset commands

* Improve tests output

Co-authored-by: Christopher Speller <crspeller@gmail.com>
Co-authored-by: Ibrahim Serdar Acikgoz <serdaracikgoz86@gmail.com>
Этот коммит содержится в:
Claudio Costa
2021-05-21 09:04:39 +02:00
коммит произвёл GitHub
родитель ca9d8ab0a4
Коммит e1b13c10fc
23 изменённых файлов: 841 добавлений и 136 удалений

Просмотреть файл

@@ -160,7 +160,7 @@ func TestConfigEnvironmentOverrides(t *testing.T) {
})
t.Run("setting config should respect environment variable overrides", func(t *testing.T) {
_, err := base.Set(originalConfig)
_, _, err := base.Set(originalConfig)
require.NoError(t, err)
assert.Equal(t, "http://overridden.ca", *base.Get().ServiceSettings.SiteURL)

Просмотреть файл

@@ -420,7 +420,7 @@ func TestDatabaseStoreSet(t *testing.T) {
require.NoError(t, err)
defer ds.Close()
_, err = ds.Set(ds.Get())
_, _, err = ds.Set(ds.Get())
if assert.Error(t, err) {
assert.EqualError(t, err, "old configuration modified instead of cloning")
}
@@ -436,7 +436,7 @@ func TestDatabaseStoreSet(t *testing.T) {
newCfg := &model.Config{}
_, err = ds.Set(newCfg)
_, _, err = ds.Set(newCfg)
require.NoError(t, err)
assert.Equal(t, "", *ds.Get().ServiceSettings.SiteURL)
@@ -453,7 +453,7 @@ func TestDatabaseStoreSet(t *testing.T) {
newCfg := &model.Config{}
newCfg.LdapSettings.BindPassword = model.NewString(model.FAKE_SETTING)
_, err = ds.Set(newCfg)
_, _, err = ds.Set(newCfg)
require.NoError(t, err)
assert.Equal(t, "password", *ds.Get().LdapSettings.BindPassword)
@@ -470,7 +470,7 @@ func TestDatabaseStoreSet(t *testing.T) {
newCfg := &model.Config{}
newCfg.ServiceSettings.SiteURL = model.NewString("invalid")
_, err = ds.Set(newCfg)
_, _, err = ds.Set(newCfg)
if assert.Error(t, err) {
assert.EqualError(t, err, "new configuration is invalid: Config.IsValid: model.config.is_valid.site_url.app_error, ")
}
@@ -486,11 +486,11 @@ func TestDatabaseStoreSet(t *testing.T) {
require.NoError(t, err)
defer ds.Close()
_, err = ds.Set(ds.Get())
_, _, err = ds.Set(ds.Get())
require.NoError(t, err)
beforeID, _ := getActualDatabaseConfig(t)
_, err = ds.Set(ds.Get())
_, _, err = ds.Set(ds.Get())
require.NoError(t, err)
afterID, _ := getActualDatabaseConfig(t)
@@ -511,7 +511,7 @@ func TestDatabaseStoreSet(t *testing.T) {
},
}
_, err = ds.Set(newCfg)
_, _, err = ds.Set(newCfg)
require.NoError(t, err)
assert.Equal(t, "http://new", *ds.Get().ServiceSettings.SiteURL)
@@ -531,7 +531,7 @@ func TestDatabaseStoreSet(t *testing.T) {
},
}
_, err = ds.Set(newCfg)
_, _, err = ds.Set(newCfg)
require.NoError(t, err)
err = ds.Load()
@@ -555,7 +555,7 @@ func TestDatabaseStoreSet(t *testing.T) {
newCfg := minimalConfig
_, err = ds.Set(newCfg)
_, _, err = ds.Set(newCfg)
require.Error(t, err)
assert.True(t, strings.HasPrefix(err.Error(), "failed to persist: failed to query active configuration"), "unexpected error: "+err.Error())
@@ -577,7 +577,7 @@ func TestDatabaseStoreSet(t *testing.T) {
newCfg := emptyConfig.Clone()
newCfg.ServiceSettings.SiteURL = model.NewString(longSiteURL)
_, err = ds.Set(newCfg)
_, _, err = ds.Set(newCfg)
require.Error(t, err)
assert.True(t, strings.HasPrefix(err.Error(), "failed to persist: marshalled configuration failed length check: value is too long"), "unexpected error: "+err.Error())
})
@@ -598,7 +598,7 @@ func TestDatabaseStoreSet(t *testing.T) {
newCfg := minimalConfig
_, err = ds.Set(newCfg)
_, _, err = ds.Set(newCfg)
require.NoError(t, err)
id, _ := getActualDatabaseConfig(t)
@@ -616,7 +616,7 @@ func TestDatabaseStoreSet(t *testing.T) {
defer ds.Close()
ds.SetReadOnlyFF(true)
_, err = ds.Set(minimalConfig)
_, _, err = ds.Set(minimalConfig)
require.NoError(t, err)
assert.Equal(t, "http://minimal", *ds.Get().ServiceSettings.SiteURL)
@@ -633,7 +633,7 @@ func TestDatabaseStoreSet(t *testing.T) {
ds.SetReadOnlyFF(false)
_, err = ds.Set(minimalConfig)
_, _, err = ds.Set(minimalConfig)
require.NoError(t, err)
assert.Equal(t, "http://minimal", *ds.Get().ServiceSettings.SiteURL)
@@ -692,7 +692,7 @@ func TestDatabaseStoreLoad(t *testing.T) {
require.NoError(t, err)
defer ds.Close()
_, err = ds.Set(ds.Get())
_, _, err = ds.Set(ds.Get())
require.NoError(t, err)
assert.Equal(t, "http://overridePersistEnvVariables", *ds.Get().ServiceSettings.SiteURL)
@@ -715,7 +715,7 @@ func TestDatabaseStoreLoad(t *testing.T) {
assert.Equal(t, true, *ds.Get().PluginSettings.EnableUploads)
_, err = ds.Set(ds.Get())
_, _, err = ds.Set(ds.Get())
require.NoError(t, err)
assert.Equal(t, true, *ds.Get().PluginSettings.EnableUploads)
@@ -738,7 +738,7 @@ func TestDatabaseStoreLoad(t *testing.T) {
assert.Equal(t, 3000, *ds.Get().TeamSettings.MaxUsersPerTeam)
_, err = ds.Set(ds.Get())
_, _, err = ds.Set(ds.Get())
require.NoError(t, err)
assert.Equal(t, 3000, *ds.Get().TeamSettings.MaxUsersPerTeam)
@@ -761,7 +761,7 @@ func TestDatabaseStoreLoad(t *testing.T) {
assert.Equal(t, int64(123456), *ds.Get().ServiceSettings.TLSStrictTransportMaxAge)
_, err = ds.Set(ds.Get())
_, _, err = ds.Set(ds.Get())
require.NoError(t, err)
assert.Equal(t, int64(123456), *ds.Get().ServiceSettings.TLSStrictTransportMaxAge)
@@ -784,7 +784,7 @@ func TestDatabaseStoreLoad(t *testing.T) {
assert.Equal(t, []string{"user:pwd@db:5432/test-db"}, ds.Get().SqlSettings.DataSourceReplicas)
_, err = ds.Set(ds.Get())
_, _, err = ds.Set(ds.Get())
require.NoError(t, err)
assert.Equal(t, []string{"user:pwd@db:5432/test-db"}, ds.Get().SqlSettings.DataSourceReplicas)
@@ -809,7 +809,7 @@ func TestDatabaseStoreLoad(t *testing.T) {
assert.Equal(t, []string{"user:pwd@db:5432/test-db"}, ds.Get().SqlSettings.DataSourceReplicas)
_, err = ds.Set(ds.Get())
_, _, err = ds.Set(ds.Get())
require.NoError(t, err)
assert.Equal(t, []string{"user:pwd@db:5432/test-db"}, ds.Get().SqlSettings.DataSourceReplicas)

90
config/diff.go Обычный файл
Просмотреть файл

@@ -0,0 +1,90 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package config
import (
"fmt"
"reflect"
"github.com/mattermost/mattermost-server/v5/model"
)
type ConfigDiffs []ConfigDiff
type ConfigDiff struct {
Path string `json:"path"`
BaseVal interface{} `json:"base_val"`
ActualVal interface{} `json:"actual_val"`
}
func diff(base, actual reflect.Value, label string) ([]ConfigDiff, error) {
var diffs []ConfigDiff
if base.IsZero() && actual.IsZero() {
return diffs, nil
}
if base.IsZero() || actual.IsZero() {
return append(diffs, ConfigDiff{
Path: label,
BaseVal: base.Interface(),
ActualVal: actual.Interface(),
}), nil
}
baseType := base.Type()
actualType := actual.Type()
if baseType.Kind() == reflect.Ptr {
base = reflect.Indirect(base)
actual = reflect.Indirect(actual)
baseType = base.Type()
actualType = actual.Type()
}
if baseType != actualType {
return nil, fmt.Errorf("not same type %s %s", baseType, actualType)
}
switch baseType.Kind() {
case reflect.Struct:
if base.NumField() != actual.NumField() {
return nil, fmt.Errorf("not same number of fields in struct")
}
for i := 0; i < base.NumField(); i++ {
fieldLabel := baseType.Field(i).Name
if label != "" {
fieldLabel = label + "." + fieldLabel
}
d, err := diff(base.Field(i), actual.Field(i), fieldLabel)
if err != nil {
return nil, err
}
diffs = append(diffs, d...)
}
default:
if !reflect.DeepEqual(base.Interface(), actual.Interface()) {
diffs = append(diffs, ConfigDiff{
Path: label,
BaseVal: base.Interface(),
ActualVal: actual.Interface(),
})
}
}
return diffs, nil
}
func Diff(base, actual *model.Config) (ConfigDiffs, error) {
if base == nil || actual == nil {
return nil, fmt.Errorf("input configs should not be nil")
}
baseVal := reflect.Indirect(reflect.ValueOf(base))
actualVal := reflect.Indirect(reflect.ValueOf(actual))
return diff(baseVal, actualVal, "")
}
func (cd ConfigDiffs) String() string {
return fmt.Sprintf("%+v", []ConfigDiff(cd))
}

466
config/diff_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,466 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package config
import (
"testing"
"github.com/mattermost/mattermost-server/v5/model"
"github.com/stretchr/testify/require"
)
func defaultConfigGen() *model.Config {
cfg := &model.Config{}
cfg.SetDefaults()
return cfg
}
func BenchmarkDiff(b *testing.B) {
b.Run("equal empty", func(b *testing.B) {
baseCfg := &model.Config{}
actualCfg := &model.Config{}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = Diff(baseCfg, actualCfg)
}
})
b.Run("equal with defaults", func(b *testing.B) {
baseCfg := defaultConfigGen()
actualCfg := defaultConfigGen()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = Diff(baseCfg, actualCfg)
}
})
b.Run("actual empty", func(b *testing.B) {
baseCfg := defaultConfigGen()
actualCfg := &model.Config{}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = Diff(baseCfg, actualCfg)
}
})
b.Run("base empty", func(b *testing.B) {
baseCfg := &model.Config{}
actualCfg := defaultConfigGen()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = Diff(baseCfg, actualCfg)
}
})
b.Run("some diffs", func(b *testing.B) {
baseCfg := defaultConfigGen()
actualCfg := defaultConfigGen()
baseCfg.ServiceSettings.SiteURL = model.NewString("http://localhost")
baseCfg.ServiceSettings.ReadTimeout = model.NewInt(300)
baseCfg.SqlSettings.QueryTimeout = model.NewInt(0)
actualCfg.PluginSettings.EnableUploads = nil
actualCfg.TeamSettings.MaxChannelsPerTeam = model.NewInt64(100000)
actualCfg.FeatureFlags = nil
actualCfg.SqlSettings.DataSourceReplicas = []string{
"ds0",
"ds1",
"ds2",
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = Diff(baseCfg, actualCfg)
}
})
}
func TestDiff(t *testing.T) {
tcs := []struct {
name string
base *model.Config
actual *model.Config
diffs ConfigDiffs
err string
}{
{
"nil",
nil,
nil,
nil,
"input configs should not be nil",
},
{
"empty",
&model.Config{},
&model.Config{},
nil,
"",
},
{
"defaults",
defaultConfigGen(),
defaultConfigGen(),
nil,
"",
},
{
"default base, actual empty",
defaultConfigGen(),
&model.Config{},
ConfigDiffs{
{
"",
*defaultConfigGen(),
model.Config{},
},
},
"",
},
{
"empty base, actual default",
&model.Config{},
defaultConfigGen(),
ConfigDiffs{
{
"",
model.Config{},
*defaultConfigGen(),
},
},
"",
},
{
"string change",
defaultConfigGen(),
func() *model.Config {
cfg := defaultConfigGen()
cfg.ServiceSettings.SiteURL = model.NewString("http://changed")
return cfg
}(),
ConfigDiffs{
{
Path: "ServiceSettings.SiteURL",
BaseVal: *defaultConfigGen().ServiceSettings.SiteURL,
ActualVal: "http://changed",
},
},
"",
},
{
"string nil",
defaultConfigGen(),
func() *model.Config {
cfg := defaultConfigGen()
cfg.ServiceSettings.SiteURL = nil
return cfg
}(),
ConfigDiffs{
{
Path: "ServiceSettings.SiteURL",
BaseVal: defaultConfigGen().ServiceSettings.SiteURL,
ActualVal: func() *string {
return nil
}(),
},
},
"",
},
{
"bool change",
defaultConfigGen(),
func() *model.Config {
cfg := defaultConfigGen()
cfg.PluginSettings.Enable = model.NewBool(!*cfg.PluginSettings.Enable)
return cfg
}(),
ConfigDiffs{
{
Path: "PluginSettings.Enable",
BaseVal: true,
ActualVal: false,
},
},
"",
},
{
"bool nil",
defaultConfigGen(),
func() *model.Config {
cfg := defaultConfigGen()
cfg.PluginSettings.Enable = nil
return cfg
}(),
ConfigDiffs{
{
Path: "PluginSettings.Enable",
BaseVal: defaultConfigGen().PluginSettings.Enable,
ActualVal: func() *bool {
return nil
}(),
},
},
"",
},
{
"int change",
defaultConfigGen(),
func() *model.Config {
cfg := defaultConfigGen()
cfg.ServiceSettings.ReadTimeout = model.NewInt(0)
return cfg
}(),
ConfigDiffs{
{
Path: "ServiceSettings.ReadTimeout",
BaseVal: *defaultConfigGen().ServiceSettings.ReadTimeout,
ActualVal: 0,
},
},
"",
},
{
"int nil",
defaultConfigGen(),
func() *model.Config {
cfg := defaultConfigGen()
cfg.ServiceSettings.ReadTimeout = nil
return cfg
}(),
ConfigDiffs{
{
Path: "ServiceSettings.ReadTimeout",
BaseVal: defaultConfigGen().ServiceSettings.ReadTimeout,
ActualVal: func() *int {
return nil
}(),
},
},
"",
},
{
"slice addition",
defaultConfigGen(),
func() *model.Config {
cfg := defaultConfigGen()
cfg.SqlSettings.DataSourceReplicas = []string{
"ds0",
"ds1",
}
return cfg
}(),
ConfigDiffs{
{
Path: "SqlSettings.DataSourceReplicas",
BaseVal: defaultConfigGen().SqlSettings.DataSourceReplicas,
ActualVal: []string{
"ds0",
"ds1",
},
},
},
"",
},
{
"slice deletion",
func() *model.Config {
cfg := defaultConfigGen()
cfg.SqlSettings.DataSourceReplicas = []string{
"ds0",
"ds1",
}
return cfg
}(),
func() *model.Config {
cfg := defaultConfigGen()
cfg.SqlSettings.DataSourceReplicas = []string{
"ds0",
}
return cfg
}(),
ConfigDiffs{
{
Path: "SqlSettings.DataSourceReplicas",
BaseVal: []string{
"ds0",
"ds1",
},
ActualVal: []string{
"ds0",
},
},
},
"",
},
{
"slice nil",
func() *model.Config {
cfg := defaultConfigGen()
cfg.SqlSettings.DataSourceReplicas = []string{
"ds0",
"ds1",
}
return cfg
}(),
func() *model.Config {
cfg := defaultConfigGen()
cfg.SqlSettings.DataSourceReplicas = nil
return cfg
}(),
ConfigDiffs{
{
Path: "SqlSettings.DataSourceReplicas",
BaseVal: []string{
"ds0",
"ds1",
},
ActualVal: func() []string {
return nil
}(),
},
},
"",
},
{
"map change",
defaultConfigGen(),
func() *model.Config {
cfg := defaultConfigGen()
cfg.PluginSettings.PluginStates["com.mattermost.nps"] = &model.PluginState{
Enable: !cfg.PluginSettings.PluginStates["com.mattermost.nps"].Enable,
}
return cfg
}(),
ConfigDiffs{
{
Path: "PluginSettings.PluginStates",
BaseVal: defaultConfigGen().PluginSettings.PluginStates,
ActualVal: map[string]*model.PluginState{
"com.mattermost.nps": {
Enable: !defaultConfigGen().PluginSettings.PluginStates["com.mattermost.nps"].Enable,
},
},
},
},
"",
},
{
"map addition",
defaultConfigGen(),
func() *model.Config {
cfg := defaultConfigGen()
cfg.PluginSettings.PluginStates["com.mattermost.newplugin"] = &model.PluginState{
Enable: true,
}
return cfg
}(),
ConfigDiffs{
{
Path: "PluginSettings.PluginStates",
BaseVal: defaultConfigGen().PluginSettings.PluginStates,
ActualVal: map[string]*model.PluginState{
"com.mattermost.nps": {
Enable: defaultConfigGen().PluginSettings.PluginStates["com.mattermost.nps"].Enable,
},
"com.mattermost.newplugin": {
Enable: true,
},
},
},
},
"",
},
{
"map deletion",
defaultConfigGen(),
func() *model.Config {
cfg := defaultConfigGen()
delete(cfg.PluginSettings.PluginStates, "com.mattermost.nps")
return cfg
}(),
ConfigDiffs{
{
Path: "PluginSettings.PluginStates",
BaseVal: defaultConfigGen().PluginSettings.PluginStates,
ActualVal: func() interface{} {
return map[string]*model.PluginState{}
}(),
},
},
"",
},
{
"map nil",
defaultConfigGen(),
func() *model.Config {
cfg := defaultConfigGen()
cfg.PluginSettings.PluginStates = nil
return cfg
}(),
ConfigDiffs{
{
Path: "PluginSettings.PluginStates",
BaseVal: defaultConfigGen().PluginSettings.PluginStates,
ActualVal: func() map[string]*model.PluginState {
return nil
}(),
},
},
"",
},
{
"map type change",
func() *model.Config {
cfg := defaultConfigGen()
cfg.PluginSettings.Plugins = map[string]map[string]interface{}{
"com.mattermost.newplugin": {
"key": true,
},
}
return cfg
}(),
func() *model.Config {
cfg := defaultConfigGen()
cfg.PluginSettings.Plugins = map[string]map[string]interface{}{
"com.mattermost.newplugin": {
"key": "string",
},
}
return cfg
}(),
ConfigDiffs{
{
Path: "PluginSettings.Plugins",
BaseVal: func() interface{} {
return map[string]map[string]interface{}{
"com.mattermost.newplugin": {
"key": true,
},
}
}(),
ActualVal: func() interface{} {
return map[string]map[string]interface{}{
"com.mattermost.newplugin": {
"key": "string",
},
}
}(),
},
},
"",
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
diffs, err := Diff(tc.base, tc.actual)
if tc.err != "" {
require.EqualError(t, err, tc.err)
require.Nil(t, diffs)
} else {
require.NoError(t, err)
}
require.Equal(t, tc.diffs, diffs)
})
}
}

Просмотреть файл

@@ -273,7 +273,7 @@ func TestFileStoreGet(t *testing.T) {
assert.True(t, cfg == cfg2, "Get() returned different configuration instances")
newCfg := &model.Config{}
_, err := configStore.Set(newCfg)
_, _, err := configStore.Set(newCfg)
require.NoError(t, err)
assert.False(t, newCfg == cfg, "returned config should have been different from original")
@@ -472,9 +472,10 @@ func TestFileStoreSet(t *testing.T) {
oldCfg := configStore.Get().Clone()
newCfg := &model.Config{}
retCfg, err := configStore.Set(newCfg)
retCfg, newConfig, err := configStore.Set(newCfg)
require.NoError(t, err)
require.Equal(t, oldCfg, retCfg)
require.NotEqual(t, newCfg, newConfig)
assert.Equal(t, "", *configStore.Get().ServiceSettings.SiteURL)
})
@@ -486,8 +487,9 @@ func TestFileStoreSet(t *testing.T) {
newCfg := &model.Config{}
newCfg.LdapSettings.BindPassword = model.NewString(model.FAKE_SETTING)
_, err := configStore.Set(newCfg)
_, newConfig, err := configStore.Set(newCfg)
require.NoError(t, err)
require.NotEqual(t, newCfg, newConfig)
assert.Equal(t, "password", *configStore.Get().LdapSettings.BindPassword)
})
@@ -499,7 +501,7 @@ func TestFileStoreSet(t *testing.T) {
newCfg := &model.Config{}
newCfg.ServiceSettings.SiteURL = model.NewString("invalid")
_, err := configStore.Set(newCfg)
_, _, err := configStore.Set(newCfg)
if assert.Error(t, err) {
assert.EqualError(t, err, "new configuration is invalid: Config.IsValid: model.config.is_valid.site_url.app_error, ")
}
@@ -515,7 +517,7 @@ func TestFileStoreSet(t *testing.T) {
newReadOnlyConfig.ServiceSettings = model.ServiceSettings{
SiteURL: model.NewString("http://test"),
}
_, err := configStore.Set(newReadOnlyConfig)
_, _, err := configStore.Set(newReadOnlyConfig)
if assert.Error(t, err) {
assert.Equal(t, ErrReadOnlyConfiguration, errors.Cause(err))
}
@@ -537,7 +539,7 @@ func TestFileStoreSet(t *testing.T) {
newCfg := &model.Config{}
_, err = fs.Set(newCfg)
_, _, err = fs.Set(newCfg)
if assert.Error(t, err) {
assert.True(t, strings.HasPrefix(err.Error(), "failed to persist: failed to write file"))
}
@@ -558,7 +560,7 @@ func TestFileStoreSet(t *testing.T) {
newCfg := minimalConfig
_, err := configStore.Set(newCfg)
_, _, err := configStore.Set(newCfg)
require.NoError(t, err)
require.True(t, wasCalled(called, 5*time.Second), "callback should have been called when config written")
@@ -583,7 +585,7 @@ func TestFileStoreSet(t *testing.T) {
os.Setenv("MM_SERVICESETTINGS_SITEURL", "http://override")
defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL")
_, err := configStore.Set(newCfg)
_, _, err := configStore.Set(newCfg)
require.NoError(t, err)
require.True(t, wasCalled(called, 5*time.Second), "callback should have been called when config changed")
@@ -608,7 +610,7 @@ func TestFileStoreSet(t *testing.T) {
expectedNewConfig = minimalConfig.Clone()
expectedNewConfig.FeatureFlags.TestFeature = "test"
_, err := configStore.Set(expectedNewConfig)
_, _, err := configStore.Set(expectedNewConfig)
require.NoError(t, err)
require.False(t, wasCalled(called, 5*time.Second))
@@ -616,7 +618,7 @@ func TestFileStoreSet(t *testing.T) {
configStore.SetReadOnlyFF(false)
expectedNewConfig.FeatureFlags.TestFeature = "test2"
_, err = configStore.Set(expectedNewConfig)
_, _, err = configStore.Set(expectedNewConfig)
require.NoError(t, err)
require.True(t, wasCalled(called, 5*time.Second))
@@ -636,7 +638,7 @@ func TestFileStoreSet(t *testing.T) {
require.NoError(t, err)
defer fs.Close()
_, err = fs.Set(minimalConfig)
_, _, err = fs.Set(minimalConfig)
require.NoError(t, err)
// Let the initial call to invokeConfigListeners finish.
@@ -705,7 +707,7 @@ func TestFileStoreLoad(t *testing.T) {
assert.Equal(t, "http://overridePersistEnvVariables", *fs.Get().ServiceSettings.SiteURL)
_, err = fs.Set(fs.Get())
_, _, err = fs.Set(fs.Get())
require.NoError(t, err)
assert.Equal(t, "http://overridePersistEnvVariables", *fs.Get().ServiceSettings.SiteURL)
@@ -730,7 +732,7 @@ func TestFileStoreLoad(t *testing.T) {
assert.Equal(t, true, *fs.Get().PluginSettings.EnableUploads)
_, err = fs.Set(fs.Get())
_, _, err = fs.Set(fs.Get())
require.NoError(t, err)
assert.Equal(t, true, *fs.Get().PluginSettings.EnableUploads)
@@ -755,7 +757,7 @@ func TestFileStoreLoad(t *testing.T) {
assert.Equal(t, 3000, *fs.Get().TeamSettings.MaxUsersPerTeam)
_, err = fs.Set(fs.Get())
_, _, err = fs.Set(fs.Get())
require.NoError(t, err)
assert.Equal(t, 3000, *fs.Get().TeamSettings.MaxUsersPerTeam)
@@ -780,7 +782,7 @@ func TestFileStoreLoad(t *testing.T) {
assert.Equal(t, int64(123456), *fs.Get().ServiceSettings.TLSStrictTransportMaxAge)
_, err = fs.Set(fs.Get())
_, _, err = fs.Set(fs.Get())
require.NoError(t, err)
assert.Equal(t, int64(123456), *fs.Get().ServiceSettings.TLSStrictTransportMaxAge)
@@ -805,7 +807,7 @@ func TestFileStoreLoad(t *testing.T) {
assert.Equal(t, []string{"user:pwd@db:5432/test-db"}, fs.Get().SqlSettings.DataSourceReplicas)
_, err = fs.Set(fs.Get())
_, _, err = fs.Set(fs.Get())
require.NoError(t, err)
assert.Equal(t, []string{"user:pwd@db:5432/test-db"}, fs.Get().SqlSettings.DataSourceReplicas)
@@ -832,7 +834,7 @@ func TestFileStoreLoad(t *testing.T) {
assert.Equal(t, []string{"user:pwd@db:5432/test-db"}, fs.Get().SqlSettings.DataSourceReplicas)
_, err = fs.Set(fs.Get())
_, _, err = fs.Set(fs.Get())
require.NoError(t, err)
assert.Equal(t, []string{"user:pwd@db:5432/test-db"}, fs.Get().SqlSettings.DataSourceReplicas)
@@ -871,7 +873,7 @@ func TestFileStoreLoad(t *testing.T) {
defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL")
newCfg := minimalConfig
_, err := configStore.Set(newCfg)
_, _, err := configStore.Set(newCfg)
require.Error(t, err)
require.EqualError(t, err, "new configuration is invalid: Config.IsValid: model.config.is_valid.site_url.app_error, ")
})
@@ -1043,7 +1045,7 @@ func TestFileStoreWatcherEmitter(t *testing.T) {
}
fs.AddListener(callback)
_, err = fs.Set(minimalConfig)
_, _, err = fs.Set(minimalConfig)
require.NoError(t, err)
require.False(t, wasCalled(called, 1*time.Second), "callback should not have been called since no change has happened")
@@ -1074,7 +1076,7 @@ func TestFileStoreWatcherEmitter(t *testing.T) {
os.Setenv("MM_SERVICESETTINGS_SITEURL", "http://override")
defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL")
_, err = fs.Set(minimalConfig)
_, _, err = fs.Set(minimalConfig)
require.NoError(t, err)
require.True(t, wasCalled(called, 5*time.Second), "callback should have been called since no change has happened")
@@ -1092,7 +1094,7 @@ func TestFileStoreSave(t *testing.T) {
}
t.Run("set with automatic save", func(t *testing.T) {
_, err := store.Set(newCfg)
_, _, err := store.Set(newCfg)
require.NoError(t, err)
err = store.Load()
@@ -1421,7 +1423,7 @@ func TestFileStoreReadOnly(t *testing.T) {
}
fs.AddListener(callback)
cfg, err := fs.Set(minimalConfig)
cfg, _, err := fs.Set(minimalConfig)
require.Nil(t, cfg)
require.Equal(t, ErrReadOnlyStore, err)
@@ -1439,7 +1441,7 @@ func TestFileStoreSetReadOnlyFF(t *testing.T) {
newCfg.FeatureFlags.TestFeature = "test"
// store has read-only FF by default.
_, err := store.Set(newCfg)
_, _, err := store.Set(newCfg)
require.NoError(t, err)
config = store.Get()
@@ -1457,7 +1459,7 @@ func TestFileStoreSetReadOnlyFF(t *testing.T) {
store.SetReadOnlyFF(false)
_, err := store.Set(newCfg)
_, _, err := store.Set(newCfg)
require.NoError(t, err)
config = store.Get()

Просмотреть файл

@@ -22,7 +22,7 @@ func Migrate(from, to string) error {
defer destination.Close()
sourceConfig := source.Get()
if _, err = destination.Set(sourceConfig); err != nil {
if _, _, err = destination.Set(sourceConfig); err != nil {
return errors.Wrapf(err, "failed to set config")
}

Просмотреть файл

@@ -68,7 +68,7 @@ func TestMigrate(t *testing.T) {
"mysql://mmuser:password@tcp(searchreplicahost:3306)/mattermost",
}
_, err := source.Set(cfg)
_, _, err := source.Set(cfg)
require.NoError(t, err)
for i, file := range files {
@@ -77,7 +77,7 @@ func TestMigrate(t *testing.T) {
}
return func(store *Store) {
_, err := store.Set(originalCfg)
_, _, err := store.Set(originalCfg)
require.NoError(t, err)
}
}

Просмотреть файл

@@ -170,12 +170,13 @@ func (s *Store) SetReadOnlyFF(readOnly bool) {
}
// Set replaces the current configuration in its entirety and updates the backing store.
func (s *Store) Set(newCfg *model.Config) (*model.Config, error) {
// It returns both old and new versions of the config.
func (s *Store) Set(newCfg *model.Config) (*model.Config, *model.Config, error) {
s.configLock.Lock()
defer s.configLock.Unlock()
if s.readOnly {
return nil, ErrReadOnlyStore
return nil, nil, ErrReadOnlyStore
}
newCfg = newCfg.Clone()
@@ -190,7 +191,7 @@ func (s *Store) Set(newCfg *model.Config) (*model.Config, error) {
desanitize(oldCfg, newCfg)
if err := newCfg.IsValid(); err != nil {
return nil, errors.Wrap(err, "new configuration is invalid")
return nil, nil, errors.Wrap(err, "new configuration is invalid")
}
// We attempt to remove any environment override that may be present in the input config.
@@ -211,7 +212,7 @@ func (s *Store) Set(newCfg *model.Config) (*model.Config, error) {
}
if err := s.backingStore.Set(newCfgNoEnv); err != nil {
return nil, errors.Wrap(err, "failed to persist")
return nil, nil, errors.Wrap(err, "failed to persist")
}
// We apply back environment overrides since the input config may or
@@ -219,12 +220,12 @@ func (s *Store) Set(newCfg *model.Config) (*model.Config, error) {
newCfg = applyEnvironmentMap(newCfgNoEnv, GetEnvironment())
fixConfig(newCfg)
if err := newCfg.IsValid(); err != nil {
return nil, errors.Wrap(err, "new configuration is invalid")
return nil, nil, errors.Wrap(err, "new configuration is invalid")
}
hasChanged, err := equal(oldCfg, newCfg)
if err != nil {
return nil, errors.Wrap(err, "failed to compare configs")
return nil, nil, errors.Wrap(err, "failed to compare configs")
}
// We restore the previously cleared feature flags sections back.
@@ -237,13 +238,15 @@ func (s *Store) Set(newCfg *model.Config) (*model.Config, error) {
s.configNoEnv = newCfgNoEnv
s.config = newCfg
newCfgCopy := newCfg.Clone()
if hasChanged {
s.configLock.Unlock()
s.invokeConfigListeners(oldCfg, newCfg.Clone())
s.invokeConfigListeners(oldCfg, newCfgCopy.Clone())
s.configLock.Lock()
}
return oldCfg, nil
return oldCfg, newCfgCopy, nil
}
// Load updates the current configuration from the backing store, possibly initializing.
@@ -337,9 +340,11 @@ func (s *Store) Load() error {
s.config = loadedCfg
s.configNoEnv = loadedCfgNoEnv
loadedCfgCopy := loadedCfg.Clone()
if hasChanged {
s.configLock.Unlock()
s.invokeConfigListeners(oldCfg, loadedCfg.Clone())
s.invokeConfigListeners(oldCfg, loadedCfgCopy)
s.configLock.Lock()
}

Просмотреть файл

@@ -70,8 +70,9 @@ func TestNewStoreReadOnly(t *testing.T) {
require.NoError(t, err)
t.Run("Set", func(t *testing.T) {
cfg, err := ds.Set(emptyConfig)
require.Nil(t, cfg)
oldCfg, newCfg, err := ds.Set(emptyConfig)
require.Nil(t, oldCfg)
require.Nil(t, newCfg)
require.Equal(t, ErrReadOnlyStore, err)
})
@@ -93,8 +94,9 @@ func TestNewStoreReadOnly(t *testing.T) {
require.NoError(t, err)
t.Run("Set", func(t *testing.T) {
cfg, err := fs.Set(emptyConfig)
require.Nil(t, cfg)
oldCfg, newCfg, err := fs.Set(emptyConfig)
require.Nil(t, oldCfg)
require.Nil(t, newCfg)
require.Equal(t, ErrReadOnlyStore, err)
})