[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>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
b810a40062
Коммит
3681cd3688
2
Makefile
2
Makefile
@@ -549,7 +549,7 @@ config-ldap: ## Configures LDAP.
|
|||||||
config-reset: ## Resets the config/config.json file to the default.
|
config-reset: ## Resets the config/config.json file to the default.
|
||||||
@echo Resetting configuration to default
|
@echo Resetting configuration to default
|
||||||
rm -f config/config.json
|
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
|
diff-config: ## Compares default configuration between two mattermost versions
|
||||||
@./scripts/diff-config.sh
|
@./scripts/diff-config.sh
|
||||||
|
|||||||
@@ -721,11 +721,11 @@ func TestMigrateConfig(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
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)
|
require.NoError(t, err)
|
||||||
defer f.RemoveFile("from.json")
|
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)
|
require.NoError(t, err)
|
||||||
defer f.RemoveFile("to.json")
|
defer f.RemoveFile("to.json")
|
||||||
|
|
||||||
|
|||||||
@@ -5523,6 +5523,9 @@ func TestThreadSocketEvents(t *testing.T) {
|
|||||||
os.Setenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS", "true")
|
os.Setenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS", "true")
|
||||||
defer os.Unsetenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS")
|
defer os.Unsetenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS")
|
||||||
|
|
||||||
|
th.ConfigStore.SetReadOnlyFF(false)
|
||||||
|
defer th.ConfigStore.SetReadOnlyFF(true)
|
||||||
|
|
||||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||||
*cfg.ServiceSettings.ThreadAutoFollow = true
|
*cfg.ServiceSettings.ThreadAutoFollow = true
|
||||||
*cfg.ServiceSettings.CollapsedThreads = model.COLLAPSED_THREADS_DEFAULT_ON
|
*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 == "" {
|
if !bAllowAdvancedLogging || dsn == "" {
|
||||||
return errs
|
return errs
|
||||||
}
|
}
|
||||||
isJson := config.IsJsonMap(dsn)
|
cfg, err := config.NewLogConfigSrc(dsn, s.configStore)
|
||||||
cfg, err := config.NewLogConfigSrc(dsn, isJson, s.configStore)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = multierror.Append(fmt.Errorf("invalid config for audit, %w", err))
|
errs = multierror.Append(fmt.Errorf("invalid config for audit, %w", err))
|
||||||
return errs
|
return errs
|
||||||
}
|
}
|
||||||
if !isJson {
|
mlog.Debug("Loaded audit configuration", mlog.String("source", dsn))
|
||||||
mlog.Debug("Loaded audit configuration", mlog.String("filename", dsn))
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, t := range cfg.Get() {
|
for name, t := range cfg.Get() {
|
||||||
if len(t.Levels) == 0 {
|
if len(t.Levels) == 0 {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mattermost/mattermost-server/v5/config"
|
"github.com/mattermost/mattermost-server/v5/app/featureflag"
|
||||||
"github.com/mattermost/mattermost-server/v5/shared/mlog"
|
"github.com/mattermost/mattermost-server/v5/shared/mlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ func (s *Server) setupFeatureFlags() {
|
|||||||
splitConfigured := splitKey != ""
|
splitConfigured := splitKey != ""
|
||||||
syncFeatureFlags := splitConfigured && s.IsLeader()
|
syncFeatureFlags := splitConfigured && s.IsLeader()
|
||||||
|
|
||||||
s.configStore.PersistFeatures(splitConfigured)
|
s.configStore.SetReadOnlyFF(!splitConfigured)
|
||||||
|
|
||||||
if syncFeatureFlags {
|
if syncFeatureFlags {
|
||||||
if err := s.startFeatureFlagUpdateJob(); err != nil {
|
if err := s.startFeatureFlagUpdateJob(); err != nil {
|
||||||
@@ -71,7 +71,7 @@ func (s *Server) startFeatureFlagUpdateJob() error {
|
|||||||
attributes["group_id"] = groupId
|
attributes["group_id"] = groupId
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronizer, err := config.NewFeatureFlagSynchronizer(config.FeatureFlagSyncParams{
|
synchronizer, err := featureflag.NewSynchronizer(featureflag.SyncParams{
|
||||||
ServerID: s.TelemetryId(),
|
ServerID: s.TelemetryId(),
|
||||||
SplitKey: *s.Config().ServiceSettings.SplitKey,
|
SplitKey: *s.Config().ServiceSettings.SplitKey,
|
||||||
Log: log,
|
Log: log,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
package config
|
package featureflag
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"math"
|
"math"
|
||||||
@@ -17,7 +17,7 @@ import (
|
|||||||
"github.com/mattermost/mattermost-server/v5/shared/mlog"
|
"github.com/mattermost/mattermost-server/v5/shared/mlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FeatureFlagSyncParams struct {
|
type SyncParams struct {
|
||||||
ServerID string
|
ServerID string
|
||||||
SplitKey string
|
SplitKey string
|
||||||
SyncIntervalSeconds int
|
SyncIntervalSeconds int
|
||||||
@@ -25,8 +25,8 @@ type FeatureFlagSyncParams struct {
|
|||||||
Attributes map[string]interface{}
|
Attributes map[string]interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
type FeatureFlagSynchronizer struct {
|
type Synchronizer struct {
|
||||||
FeatureFlagSyncParams
|
SyncParams
|
||||||
|
|
||||||
client *client.SplitClient
|
client *client.SplitClient
|
||||||
stop chan struct{}
|
stop chan struct{}
|
||||||
@@ -35,7 +35,7 @@ type FeatureFlagSynchronizer struct {
|
|||||||
|
|
||||||
var featureNames = getStructFields(model.FeatureFlags{})
|
var featureNames = getStructFields(model.FeatureFlags{})
|
||||||
|
|
||||||
func NewFeatureFlagSynchronizer(params FeatureFlagSyncParams) (*FeatureFlagSynchronizer, error) {
|
func NewSynchronizer(params SyncParams) (*Synchronizer, error) {
|
||||||
cfg := conf.Default()
|
cfg := conf.Default()
|
||||||
if params.Log != nil {
|
if params.Log != nil {
|
||||||
cfg.Logger = &splitLogger{wrappedLog: params.Log.With(mlog.String("service", "split"))}
|
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 nil, errors.Wrap(err, "unable to create split factory")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &FeatureFlagSynchronizer{
|
return &Synchronizer{
|
||||||
FeatureFlagSyncParams: params,
|
SyncParams: params,
|
||||||
client: factory.Client(),
|
client: factory.Client(),
|
||||||
stop: make(chan struct{}),
|
stop: make(chan struct{}),
|
||||||
stopped: make(chan struct{}),
|
stopped: make(chan struct{}),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// EnsureReady blocks until the syncronizer is ready to update feature flag values
|
// 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 {
|
if err := f.client.BlockUntilReady(10); err != nil {
|
||||||
return errors.Wrap(err, "split.io client could not initialize")
|
return errors.Wrap(err, "split.io client could not initialize")
|
||||||
}
|
}
|
||||||
@@ -64,13 +64,13 @@ func (f *FeatureFlagSynchronizer) EnsureReady() error {
|
|||||||
return nil
|
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)
|
featuresMap := f.client.Treatments(f.ServerID, featureNames, f.Attributes)
|
||||||
ffm := featureFlagsFromMap(featuresMap, base)
|
ffm := featureFlagsFromMap(featuresMap, base)
|
||||||
return ffm
|
return ffm
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FeatureFlagSynchronizer) Close() {
|
func (f *Synchronizer) Close() {
|
||||||
f.client.Destroy()
|
f.client.Destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
package config
|
package featureflag
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
package config
|
package featureflag
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -45,7 +45,7 @@ func StoreOverride(override interface{}) Option {
|
|||||||
// config loaded from the dsn on top of the normal defaults
|
// config loaded from the dsn on top of the normal defaults
|
||||||
func Config(dsn string, watch, readOnly bool, configDefaults *model.Config) Option {
|
func Config(dsn string, watch, readOnly bool, configDefaults *model.Config) Option {
|
||||||
return func(s *Server) error {
|
return func(s *Server) error {
|
||||||
configStore, err := config.NewStore(dsn, watch, readOnly, configDefaults)
|
configStore, err := config.NewStoreFromDSN(dsn, watch, readOnly, configDefaults)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to apply Config option")
|
return errors.Wrap(err, "failed to apply Config option")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -38,6 +37,7 @@ import (
|
|||||||
"github.com/rs/cors"
|
"github.com/rs/cors"
|
||||||
"golang.org/x/crypto/acme/autocert"
|
"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/app/request"
|
||||||
"github.com/mattermost/mattermost-server/v5/audit"
|
"github.com/mattermost/mattermost-server/v5/audit"
|
||||||
"github.com/mattermost/mattermost-server/v5/config"
|
"github.com/mattermost/mattermost-server/v5/config"
|
||||||
@@ -200,7 +200,7 @@ type Server struct {
|
|||||||
uploadLockMapMut sync.Mutex
|
uploadLockMapMut sync.Mutex
|
||||||
uploadLockMap map[string]bool
|
uploadLockMap map[string]bool
|
||||||
|
|
||||||
featureFlagSynchronizer *config.FeatureFlagSynchronizer
|
featureFlagSynchronizer *featureflag.Synchronizer
|
||||||
featureFlagStop chan struct{}
|
featureFlagStop chan struct{}
|
||||||
featureFlagStopped chan struct{}
|
featureFlagStopped chan struct{}
|
||||||
featureFlagSynchronizerMutex sync.Mutex
|
featureFlagSynchronizerMutex sync.Mutex
|
||||||
@@ -805,15 +805,8 @@ func (s *Server) initLogging() error {
|
|||||||
// shutdown once license is loaded/checked.
|
// shutdown once license is loaded/checked.
|
||||||
if *s.Config().LogSettings.AdvancedLoggingConfig != "" {
|
if *s.Config().LogSettings.AdvancedLoggingConfig != "" {
|
||||||
dsn := *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.
|
cfg, err := config.NewLogConfigSrc(dsn, s.configStore)
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid advanced logging config, %w", err)
|
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)
|
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) {
|
listenerId := cfg.AddListener(func(_, newCfg mlog.LogTargetCfg) {
|
||||||
if err := s.Log.ConfigAdvancedLogging(newCfg); err != nil {
|
if err := s.Log.ConfigAdvancedLogging(newCfg); err != nil {
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ package:
|
|||||||
@# Resource directories
|
@# Resource directories
|
||||||
mkdir -p $(DIST_PATH)/config
|
mkdir -p $(DIST_PATH)/config
|
||||||
cp -L config/README.md $(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 fonts $(DIST_PATH)
|
||||||
cp -RL templates $(DIST_PATH)
|
cp -RL templates $(DIST_PATH)
|
||||||
rm -rf $(DIST_PATH)/templates/*.mjml $(DIST_PATH)/templates/partials/
|
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")
|
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 {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to initialize config store")
|
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)
|
sqlDSN := getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)
|
||||||
fileDSN := "config.json"
|
fileDSN := "config.json"
|
||||||
|
|
||||||
ds, err := config.NewStore(sqlDSN, false, false, nil)
|
ds, err := config.NewStoreFromDSN(sqlDSN, false, false, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
fs, err := config.NewStore(fileDSN, false, false, nil)
|
fs, err := config.NewStoreFromDSN(fileDSN, false, false, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
defer ds.Close()
|
defer ds.Close()
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ func initDbCmdF(command *cobra.Command, _ []string) error {
|
|||||||
return errors.Wrap(err, "error loading custom configuration defaults")
|
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 {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to load configuration")
|
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())
|
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 {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to load configuration")
|
return errors.Wrap(err, "failed to load configuration")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
package config_test
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"github.com/mattermost/mattermost-server/v5/config"
|
|
||||||
"github.com/mattermost/mattermost-server/v5/model"
|
"github.com/mattermost/mattermost-server/v5/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -26,16 +25,16 @@ func TestGetClientConfig(t *testing.T) {
|
|||||||
"unlicensed",
|
"unlicensed",
|
||||||
&model.Config{
|
&model.Config{
|
||||||
EmailSettings: model.EmailSettings{
|
EmailSettings: model.EmailSettings{
|
||||||
EmailNotificationContentsType: sToP(model.EMAIL_NOTIFICATION_CONTENTS_FULL),
|
EmailNotificationContentsType: model.NewString(model.EMAIL_NOTIFICATION_CONTENTS_FULL),
|
||||||
},
|
},
|
||||||
ThemeSettings: model.ThemeSettings{
|
ThemeSettings: model.ThemeSettings{
|
||||||
// Ignored, since not licensed.
|
// Ignored, since not licensed.
|
||||||
AllowCustomThemes: bToP(false),
|
AllowCustomThemes: model.NewBool(false),
|
||||||
},
|
},
|
||||||
ServiceSettings: model.ServiceSettings{
|
ServiceSettings: model.ServiceSettings{
|
||||||
WebsocketURL: sToP("ws://mattermost.example.com:8065"),
|
WebsocketURL: model.NewString("ws://mattermost.example.com:8065"),
|
||||||
WebsocketPort: iToP(80),
|
WebsocketPort: model.NewInt(80),
|
||||||
WebsocketSecurePort: iToP(443),
|
WebsocketSecurePort: model.NewInt(443),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"",
|
"",
|
||||||
@@ -54,17 +53,17 @@ func TestGetClientConfig(t *testing.T) {
|
|||||||
"licensed, but not for theme management",
|
"licensed, but not for theme management",
|
||||||
&model.Config{
|
&model.Config{
|
||||||
EmailSettings: model.EmailSettings{
|
EmailSettings: model.EmailSettings{
|
||||||
EmailNotificationContentsType: sToP(model.EMAIL_NOTIFICATION_CONTENTS_FULL),
|
EmailNotificationContentsType: model.NewString(model.EMAIL_NOTIFICATION_CONTENTS_FULL),
|
||||||
},
|
},
|
||||||
ThemeSettings: model.ThemeSettings{
|
ThemeSettings: model.ThemeSettings{
|
||||||
// Ignored, since not licensed.
|
// Ignored, since not licensed.
|
||||||
AllowCustomThemes: bToP(false),
|
AllowCustomThemes: model.NewBool(false),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"tag1",
|
"tag1",
|
||||||
&model.License{
|
&model.License{
|
||||||
Features: &model.Features{
|
Features: &model.Features{
|
||||||
ThemeManagement: bToP(false),
|
ThemeManagement: model.NewBool(false),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
map[string]string{
|
map[string]string{
|
||||||
@@ -77,16 +76,16 @@ func TestGetClientConfig(t *testing.T) {
|
|||||||
"licensed for theme management",
|
"licensed for theme management",
|
||||||
&model.Config{
|
&model.Config{
|
||||||
EmailSettings: model.EmailSettings{
|
EmailSettings: model.EmailSettings{
|
||||||
EmailNotificationContentsType: sToP(model.EMAIL_NOTIFICATION_CONTENTS_FULL),
|
EmailNotificationContentsType: model.NewString(model.EMAIL_NOTIFICATION_CONTENTS_FULL),
|
||||||
},
|
},
|
||||||
ThemeSettings: model.ThemeSettings{
|
ThemeSettings: model.ThemeSettings{
|
||||||
AllowCustomThemes: bToP(false),
|
AllowCustomThemes: model.NewBool(false),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"tag2",
|
"tag2",
|
||||||
&model.License{
|
&model.License{
|
||||||
Features: &model.Features{
|
Features: &model.Features{
|
||||||
ThemeManagement: bToP(true),
|
ThemeManagement: model.NewBool(true),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
map[string]string{
|
map[string]string{
|
||||||
@@ -99,13 +98,13 @@ func TestGetClientConfig(t *testing.T) {
|
|||||||
"licensed for enforcement",
|
"licensed for enforcement",
|
||||||
&model.Config{
|
&model.Config{
|
||||||
ServiceSettings: model.ServiceSettings{
|
ServiceSettings: model.ServiceSettings{
|
||||||
EnforceMultifactorAuthentication: bToP(true),
|
EnforceMultifactorAuthentication: model.NewBool(true),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"tag1",
|
"tag1",
|
||||||
&model.License{
|
&model.License{
|
||||||
Features: &model.Features{
|
Features: &model.Features{
|
||||||
MFA: bToP(true),
|
MFA: model.NewBool(true),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
map[string]string{
|
map[string]string{
|
||||||
@@ -116,7 +115,7 @@ func TestGetClientConfig(t *testing.T) {
|
|||||||
"experimental channel organization enabled",
|
"experimental channel organization enabled",
|
||||||
&model.Config{
|
&model.Config{
|
||||||
ServiceSettings: model.ServiceSettings{
|
ServiceSettings: model.ServiceSettings{
|
||||||
ExperimentalChannelOrganization: bToP(true),
|
ExperimentalChannelOrganization: model.NewBool(true),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"tag1",
|
"tag1",
|
||||||
@@ -129,8 +128,8 @@ func TestGetClientConfig(t *testing.T) {
|
|||||||
"experimental channel organization disabled, but experimental group unread channels on",
|
"experimental channel organization disabled, but experimental group unread channels on",
|
||||||
&model.Config{
|
&model.Config{
|
||||||
ServiceSettings: model.ServiceSettings{
|
ServiceSettings: model.ServiceSettings{
|
||||||
ExperimentalChannelOrganization: bToP(false),
|
ExperimentalChannelOrganization: model.NewBool(false),
|
||||||
ExperimentalGroupUnreadChannels: sToP(model.GROUP_UNREAD_CHANNELS_DEFAULT_ON),
|
ExperimentalGroupUnreadChannels: model.NewString(model.GROUP_UNREAD_CHANNELS_DEFAULT_ON),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"tag1",
|
"tag1",
|
||||||
@@ -143,7 +142,7 @@ func TestGetClientConfig(t *testing.T) {
|
|||||||
"default marketplace",
|
"default marketplace",
|
||||||
&model.Config{
|
&model.Config{
|
||||||
PluginSettings: model.PluginSettings{
|
PluginSettings: model.PluginSettings{
|
||||||
MarketplaceUrl: sToP(model.PLUGIN_SETTINGS_DEFAULT_MARKETPLACE_URL),
|
MarketplaceUrl: model.NewString(model.PLUGIN_SETTINGS_DEFAULT_MARKETPLACE_URL),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"tag1",
|
"tag1",
|
||||||
@@ -156,7 +155,7 @@ func TestGetClientConfig(t *testing.T) {
|
|||||||
"non-default marketplace",
|
"non-default marketplace",
|
||||||
&model.Config{
|
&model.Config{
|
||||||
PluginSettings: model.PluginSettings{
|
PluginSettings: model.PluginSettings{
|
||||||
MarketplaceUrl: sToP("http://example.com"),
|
MarketplaceUrl: model.NewString("http://example.com"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"tag1",
|
"tag1",
|
||||||
@@ -169,7 +168,7 @@ func TestGetClientConfig(t *testing.T) {
|
|||||||
"enable ShowFullName prop",
|
"enable ShowFullName prop",
|
||||||
&model.Config{
|
&model.Config{
|
||||||
PrivacySettings: model.PrivacySettings{
|
PrivacySettings: model.PrivacySettings{
|
||||||
ShowFullName: bToP(true),
|
ShowFullName: model.NewBool(true),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"tag1",
|
"tag1",
|
||||||
@@ -190,7 +189,7 @@ func TestGetClientConfig(t *testing.T) {
|
|||||||
testCase.license.Features.SetDefaults()
|
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 {
|
for expectedField, expectedValue := range testCase.expectedFields {
|
||||||
actualValue, ok := configMap[expectedField]
|
actualValue, ok := configMap[expectedField]
|
||||||
if assert.True(t, ok, fmt.Sprintf("config does not contain %v", expectedField)) {
|
if assert.True(t, ok, fmt.Sprintf("config does not contain %v", expectedField)) {
|
||||||
@@ -214,16 +213,16 @@ func TestGetLimitedClientConfig(t *testing.T) {
|
|||||||
"unlicensed",
|
"unlicensed",
|
||||||
&model.Config{
|
&model.Config{
|
||||||
EmailSettings: model.EmailSettings{
|
EmailSettings: model.EmailSettings{
|
||||||
EmailNotificationContentsType: sToP(model.EMAIL_NOTIFICATION_CONTENTS_FULL),
|
EmailNotificationContentsType: model.NewString(model.EMAIL_NOTIFICATION_CONTENTS_FULL),
|
||||||
},
|
},
|
||||||
ThemeSettings: model.ThemeSettings{
|
ThemeSettings: model.ThemeSettings{
|
||||||
// Ignored, since not licensed.
|
// Ignored, since not licensed.
|
||||||
AllowCustomThemes: bToP(false),
|
AllowCustomThemes: model.NewBool(false),
|
||||||
},
|
},
|
||||||
ServiceSettings: model.ServiceSettings{
|
ServiceSettings: model.ServiceSettings{
|
||||||
WebsocketURL: sToP("ws://mattermost.example.com:8065"),
|
WebsocketURL: model.NewString("ws://mattermost.example.com:8065"),
|
||||||
WebsocketPort: iToP(80),
|
WebsocketPort: model.NewInt(80),
|
||||||
WebsocketSecurePort: iToP(443),
|
WebsocketSecurePort: model.NewInt(443),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"",
|
"",
|
||||||
@@ -240,11 +239,11 @@ func TestGetLimitedClientConfig(t *testing.T) {
|
|||||||
"password settings",
|
"password settings",
|
||||||
&model.Config{
|
&model.Config{
|
||||||
PasswordSettings: model.PasswordSettings{
|
PasswordSettings: model.PasswordSettings{
|
||||||
MinimumLength: iToP(15),
|
MinimumLength: model.NewInt(15),
|
||||||
Lowercase: bToP(true),
|
Lowercase: model.NewBool(true),
|
||||||
Uppercase: bToP(true),
|
Uppercase: model.NewBool(true),
|
||||||
Number: bToP(true),
|
Number: model.NewBool(true),
|
||||||
Symbol: bToP(false),
|
Symbol: model.NewBool(false),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"",
|
"",
|
||||||
@@ -282,7 +281,7 @@ func TestGetLimitedClientConfig(t *testing.T) {
|
|||||||
testCase.license.Features.SetDefaults()
|
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 {
|
for expectedField, expectedValue := range testCase.expectedFields {
|
||||||
actualValue, ok := configMap[expectedField]
|
actualValue, ok := configMap[expectedField]
|
||||||
if assert.True(t, ok, fmt.Sprintf("config does not contain %v", 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.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
package config_test
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/mattermost/mattermost-server/v5/config"
|
|
||||||
"github.com/mattermost/mattermost-server/v5/model"
|
"github.com/mattermost/mattermost-server/v5/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,23 +19,23 @@ func init() {
|
|||||||
emptyConfig = &model.Config{}
|
emptyConfig = &model.Config{}
|
||||||
readOnlyConfig = &model.Config{
|
readOnlyConfig = &model.Config{
|
||||||
ClusterSettings: model.ClusterSettings{
|
ClusterSettings: model.ClusterSettings{
|
||||||
Enable: bToP(true),
|
Enable: model.NewBool(true),
|
||||||
ReadOnlyConfig: bToP(true),
|
ReadOnlyConfig: model.NewBool(true),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
minimalConfig = &model.Config{
|
minimalConfig = &model.Config{
|
||||||
ServiceSettings: model.ServiceSettings{
|
ServiceSettings: model.ServiceSettings{
|
||||||
SiteURL: sToP("http://minimal"),
|
SiteURL: model.NewString("http://minimal"),
|
||||||
},
|
},
|
||||||
SqlSettings: model.SqlSettings{
|
SqlSettings: model.SqlSettings{
|
||||||
AtRestEncryptKey: sToP("abcdefghijklmnopqrstuvwxyz0123456789"),
|
AtRestEncryptKey: model.NewString("abcdefghijklmnopqrstuvwxyz0123456789"),
|
||||||
},
|
},
|
||||||
FileSettings: model.FileSettings{
|
FileSettings: model.FileSettings{
|
||||||
PublicLinkSalt: sToP("abcdefghijklmnopqrstuvwxyz0123456789"),
|
PublicLinkSalt: model.NewString("abcdefghijklmnopqrstuvwxyz0123456789"),
|
||||||
},
|
},
|
||||||
LocalizationSettings: model.LocalizationSettings{
|
LocalizationSettings: model.LocalizationSettings{
|
||||||
DefaultServerLocale: sToP("en"),
|
DefaultServerLocale: model.NewString("en"),
|
||||||
DefaultClientLocale: sToP("en"),
|
DefaultClientLocale: model.NewString("en"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,34 +46,34 @@ func init() {
|
|||||||
|
|
||||||
invalidConfig = &model.Config{
|
invalidConfig = &model.Config{
|
||||||
ServiceSettings: model.ServiceSettings{
|
ServiceSettings: model.ServiceSettings{
|
||||||
SiteURL: sToP("invalid"),
|
SiteURL: model.NewString("invalid"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
fixesRequiredConfig = &model.Config{
|
fixesRequiredConfig = &model.Config{
|
||||||
ServiceSettings: model.ServiceSettings{
|
ServiceSettings: model.ServiceSettings{
|
||||||
SiteURL: sToP("http://trailingslash/"),
|
SiteURL: model.NewString("http://trailingslash/"),
|
||||||
},
|
},
|
||||||
SqlSettings: model.SqlSettings{
|
SqlSettings: model.SqlSettings{
|
||||||
AtRestEncryptKey: sToP("abcdefghijklmnopqrstuvwxyz0123456789"),
|
AtRestEncryptKey: model.NewString("abcdefghijklmnopqrstuvwxyz0123456789"),
|
||||||
},
|
},
|
||||||
FileSettings: model.FileSettings{
|
FileSettings: model.FileSettings{
|
||||||
DriverName: sToP(model.IMAGE_DRIVER_LOCAL),
|
DriverName: model.NewString(model.IMAGE_DRIVER_LOCAL),
|
||||||
Directory: sToP("/path/to/directory"),
|
Directory: model.NewString("/path/to/directory"),
|
||||||
PublicLinkSalt: sToP("abcdefghijklmnopqrstuvwxyz0123456789"),
|
PublicLinkSalt: model.NewString("abcdefghijklmnopqrstuvwxyz0123456789"),
|
||||||
},
|
},
|
||||||
LocalizationSettings: model.LocalizationSettings{
|
LocalizationSettings: model.LocalizationSettings{
|
||||||
DefaultServerLocale: sToP("garbage"),
|
DefaultServerLocale: model.NewString("garbage"),
|
||||||
DefaultClientLocale: sToP("garbage"),
|
DefaultClientLocale: model.NewString("garbage"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
ldapConfig = &model.Config{
|
ldapConfig = &model.Config{
|
||||||
LdapSettings: model.LdapSettings{
|
LdapSettings: model.LdapSettings{
|
||||||
BindPassword: sToP("password"),
|
BindPassword: model.NewString("password"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
testConfig = &model.Config{
|
testConfig = &model.Config{
|
||||||
ServiceSettings: model.ServiceSettings{
|
ServiceSettings: model.ServiceSettings{
|
||||||
SiteURL: sToP("http://TestStoreNew"),
|
SiteURL: model.NewString("http://TestStoreNew"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
customConfigDefaults = &model.Config{
|
customConfigDefaults = &model.Config{
|
||||||
@@ -94,7 +93,7 @@ func TestMergeConfigs(t *testing.T) {
|
|||||||
patch := &model.Config{}
|
patch := &model.Config{}
|
||||||
patch.SetDefaults()
|
patch.SetDefaults()
|
||||||
|
|
||||||
merged, err := config.Merge(base, patch, nil)
|
merged, err := Merge(base, patch, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, patch, merged)
|
assert.Equal(t, patch, merged)
|
||||||
@@ -104,7 +103,7 @@ func TestMergeConfigs(t *testing.T) {
|
|||||||
base.SetDefaults()
|
base.SetDefaults()
|
||||||
patch := base.Clone()
|
patch := base.Clone()
|
||||||
|
|
||||||
merged, err := config.Merge(base, patch, nil)
|
merged, err := Merge(base, patch, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, base, merged)
|
assert.Equal(t, base, merged)
|
||||||
@@ -114,9 +113,9 @@ func TestMergeConfigs(t *testing.T) {
|
|||||||
base := &model.Config{}
|
base := &model.Config{}
|
||||||
base.SetDefaults()
|
base.SetDefaults()
|
||||||
patch := base.Clone()
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.NotEqual(t, base, merged)
|
assert.NotEqual(t, base, merged)
|
||||||
@@ -126,14 +125,14 @@ func TestMergeConfigs(t *testing.T) {
|
|||||||
base := &model.Config{}
|
base := &model.Config{}
|
||||||
base.SetDefaults()
|
base.SetDefaults()
|
||||||
patch := &model.Config{}
|
patch := &model.Config{}
|
||||||
patch.ServiceSettings.SiteURL = newString("http://newhost.ca")
|
patch.ServiceSettings.SiteURL = model.NewString("http://newhost.ca")
|
||||||
patch.GoogleSettings.Enable = newBool(true)
|
patch.GoogleSettings.Enable = model.NewBool(true)
|
||||||
|
|
||||||
expected := base.Clone()
|
expected := base.Clone()
|
||||||
expected.ServiceSettings.SiteURL = newString("http://newhost.ca")
|
expected.ServiceSettings.SiteURL = model.NewString("http://newhost.ca")
|
||||||
expected.GoogleSettings.Enable = newBool(true)
|
expected.GoogleSettings.Enable = model.NewBool(true)
|
||||||
|
|
||||||
merged, err := config.Merge(base, patch, nil)
|
merged, err := Merge(base, patch, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.NotEqual(t, base, merged)
|
assert.NotEqual(t, base, merged)
|
||||||
@@ -143,12 +142,12 @@ func TestMergeConfigs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigEnvironmentOverrides(t *testing.T) {
|
func TestConfigEnvironmentOverrides(t *testing.T) {
|
||||||
memstore, err := config.NewMemoryStore()
|
memstore, err := NewMemoryStore()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
base, err := config.NewStoreFromBacking(memstore, nil, false)
|
base, err := NewStoreFromBacking(memstore, nil, false)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
originalConfig := &model.Config{}
|
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")
|
os.Setenv("MM_SERVICESETTINGS_SITEURL", "http://overridden.ca")
|
||||||
defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL")
|
defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL")
|
||||||
@@ -172,15 +171,12 @@ func TestRemoveEnvironmentOverrides(t *testing.T) {
|
|||||||
os.Setenv("MM_SERVICESETTINGS_SITEURL", "http://overridden.ca")
|
os.Setenv("MM_SERVICESETTINGS_SITEURL", "http://overridden.ca")
|
||||||
defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL")
|
defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL")
|
||||||
|
|
||||||
memstore, err := config.NewMemoryStore()
|
memstore, err := NewMemoryStore()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
base, err := config.NewStoreFromBacking(memstore, nil, false)
|
base, err := NewStoreFromBacking(memstore, nil, false)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
oldCfg := base.Get()
|
oldCfg := base.Get()
|
||||||
assert.Equal(t, "http://overridden.ca", *oldCfg.ServiceSettings.SiteURL)
|
assert.Equal(t, "http://overridden.ca", *oldCfg.ServiceSettings.SiteURL)
|
||||||
newCfg := base.RemoveEnvironmentOverrides(oldCfg)
|
newCfg := base.RemoveEnvironmentOverrides(oldCfg)
|
||||||
assert.Equal(t, "", *newCfg.ServiceSettings.SiteURL)
|
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.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
package config_test
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -16,7 +16,6 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/mattermost/mattermost-server/v5/config"
|
|
||||||
"github.com/mattermost/mattermost-server/v5/model"
|
"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()
|
os.Clearenv()
|
||||||
truncateTables(t)
|
truncateTables(t)
|
||||||
|
|
||||||
cfgData, err := config.MarshalConfig(cfg)
|
cfgData, err := marshalConfig(cfg)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
db := sqlx.NewDb(mainHelper.GetSQLStore().GetMaster().Db, *mainHelper.GetSQLSettings().DriverName)
|
db := sqlx.NewDb(mainHelper.GetSQLStore().GetMaster().Db, *mainHelper.GetSQLSettings().DriverName)
|
||||||
err = config.InitializeConfigurationsTable(db)
|
err = initializeConfigurationsTable(db)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
id := model.NewId()
|
id := model.NewId()
|
||||||
@@ -115,14 +114,14 @@ func assertDatabaseNotEqualsConfig(t *testing.T, expectedCfg *model.Config) {
|
|||||||
assert.NotEqual(t, expectedCfg, actualCfg)
|
assert.NotEqual(t, expectedCfg, actualCfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTestDatabaseStore(customDefaults *model.Config) (*config.Store, error) {
|
func newTestDatabaseStore(customDefaults *model.Config) (*Store, error) {
|
||||||
sqlSettings := mainHelper.GetSQLSettings()
|
sqlSettings := mainHelper.GetSQLSettings()
|
||||||
dss, err := config.NewDatabaseStore(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource))
|
dss, err := NewDatabaseStore(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
cStore, err := config.NewStoreFromBacking(dss, customDefaults, false)
|
cStore, err := NewStoreFromBacking(dss, customDefaults, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -211,20 +210,20 @@ func TestDatabaseStoreNew(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("invalid url", func(t *testing.T) {
|
t.Run("invalid url", func(t *testing.T) {
|
||||||
_, err := config.NewDatabaseStore("")
|
_, err := NewDatabaseStore("")
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
|
|
||||||
_, err = config.NewDatabaseStore("mysql")
|
_, err = NewDatabaseStore("mysql")
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("unsupported scheme", func(t *testing.T) {
|
t.Run("unsupported scheme", func(t *testing.T) {
|
||||||
_, err := config.NewDatabaseStore("invalid")
|
_, err := NewDatabaseStore("invalid")
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("unsupported scheme with valid data source", func(t *testing.T) {
|
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)
|
require.Error(t, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -452,7 +451,7 @@ func TestDatabaseStoreSet(t *testing.T) {
|
|||||||
defer ds.Close()
|
defer ds.Close()
|
||||||
|
|
||||||
newCfg := &model.Config{}
|
newCfg := &model.Config{}
|
||||||
newCfg.LdapSettings.BindPassword = sToP(model.FAKE_SETTING)
|
newCfg.LdapSettings.BindPassword = model.NewString(model.FAKE_SETTING)
|
||||||
|
|
||||||
_, err = ds.Set(newCfg)
|
_, err = ds.Set(newCfg)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -469,7 +468,7 @@ func TestDatabaseStoreSet(t *testing.T) {
|
|||||||
defer ds.Close()
|
defer ds.Close()
|
||||||
|
|
||||||
newCfg := &model.Config{}
|
newCfg := &model.Config{}
|
||||||
newCfg.ServiceSettings.SiteURL = sToP("invalid")
|
newCfg.ServiceSettings.SiteURL = model.NewString("invalid")
|
||||||
|
|
||||||
_, err = ds.Set(newCfg)
|
_, err = ds.Set(newCfg)
|
||||||
if assert.Error(t, err) {
|
if assert.Error(t, err) {
|
||||||
@@ -508,7 +507,7 @@ func TestDatabaseStoreSet(t *testing.T) {
|
|||||||
|
|
||||||
newCfg := &model.Config{
|
newCfg := &model.Config{
|
||||||
ServiceSettings: model.ServiceSettings{
|
ServiceSettings: model.ServiceSettings{
|
||||||
SiteURL: sToP("http://new"),
|
SiteURL: model.NewString("http://new"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -528,7 +527,7 @@ func TestDatabaseStoreSet(t *testing.T) {
|
|||||||
|
|
||||||
newCfg := &model.Config{
|
newCfg := &model.Config{
|
||||||
ServiceSettings: model.ServiceSettings{
|
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")
|
_, err = db.Exec("DROP TABLE Configurations")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
newCfg := &model.Config{}
|
newCfg := minimalConfig
|
||||||
|
|
||||||
_, err = ds.Set(newCfg)
|
_, err = ds.Set(newCfg)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
@@ -574,9 +573,9 @@ func TestDatabaseStoreSet(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer ds.Close()
|
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 := emptyConfig.Clone()
|
||||||
newCfg.ServiceSettings.SiteURL = sToP(longSiteURL)
|
newCfg.ServiceSettings.SiteURL = model.NewString(longSiteURL)
|
||||||
|
|
||||||
_, err = ds.Set(newCfg)
|
_, err = ds.Set(newCfg)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
@@ -597,7 +596,7 @@ func TestDatabaseStoreSet(t *testing.T) {
|
|||||||
}
|
}
|
||||||
ds.AddListener(callback)
|
ds.AddListener(callback)
|
||||||
|
|
||||||
newCfg := &model.Config{}
|
newCfg := minimalConfig
|
||||||
|
|
||||||
_, err = ds.Set(newCfg)
|
_, err = ds.Set(newCfg)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -616,7 +615,7 @@ func TestDatabaseStoreSet(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer ds.Close()
|
defer ds.Close()
|
||||||
|
|
||||||
ds.PersistFeatures(false)
|
ds.SetReadOnlyFF(true)
|
||||||
_, err = ds.Set(minimalConfig)
|
_, err = ds.Set(minimalConfig)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -632,7 +631,7 @@ func TestDatabaseStoreSet(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer ds.Close()
|
defer ds.Close()
|
||||||
|
|
||||||
ds.PersistFeatures(true)
|
ds.SetReadOnlyFF(false)
|
||||||
|
|
||||||
_, err = ds.Set(minimalConfig)
|
_, err = ds.Set(minimalConfig)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -828,7 +827,7 @@ func TestDatabaseStoreLoad(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer ds.Close()
|
defer ds.Close()
|
||||||
|
|
||||||
cfgData, err := config.MarshalConfig(invalidConfig)
|
cfgData, err := marshalConfig(invalidConfig)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
sqlSettings := mainHelper.GetSQLSettings()
|
sqlSettings := mainHelper.GetSQLSettings()
|
||||||
@@ -862,7 +861,7 @@ func TestDatabaseStoreLoad(t *testing.T) {
|
|||||||
assert.Equal(t, "http://trailingslash", *ds.Get().ServiceSettings.SiteURL)
|
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)
|
_, tearDown := setupConfigDatabase(t, emptyConfig, nil)
|
||||||
defer tearDown()
|
defer tearDown()
|
||||||
|
|
||||||
@@ -876,10 +875,16 @@ func TestDatabaseStoreLoad(t *testing.T) {
|
|||||||
}
|
}
|
||||||
ds.AddListener(callback)
|
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()
|
err = ds.Load()
|
||||||
require.NoError(t, err)
|
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" {
|
if *mainHelper.Settings.DriverName == "postgres" {
|
||||||
t.Skip("No limit for 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)
|
err := ds.SetFile("toolong", longFile)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -960,7 +965,7 @@ func TestDatabaseSetFile(t *testing.T) {
|
|||||||
if *mainHelper.Settings.DriverName == "postgres" {
|
if *mainHelper.Settings.DriverName == "postgres" {
|
||||||
t.Skip("No limit for 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)
|
err := ds.SetFile("toolong", longFile)
|
||||||
if assert.Error(t, err) {
|
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"
|
"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
|
// emitter enables threadsafe registration and broadcasting to configuration listeners
|
||||||
type emitter struct {
|
type emitter struct {
|
||||||
listeners sync.Map
|
listeners sync.Map
|
||||||
@@ -18,9 +21,7 @@ type emitter struct {
|
|||||||
// AddListener adds a callback function to invoke when the configuration is modified.
|
// AddListener adds a callback function to invoke when the configuration is modified.
|
||||||
func (e *emitter) AddListener(listener Listener) string {
|
func (e *emitter) AddListener(listener Listener) string {
|
||||||
id := model.NewId()
|
id := model.NewId()
|
||||||
|
|
||||||
e.listeners.Store(id, listener)
|
e.listeners.Store(id, listener)
|
||||||
|
|
||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,7 +35,6 @@ func (e *emitter) invokeConfigListeners(oldCfg, newCfg *model.Config) {
|
|||||||
e.listeners.Range(func(key, value interface{}) bool {
|
e.listeners.Range(func(key, value interface{}) bool {
|
||||||
listener := value.(Listener)
|
listener := value.(Listener)
|
||||||
listener(oldCfg, newCfg)
|
listener(oldCfg, newCfg)
|
||||||
|
|
||||||
return true
|
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
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
@@ -13,10 +16,6 @@ import (
|
|||||||
|
|
||||||
type LogSrcListener func(old, new mlog.LogTargetCfg)
|
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
|
// LogConfigSrc abstracts the Advanced Logging configuration so that implementations can
|
||||||
// fetch from file, database, etc.
|
// fetch from file, database, etc.
|
||||||
type LogConfigSrc interface {
|
type LogConfigSrc interface {
|
||||||
@@ -24,7 +23,7 @@ type LogConfigSrc interface {
|
|||||||
Get() mlog.LogTargetCfg
|
Get() mlog.LogTargetCfg
|
||||||
|
|
||||||
// Set updates the dsn specifying the source and reloads
|
// 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 adds a callback function to invoke when the configuration is modified.
|
||||||
AddListener(listener LogSrcListener) string
|
AddListener(listener LogSrcListener) string
|
||||||
@@ -38,13 +37,25 @@ type LogConfigSrc interface {
|
|||||||
|
|
||||||
// NewLogConfigSrc creates an advanced logging configuration source, backed by a
|
// NewLogConfigSrc creates an advanced logging configuration source, backed by a
|
||||||
// file, JSON string, or database.
|
// 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)
|
dsn = strings.TrimSpace(dsn)
|
||||||
|
|
||||||
if isJSON {
|
if isJSONMap(dsn) {
|
||||||
return newJSONSrc(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
|
// jsonSrc
|
||||||
@@ -68,8 +79,8 @@ func (src *jsonSrc) Get() mlog.LogTargetCfg {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set updates the JSON specifying the source and reloads
|
// Set updates the JSON specifying the source and reloads
|
||||||
func (src *jsonSrc) Set(data string, _ FileGetter) error {
|
func (src *jsonSrc) Set(data string, _ *Store) error {
|
||||||
cfg, err := JSONToLogTargetCfg([]byte(data))
|
cfg, err := logTargetCfgFromJSON([]byte(data))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -103,11 +114,11 @@ type fileSrc struct {
|
|||||||
watcher *watcher
|
watcher *watcher
|
||||||
}
|
}
|
||||||
|
|
||||||
func newFileSrc(path string, fget FileGetter) (*fileSrc, error) {
|
func newFileSrc(path string, configStore *Store) (*fileSrc, error) {
|
||||||
src := &fileSrc{
|
src := &fileSrc{
|
||||||
path: path,
|
path: path,
|
||||||
}
|
}
|
||||||
if err := src.Set(path, fget); err != nil {
|
if err := src.Set(path, configStore); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return src, nil
|
return src, nil
|
||||||
@@ -123,13 +134,13 @@ func (src *fileSrc) Get() mlog.LogTargetCfg {
|
|||||||
// Set updates the dsn specifying the file source and reloads.
|
// Set updates the dsn specifying the file source and reloads.
|
||||||
// The file will be watched for changes and reloaded as needed,
|
// The file will be watched for changes and reloaded as needed,
|
||||||
// and all listeners notified.
|
// and all listeners notified.
|
||||||
func (src *fileSrc) Set(path string, fget FileGetter) error {
|
func (src *fileSrc) Set(path string, configStore *Store) error {
|
||||||
data, err := fget.GetFile(path)
|
data, err := configStore.GetFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, err := JSONToLogTargetCfg(data)
|
cfg, err := logTargetCfgFromJSON(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -153,7 +164,7 @@ func (src *fileSrc) Set(path string, fget FileGetter) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
watcher, err := newWatcher(path, func() {
|
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))
|
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
|
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
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -15,38 +15,29 @@ const (
|
|||||||
badJSON = `{"file":{ Type="file"}}`
|
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) {
|
func TestNewLogConfigSrc(t *testing.T) {
|
||||||
|
store := NewTestMemoryStore()
|
||||||
|
require.NotNil(t, store)
|
||||||
|
err := store.SetFile("advancedlogging.conf", []byte(validJSON))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
dsn string
|
dsn string
|
||||||
fget FileGetter
|
configStore *Store
|
||||||
wantErr bool
|
wantErr bool
|
||||||
wantType LogConfigSrc
|
wantType LogConfigSrc
|
||||||
}{
|
}{
|
||||||
{name: "empty dsn", dsn: "", fget: fgetFunc(getInvalidFile), wantErr: true, wantType: nil},
|
{name: "empty dsn", dsn: "", configStore: store, wantErr: true, wantType: nil},
|
||||||
{name: "garbage dsn", dsn: "!@wfejwcevioj", fget: fgetFunc(getInvalidFile), wantErr: true, wantType: nil},
|
{name: "garbage dsn", dsn: "!@wfejwcevioj", configStore: store, wantErr: true, wantType: nil},
|
||||||
{name: "valid json dsn", dsn: validJSON, fget: fgetFunc(getInvalidFile), wantErr: false, wantType: &jsonSrc{}},
|
{name: "valid json dsn", dsn: validJSON, configStore: store, wantErr: false, wantType: &jsonSrc{}},
|
||||||
{name: "invalid json dsn", dsn: badJSON, fget: fgetFunc(getInvalidFile), wantErr: true, wantType: nil},
|
{name: "invalid json dsn", dsn: badJSON, configStore: store, wantErr: true, wantType: nil},
|
||||||
{name: "valid filespec dsn", dsn: "advancedlogging.conf", fget: fgetFunc(getValidFile), wantErr: false, wantType: &fileSrc{}},
|
{name: "valid filespec dsn", dsn: "advancedlogging.conf", configStore: store, wantErr: false, wantType: &fileSrc{}},
|
||||||
{name: "invalid filespec dsn", dsn: "/nobody/here.conf", fget: fgetFunc(getInvalidFile), wantErr: true, wantType: nil},
|
{name: "invalid filespec dsn", dsn: "/nobody/here.conf", configStore: store, wantErr: true, wantType: nil},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
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 {
|
if tt.wantErr {
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
package config_test
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
package config_test
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
@@ -9,8 +9,6 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/mattermost/mattermost-server/v5/config"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func setupConfigMemory(t *testing.T) {
|
func setupConfigMemory(t *testing.T) {
|
||||||
@@ -21,7 +19,7 @@ func setupConfigMemory(t *testing.T) {
|
|||||||
func TestMemoryGetFile(t *testing.T) {
|
func TestMemoryGetFile(t *testing.T) {
|
||||||
setupConfigMemory(t)
|
setupConfigMemory(t)
|
||||||
|
|
||||||
ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{
|
ms, err := NewMemoryStoreWithOptions(&MemoryStoreOptions{
|
||||||
InitialConfig: minimalConfig,
|
InitialConfig: minimalConfig,
|
||||||
InitialFiles: map[string][]byte{
|
InitialFiles: map[string][]byte{
|
||||||
"empty-file": {},
|
"empty-file": {},
|
||||||
@@ -57,7 +55,7 @@ func TestMemoryGetFile(t *testing.T) {
|
|||||||
func TestMemorySetFile(t *testing.T) {
|
func TestMemorySetFile(t *testing.T) {
|
||||||
setupConfigMemory(t)
|
setupConfigMemory(t)
|
||||||
|
|
||||||
ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{
|
ms, err := NewMemoryStoreWithOptions(&MemoryStoreOptions{
|
||||||
InitialConfig: minimalConfig,
|
InitialConfig: minimalConfig,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -89,7 +87,7 @@ func TestMemoryHasFile(t *testing.T) {
|
|||||||
t.Run("has non-existent", func(t *testing.T) {
|
t.Run("has non-existent", func(t *testing.T) {
|
||||||
setupConfigMemory(t)
|
setupConfigMemory(t)
|
||||||
|
|
||||||
ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{
|
ms, err := NewMemoryStoreWithOptions(&MemoryStoreOptions{
|
||||||
InitialConfig: minimalConfig,
|
InitialConfig: minimalConfig,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -103,7 +101,7 @@ func TestMemoryHasFile(t *testing.T) {
|
|||||||
t.Run("has existing", func(t *testing.T) {
|
t.Run("has existing", func(t *testing.T) {
|
||||||
setupConfigMemory(t)
|
setupConfigMemory(t)
|
||||||
|
|
||||||
ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{
|
ms, err := NewMemoryStoreWithOptions(&MemoryStoreOptions{
|
||||||
InitialConfig: minimalConfig,
|
InitialConfig: minimalConfig,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -120,7 +118,7 @@ func TestMemoryHasFile(t *testing.T) {
|
|||||||
t.Run("has manually created file", func(t *testing.T) {
|
t.Run("has manually created file", func(t *testing.T) {
|
||||||
setupConfigMemory(t)
|
setupConfigMemory(t)
|
||||||
|
|
||||||
ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{
|
ms, err := NewMemoryStoreWithOptions(&MemoryStoreOptions{
|
||||||
InitialConfig: minimalConfig,
|
InitialConfig: minimalConfig,
|
||||||
InitialFiles: map[string][]byte{
|
InitialFiles: map[string][]byte{
|
||||||
"manual": []byte("manual file"),
|
"manual": []byte("manual file"),
|
||||||
@@ -139,7 +137,7 @@ func TestMemoryRemoveFile(t *testing.T) {
|
|||||||
t.Run("remove non-existent", func(t *testing.T) {
|
t.Run("remove non-existent", func(t *testing.T) {
|
||||||
setupConfigMemory(t)
|
setupConfigMemory(t)
|
||||||
|
|
||||||
ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{
|
ms, err := NewMemoryStoreWithOptions(&MemoryStoreOptions{
|
||||||
InitialConfig: minimalConfig,
|
InitialConfig: minimalConfig,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -152,7 +150,7 @@ func TestMemoryRemoveFile(t *testing.T) {
|
|||||||
t.Run("remove existing", func(t *testing.T) {
|
t.Run("remove existing", func(t *testing.T) {
|
||||||
setupConfigMemory(t)
|
setupConfigMemory(t)
|
||||||
|
|
||||||
ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{
|
ms, err := NewMemoryStoreWithOptions(&MemoryStoreOptions{
|
||||||
InitialConfig: minimalConfig,
|
InitialConfig: minimalConfig,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -175,7 +173,7 @@ func TestMemoryRemoveFile(t *testing.T) {
|
|||||||
t.Run("remove manually created file", func(t *testing.T) {
|
t.Run("remove manually created file", func(t *testing.T) {
|
||||||
setupConfigMemory(t)
|
setupConfigMemory(t)
|
||||||
|
|
||||||
ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{
|
ms, err := NewMemoryStoreWithOptions(&MemoryStoreOptions{
|
||||||
InitialConfig: minimalConfig,
|
InitialConfig: minimalConfig,
|
||||||
InitialFiles: map[string][]byte{
|
InitialFiles: map[string][]byte{
|
||||||
"manual": []byte("manual file"),
|
"manual": []byte("manual file"),
|
||||||
@@ -199,7 +197,7 @@ func TestMemoryRemoveFile(t *testing.T) {
|
|||||||
func TestMemoryStoreString(t *testing.T) {
|
func TestMemoryStoreString(t *testing.T) {
|
||||||
setupConfigMemory(t)
|
setupConfigMemory(t)
|
||||||
|
|
||||||
ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{InitialConfig: emptyConfig})
|
ms, err := NewMemoryStoreWithOptions(&MemoryStoreOptions{InitialConfig: emptyConfig})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer ms.Close()
|
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.
|
// 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 {
|
func Migrate(from, to string) error {
|
||||||
source, err := NewStore(from, false, false, nil)
|
source, err := NewStoreFromDSN(from, false, false, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(err, "failed to access source config %s", from)
|
return errors.Wrapf(err, "failed to access source config %s", from)
|
||||||
}
|
}
|
||||||
defer source.Close()
|
defer source.Close()
|
||||||
|
|
||||||
destination, err := NewStore(to, false, false, nil)
|
destination, err := NewStoreFromDSN(to, false, false, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(err, "failed to access destination config %s", to)
|
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.
|
// 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)
|
files = append(files, *sourceConfig.LogSettings.AdvancedLoggingConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
package config_test
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@@ -12,11 +12,10 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/mattermost/mattermost-server/v5/config"
|
|
||||||
"github.com/mattermost/mattermost-server/v5/model"
|
"github.com/mattermost/mattermost-server/v5/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
type cleanUpFn func(store *config.Store)
|
type cleanUpFn func(store *Store)
|
||||||
|
|
||||||
func TestMigrate(t *testing.T) {
|
func TestMigrate(t *testing.T) {
|
||||||
files := []string{
|
files := []string{
|
||||||
@@ -49,12 +48,12 @@ func TestMigrate(t *testing.T) {
|
|||||||
truncateTables(t)
|
truncateTables(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
setupSource := func(t *testing.T, source *config.Store) cleanUpFn {
|
setupSource := func(t *testing.T, source *Store) cleanUpFn {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
cfg := source.Get()
|
cfg := source.Get()
|
||||||
originalCfg := cfg.Clone()
|
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.IdpCertificateFile = &files[0]
|
||||||
cfg.SamlSettings.PublicCertificateFile = &files[1]
|
cfg.SamlSettings.PublicCertificateFile = &files[1]
|
||||||
cfg.SamlSettings.PrivateKeyFile = &files[2]
|
cfg.SamlSettings.PrivateKeyFile = &files[2]
|
||||||
@@ -77,13 +76,13 @@ func TestMigrate(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return func(store *config.Store) {
|
return func(store *Store) {
|
||||||
_, err := store.Set(originalCfg)
|
_, err := store.Set(originalCfg)
|
||||||
require.NoError(t, err)
|
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()
|
t.Helper()
|
||||||
|
|
||||||
for i, file := range files {
|
for i, file := range files {
|
||||||
@@ -109,19 +108,19 @@ func TestMigrate(t *testing.T) {
|
|||||||
destinationDSN := path.Join(pwd, "config-custom.json")
|
destinationDSN := path.Join(pwd, "config-custom.json")
|
||||||
sourceDSN := getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)
|
sourceDSN := getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)
|
||||||
|
|
||||||
sourcedb, err := config.NewDatabaseStore(sourceDSN)
|
sourcedb, err := NewDatabaseStore(sourceDSN)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
source, err := config.NewStoreFromBacking(sourcedb, nil, false)
|
source, err := NewStoreFromBacking(sourcedb, nil, false)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer source.Close()
|
defer source.Close()
|
||||||
|
|
||||||
cleanUp := setupSource(t, source)
|
cleanUp := setupSource(t, source)
|
||||||
err = config.Migrate(sourceDSN, destinationDSN)
|
err = Migrate(sourceDSN, destinationDSN)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
destinationfile, err := config.NewFileStore(destinationDSN, false)
|
destinationfile, err := NewFileStore(destinationDSN, false)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
destination, err := config.NewStoreFromBacking(destinationfile, nil, false)
|
destination, err := NewStoreFromBacking(destinationfile, nil, false)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer destination.Close()
|
defer destination.Close()
|
||||||
defer cleanUp(destination)
|
defer cleanUp(destination)
|
||||||
@@ -139,19 +138,19 @@ func TestMigrate(t *testing.T) {
|
|||||||
sourceDSN := path.Join(pwd, "config-custom.json")
|
sourceDSN := path.Join(pwd, "config-custom.json")
|
||||||
destinationDSN := getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)
|
destinationDSN := getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)
|
||||||
|
|
||||||
sourcefile, err := config.NewFileStore(sourceDSN, false)
|
sourcefile, err := NewFileStore(sourceDSN, false)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
source, err := config.NewStoreFromBacking(sourcefile, nil, false)
|
source, err := NewStoreFromBacking(sourcefile, nil, false)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer source.Close()
|
defer source.Close()
|
||||||
|
|
||||||
cleanUp := setupSource(t, source)
|
cleanUp := setupSource(t, source)
|
||||||
err = config.Migrate(sourceDSN, destinationDSN)
|
err = Migrate(sourceDSN, destinationDSN)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
destinationdb, err := config.NewDatabaseStore(destinationDSN)
|
destinationdb, err := NewDatabaseStore(destinationDSN)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
destination, err := config.NewStoreFromBacking(destinationdb, nil, false)
|
destination, err := NewStoreFromBacking(destinationdb, nil, false)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer destination.Close()
|
defer destination.Close()
|
||||||
defer cleanUp(destination)
|
defer cleanUp(destination)
|
||||||
|
|||||||
256
config/store.go
256
config/store.go
@@ -4,7 +4,6 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -21,9 +20,23 @@ var (
|
|||||||
ErrReadOnlyStore = errors.New("configuration store is read-only")
|
ErrReadOnlyStore = errors.New("configuration store is read-only")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Listener is a callback function invoked when the configuration changes.
|
// Store is the higher level object that handles storing and retrieval of config data.
|
||||||
type Listener func(oldConfig *model.Config, newConfig *model.Config)
|
// 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 {
|
type BackingStore interface {
|
||||||
// Set replaces the current configuration in its entirety and updates the backing store.
|
// Set replaces the current configuration in its entirety and updates the backing store.
|
||||||
Set(*model.Config) error
|
Set(*model.Config) error
|
||||||
@@ -54,27 +67,13 @@ type BackingStore interface {
|
|||||||
Close() error
|
Close() error
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewStore creates a database or file store given a data source name by which to connect.
|
// NewStoreFromBacking creates and returns a new config store given a backing store.
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewStoreFromBacking(backingStore BackingStore, customDefaults *model.Config, readOnly bool) (*Store, error) {
|
func NewStoreFromBacking(backingStore BackingStore, customDefaults *model.Config, readOnly bool) (*Store, error) {
|
||||||
store := &Store{
|
store := &Store{
|
||||||
backingStore: backingStore,
|
backingStore: backingStore,
|
||||||
configCustomDefaults: customDefaults,
|
configCustomDefaults: customDefaults,
|
||||||
readOnly: readOnly,
|
readOnly: readOnly,
|
||||||
|
readOnlyFF: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := store.Load(); err != nil {
|
if err := store.Load(); err != nil {
|
||||||
@@ -90,14 +89,31 @@ func NewStoreFromBacking(backingStore BackingStore, customDefaults *model.Config
|
|||||||
return store, nil
|
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) {
|
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 {
|
func NewTestMemoryStore() *Store {
|
||||||
memoryStore, err := NewMemoryStore()
|
memoryStore, err := NewMemoryStore()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -112,19 +128,6 @@ func NewTestMemoryStore() *Store {
|
|||||||
return configStore
|
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.
|
// Get fetches the current, cached configuration.
|
||||||
func (s *Store) Get() *model.Config {
|
func (s *Store) Get() *model.Config {
|
||||||
s.configLock.RLock()
|
s.configLock.RLock()
|
||||||
@@ -132,7 +135,7 @@ func (s *Store) Get() *model.Config {
|
|||||||
return s.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 {
|
func (s *Store) GetNoEnv() *model.Config {
|
||||||
s.configLock.RLock()
|
s.configLock.RLock()
|
||||||
defer s.configLock.RUnlock()
|
defer s.configLock.RUnlock()
|
||||||
@@ -151,33 +154,37 @@ func (s *Store) GetEnvironmentOverridesWithFilter(filter func(reflect.StructFiel
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RemoveEnvironmentOverrides returns a new config without the environment
|
// RemoveEnvironmentOverrides returns a new config without the environment
|
||||||
// overrides
|
// overrides.
|
||||||
func (s *Store) RemoveEnvironmentOverrides(cfg *model.Config) *model.Config {
|
func (s *Store) RemoveEnvironmentOverrides(cfg *model.Config) *model.Config {
|
||||||
s.configLock.RLock()
|
s.configLock.RLock()
|
||||||
defer s.configLock.RUnlock()
|
defer s.configLock.RUnlock()
|
||||||
return removeEnvOverrides(cfg, s.configNoEnv, s.GetEnvironmentOverrides())
|
return removeEnvOverrides(cfg, s.configNoEnv, s.GetEnvironmentOverrides())
|
||||||
}
|
}
|
||||||
|
|
||||||
// PersistFeatures sets if the store should persist feature flags.
|
// SetReadOnlyFF sets whether feature flags should be written out to
|
||||||
func (s *Store) PersistFeatures(persist bool) {
|
// config or treated as read-only.
|
||||||
|
func (s *Store) SetReadOnlyFF(readOnly bool) {
|
||||||
s.configLock.Lock()
|
s.configLock.Lock()
|
||||||
defer s.configLock.Unlock()
|
defer s.configLock.Unlock()
|
||||||
s.persistFeatureFlags = persist
|
s.readOnlyFF = readOnly
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set replaces the current configuration in its entirety and updates the backing store.
|
// Set replaces the current configuration in its entirety and updates the backing store.
|
||||||
func (s *Store) Set(newCfg *model.Config) (*model.Config, error) {
|
func (s *Store) Set(newCfg *model.Config) (*model.Config, error) {
|
||||||
s.configLock.Lock()
|
s.configLock.Lock()
|
||||||
var unlockOnce sync.Once
|
defer s.configLock.Unlock()
|
||||||
defer unlockOnce.Do(s.configLock.Unlock)
|
|
||||||
|
|
||||||
if s.readOnly {
|
if s.readOnly {
|
||||||
return nil, ErrReadOnlyStore
|
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()
|
newCfg.SetDefaults()
|
||||||
|
|
||||||
// Sometimes the config is received with "fake" data in sensitive fields. Apply the real
|
// 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")
|
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
|
// 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.
|
// 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
|
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")
|
return nil, errors.Wrap(err, "failed to persist")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.loadLockedWithOld(oldCfg, &unlockOnce); err != nil {
|
// We apply back environment overrides since the input config may or
|
||||||
return nil, errors.Wrap(err, "failed to load on save")
|
// 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
|
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()
|
configBytes, err := s.backingStore.Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
loadedConfig := &model.Config{}
|
loadedCfg := &model.Config{}
|
||||||
if len(configBytes) != 0 {
|
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)
|
return jsonutils.HumanizeJSONError(err, configBytes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadedFeatureFlags := loadedConfig.FeatureFlags
|
|
||||||
|
|
||||||
// If we have custom defaults set, the initial config is merged on
|
// 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
|
// top of them and we delete them not to be used again in the
|
||||||
// configuration reloads
|
// configuration reloads
|
||||||
if s.configCustomDefaults != nil {
|
if s.configCustomDefaults != nil {
|
||||||
var mErr error
|
var mErr error
|
||||||
loadedConfig, mErr = Merge(s.configCustomDefaults, loadedConfig, nil)
|
loadedCfg, mErr = Merge(s.configCustomDefaults, loadedCfg, nil)
|
||||||
if mErr != nil {
|
if mErr != nil {
|
||||||
return errors.Wrap(mErr, "failed to merge custom config defaults")
|
return errors.Wrap(mErr, "failed to merge custom config defaults")
|
||||||
}
|
}
|
||||||
s.configCustomDefaults = nil
|
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()
|
// Setting defaults allows us to accept partial config objects.
|
||||||
fixConfig(s.configNoEnv)
|
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)
|
loadedCfg = applyEnvironmentMap(loadedCfg, GetEnvironment())
|
||||||
|
fixConfig(loadedCfg)
|
||||||
if err := loadedConfig.IsValid(); err != nil {
|
if err := loadedCfg.IsValid(); err != nil {
|
||||||
return errors.Wrap(err, "invalid config")
|
return errors.Wrap(err, "invalid config")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply changes that may have happened on load to the backing store.
|
// Backing up feature flags section in case we need to restore them later on.
|
||||||
oldCfgBytes, err := json.Marshal(oldCfg)
|
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 {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to marshal old config")
|
return errors.Wrap(err, "failed to compare configs")
|
||||||
}
|
|
||||||
newCfgBytes, err := json.Marshal(loadedConfig)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "failed to marshal loaded config")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var shouldStore bool
|
// We write back to the backing store only if the store is not read-only
|
||||||
hasChanged := len(configBytes) == 0 || !bytes.Equal(oldCfgBytes, newCfgBytes)
|
// and the config has either changed or is missing.
|
||||||
if hasChanged {
|
if !s.readOnly && (hasChanged || len(configBytes) == 0) {
|
||||||
featureFlags := s.configNoEnv.FeatureFlags
|
err := s.backingStore.Set(loadedCfgNoEnv)
|
||||||
// Don't persist feature flags unless we are on MM cloud
|
if err != nil && !errors.Is(err, ErrReadOnlyConfiguration) {
|
||||||
// MM cloud uses config in the DB as a cache of the feature flag
|
return errors.Wrap(err, "failed to persist")
|
||||||
// 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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
if hasChanged {
|
||||||
s.invokeConfigListeners(oldCfg, loadedConfig)
|
s.configLock.Unlock()
|
||||||
|
s.invokeConfigListeners(oldCfg, loadedCfg.Clone())
|
||||||
|
s.configLock.Lock()
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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.
|
// GetFile fetches the contents of a previously persisted configuration file.
|
||||||
// If no such file exists, an empty byte array will be returned without error.
|
// If no such file exists, an empty byte array will be returned without error.
|
||||||
func (s *Store) GetFile(name string) ([]byte, error) {
|
func (s *Store) GetFile(name string) ([]byte, error) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
package config_test
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@@ -10,11 +10,9 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"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() {
|
if testing.Short() {
|
||||||
t.SkipNow()
|
t.SkipNow()
|
||||||
}
|
}
|
||||||
@@ -29,25 +27,25 @@ func TestNewStore(t *testing.T) {
|
|||||||
require.NoError(t, os.Mkdir(filepath.Join(tempDir, "config"), 0700))
|
require.NoError(t, os.Mkdir(filepath.Join(tempDir, "config"), 0700))
|
||||||
|
|
||||||
t.Run("database dsn", func(t *testing.T) {
|
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)
|
require.NoError(t, err)
|
||||||
ds.Close()
|
ds.Close()
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("database dsn, watch ignored", func(t *testing.T) {
|
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)
|
require.NoError(t, err)
|
||||||
ds.Close()
|
ds.Close()
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("file dsn", func(t *testing.T) {
|
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)
|
require.NoError(t, err)
|
||||||
fs.Close()
|
fs.Close()
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("file dsn, watch", func(t *testing.T) {
|
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)
|
require.NoError(t, err)
|
||||||
fs.Close()
|
fs.Close()
|
||||||
})
|
})
|
||||||
@@ -68,46 +66,46 @@ func TestNewStoreReadOnly(t *testing.T) {
|
|||||||
require.NoError(t, os.Mkdir(filepath.Join(tempDir, "config"), 0700))
|
require.NoError(t, os.Mkdir(filepath.Join(tempDir, "config"), 0700))
|
||||||
|
|
||||||
t.Run("database dsn", func(t *testing.T) {
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
t.Run("Set", func(t *testing.T) {
|
t.Run("Set", func(t *testing.T) {
|
||||||
cfg, err := ds.Set(emptyConfig)
|
cfg, err := ds.Set(emptyConfig)
|
||||||
require.Nil(t, cfg)
|
require.Nil(t, cfg)
|
||||||
require.Equal(t, config.ErrReadOnlyStore, err)
|
require.Equal(t, ErrReadOnlyStore, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("SetFile", func(t *testing.T) {
|
t.Run("SetFile", func(t *testing.T) {
|
||||||
err := ds.SetFile("config.json", []byte{})
|
err := ds.SetFile("config.json", []byte{})
|
||||||
require.Equal(t, config.ErrReadOnlyStore, err)
|
require.Equal(t, ErrReadOnlyStore, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("RemoveFile", func(t *testing.T) {
|
t.Run("RemoveFile", func(t *testing.T) {
|
||||||
err := ds.RemoveFile("config.json")
|
err := ds.RemoveFile("config.json")
|
||||||
require.Equal(t, config.ErrReadOnlyStore, err)
|
require.Equal(t, ErrReadOnlyStore, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
ds.Close()
|
ds.Close()
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("file dsn", func(t *testing.T) {
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
t.Run("Set", func(t *testing.T) {
|
t.Run("Set", func(t *testing.T) {
|
||||||
cfg, err := fs.Set(emptyConfig)
|
cfg, err := fs.Set(emptyConfig)
|
||||||
require.Nil(t, cfg)
|
require.Nil(t, cfg)
|
||||||
require.Equal(t, config.ErrReadOnlyStore, err)
|
require.Equal(t, ErrReadOnlyStore, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("SetFile", func(t *testing.T) {
|
t.Run("SetFile", func(t *testing.T) {
|
||||||
err := fs.SetFile("config.json", []byte{})
|
err := fs.SetFile("config.json", []byte{})
|
||||||
require.Equal(t, config.ErrReadOnlyStore, err)
|
require.Equal(t, ErrReadOnlyStore, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("RemoveFile", func(t *testing.T) {
|
t.Run("RemoveFile", func(t *testing.T) {
|
||||||
err := fs.RemoveFile("config.json")
|
err := fs.RemoveFile("config.json")
|
||||||
require.Equal(t, config.ErrReadOnlyStore, err)
|
require.Equal(t, ErrReadOnlyStore, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
fs.Close()
|
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
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -14,6 +16,11 @@ import (
|
|||||||
"github.com/mattermost/mattermost-server/v5/utils"
|
"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.
|
// desanitize replaces fake settings with their actual values.
|
||||||
func desanitize(actual, target *model.Config) {
|
func desanitize(actual, target *model.Config) {
|
||||||
if target.LdapSettings.BindPassword != nil && *target.LdapSettings.BindPassword == model.FAKE_SETTING {
|
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:]
|
return prefix + dsn[:i+1] + dsn[j:]
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsJsonMap(data string) bool {
|
func isJSONMap(data string) bool {
|
||||||
var m map[string]interface{}
|
var m map[string]interface{}
|
||||||
return json.Unmarshal([]byte(data), &m) == nil
|
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) {
|
func GetValueByPath(path []string, obj interface{}) (interface{}, bool) {
|
||||||
r := reflect.ValueOf(obj)
|
r := reflect.ValueOf(obj)
|
||||||
var val reflect.Value
|
var val reflect.Value
|
||||||
@@ -249,3 +247,15 @@ func GetValueByPath(path []string, obj interface{}) (interface{}, bool) {
|
|||||||
}
|
}
|
||||||
return nil, false
|
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"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/mattermost/mattermost-server/v5/model"
|
"github.com/mattermost/mattermost-server/v5/model"
|
||||||
"github.com/mattermost/mattermost-server/v5/utils"
|
"github.com/mattermost/mattermost-server/v5/utils"
|
||||||
@@ -17,19 +18,19 @@ func TestDesanitize(t *testing.T) {
|
|||||||
actual.SetDefaults()
|
actual.SetDefaults()
|
||||||
|
|
||||||
// These setting should be ignored
|
// These setting should be ignored
|
||||||
actual.LdapSettings.Enable = bToP(false)
|
actual.LdapSettings.Enable = model.NewBool(false)
|
||||||
actual.FileSettings.DriverName = sToP("s3")
|
actual.FileSettings.DriverName = model.NewString("s3")
|
||||||
|
|
||||||
// These settings should be desanitized into target.
|
// These settings should be desanitized into target.
|
||||||
actual.LdapSettings.BindPassword = sToP("bind_password")
|
actual.LdapSettings.BindPassword = model.NewString("bind_password")
|
||||||
actual.FileSettings.PublicLinkSalt = sToP("public_link_salt")
|
actual.FileSettings.PublicLinkSalt = model.NewString("public_link_salt")
|
||||||
actual.FileSettings.AmazonS3SecretAccessKey = sToP("amazon_s3_secret_access_key")
|
actual.FileSettings.AmazonS3SecretAccessKey = model.NewString("amazon_s3_secret_access_key")
|
||||||
actual.EmailSettings.SMTPPassword = sToP("smtp_password")
|
actual.EmailSettings.SMTPPassword = model.NewString("smtp_password")
|
||||||
actual.GitLabSettings.Secret = sToP("secret")
|
actual.GitLabSettings.Secret = model.NewString("secret")
|
||||||
actual.OpenIdSettings.Secret = sToP("secret")
|
actual.OpenIdSettings.Secret = model.NewString("secret")
|
||||||
actual.SqlSettings.DataSource = sToP("data_source")
|
actual.SqlSettings.DataSource = model.NewString("data_source")
|
||||||
actual.SqlSettings.AtRestEncryptKey = sToP("at_rest_encrypt_key")
|
actual.SqlSettings.AtRestEncryptKey = model.NewString("at_rest_encrypt_key")
|
||||||
actual.ElasticsearchSettings.Password = sToP("password")
|
actual.ElasticsearchSettings.Password = model.NewString("password")
|
||||||
actual.SqlSettings.DataSourceReplicas = append(actual.SqlSettings.DataSourceReplicas, "replica0")
|
actual.SqlSettings.DataSourceReplicas = append(actual.SqlSettings.DataSourceReplicas, "replica0")
|
||||||
actual.SqlSettings.DataSourceReplicas = append(actual.SqlSettings.DataSourceReplicas, "replica1")
|
actual.SqlSettings.DataSourceReplicas = append(actual.SqlSettings.DataSourceReplicas, "replica1")
|
||||||
actual.SqlSettings.DataSourceSearchReplicas = append(actual.SqlSettings.DataSourceSearchReplicas, "search_replica0")
|
actual.SqlSettings.DataSourceSearchReplicas = append(actual.SqlSettings.DataSourceSearchReplicas, "search_replica0")
|
||||||
@@ -39,19 +40,19 @@ func TestDesanitize(t *testing.T) {
|
|||||||
target.SetDefaults()
|
target.SetDefaults()
|
||||||
|
|
||||||
// These setting should be ignored
|
// These setting should be ignored
|
||||||
target.LdapSettings.Enable = bToP(true)
|
target.LdapSettings.Enable = model.NewBool(true)
|
||||||
target.FileSettings.DriverName = sToP("file")
|
target.FileSettings.DriverName = model.NewString("file")
|
||||||
|
|
||||||
// These settings should be updated from actual
|
// These settings should be updated from actual
|
||||||
target.LdapSettings.BindPassword = sToP(model.FAKE_SETTING)
|
target.LdapSettings.BindPassword = model.NewString(model.FAKE_SETTING)
|
||||||
target.FileSettings.PublicLinkSalt = sToP(model.FAKE_SETTING)
|
target.FileSettings.PublicLinkSalt = model.NewString(model.FAKE_SETTING)
|
||||||
target.FileSettings.AmazonS3SecretAccessKey = sToP(model.FAKE_SETTING)
|
target.FileSettings.AmazonS3SecretAccessKey = model.NewString(model.FAKE_SETTING)
|
||||||
target.EmailSettings.SMTPPassword = sToP(model.FAKE_SETTING)
|
target.EmailSettings.SMTPPassword = model.NewString(model.FAKE_SETTING)
|
||||||
target.GitLabSettings.Secret = sToP(model.FAKE_SETTING)
|
target.GitLabSettings.Secret = model.NewString(model.FAKE_SETTING)
|
||||||
target.OpenIdSettings.Secret = sToP(model.FAKE_SETTING)
|
target.OpenIdSettings.Secret = model.NewString(model.FAKE_SETTING)
|
||||||
target.SqlSettings.DataSource = sToP(model.FAKE_SETTING)
|
target.SqlSettings.DataSource = model.NewString(model.FAKE_SETTING)
|
||||||
target.SqlSettings.AtRestEncryptKey = sToP(model.FAKE_SETTING)
|
target.SqlSettings.AtRestEncryptKey = model.NewString(model.FAKE_SETTING)
|
||||||
target.ElasticsearchSettings.Password = sToP(model.FAKE_SETTING)
|
target.ElasticsearchSettings.Password = model.NewString(model.FAKE_SETTING)
|
||||||
target.SqlSettings.DataSourceReplicas = []string{model.FAKE_SETTING, model.FAKE_SETTING}
|
target.SqlSettings.DataSourceReplicas = []string{model.FAKE_SETTING, model.FAKE_SETTING}
|
||||||
target.SqlSettings.DataSourceSearchReplicas = []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 {
|
func TestIsJSONMap(t *testing.T) {
|
||||||
return &s
|
|
||||||
}
|
|
||||||
|
|
||||||
func bToP(b bool) *bool {
|
|
||||||
return &b
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIsJsonMap(t *testing.T) {
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
data string
|
data string
|
||||||
@@ -278,9 +271,34 @@ func TestIsJsonMap(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
if got := IsJsonMap(tt.data); got != tt.want {
|
if got := isJSONMap(tt.data); got != tt.want {
|
||||||
t.Errorf("IsJsonMap() = %v, want %v", 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
|
// watcher monitors a file for changes
|
||||||
type watcher struct {
|
type watcher struct {
|
||||||
emitter
|
|
||||||
|
|
||||||
fsWatcher *fsnotify.Watcher
|
fsWatcher *fsnotify.Watcher
|
||||||
close chan struct{}
|
close chan struct{}
|
||||||
closed chan struct{}
|
closed chan struct{}
|
||||||
|
|||||||
@@ -4,12 +4,25 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"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() {
|
func main() {
|
||||||
outputFile := os.Getenv("OUTPUT_CONFIG")
|
outputFile := os.Getenv("OUTPUT_CONFIG")
|
||||||
if outputFile == "" {
|
if outputFile == "" {
|
||||||
@@ -22,7 +35,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if file, err := os.Create(outputFile); err == nil {
|
if file, err := os.Create(outputFile); err == nil {
|
||||||
err = generator.GenerateDefaultConfig(file)
|
err = generateDefaultConfig(file)
|
||||||
_ = file.Close()
|
_ = file.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@@ -30,5 +43,4 @@ func main() {
|
|||||||
} else {
|
} else {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
package config
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -9,17 +9,16 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"testing"
|
"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/mattermost/mattermost-server/v5/model"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDefaultsGenerator(t *testing.T) {
|
func TestDefaultsGenerator(t *testing.T) {
|
||||||
tmpFile, err := ioutil.TempFile("", "tempconfig")
|
tmpFile, err := ioutil.TempFile("", "tempconfig")
|
||||||
defer os.Remove(tmpFile.Name())
|
defer os.Remove(tmpFile.Name())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NoError(t, generator.GenerateDefaultConfig(tmpFile))
|
require.NoError(t, generateDefaultConfig(tmpFile))
|
||||||
_ = tmpFile.Close()
|
_ = tmpFile.Close()
|
||||||
var config model.Config
|
var config model.Config
|
||||||
|
|
||||||
Ссылка в новой задаче
Block a user