[MM-32390] Config logic refactor (#17578)

* 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>

* Move FF synchronizer to its own package

* Remove unidiomatic use of sync.Once

* Add some comments

* Rename function

* More comment

Co-authored-by: Christopher Speller <crspeller@gmail.com>
Co-authored-by: Ibrahim Serdar Acikgoz <serdaracikgoz86@gmail.com>
Этот коммит содержится в:
Claudio Costa
2021-05-19 13:30:26 +02:00
коммит произвёл GitHub
родитель b810a40062
Коммит 3681cd3688
37 изменённых файлов: 819 добавлений и 668 удалений

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

@@ -549,7 +549,7 @@ config-ldap: ## Configures LDAP.
config-reset: ## Resets the config/config.json file to the default.
@echo Resetting configuration to default
rm -f config/config.json
OUTPUT_CONFIG=$(PWD)/config/config.json $(GO) generate $(GOFLAGS) ./config
OUTPUT_CONFIG=$(PWD)/config/config.json $(GO) $(GOFLAGS) run ./scripts/config_generator
diff-config: ## Compares default configuration between two mattermost versions
@./scripts/diff-config.sh

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

@@ -721,11 +721,11 @@ func TestMigrateConfig(t *testing.T) {
})
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
f, err := config.NewStore("from.json", false, false, nil)
f, err := config.NewStoreFromDSN("from.json", false, false, nil)
require.NoError(t, err)
defer f.RemoveFile("from.json")
_, err = config.NewStore("to.json", false, false, nil)
_, err = config.NewStoreFromDSN("to.json", false, false, nil)
require.NoError(t, err)
defer f.RemoveFile("to.json")

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

@@ -5523,6 +5523,9 @@ func TestThreadSocketEvents(t *testing.T) {
os.Setenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS", "true")
defer os.Unsetenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS")
th.ConfigStore.SetReadOnlyFF(false)
defer th.ConfigStore.SetReadOnlyFF(true)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.ThreadAutoFollow = true
*cfg.ServiceSettings.CollapsedThreads = model.COLLAPSED_THREADS_DEFAULT_ON

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

@@ -143,15 +143,12 @@ func (s *Server) configureAudit(adt *audit.Audit, bAllowAdvancedLogging bool) er
if !bAllowAdvancedLogging || dsn == "" {
return errs
}
isJson := config.IsJsonMap(dsn)
cfg, err := config.NewLogConfigSrc(dsn, isJson, s.configStore)
cfg, err := config.NewLogConfigSrc(dsn, s.configStore)
if err != nil {
errs = multierror.Append(fmt.Errorf("invalid config for audit, %w", err))
return errs
}
if !isJson {
mlog.Debug("Loaded audit configuration", mlog.String("filename", dsn))
}
mlog.Debug("Loaded audit configuration", mlog.String("source", dsn))
for name, t := range cfg.Get() {
if len(t.Levels) == 0 {

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

@@ -8,7 +8,7 @@ import (
"os"
"time"
"github.com/mattermost/mattermost-server/v5/config"
"github.com/mattermost/mattermost-server/v5/app/featureflag"
"github.com/mattermost/mattermost-server/v5/shared/mlog"
)
@@ -21,7 +21,7 @@ func (s *Server) setupFeatureFlags() {
splitConfigured := splitKey != ""
syncFeatureFlags := splitConfigured && s.IsLeader()
s.configStore.PersistFeatures(splitConfigured)
s.configStore.SetReadOnlyFF(!splitConfigured)
if syncFeatureFlags {
if err := s.startFeatureFlagUpdateJob(); err != nil {
@@ -71,7 +71,7 @@ func (s *Server) startFeatureFlagUpdateJob() error {
attributes["group_id"] = groupId
}
synchronizer, err := config.NewFeatureFlagSynchronizer(config.FeatureFlagSyncParams{
synchronizer, err := featureflag.NewSynchronizer(featureflag.SyncParams{
ServerID: s.TelemetryId(),
SplitKey: *s.Config().ServiceSettings.SplitKey,
Log: log,

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package config
package featureflag
import (
"math"
@@ -17,7 +17,7 @@ import (
"github.com/mattermost/mattermost-server/v5/shared/mlog"
)
type FeatureFlagSyncParams struct {
type SyncParams struct {
ServerID string
SplitKey string
SyncIntervalSeconds int
@@ -25,8 +25,8 @@ type FeatureFlagSyncParams struct {
Attributes map[string]interface{}
}
type FeatureFlagSynchronizer struct {
FeatureFlagSyncParams
type Synchronizer struct {
SyncParams
client *client.SplitClient
stop chan struct{}
@@ -35,7 +35,7 @@ type FeatureFlagSynchronizer struct {
var featureNames = getStructFields(model.FeatureFlags{})
func NewFeatureFlagSynchronizer(params FeatureFlagSyncParams) (*FeatureFlagSynchronizer, error) {
func NewSynchronizer(params SyncParams) (*Synchronizer, error) {
cfg := conf.Default()
if params.Log != nil {
cfg.Logger = &splitLogger{wrappedLog: params.Log.With(mlog.String("service", "split"))}
@@ -47,16 +47,16 @@ func NewFeatureFlagSynchronizer(params FeatureFlagSyncParams) (*FeatureFlagSynch
return nil, errors.Wrap(err, "unable to create split factory")
}
return &FeatureFlagSynchronizer{
FeatureFlagSyncParams: params,
client: factory.Client(),
stop: make(chan struct{}),
stopped: make(chan struct{}),
return &Synchronizer{
SyncParams: params,
client: factory.Client(),
stop: make(chan struct{}),
stopped: make(chan struct{}),
}, nil
}
// EnsureReady blocks until the syncronizer is ready to update feature flag values
func (f *FeatureFlagSynchronizer) EnsureReady() error {
func (f *Synchronizer) EnsureReady() error {
if err := f.client.BlockUntilReady(10); err != nil {
return errors.Wrap(err, "split.io client could not initialize")
}
@@ -64,13 +64,13 @@ func (f *FeatureFlagSynchronizer) EnsureReady() error {
return nil
}
func (f *FeatureFlagSynchronizer) UpdateFeatureFlagValues(base model.FeatureFlags) model.FeatureFlags {
func (f *Synchronizer) UpdateFeatureFlagValues(base model.FeatureFlags) model.FeatureFlags {
featuresMap := f.client.Treatments(f.ServerID, featureNames, f.Attributes)
ffm := featureFlagsFromMap(featuresMap, base)
return ffm
}
func (f *FeatureFlagSynchronizer) Close() {
func (f *Synchronizer) Close() {
f.client.Destroy()
}

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package config
package featureflag
import (
"testing"

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package config
package featureflag
import (
"fmt"

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

@@ -45,7 +45,7 @@ func StoreOverride(override interface{}) Option {
// config loaded from the dsn on top of the normal defaults
func Config(dsn string, watch, readOnly bool, configDefaults *model.Config) Option {
return func(s *Server) error {
configStore, err := config.NewStore(dsn, watch, readOnly, configDefaults)
configStore, err := config.NewStoreFromDSN(dsn, watch, readOnly, configDefaults)
if err != nil {
return errors.Wrap(err, "failed to apply Config option")
}

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

@@ -19,7 +19,6 @@ import (
"os"
"os/exec"
"path"
"path/filepath"
"runtime"
"strconv"
"strings"
@@ -38,6 +37,7 @@ import (
"github.com/rs/cors"
"golang.org/x/crypto/acme/autocert"
"github.com/mattermost/mattermost-server/v5/app/featureflag"
"github.com/mattermost/mattermost-server/v5/app/request"
"github.com/mattermost/mattermost-server/v5/audit"
"github.com/mattermost/mattermost-server/v5/config"
@@ -200,7 +200,7 @@ type Server struct {
uploadLockMapMut sync.Mutex
uploadLockMap map[string]bool
featureFlagSynchronizer *config.FeatureFlagSynchronizer
featureFlagSynchronizer *featureflag.Synchronizer
featureFlagStop chan struct{}
featureFlagStopped chan struct{}
featureFlagSynchronizerMutex sync.Mutex
@@ -805,15 +805,8 @@ func (s *Server) initLogging() error {
// shutdown once license is loaded/checked.
if *s.Config().LogSettings.AdvancedLoggingConfig != "" {
dsn := *s.Config().LogSettings.AdvancedLoggingConfig
isJson := config.IsJsonMap(dsn)
// If this is a file based config we need the full path so it can be watched.
if !isJson && strings.HasPrefix(s.configStore.String(), "file://") && !filepath.IsAbs(dsn) {
configPath := strings.TrimPrefix(s.configStore.String(), "file://")
dsn = filepath.Join(filepath.Dir(configPath), dsn)
}
cfg, err := config.NewLogConfigSrc(dsn, isJson, s.configStore)
cfg, err := config.NewLogConfigSrc(dsn, s.configStore)
if err != nil {
return fmt.Errorf("invalid advanced logging config, %w", err)
}
@@ -822,9 +815,7 @@ func (s *Server) initLogging() error {
return fmt.Errorf("error configuring advanced logging, %w", err)
}
if !isJson {
mlog.Info("Loaded advanced logging config", mlog.String("source", dsn))
}
mlog.Info("Loaded advanced logging config", mlog.String("source", dsn))
listenerId := cfg.AddListener(func(_, newCfg mlog.LogTargetCfg) {
if err := s.Log.ConfigAdvancedLogging(newCfg); err != nil {

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

@@ -76,7 +76,7 @@ package:
@# Resource directories
mkdir -p $(DIST_PATH)/config
cp -L config/README.md $(DIST_PATH)/config
OUTPUT_CONFIG=$(PWD)/$(DIST_PATH)/config/config.json go generate ./config
OUTPUT_CONFIG=$(PWD)/$(DIST_PATH)/config/config.json go run ./scripts/config_generator
cp -RL fonts $(DIST_PATH)
cp -RL templates $(DIST_PATH)
rm -rf $(DIST_PATH)/templates/*.mjml $(DIST_PATH)/templates/partials/

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

@@ -146,7 +146,7 @@ func getConfigStore(command *cobra.Command) (*config.Store, error) {
return nil, errors.Wrap(err, "failed to initialize i18n")
}
configStore, err := config.NewStore(getConfigDSN(command, config.GetEnvironment()), false, false, nil)
configStore, err := config.NewStoreFromDSN(getConfigDSN(command, config.GetEnvironment()), false, false, nil)
if err != nil {
return nil, errors.Wrap(err, "failed to initialize config store")
}

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

@@ -543,9 +543,9 @@ func TestConfigMigrate(t *testing.T) {
sqlDSN := getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)
fileDSN := "config.json"
ds, err := config.NewStore(sqlDSN, false, false, nil)
ds, err := config.NewStoreFromDSN(sqlDSN, false, false, nil)
require.NoError(t, err)
fs, err := config.NewStore(fileDSN, false, false, nil)
fs, err := config.NewStoreFromDSN(fileDSN, false, false, nil)
require.NoError(t, err)
defer ds.Close()

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

@@ -57,7 +57,7 @@ func initDbCmdF(command *cobra.Command, _ []string) error {
return errors.Wrap(err, "error loading custom configuration defaults")
}
configStore, err := config.NewStore(getConfigDSN(command, config.GetEnvironment()), false, false, customDefaults)
configStore, err := config.NewStoreFromDSN(getConfigDSN(command, config.GetEnvironment()), false, false, customDefaults)
if err != nil {
return errors.Wrap(err, "failed to load configuration")
}

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

@@ -52,7 +52,7 @@ func serverCmdF(command *cobra.Command, args []string) error {
mlog.Warn("Error loading custom configuration defaults: " + err.Error())
}
configStore, err := config.NewStore(getConfigDSN(command, config.GetEnvironment()), !disableConfigWatch, false, customDefaults)
configStore, err := config.NewStoreFromDSN(getConfigDSN(command, config.GetEnvironment()), !disableConfigWatch, false, customDefaults)
if err != nil {
return errors.Wrap(err, "failed to load configuration")
}

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package config_test
package config
import (
"fmt"
@@ -9,7 +9,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/mattermost/mattermost-server/v5/config"
"github.com/mattermost/mattermost-server/v5/model"
)
@@ -26,16 +25,16 @@ func TestGetClientConfig(t *testing.T) {
"unlicensed",
&model.Config{
EmailSettings: model.EmailSettings{
EmailNotificationContentsType: sToP(model.EMAIL_NOTIFICATION_CONTENTS_FULL),
EmailNotificationContentsType: model.NewString(model.EMAIL_NOTIFICATION_CONTENTS_FULL),
},
ThemeSettings: model.ThemeSettings{
// Ignored, since not licensed.
AllowCustomThemes: bToP(false),
AllowCustomThemes: model.NewBool(false),
},
ServiceSettings: model.ServiceSettings{
WebsocketURL: sToP("ws://mattermost.example.com:8065"),
WebsocketPort: iToP(80),
WebsocketSecurePort: iToP(443),
WebsocketURL: model.NewString("ws://mattermost.example.com:8065"),
WebsocketPort: model.NewInt(80),
WebsocketSecurePort: model.NewInt(443),
},
},
"",
@@ -54,17 +53,17 @@ func TestGetClientConfig(t *testing.T) {
"licensed, but not for theme management",
&model.Config{
EmailSettings: model.EmailSettings{
EmailNotificationContentsType: sToP(model.EMAIL_NOTIFICATION_CONTENTS_FULL),
EmailNotificationContentsType: model.NewString(model.EMAIL_NOTIFICATION_CONTENTS_FULL),
},
ThemeSettings: model.ThemeSettings{
// Ignored, since not licensed.
AllowCustomThemes: bToP(false),
AllowCustomThemes: model.NewBool(false),
},
},
"tag1",
&model.License{
Features: &model.Features{
ThemeManagement: bToP(false),
ThemeManagement: model.NewBool(false),
},
},
map[string]string{
@@ -77,16 +76,16 @@ func TestGetClientConfig(t *testing.T) {
"licensed for theme management",
&model.Config{
EmailSettings: model.EmailSettings{
EmailNotificationContentsType: sToP(model.EMAIL_NOTIFICATION_CONTENTS_FULL),
EmailNotificationContentsType: model.NewString(model.EMAIL_NOTIFICATION_CONTENTS_FULL),
},
ThemeSettings: model.ThemeSettings{
AllowCustomThemes: bToP(false),
AllowCustomThemes: model.NewBool(false),
},
},
"tag2",
&model.License{
Features: &model.Features{
ThemeManagement: bToP(true),
ThemeManagement: model.NewBool(true),
},
},
map[string]string{
@@ -99,13 +98,13 @@ func TestGetClientConfig(t *testing.T) {
"licensed for enforcement",
&model.Config{
ServiceSettings: model.ServiceSettings{
EnforceMultifactorAuthentication: bToP(true),
EnforceMultifactorAuthentication: model.NewBool(true),
},
},
"tag1",
&model.License{
Features: &model.Features{
MFA: bToP(true),
MFA: model.NewBool(true),
},
},
map[string]string{
@@ -116,7 +115,7 @@ func TestGetClientConfig(t *testing.T) {
"experimental channel organization enabled",
&model.Config{
ServiceSettings: model.ServiceSettings{
ExperimentalChannelOrganization: bToP(true),
ExperimentalChannelOrganization: model.NewBool(true),
},
},
"tag1",
@@ -129,8 +128,8 @@ func TestGetClientConfig(t *testing.T) {
"experimental channel organization disabled, but experimental group unread channels on",
&model.Config{
ServiceSettings: model.ServiceSettings{
ExperimentalChannelOrganization: bToP(false),
ExperimentalGroupUnreadChannels: sToP(model.GROUP_UNREAD_CHANNELS_DEFAULT_ON),
ExperimentalChannelOrganization: model.NewBool(false),
ExperimentalGroupUnreadChannels: model.NewString(model.GROUP_UNREAD_CHANNELS_DEFAULT_ON),
},
},
"tag1",
@@ -143,7 +142,7 @@ func TestGetClientConfig(t *testing.T) {
"default marketplace",
&model.Config{
PluginSettings: model.PluginSettings{
MarketplaceUrl: sToP(model.PLUGIN_SETTINGS_DEFAULT_MARKETPLACE_URL),
MarketplaceUrl: model.NewString(model.PLUGIN_SETTINGS_DEFAULT_MARKETPLACE_URL),
},
},
"tag1",
@@ -156,7 +155,7 @@ func TestGetClientConfig(t *testing.T) {
"non-default marketplace",
&model.Config{
PluginSettings: model.PluginSettings{
MarketplaceUrl: sToP("http://example.com"),
MarketplaceUrl: model.NewString("http://example.com"),
},
},
"tag1",
@@ -169,7 +168,7 @@ func TestGetClientConfig(t *testing.T) {
"enable ShowFullName prop",
&model.Config{
PrivacySettings: model.PrivacySettings{
ShowFullName: bToP(true),
ShowFullName: model.NewBool(true),
},
},
"tag1",
@@ -190,7 +189,7 @@ func TestGetClientConfig(t *testing.T) {
testCase.license.Features.SetDefaults()
}
configMap := config.GenerateClientConfig(testCase.config, testCase.telemetryID, testCase.license)
configMap := GenerateClientConfig(testCase.config, testCase.telemetryID, testCase.license)
for expectedField, expectedValue := range testCase.expectedFields {
actualValue, ok := configMap[expectedField]
if assert.True(t, ok, fmt.Sprintf("config does not contain %v", expectedField)) {
@@ -214,16 +213,16 @@ func TestGetLimitedClientConfig(t *testing.T) {
"unlicensed",
&model.Config{
EmailSettings: model.EmailSettings{
EmailNotificationContentsType: sToP(model.EMAIL_NOTIFICATION_CONTENTS_FULL),
EmailNotificationContentsType: model.NewString(model.EMAIL_NOTIFICATION_CONTENTS_FULL),
},
ThemeSettings: model.ThemeSettings{
// Ignored, since not licensed.
AllowCustomThemes: bToP(false),
AllowCustomThemes: model.NewBool(false),
},
ServiceSettings: model.ServiceSettings{
WebsocketURL: sToP("ws://mattermost.example.com:8065"),
WebsocketPort: iToP(80),
WebsocketSecurePort: iToP(443),
WebsocketURL: model.NewString("ws://mattermost.example.com:8065"),
WebsocketPort: model.NewInt(80),
WebsocketSecurePort: model.NewInt(443),
},
},
"",
@@ -240,11 +239,11 @@ func TestGetLimitedClientConfig(t *testing.T) {
"password settings",
&model.Config{
PasswordSettings: model.PasswordSettings{
MinimumLength: iToP(15),
Lowercase: bToP(true),
Uppercase: bToP(true),
Number: bToP(true),
Symbol: bToP(false),
MinimumLength: model.NewInt(15),
Lowercase: model.NewBool(true),
Uppercase: model.NewBool(true),
Number: model.NewBool(true),
Symbol: model.NewBool(false),
},
},
"",
@@ -282,7 +281,7 @@ func TestGetLimitedClientConfig(t *testing.T) {
testCase.license.Features.SetDefaults()
}
configMap := config.GenerateLimitedClientConfig(testCase.config, testCase.telemetryID, testCase.license)
configMap := GenerateLimitedClientConfig(testCase.config, testCase.telemetryID, testCase.license)
for expectedField, expectedValue := range testCase.expectedFields {
actualValue, ok := configMap[expectedField]
if assert.True(t, ok, fmt.Sprintf("config does not contain %v", expectedField)) {
@@ -292,15 +291,3 @@ func TestGetLimitedClientConfig(t *testing.T) {
})
}
}
func sToP(s string) *string {
return &s
}
func bToP(b bool) *bool {
return &b
}
func iToP(i int) *int {
return &i
}

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package config_test
package config
import (
"os"
@@ -10,7 +10,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v5/config"
"github.com/mattermost/mattermost-server/v5/model"
)
@@ -20,23 +19,23 @@ func init() {
emptyConfig = &model.Config{}
readOnlyConfig = &model.Config{
ClusterSettings: model.ClusterSettings{
Enable: bToP(true),
ReadOnlyConfig: bToP(true),
Enable: model.NewBool(true),
ReadOnlyConfig: model.NewBool(true),
},
}
minimalConfig = &model.Config{
ServiceSettings: model.ServiceSettings{
SiteURL: sToP("http://minimal"),
SiteURL: model.NewString("http://minimal"),
},
SqlSettings: model.SqlSettings{
AtRestEncryptKey: sToP("abcdefghijklmnopqrstuvwxyz0123456789"),
AtRestEncryptKey: model.NewString("abcdefghijklmnopqrstuvwxyz0123456789"),
},
FileSettings: model.FileSettings{
PublicLinkSalt: sToP("abcdefghijklmnopqrstuvwxyz0123456789"),
PublicLinkSalt: model.NewString("abcdefghijklmnopqrstuvwxyz0123456789"),
},
LocalizationSettings: model.LocalizationSettings{
DefaultServerLocale: sToP("en"),
DefaultClientLocale: sToP("en"),
DefaultServerLocale: model.NewString("en"),
DefaultClientLocale: model.NewString("en"),
},
}
@@ -47,34 +46,34 @@ func init() {
invalidConfig = &model.Config{
ServiceSettings: model.ServiceSettings{
SiteURL: sToP("invalid"),
SiteURL: model.NewString("invalid"),
},
}
fixesRequiredConfig = &model.Config{
ServiceSettings: model.ServiceSettings{
SiteURL: sToP("http://trailingslash/"),
SiteURL: model.NewString("http://trailingslash/"),
},
SqlSettings: model.SqlSettings{
AtRestEncryptKey: sToP("abcdefghijklmnopqrstuvwxyz0123456789"),
AtRestEncryptKey: model.NewString("abcdefghijklmnopqrstuvwxyz0123456789"),
},
FileSettings: model.FileSettings{
DriverName: sToP(model.IMAGE_DRIVER_LOCAL),
Directory: sToP("/path/to/directory"),
PublicLinkSalt: sToP("abcdefghijklmnopqrstuvwxyz0123456789"),
DriverName: model.NewString(model.IMAGE_DRIVER_LOCAL),
Directory: model.NewString("/path/to/directory"),
PublicLinkSalt: model.NewString("abcdefghijklmnopqrstuvwxyz0123456789"),
},
LocalizationSettings: model.LocalizationSettings{
DefaultServerLocale: sToP("garbage"),
DefaultClientLocale: sToP("garbage"),
DefaultServerLocale: model.NewString("garbage"),
DefaultClientLocale: model.NewString("garbage"),
},
}
ldapConfig = &model.Config{
LdapSettings: model.LdapSettings{
BindPassword: sToP("password"),
BindPassword: model.NewString("password"),
},
}
testConfig = &model.Config{
ServiceSettings: model.ServiceSettings{
SiteURL: sToP("http://TestStoreNew"),
SiteURL: model.NewString("http://TestStoreNew"),
},
}
customConfigDefaults = &model.Config{
@@ -94,7 +93,7 @@ func TestMergeConfigs(t *testing.T) {
patch := &model.Config{}
patch.SetDefaults()
merged, err := config.Merge(base, patch, nil)
merged, err := Merge(base, patch, nil)
require.NoError(t, err)
assert.Equal(t, patch, merged)
@@ -104,7 +103,7 @@ func TestMergeConfigs(t *testing.T) {
base.SetDefaults()
patch := base.Clone()
merged, err := config.Merge(base, patch, nil)
merged, err := Merge(base, patch, nil)
require.NoError(t, err)
assert.Equal(t, base, merged)
@@ -114,9 +113,9 @@ func TestMergeConfigs(t *testing.T) {
base := &model.Config{}
base.SetDefaults()
patch := base.Clone()
patch.ServiceSettings.SiteURL = newString("http://newhost.ca")
patch.ServiceSettings.SiteURL = model.NewString("http://newhost.ca")
merged, err := config.Merge(base, patch, nil)
merged, err := Merge(base, patch, nil)
require.NoError(t, err)
assert.NotEqual(t, base, merged)
@@ -126,14 +125,14 @@ func TestMergeConfigs(t *testing.T) {
base := &model.Config{}
base.SetDefaults()
patch := &model.Config{}
patch.ServiceSettings.SiteURL = newString("http://newhost.ca")
patch.GoogleSettings.Enable = newBool(true)
patch.ServiceSettings.SiteURL = model.NewString("http://newhost.ca")
patch.GoogleSettings.Enable = model.NewBool(true)
expected := base.Clone()
expected.ServiceSettings.SiteURL = newString("http://newhost.ca")
expected.GoogleSettings.Enable = newBool(true)
expected.ServiceSettings.SiteURL = model.NewString("http://newhost.ca")
expected.GoogleSettings.Enable = model.NewBool(true)
merged, err := config.Merge(base, patch, nil)
merged, err := Merge(base, patch, nil)
require.NoError(t, err)
assert.NotEqual(t, base, merged)
@@ -143,12 +142,12 @@ func TestMergeConfigs(t *testing.T) {
}
func TestConfigEnvironmentOverrides(t *testing.T) {
memstore, err := config.NewMemoryStore()
memstore, err := NewMemoryStore()
require.NoError(t, err)
base, err := config.NewStoreFromBacking(memstore, nil, false)
base, err := NewStoreFromBacking(memstore, nil, false)
require.NoError(t, err)
originalConfig := &model.Config{}
originalConfig.ServiceSettings.SiteURL = newString("http://notoverriden.ca")
originalConfig.ServiceSettings.SiteURL = model.NewString("http://notoverriden.ca")
os.Setenv("MM_SERVICESETTINGS_SITEURL", "http://overridden.ca")
defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL")
@@ -172,15 +171,12 @@ func TestRemoveEnvironmentOverrides(t *testing.T) {
os.Setenv("MM_SERVICESETTINGS_SITEURL", "http://overridden.ca")
defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL")
memstore, err := config.NewMemoryStore()
memstore, err := NewMemoryStore()
require.NoError(t, err)
base, err := config.NewStoreFromBacking(memstore, nil, false)
base, err := NewStoreFromBacking(memstore, nil, false)
require.NoError(t, err)
oldCfg := base.Get()
assert.Equal(t, "http://overridden.ca", *oldCfg.ServiceSettings.SiteURL)
newCfg := base.RemoveEnvironmentOverrides(oldCfg)
assert.Equal(t, "", *newCfg.ServiceSettings.SiteURL)
}
func newBool(b bool) *bool { return &b }
func newString(s string) *string { return &s }

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

@@ -1,23 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package generator
import (
"encoding/json"
"os"
"github.com/mattermost/mattermost-server/v5/model"
)
// GenerateDefaultConfig writes default config to outputFile.
func GenerateDefaultConfig(outputFile *os.File) error {
defaultCfg := &model.Config{}
defaultCfg.SetDefaults()
if data, err := json.MarshalIndent(defaultCfg, "", " "); err != nil {
return err
} else if _, err := outputFile.Write(data); err != nil {
return err
}
return nil
}

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package config_test
package config
import (
"bytes"
@@ -16,7 +16,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v5/config"
"github.com/mattermost/mattermost-server/v5/model"
)
@@ -35,11 +34,11 @@ func setupConfigDatabase(t *testing.T, cfg *model.Config, files map[string][]byt
os.Clearenv()
truncateTables(t)
cfgData, err := config.MarshalConfig(cfg)
cfgData, err := marshalConfig(cfg)
require.NoError(t, err)
db := sqlx.NewDb(mainHelper.GetSQLStore().GetMaster().Db, *mainHelper.GetSQLSettings().DriverName)
err = config.InitializeConfigurationsTable(db)
err = initializeConfigurationsTable(db)
require.NoError(t, err)
id := model.NewId()
@@ -115,14 +114,14 @@ func assertDatabaseNotEqualsConfig(t *testing.T, expectedCfg *model.Config) {
assert.NotEqual(t, expectedCfg, actualCfg)
}
func newTestDatabaseStore(customDefaults *model.Config) (*config.Store, error) {
func newTestDatabaseStore(customDefaults *model.Config) (*Store, error) {
sqlSettings := mainHelper.GetSQLSettings()
dss, err := config.NewDatabaseStore(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource))
dss, err := NewDatabaseStore(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource))
if err != nil {
return nil, err
}
cStore, err := config.NewStoreFromBacking(dss, customDefaults, false)
cStore, err := NewStoreFromBacking(dss, customDefaults, false)
if err != nil {
return nil, err
}
@@ -211,20 +210,20 @@ func TestDatabaseStoreNew(t *testing.T) {
})
t.Run("invalid url", func(t *testing.T) {
_, err := config.NewDatabaseStore("")
_, err := NewDatabaseStore("")
require.Error(t, err)
_, err = config.NewDatabaseStore("mysql")
_, err = NewDatabaseStore("mysql")
require.Error(t, err)
})
t.Run("unsupported scheme", func(t *testing.T) {
_, err := config.NewDatabaseStore("invalid")
_, err := NewDatabaseStore("invalid")
require.Error(t, err)
})
t.Run("unsupported scheme with valid data source", func(t *testing.T) {
_, err := config.NewDatabaseStore(fmt.Sprintf("invalid://%s", *sqlSettings.DataSource))
_, err := NewDatabaseStore(fmt.Sprintf("invalid://%s", *sqlSettings.DataSource))
require.Error(t, err)
})
}
@@ -452,7 +451,7 @@ func TestDatabaseStoreSet(t *testing.T) {
defer ds.Close()
newCfg := &model.Config{}
newCfg.LdapSettings.BindPassword = sToP(model.FAKE_SETTING)
newCfg.LdapSettings.BindPassword = model.NewString(model.FAKE_SETTING)
_, err = ds.Set(newCfg)
require.NoError(t, err)
@@ -469,7 +468,7 @@ func TestDatabaseStoreSet(t *testing.T) {
defer ds.Close()
newCfg := &model.Config{}
newCfg.ServiceSettings.SiteURL = sToP("invalid")
newCfg.ServiceSettings.SiteURL = model.NewString("invalid")
_, err = ds.Set(newCfg)
if assert.Error(t, err) {
@@ -508,7 +507,7 @@ func TestDatabaseStoreSet(t *testing.T) {
newCfg := &model.Config{
ServiceSettings: model.ServiceSettings{
SiteURL: sToP("http://new"),
SiteURL: model.NewString("http://new"),
},
}
@@ -528,7 +527,7 @@ func TestDatabaseStoreSet(t *testing.T) {
newCfg := &model.Config{
ServiceSettings: model.ServiceSettings{
SiteURL: sToP("http://new"),
SiteURL: model.NewString("http://new"),
},
}
@@ -554,7 +553,7 @@ func TestDatabaseStoreSet(t *testing.T) {
_, err = db.Exec("DROP TABLE Configurations")
require.NoError(t, err)
newCfg := &model.Config{}
newCfg := minimalConfig
_, err = ds.Set(newCfg)
require.Error(t, err)
@@ -574,9 +573,9 @@ func TestDatabaseStoreSet(t *testing.T) {
require.NoError(t, err)
defer ds.Close()
longSiteURL := fmt.Sprintf("http://%s", strings.Repeat("a", config.MaxWriteLength))
longSiteURL := fmt.Sprintf("http://%s", strings.Repeat("a", MaxWriteLength))
newCfg := emptyConfig.Clone()
newCfg.ServiceSettings.SiteURL = sToP(longSiteURL)
newCfg.ServiceSettings.SiteURL = model.NewString(longSiteURL)
_, err = ds.Set(newCfg)
require.Error(t, err)
@@ -597,7 +596,7 @@ func TestDatabaseStoreSet(t *testing.T) {
}
ds.AddListener(callback)
newCfg := &model.Config{}
newCfg := minimalConfig
_, err = ds.Set(newCfg)
require.NoError(t, err)
@@ -616,7 +615,7 @@ func TestDatabaseStoreSet(t *testing.T) {
require.NoError(t, err)
defer ds.Close()
ds.PersistFeatures(false)
ds.SetReadOnlyFF(true)
_, err = ds.Set(minimalConfig)
require.NoError(t, err)
@@ -632,7 +631,7 @@ func TestDatabaseStoreSet(t *testing.T) {
require.NoError(t, err)
defer ds.Close()
ds.PersistFeatures(true)
ds.SetReadOnlyFF(false)
_, err = ds.Set(minimalConfig)
require.NoError(t, err)
@@ -828,7 +827,7 @@ func TestDatabaseStoreLoad(t *testing.T) {
require.NoError(t, err)
defer ds.Close()
cfgData, err := config.MarshalConfig(invalidConfig)
cfgData, err := marshalConfig(invalidConfig)
require.NoError(t, err)
sqlSettings := mainHelper.GetSQLSettings()
@@ -862,7 +861,7 @@ func TestDatabaseStoreLoad(t *testing.T) {
assert.Equal(t, "http://trailingslash", *ds.Get().ServiceSettings.SiteURL)
})
t.Run("listeners notifed", func(t *testing.T) {
t.Run("listeners notifed on change", func(t *testing.T) {
_, tearDown := setupConfigDatabase(t, emptyConfig, nil)
defer tearDown()
@@ -876,10 +875,16 @@ func TestDatabaseStoreLoad(t *testing.T) {
}
ds.AddListener(callback)
newCfg := minimalConfig.Clone()
dbStore, ok := ds.backingStore.(*DatabaseStore)
require.True(t, ok)
err = dbStore.persist(newCfg)
require.NoError(t, err)
err = ds.Load()
require.NoError(t, err)
require.True(t, wasCalled(called, 5*time.Second), "callback should have been called when config loaded")
require.True(t, wasCalled(called, 5*time.Second), "callback should have been called when config changed on load")
})
}
@@ -950,7 +955,7 @@ func TestDatabaseSetFile(t *testing.T) {
if *mainHelper.Settings.DriverName == "postgres" {
t.Skip("No limit for postgres")
}
longFile := bytes.Repeat([]byte("a"), config.MaxWriteLength)
longFile := bytes.Repeat([]byte("a"), MaxWriteLength)
err := ds.SetFile("toolong", longFile)
require.NoError(t, err)
@@ -960,7 +965,7 @@ func TestDatabaseSetFile(t *testing.T) {
if *mainHelper.Settings.DriverName == "postgres" {
t.Skip("No limit for postgres")
}
longFile := bytes.Repeat([]byte("a"), config.MaxWriteLength+1)
longFile := bytes.Repeat([]byte("a"), MaxWriteLength+1)
err := ds.SetFile("toolong", longFile)
if assert.Error(t, err) {

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

@@ -1,6 +0,0 @@
//go:generate go run config_generator/main.go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package config

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

@@ -10,6 +10,9 @@ import (
"github.com/mattermost/mattermost-server/v5/shared/mlog"
)
// Listener is a callback function invoked when the configuration changes.
type Listener func(oldCfg, newCfg *model.Config)
// emitter enables threadsafe registration and broadcasting to configuration listeners
type emitter struct {
listeners sync.Map
@@ -18,9 +21,7 @@ type emitter struct {
// AddListener adds a callback function to invoke when the configuration is modified.
func (e *emitter) AddListener(listener Listener) string {
id := model.NewId()
e.listeners.Store(id, listener)
return id
}
@@ -34,7 +35,6 @@ func (e *emitter) invokeConfigListeners(oldCfg, newCfg *model.Config) {
e.listeners.Range(func(key, value interface{}) bool {
listener := value.(Listener)
listener(oldCfg, newCfg)
return true
})
}

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

@@ -1,20 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package config
import (
"github.com/jmoiron/sqlx"
"github.com/mattermost/mattermost-server/v5/model"
)
// MarshalConfig exposes the internal marshalConfig to tests only.
func MarshalConfig(cfg *model.Config) ([]byte, error) {
return marshalConfig(cfg)
}
// InitializeConfigurationsTable exposes the internal initializeConfigurationsTable to test only.
func InitializeConfigurationsTable(db *sqlx.DB) error {
return initializeConfigurationsTable(db)
}

Разница между файлами не показана из-за своего большого размера Загрузить разницу

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

@@ -4,7 +4,10 @@
package config
import (
"encoding/json"
"errors"
"os"
"path/filepath"
"strings"
"sync"
@@ -13,10 +16,6 @@ import (
type LogSrcListener func(old, new mlog.LogTargetCfg)
type FileGetter interface {
GetFile(name string) ([]byte, error)
}
// LogConfigSrc abstracts the Advanced Logging configuration so that implementations can
// fetch from file, database, etc.
type LogConfigSrc interface {
@@ -24,7 +23,7 @@ type LogConfigSrc interface {
Get() mlog.LogTargetCfg
// Set updates the dsn specifying the source and reloads
Set(dsn string, fget FileGetter) (err error)
Set(dsn string, configStore *Store) (err error)
// AddListener adds a callback function to invoke when the configuration is modified.
AddListener(listener LogSrcListener) string
@@ -38,13 +37,25 @@ type LogConfigSrc interface {
// NewLogConfigSrc creates an advanced logging configuration source, backed by a
// file, JSON string, or database.
func NewLogConfigSrc(dsn string, isJSON bool, fget FileGetter) (LogConfigSrc, error) {
func NewLogConfigSrc(dsn string, configStore *Store) (LogConfigSrc, error) {
if configStore == nil {
return nil, errors.New("configStore should not be nil")
}
dsn = strings.TrimSpace(dsn)
if isJSON {
if isJSONMap(dsn) {
return newJSONSrc(dsn)
}
return newFileSrc(dsn, fget)
path := dsn
// If this is a file based config we need the full path so it can be watched.
if strings.HasPrefix(configStore.String(), "file://") && !filepath.IsAbs(dsn) {
configPath := strings.TrimPrefix(configStore.String(), "file://")
path = filepath.Join(filepath.Dir(configPath), dsn)
}
return newFileSrc(path, configStore)
}
// jsonSrc
@@ -68,8 +79,8 @@ func (src *jsonSrc) Get() mlog.LogTargetCfg {
}
// Set updates the JSON specifying the source and reloads
func (src *jsonSrc) Set(data string, _ FileGetter) error {
cfg, err := JSONToLogTargetCfg([]byte(data))
func (src *jsonSrc) Set(data string, _ *Store) error {
cfg, err := logTargetCfgFromJSON([]byte(data))
if err != nil {
return err
}
@@ -103,11 +114,11 @@ type fileSrc struct {
watcher *watcher
}
func newFileSrc(path string, fget FileGetter) (*fileSrc, error) {
func newFileSrc(path string, configStore *Store) (*fileSrc, error) {
src := &fileSrc{
path: path,
}
if err := src.Set(path, fget); err != nil {
if err := src.Set(path, configStore); err != nil {
return nil, err
}
return src, nil
@@ -123,13 +134,13 @@ func (src *fileSrc) Get() mlog.LogTargetCfg {
// Set updates the dsn specifying the file source and reloads.
// The file will be watched for changes and reloaded as needed,
// and all listeners notified.
func (src *fileSrc) Set(path string, fget FileGetter) error {
data, err := fget.GetFile(path)
func (src *fileSrc) Set(path string, configStore *Store) error {
data, err := configStore.GetFile(path)
if err != nil {
return err
}
cfg, err := JSONToLogTargetCfg(data)
cfg, err := logTargetCfgFromJSON(data)
if err != nil {
return err
}
@@ -153,7 +164,7 @@ func (src *fileSrc) Set(path string, fget FileGetter) error {
}
watcher, err := newWatcher(path, func() {
if serr := src.Set(path, fget); serr != nil {
if serr := src.Set(path, configStore); serr != nil {
mlog.Error("Failed to reload file on change", mlog.String("path", path), mlog.Err(serr))
}
})
@@ -186,3 +197,12 @@ func (src *fileSrc) Close() error {
}
return err
}
func logTargetCfgFromJSON(data []byte) (mlog.LogTargetCfg, error) {
cfg := make(mlog.LogTargetCfg)
err := json.Unmarshal(data, &cfg)
if err != nil {
return nil, err
}
return cfg, nil
}

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

@@ -4,10 +4,10 @@
package config
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
@@ -15,38 +15,29 @@ const (
badJSON = `{"file":{ Type="file"}}`
)
type fgetFunc func(string) ([]byte, error)
func (f fgetFunc) GetFile(path string) ([]byte, error) {
return f(path)
}
func getValidFile(path string) ([]byte, error) {
return []byte(validJSON), nil
}
func getInvalidFile(path string) ([]byte, error) {
return nil, os.ErrNotExist
}
func TestNewLogConfigSrc(t *testing.T) {
store := NewTestMemoryStore()
require.NotNil(t, store)
err := store.SetFile("advancedlogging.conf", []byte(validJSON))
require.NoError(t, err)
tests := []struct {
name string
dsn string
fget FileGetter
wantErr bool
wantType LogConfigSrc
name string
dsn string
configStore *Store
wantErr bool
wantType LogConfigSrc
}{
{name: "empty dsn", dsn: "", fget: fgetFunc(getInvalidFile), wantErr: true, wantType: nil},
{name: "garbage dsn", dsn: "!@wfejwcevioj", fget: fgetFunc(getInvalidFile), wantErr: true, wantType: nil},
{name: "valid json dsn", dsn: validJSON, fget: fgetFunc(getInvalidFile), wantErr: false, wantType: &jsonSrc{}},
{name: "invalid json dsn", dsn: badJSON, fget: fgetFunc(getInvalidFile), wantErr: true, wantType: nil},
{name: "valid filespec dsn", dsn: "advancedlogging.conf", fget: fgetFunc(getValidFile), wantErr: false, wantType: &fileSrc{}},
{name: "invalid filespec dsn", dsn: "/nobody/here.conf", fget: fgetFunc(getInvalidFile), wantErr: true, wantType: nil},
{name: "empty dsn", dsn: "", configStore: store, wantErr: true, wantType: nil},
{name: "garbage dsn", dsn: "!@wfejwcevioj", configStore: store, wantErr: true, wantType: nil},
{name: "valid json dsn", dsn: validJSON, configStore: store, wantErr: false, wantType: &jsonSrc{}},
{name: "invalid json dsn", dsn: badJSON, configStore: store, wantErr: true, wantType: nil},
{name: "valid filespec dsn", dsn: "advancedlogging.conf", configStore: store, wantErr: false, wantType: &fileSrc{}},
{name: "invalid filespec dsn", dsn: "/nobody/here.conf", configStore: store, wantErr: true, wantType: nil},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := NewLogConfigSrc(tt.dsn, IsJsonMap(tt.dsn), tt.fget)
got, err := NewLogConfigSrc(tt.dsn, tt.configStore)
if tt.wantErr {
assert.Error(t, err)
} else {

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package config_test
package config
import (
"fmt"

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package config_test
package config
import (
"os"
@@ -9,8 +9,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v5/config"
)
func setupConfigMemory(t *testing.T) {
@@ -21,7 +19,7 @@ func setupConfigMemory(t *testing.T) {
func TestMemoryGetFile(t *testing.T) {
setupConfigMemory(t)
ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{
ms, err := NewMemoryStoreWithOptions(&MemoryStoreOptions{
InitialConfig: minimalConfig,
InitialFiles: map[string][]byte{
"empty-file": {},
@@ -57,7 +55,7 @@ func TestMemoryGetFile(t *testing.T) {
func TestMemorySetFile(t *testing.T) {
setupConfigMemory(t)
ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{
ms, err := NewMemoryStoreWithOptions(&MemoryStoreOptions{
InitialConfig: minimalConfig,
})
require.NoError(t, err)
@@ -89,7 +87,7 @@ func TestMemoryHasFile(t *testing.T) {
t.Run("has non-existent", func(t *testing.T) {
setupConfigMemory(t)
ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{
ms, err := NewMemoryStoreWithOptions(&MemoryStoreOptions{
InitialConfig: minimalConfig,
})
require.NoError(t, err)
@@ -103,7 +101,7 @@ func TestMemoryHasFile(t *testing.T) {
t.Run("has existing", func(t *testing.T) {
setupConfigMemory(t)
ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{
ms, err := NewMemoryStoreWithOptions(&MemoryStoreOptions{
InitialConfig: minimalConfig,
})
require.NoError(t, err)
@@ -120,7 +118,7 @@ func TestMemoryHasFile(t *testing.T) {
t.Run("has manually created file", func(t *testing.T) {
setupConfigMemory(t)
ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{
ms, err := NewMemoryStoreWithOptions(&MemoryStoreOptions{
InitialConfig: minimalConfig,
InitialFiles: map[string][]byte{
"manual": []byte("manual file"),
@@ -139,7 +137,7 @@ func TestMemoryRemoveFile(t *testing.T) {
t.Run("remove non-existent", func(t *testing.T) {
setupConfigMemory(t)
ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{
ms, err := NewMemoryStoreWithOptions(&MemoryStoreOptions{
InitialConfig: minimalConfig,
})
require.NoError(t, err)
@@ -152,7 +150,7 @@ func TestMemoryRemoveFile(t *testing.T) {
t.Run("remove existing", func(t *testing.T) {
setupConfigMemory(t)
ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{
ms, err := NewMemoryStoreWithOptions(&MemoryStoreOptions{
InitialConfig: minimalConfig,
})
require.NoError(t, err)
@@ -175,7 +173,7 @@ func TestMemoryRemoveFile(t *testing.T) {
t.Run("remove manually created file", func(t *testing.T) {
setupConfigMemory(t)
ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{
ms, err := NewMemoryStoreWithOptions(&MemoryStoreOptions{
InitialConfig: minimalConfig,
InitialFiles: map[string][]byte{
"manual": []byte("manual file"),
@@ -199,7 +197,7 @@ func TestMemoryRemoveFile(t *testing.T) {
func TestMemoryStoreString(t *testing.T) {
setupConfigMemory(t)
ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{InitialConfig: emptyConfig})
ms, err := NewMemoryStoreWithOptions(&MemoryStoreOptions{InitialConfig: emptyConfig})
require.NoError(t, err)
defer ms.Close()

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

@@ -9,13 +9,13 @@ import (
// Migrate migrates SAML keys, certificates, and other config files from one store to another given their data source names.
func Migrate(from, to string) error {
source, err := NewStore(from, false, false, nil)
source, err := NewStoreFromDSN(from, false, false, nil)
if err != nil {
return errors.Wrapf(err, "failed to access source config %s", from)
}
defer source.Close()
destination, err := NewStore(to, false, false, nil)
destination, err := NewStoreFromDSN(to, false, false, nil)
if err != nil {
return errors.Wrapf(err, "failed to access destination config %s", to)
}
@@ -33,7 +33,7 @@ func Migrate(from, to string) error {
}
// Only migrate advanced logging config if it is not embedded JSON.
if !IsJsonMap(*sourceConfig.LogSettings.AdvancedLoggingConfig) {
if !isJSONMap(*sourceConfig.LogSettings.AdvancedLoggingConfig) {
files = append(files, *sourceConfig.LogSettings.AdvancedLoggingConfig)
}

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package config_test
package config
import (
"io/ioutil"
@@ -12,11 +12,10 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v5/config"
"github.com/mattermost/mattermost-server/v5/model"
)
type cleanUpFn func(store *config.Store)
type cleanUpFn func(store *Store)
func TestMigrate(t *testing.T) {
files := []string{
@@ -49,12 +48,12 @@ func TestMigrate(t *testing.T) {
truncateTables(t)
}
setupSource := func(t *testing.T, source *config.Store) cleanUpFn {
setupSource := func(t *testing.T, source *Store) cleanUpFn {
t.Helper()
cfg := source.Get()
originalCfg := cfg.Clone()
cfg.ServiceSettings.SiteURL = sToP("http://example.com")
cfg.ServiceSettings.SiteURL = model.NewString("http://example.com")
cfg.SamlSettings.IdpCertificateFile = &files[0]
cfg.SamlSettings.PublicCertificateFile = &files[1]
cfg.SamlSettings.PrivateKeyFile = &files[2]
@@ -77,13 +76,13 @@ func TestMigrate(t *testing.T) {
require.NoError(t, err)
}
return func(store *config.Store) {
return func(store *Store) {
_, err := store.Set(originalCfg)
require.NoError(t, err)
}
}
assertDestination := func(t *testing.T, destination *config.Store, source *config.Store) {
assertDestination := func(t *testing.T, destination *Store, source *Store) {
t.Helper()
for i, file := range files {
@@ -109,19 +108,19 @@ func TestMigrate(t *testing.T) {
destinationDSN := path.Join(pwd, "config-custom.json")
sourceDSN := getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)
sourcedb, err := config.NewDatabaseStore(sourceDSN)
sourcedb, err := NewDatabaseStore(sourceDSN)
require.NoError(t, err)
source, err := config.NewStoreFromBacking(sourcedb, nil, false)
source, err := NewStoreFromBacking(sourcedb, nil, false)
require.NoError(t, err)
defer source.Close()
cleanUp := setupSource(t, source)
err = config.Migrate(sourceDSN, destinationDSN)
err = Migrate(sourceDSN, destinationDSN)
require.NoError(t, err)
destinationfile, err := config.NewFileStore(destinationDSN, false)
destinationfile, err := NewFileStore(destinationDSN, false)
require.NoError(t, err)
destination, err := config.NewStoreFromBacking(destinationfile, nil, false)
destination, err := NewStoreFromBacking(destinationfile, nil, false)
require.NoError(t, err)
defer destination.Close()
defer cleanUp(destination)
@@ -139,19 +138,19 @@ func TestMigrate(t *testing.T) {
sourceDSN := path.Join(pwd, "config-custom.json")
destinationDSN := getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)
sourcefile, err := config.NewFileStore(sourceDSN, false)
sourcefile, err := NewFileStore(sourceDSN, false)
require.NoError(t, err)
source, err := config.NewStoreFromBacking(sourcefile, nil, false)
source, err := NewStoreFromBacking(sourcefile, nil, false)
require.NoError(t, err)
defer source.Close()
cleanUp := setupSource(t, source)
err = config.Migrate(sourceDSN, destinationDSN)
err = Migrate(sourceDSN, destinationDSN)
require.NoError(t, err)
destinationdb, err := config.NewDatabaseStore(destinationDSN)
destinationdb, err := NewDatabaseStore(destinationDSN)
require.NoError(t, err)
destination, err := config.NewStoreFromBacking(destinationdb, nil, false)
destination, err := NewStoreFromBacking(destinationdb, nil, false)
require.NoError(t, err)
defer destination.Close()
defer cleanUp(destination)

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

@@ -4,7 +4,6 @@
package config
import (
"bytes"
"encoding/json"
"reflect"
"sync"
@@ -21,9 +20,23 @@ var (
ErrReadOnlyStore = errors.New("configuration store is read-only")
)
// Listener is a callback function invoked when the configuration changes.
type Listener func(oldConfig *model.Config, newConfig *model.Config)
// Store is the higher level object that handles storing and retrieval of config data.
// To do so it relies on a variety of backing stores (e.g. file, database, memory).
type Store struct {
emitter
backingStore BackingStore
configLock sync.RWMutex
config *model.Config
configNoEnv *model.Config
configCustomDefaults *model.Config
readOnly bool
readOnlyFF bool
}
// BackingStore defines the behaviour exposed by the underlying store
// implementation (e.g. file, database).
type BackingStore interface {
// Set replaces the current configuration in its entirety and updates the backing store.
Set(*model.Config) error
@@ -54,27 +67,13 @@ type BackingStore interface {
Close() error
}
// NewStore creates a database or file store given a data source name by which to connect.
func NewStore(dsn string, watch, readOnly bool, customDefaults *model.Config) (*Store, error) {
backingStore, err := getBackingStore(dsn, watch)
if err != nil {
return nil, err
}
store, err := NewStoreFromBacking(backingStore, customDefaults, readOnly)
if err != nil {
backingStore.Close()
return nil, errors.Wrap(err, "failed to create store")
}
return store, nil
}
// NewStoreFromBacking creates and returns a new config store given a backing store.
func NewStoreFromBacking(backingStore BackingStore, customDefaults *model.Config, readOnly bool) (*Store, error) {
store := &Store{
backingStore: backingStore,
configCustomDefaults: customDefaults,
readOnly: readOnly,
readOnlyFF: true,
}
if err := store.Load(); err != nil {
@@ -90,14 +89,31 @@ func NewStoreFromBacking(backingStore BackingStore, customDefaults *model.Config
return store, nil
}
func getBackingStore(dsn string, watch bool) (BackingStore, error) {
// NewStoreFromDSN creates and returns a new config store backed by either a database or file store
// depending on the value of the given data source name string.
func NewStoreFromDSN(dsn string, watch, readOnly bool, customDefaults *model.Config) (*Store, error) {
var err error
var backingStore BackingStore
if IsDatabaseDSN(dsn) {
return NewDatabaseStore(dsn)
backingStore, err = NewDatabaseStore(dsn)
} else {
backingStore, err = NewFileStore(dsn, watch)
}
if err != nil {
return nil, err
}
return NewFileStore(dsn, watch)
store, err := NewStoreFromBacking(backingStore, customDefaults, readOnly)
if err != nil {
backingStore.Close()
return nil, errors.Wrap(err, "failed to create store")
}
return store, nil
}
// NewTestMemoryStore returns a new config store backed by a memory store
// to be used for testing purposes.
func NewTestMemoryStore() *Store {
memoryStore, err := NewMemoryStore()
if err != nil {
@@ -112,19 +128,6 @@ func NewTestMemoryStore() *Store {
return configStore
}
type Store struct {
emitter
backingStore BackingStore
configLock sync.RWMutex
config *model.Config
configNoEnv *model.Config
configCustomDefaults *model.Config
persistFeatureFlags bool
readOnly bool
}
// Get fetches the current, cached configuration.
func (s *Store) Get() *model.Config {
s.configLock.RLock()
@@ -132,7 +135,7 @@ func (s *Store) Get() *model.Config {
return s.config
}
// Get fetches the current, cached configuration without environment variable overrides.
// GetNoEnv fetches the current cached configuration without environment variable overrides.
func (s *Store) GetNoEnv() *model.Config {
s.configLock.RLock()
defer s.configLock.RUnlock()
@@ -151,33 +154,37 @@ func (s *Store) GetEnvironmentOverridesWithFilter(filter func(reflect.StructFiel
}
// RemoveEnvironmentOverrides returns a new config without the environment
// overrides
// 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) {
// SetReadOnlyFF sets whether feature flags should be written out to
// config or treated as read-only.
func (s *Store) SetReadOnlyFF(readOnly bool) {
s.configLock.Lock()
defer s.configLock.Unlock()
s.persistFeatureFlags = persist
s.readOnlyFF = readOnly
}
// 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)
defer s.configLock.Unlock()
if s.readOnly {
return nil, ErrReadOnlyStore
}
oldCfg := s.config.Clone()
newCfg = newCfg.Clone()
// no need to clone these as cached configs are getting replaced
// with brand new objects.
oldCfg := s.config
oldCfgNoEnv := s.configNoEnv
// Really just for some tests we need to set defaults here
// Setting defaults allows us to accept partial config objects.
newCfg.SetDefaults()
// Sometimes the config is received with "fake" data in sensitive fields. Apply the real
@@ -188,124 +195,159 @@ func (s *Store) Set(newCfg *model.Config) (*model.Config, error) {
return nil, errors.Wrap(err, "new configuration is invalid")
}
newCfg = removeEnvOverrides(newCfg, s.configNoEnv, s.GetEnvironmentOverrides())
// We attempt to remove any environment override that may be present in the input config.
newCfgNoEnv := removeEnvOverrides(newCfg, oldCfgNoEnv, s.GetEnvironmentOverrides())
// Don't persist feature flags unless we are on MM cloud
// Don't store 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 management system is down when a pod starts.
if !s.persistFeatureFlags {
// Backing up feature flags section in case we need to restore them later on.
oldCfgFF := oldCfg.FeatureFlags
oldCfgNoEnvFF := oldCfgNoEnv.FeatureFlags
// Clearing FF sections to avoid both comparing and persisting them.
if s.readOnlyFF {
oldCfg.FeatureFlags = nil
newCfg.FeatureFlags = nil
newCfgNoEnv.FeatureFlags = nil
}
if err := s.backingStore.Set(newCfg); err != nil {
if err := s.backingStore.Set(newCfgNoEnv); 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")
// We apply back environment overrides since the input config may or
// may not have them applied.
newCfg = applyEnvironmentMap(newCfgNoEnv, GetEnvironment())
fixConfig(newCfg)
if err := newCfg.IsValid(); err != nil {
return 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")
}
// We restore the previously cleared feature flags sections back.
if s.readOnlyFF {
oldCfg.FeatureFlags = oldCfgFF
newCfg.FeatureFlags = oldCfgFF
newCfgNoEnv.FeatureFlags = oldCfgNoEnvFF
}
s.configNoEnv = newCfgNoEnv
s.config = newCfg
if hasChanged {
s.configLock.Unlock()
s.invokeConfigListeners(oldCfg, newCfg.Clone())
s.configLock.Lock()
}
return oldCfg, nil
}
func (s *Store) loadLockedWithOld(oldCfg *model.Config, unlockOnce *sync.Once) error {
// Load updates the current configuration from the backing store, possibly initializing.
func (s *Store) Load() error {
s.configLock.Lock()
defer s.configLock.Unlock()
oldCfg := &model.Config{}
if s.config != nil {
oldCfg = s.config
}
configBytes, err := s.backingStore.Load()
if err != nil {
return err
}
loadedConfig := &model.Config{}
loadedCfg := &model.Config{}
if len(configBytes) != 0 {
if err = json.Unmarshal(configBytes, &loadedConfig); err != nil {
if err = json.Unmarshal(configBytes, &loadedCfg); err != nil {
return jsonutils.HumanizeJSONError(err, configBytes)
}
}
loadedFeatureFlags := loadedConfig.FeatureFlags
// If we have custom defaults set, the initial config is merged on
// top of them and we delete them not to be used again in the
// configuration reloads
if s.configCustomDefaults != nil {
var mErr error
loadedConfig, mErr = Merge(s.configCustomDefaults, loadedConfig, nil)
loadedCfg, mErr = Merge(s.configCustomDefaults, loadedCfg, nil)
if mErr != nil {
return errors.Wrap(mErr, "failed to merge custom config defaults")
}
s.configCustomDefaults = nil
}
loadedConfig.SetDefaults()
// We set the SiteURL to empty (if nil) so that the following call to
// SetDefaults() will generate missing data. This avoids an additional write
// to the backing store.
if loadedCfg.ServiceSettings.SiteURL == nil {
loadedCfg.ServiceSettings.SiteURL = model.NewString("")
}
s.configNoEnv = loadedConfig.Clone()
fixConfig(s.configNoEnv)
// Setting defaults allows us to accept partial config objects.
loadedCfg.SetDefaults()
loadedConfig = applyEnvironmentMap(loadedConfig, GetEnvironment())
// No need to clone here since the below call to applyEnvironmentMap
// already does that internally.
loadedCfgNoEnv := loadedCfg
fixConfig(loadedCfgNoEnv)
fixConfig(loadedConfig)
if err := loadedConfig.IsValid(); err != nil {
loadedCfg = applyEnvironmentMap(loadedCfg, GetEnvironment())
fixConfig(loadedCfg)
if err := loadedCfg.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)
// Backing up feature flags section in case we need to restore them later on.
oldCfgFF := oldCfg.FeatureFlags
loadedCfgFF := loadedCfg.FeatureFlags
loadedCfgNoEnvFF := loadedCfgNoEnv.FeatureFlags
// Clearing FF sections to avoid both comparing and persisting them.
if s.readOnlyFF {
oldCfg.FeatureFlags = nil
loadedCfg.FeatureFlags = nil
loadedCfgNoEnv.FeatureFlags = nil
}
// Check for changes that may have happened on load to the backing store.
hasChanged, err := equal(oldCfg, loadedCfg)
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")
return errors.Wrap(err, "failed to compare configs")
}
var shouldStore bool
hasChanged := len(configBytes) == 0 || !bytes.Equal(oldCfgBytes, newCfgBytes)
if hasChanged {
featureFlags := s.configNoEnv.FeatureFlags
// 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 management system is down when a pod starts.
if !s.persistFeatureFlags {
s.configNoEnv.FeatureFlags = loadedFeatureFlags
}
toStoreBytes, err := json.Marshal(s.configNoEnv)
if err != nil {
return errors.Wrap(err, "failed to marshal old config")
}
shouldStore = !bytes.Equal(toStoreBytes, configBytes)
// We write back to the backing store only if
// the config has changed and the store is not read-only.
if !s.readOnly && shouldStore {
err := s.backingStore.Set(s.configNoEnv)
s.configNoEnv.FeatureFlags = featureFlags
if err != nil && !errors.Is(err, ErrReadOnlyConfiguration) {
return errors.Wrap(err, "failed to persist")
}
// We write back to the backing store only if the store is not read-only
// and the config has either changed or is missing.
if !s.readOnly && (hasChanged || len(configBytes) == 0) {
err := s.backingStore.Set(loadedCfgNoEnv)
if err != nil && !errors.Is(err, ErrReadOnlyConfiguration) {
return errors.Wrap(err, "failed to persist")
}
}
s.config = loadedConfig
// We restore the previously cleared feature flags sections back.
if s.readOnlyFF {
oldCfg.FeatureFlags = oldCfgFF
loadedCfg.FeatureFlags = loadedCfgFF
loadedCfgNoEnv.FeatureFlags = loadedCfgNoEnvFF
}
unlockOnce.Do(s.configLock.Unlock)
s.config = loadedCfg
s.configNoEnv = loadedCfgNoEnv
if hasChanged {
s.invokeConfigListeners(oldCfg, loadedConfig)
s.configLock.Unlock()
s.invokeConfigListeners(oldCfg, loadedCfg.Clone())
s.configLock.Lock()
}
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) {

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package config_test
package config
import (
"io/ioutil"
@@ -10,11 +10,9 @@ import (
"testing"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v5/config"
)
func TestNewStore(t *testing.T) {
func TestNewStoreFromDSN(t *testing.T) {
if testing.Short() {
t.SkipNow()
}
@@ -29,25 +27,25 @@ func TestNewStore(t *testing.T) {
require.NoError(t, os.Mkdir(filepath.Join(tempDir, "config"), 0700))
t.Run("database dsn", func(t *testing.T) {
ds, err := config.NewStore(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource), false, false, nil)
ds, err := NewStoreFromDSN(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource), false, false, nil)
require.NoError(t, err)
ds.Close()
})
t.Run("database dsn, watch ignored", func(t *testing.T) {
ds, err := config.NewStore(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource), true, false, nil)
ds, err := NewStoreFromDSN(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource), true, false, nil)
require.NoError(t, err)
ds.Close()
})
t.Run("file dsn", func(t *testing.T) {
fs, err := config.NewStore("config.json", false, false, nil)
fs, err := NewStoreFromDSN("config.json", false, false, nil)
require.NoError(t, err)
fs.Close()
})
t.Run("file dsn, watch", func(t *testing.T) {
fs, err := config.NewStore("config.json", true, false, nil)
fs, err := NewStoreFromDSN("config.json", true, false, nil)
require.NoError(t, err)
fs.Close()
})
@@ -68,46 +66,46 @@ func TestNewStoreReadOnly(t *testing.T) {
require.NoError(t, os.Mkdir(filepath.Join(tempDir, "config"), 0700))
t.Run("database dsn", func(t *testing.T) {
ds, err := config.NewStore(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource), false, true, nil)
ds, err := NewStoreFromDSN(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource), false, true, nil)
require.NoError(t, err)
t.Run("Set", func(t *testing.T) {
cfg, err := ds.Set(emptyConfig)
require.Nil(t, cfg)
require.Equal(t, config.ErrReadOnlyStore, err)
require.Equal(t, ErrReadOnlyStore, err)
})
t.Run("SetFile", func(t *testing.T) {
err := ds.SetFile("config.json", []byte{})
require.Equal(t, config.ErrReadOnlyStore, err)
require.Equal(t, ErrReadOnlyStore, err)
})
t.Run("RemoveFile", func(t *testing.T) {
err := ds.RemoveFile("config.json")
require.Equal(t, config.ErrReadOnlyStore, err)
require.Equal(t, ErrReadOnlyStore, err)
})
ds.Close()
})
t.Run("file dsn", func(t *testing.T) {
fs, err := config.NewStore("config.json", false, true, nil)
fs, err := NewStoreFromDSN("config.json", false, true, nil)
require.NoError(t, err)
t.Run("Set", func(t *testing.T) {
cfg, err := fs.Set(emptyConfig)
require.Nil(t, cfg)
require.Equal(t, config.ErrReadOnlyStore, err)
require.Equal(t, ErrReadOnlyStore, err)
})
t.Run("SetFile", func(t *testing.T) {
err := fs.SetFile("config.json", []byte{})
require.Equal(t, config.ErrReadOnlyStore, err)
require.Equal(t, ErrReadOnlyStore, err)
})
t.Run("RemoveFile", func(t *testing.T) {
err := fs.RemoveFile("config.json")
require.Equal(t, config.ErrReadOnlyStore, err)
require.Equal(t, ErrReadOnlyStore, err)
})
fs.Close()

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

@@ -1,15 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package config
import (
"encoding/json"
"github.com/mattermost/mattermost-server/v5/model"
)
// marshalConfig converts the given configuration into JSON bytes for persistence.
func marshalConfig(cfg *model.Config) ([]byte, error) {
return json.MarshalIndent(cfg, "", " ")
}

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

@@ -4,7 +4,9 @@
package config
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"strings"
@@ -14,6 +16,11 @@ import (
"github.com/mattermost/mattermost-server/v5/utils"
)
// marshalConfig converts the given configuration into JSON bytes for persistence.
func marshalConfig(cfg *model.Config) ([]byte, error) {
return json.MarshalIndent(cfg, "", " ")
}
// desanitize replaces fake settings with their actual values.
func desanitize(actual, target *model.Config) {
if target.LdapSettings.BindPassword != nil && *target.LdapSettings.BindPassword == model.FAKE_SETTING {
@@ -191,20 +198,11 @@ func stripPassword(dsn, schema string) string {
return prefix + dsn[:i+1] + dsn[j:]
}
func IsJsonMap(data string) bool {
func isJSONMap(data string) bool {
var m map[string]interface{}
return json.Unmarshal([]byte(data), &m) == nil
}
func JSONToLogTargetCfg(data []byte) (mlog.LogTargetCfg, error) {
cfg := make(mlog.LogTargetCfg)
err := json.Unmarshal(data, &cfg)
if err != nil {
return nil, err
}
return cfg, nil
}
func GetValueByPath(path []string, obj interface{}) (interface{}, bool) {
r := reflect.ValueOf(obj)
var val reflect.Value
@@ -249,3 +247,15 @@ func GetValueByPath(path []string, obj interface{}) (interface{}, bool) {
}
return nil, false
}
func equal(oldCfg, newCfg *model.Config) (bool, error) {
oldCfgBytes, err := json.Marshal(oldCfg)
if err != nil {
return false, fmt.Errorf("failed to marshal old config: %w", err)
}
newCfgBytes, err := json.Marshal(newCfg)
if err != nil {
return false, fmt.Errorf("failed to marshal new config: %w", err)
}
return !bytes.Equal(oldCfgBytes, newCfgBytes), nil
}

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

@@ -7,6 +7,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v5/model"
"github.com/mattermost/mattermost-server/v5/utils"
@@ -17,19 +18,19 @@ func TestDesanitize(t *testing.T) {
actual.SetDefaults()
// These setting should be ignored
actual.LdapSettings.Enable = bToP(false)
actual.FileSettings.DriverName = sToP("s3")
actual.LdapSettings.Enable = model.NewBool(false)
actual.FileSettings.DriverName = model.NewString("s3")
// These settings should be desanitized into target.
actual.LdapSettings.BindPassword = sToP("bind_password")
actual.FileSettings.PublicLinkSalt = sToP("public_link_salt")
actual.FileSettings.AmazonS3SecretAccessKey = sToP("amazon_s3_secret_access_key")
actual.EmailSettings.SMTPPassword = sToP("smtp_password")
actual.GitLabSettings.Secret = sToP("secret")
actual.OpenIdSettings.Secret = sToP("secret")
actual.SqlSettings.DataSource = sToP("data_source")
actual.SqlSettings.AtRestEncryptKey = sToP("at_rest_encrypt_key")
actual.ElasticsearchSettings.Password = sToP("password")
actual.LdapSettings.BindPassword = model.NewString("bind_password")
actual.FileSettings.PublicLinkSalt = model.NewString("public_link_salt")
actual.FileSettings.AmazonS3SecretAccessKey = model.NewString("amazon_s3_secret_access_key")
actual.EmailSettings.SMTPPassword = model.NewString("smtp_password")
actual.GitLabSettings.Secret = model.NewString("secret")
actual.OpenIdSettings.Secret = model.NewString("secret")
actual.SqlSettings.DataSource = model.NewString("data_source")
actual.SqlSettings.AtRestEncryptKey = model.NewString("at_rest_encrypt_key")
actual.ElasticsearchSettings.Password = model.NewString("password")
actual.SqlSettings.DataSourceReplicas = append(actual.SqlSettings.DataSourceReplicas, "replica0")
actual.SqlSettings.DataSourceReplicas = append(actual.SqlSettings.DataSourceReplicas, "replica1")
actual.SqlSettings.DataSourceSearchReplicas = append(actual.SqlSettings.DataSourceSearchReplicas, "search_replica0")
@@ -39,19 +40,19 @@ func TestDesanitize(t *testing.T) {
target.SetDefaults()
// These setting should be ignored
target.LdapSettings.Enable = bToP(true)
target.FileSettings.DriverName = sToP("file")
target.LdapSettings.Enable = model.NewBool(true)
target.FileSettings.DriverName = model.NewString("file")
// These settings should be updated from actual
target.LdapSettings.BindPassword = sToP(model.FAKE_SETTING)
target.FileSettings.PublicLinkSalt = sToP(model.FAKE_SETTING)
target.FileSettings.AmazonS3SecretAccessKey = sToP(model.FAKE_SETTING)
target.EmailSettings.SMTPPassword = sToP(model.FAKE_SETTING)
target.GitLabSettings.Secret = sToP(model.FAKE_SETTING)
target.OpenIdSettings.Secret = sToP(model.FAKE_SETTING)
target.SqlSettings.DataSource = sToP(model.FAKE_SETTING)
target.SqlSettings.AtRestEncryptKey = sToP(model.FAKE_SETTING)
target.ElasticsearchSettings.Password = sToP(model.FAKE_SETTING)
target.LdapSettings.BindPassword = model.NewString(model.FAKE_SETTING)
target.FileSettings.PublicLinkSalt = model.NewString(model.FAKE_SETTING)
target.FileSettings.AmazonS3SecretAccessKey = model.NewString(model.FAKE_SETTING)
target.EmailSettings.SMTPPassword = model.NewString(model.FAKE_SETTING)
target.GitLabSettings.Secret = model.NewString(model.FAKE_SETTING)
target.OpenIdSettings.Secret = model.NewString(model.FAKE_SETTING)
target.SqlSettings.DataSource = model.NewString(model.FAKE_SETTING)
target.SqlSettings.AtRestEncryptKey = model.NewString(model.FAKE_SETTING)
target.ElasticsearchSettings.Password = model.NewString(model.FAKE_SETTING)
target.SqlSettings.DataSourceReplicas = []string{model.FAKE_SETTING, model.FAKE_SETTING}
target.SqlSettings.DataSourceSearchReplicas = []string{model.FAKE_SETTING, model.FAKE_SETTING}
@@ -246,15 +247,7 @@ func TestStripPassword(t *testing.T) {
}
}
func sToP(s string) *string {
return &s
}
func bToP(b bool) *bool {
return &b
}
func TestIsJsonMap(t *testing.T) {
func TestIsJSONMap(t *testing.T) {
tests := []struct {
name string
data string
@@ -278,9 +271,34 @@ func TestIsJsonMap(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := IsJsonMap(tt.data); got != tt.want {
t.Errorf("IsJsonMap() = %v, want %v", got, tt.want)
if got := isJSONMap(tt.data); got != tt.want {
t.Errorf("isJSONMap() = %v, want %v", got, tt.want)
}
})
}
}
func TestEqual(t *testing.T) {
t.Run("nil", func(t *testing.T) {
diff, err := equal(nil, nil)
require.NoError(t, err)
require.False(t, diff)
})
t.Run("no diff", func(t *testing.T) {
old := minimalConfig.Clone()
new := minimalConfig.Clone()
diff, err := equal(old, new)
require.NoError(t, err)
require.False(t, diff)
})
t.Run("diff", func(t *testing.T) {
old := minimalConfig.Clone()
new := minimalConfig.Clone()
new.SqlSettings = model.SqlSettings{}
diff, err := equal(old, new)
require.NoError(t, err)
require.True(t, diff)
})
}

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

@@ -14,8 +14,6 @@ import (
// watcher monitors a file for changes
type watcher struct {
emitter
fsWatcher *fsnotify.Watcher
close chan struct{}
closed chan struct{}

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

@@ -4,12 +4,25 @@
package main
import (
"encoding/json"
"fmt"
"os"
"github.com/mattermost/mattermost-server/v5/config/config_generator/generator"
"github.com/mattermost/mattermost-server/v5/model"
)
// generateDefaultConfig writes default config to outputFile.
func generateDefaultConfig(outputFile *os.File) error {
defaultCfg := &model.Config{}
defaultCfg.SetDefaults()
if data, err := json.MarshalIndent(defaultCfg, "", " "); err != nil {
return err
} else if _, err := outputFile.Write(data); err != nil {
return err
}
return nil
}
func main() {
outputFile := os.Getenv("OUTPUT_CONFIG")
if outputFile == "" {
@@ -22,7 +35,7 @@ func main() {
}
if file, err := os.Create(outputFile); err == nil {
err = generator.GenerateDefaultConfig(file)
err = generateDefaultConfig(file)
_ = file.Close()
if err != nil {
panic(err)
@@ -30,5 +43,4 @@ func main() {
} else {
panic(err)
}
}

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package config
package main
import (
"encoding/json"
@@ -9,17 +9,16 @@ import (
"os"
"testing"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v5/config/config_generator/generator"
"github.com/mattermost/mattermost-server/v5/model"
"github.com/stretchr/testify/require"
)
func TestDefaultsGenerator(t *testing.T) {
tmpFile, err := ioutil.TempFile("", "tempconfig")
defer os.Remove(tmpFile.Name())
require.NoError(t, err)
require.NoError(t, generator.GenerateDefaultConfig(tmpFile))
require.NoError(t, generateDefaultConfig(tmpFile))
_ = tmpFile.Close()
var config model.Config