коммит произвёл
GitHub
родитель
b826421716
Коммит
f0e4794cda
12
app/platform/cluster.go
Обычный файл
12
app/platform/cluster.go
Обычный файл
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package platform
|
||||
|
||||
func (ps *PlatformService) IsLeader() bool {
|
||||
if ps.License() != nil && *ps.Config().ClusterSettings.Enable && ps.cluster != nil {
|
||||
return ps.cluster.IsLeader()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -5,9 +5,14 @@ package platform
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/config"
|
||||
"github.com/mattermost/mattermost-server/v6/einterfaces"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/product"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
@@ -30,7 +35,145 @@ func (c *ServiceConfig) validate() error {
|
||||
}
|
||||
|
||||
if c.Logger == nil {
|
||||
return errors.New("Logger is required")
|
||||
var err error
|
||||
// If Logger is not set, use a default logger temporarily.
|
||||
// this should be removed once the logger is properly configured with the service config.
|
||||
// MM-45841
|
||||
c.Logger, err = mlog.NewLogger()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensure the config wrapper implements `product.ConfigService`
|
||||
var _ product.ConfigService = (*PlatformService)(nil)
|
||||
|
||||
func (ps *PlatformService) Config() *model.Config {
|
||||
return ps.configStore.Get()
|
||||
}
|
||||
|
||||
// Registers a function with a given listener to be called when the config is reloaded and may have changed. The function
|
||||
// will be called with two arguments: the old config and the new config. AddConfigListener returns a unique ID
|
||||
// for the listener that can later be used to remove it.
|
||||
func (ps *PlatformService) AddConfigListener(listener func(*model.Config, *model.Config)) string {
|
||||
return ps.configStore.AddListener(listener)
|
||||
}
|
||||
|
||||
func (ps *PlatformService) RemoveConfigListener(id string) {
|
||||
ps.configStore.RemoveListener(id)
|
||||
}
|
||||
|
||||
func (ps *PlatformService) UpdateConfig(f func(*model.Config)) {
|
||||
if ps.configStore.IsReadOnly() {
|
||||
return
|
||||
}
|
||||
old := ps.Config()
|
||||
updated := old.Clone()
|
||||
f(updated)
|
||||
if _, _, err := ps.configStore.Set(updated); err != nil {
|
||||
ps.logger.Error("Failed to update config", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
// SaveConfig replaces the active configuration, optionally notifying cluster peers.
|
||||
// It returns both the previous and current configs.
|
||||
func (ps *PlatformService) SaveConfig(newCfg *model.Config, sendConfigChangeClusterMessage bool) (*model.Config, *model.Config, *model.AppError) {
|
||||
oldCfg, newCfg, err := ps.configStore.Set(newCfg)
|
||||
if errors.Is(err, config.ErrReadOnlyConfiguration) {
|
||||
return nil, nil, model.NewAppError("saveConfig", "ent.cluster.save_config.error", nil, err.Error(), http.StatusForbidden)
|
||||
} else if err != nil {
|
||||
return nil, nil, model.NewAppError("saveConfig", "app.save_config.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
if ps.serviceConfig.StartMetrics && *ps.Config().MetricsSettings.Enable {
|
||||
ps.RestartMetrics()
|
||||
} else {
|
||||
ps.ShutdownMetrics()
|
||||
}
|
||||
|
||||
if ps.cluster != nil {
|
||||
err := ps.cluster.ConfigChanged(ps.configStore.RemoveEnvironmentOverrides(oldCfg),
|
||||
ps.configStore.RemoveEnvironmentOverrides(newCfg), sendConfigChangeClusterMessage)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return oldCfg, newCfg, nil
|
||||
}
|
||||
|
||||
func (ps *PlatformService) ReloadConfig() error {
|
||||
if err := ps.configStore.Load(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *PlatformService) GetEnvironmentOverridesWithFilter(filter func(reflect.StructField) bool) map[string]interface{} {
|
||||
return ps.configStore.GetEnvironmentOverridesWithFilter(filter)
|
||||
}
|
||||
|
||||
func (ps *PlatformService) GetEnvironmentOverrides() map[string]interface{} {
|
||||
return ps.configStore.GetEnvironmentOverrides()
|
||||
}
|
||||
|
||||
func (ps *PlatformService) DescribeConfig() string {
|
||||
return ps.configStore.String()
|
||||
}
|
||||
|
||||
func (ps *PlatformService) CleanUpConfig() error {
|
||||
return ps.configStore.CleanUp()
|
||||
}
|
||||
|
||||
// ConfigureLogger applies the specified configuration to a logger.
|
||||
func (ps *PlatformService) ConfigureLogger(name string, logger *mlog.Logger, logSettings *model.LogSettings, getPath func(string) string) error {
|
||||
// Advanced logging is E20 only, however logging must be initialized before the license
|
||||
// file is loaded. If no valid E20 license exists then advanced logging will be
|
||||
// shutdown once license is loaded/checked.
|
||||
var err error
|
||||
dsn := *logSettings.AdvancedLoggingConfig
|
||||
var logConfigSrc config.LogConfigSrc
|
||||
if dsn != "" {
|
||||
logConfigSrc, err = config.NewLogConfigSrc(dsn, ps.configStore)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid config source for %s, %w", name, err)
|
||||
}
|
||||
ps.logger.Info("Loaded configuration for "+name, mlog.String("source", dsn))
|
||||
}
|
||||
|
||||
cfg, err := config.MloggerConfigFromLoggerConfig(logSettings, logConfigSrc, getPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid config source for %s, %w", name, err)
|
||||
}
|
||||
|
||||
if err := logger.ConfigureTargets(cfg, nil); err != nil {
|
||||
return fmt.Errorf("invalid config for %s, %w", name, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *PlatformService) GetConfigStore() *config.Store {
|
||||
return ps.configStore
|
||||
}
|
||||
|
||||
func (ps *PlatformService) GetConfigFile(name string) ([]byte, error) {
|
||||
return ps.configStore.GetFile(name)
|
||||
}
|
||||
|
||||
func (ps *PlatformService) SetConfigFile(name string, data []byte) error {
|
||||
return ps.configStore.SetFile(name, data)
|
||||
}
|
||||
|
||||
func (ps *PlatformService) RemoveConfigFile(name string) error {
|
||||
return ps.configStore.RemoveFile(name)
|
||||
}
|
||||
|
||||
func (ps *PlatformService) HasConfigFile(name string) (bool, error) {
|
||||
return ps.configStore.HasFile(name)
|
||||
}
|
||||
|
||||
func (ps *PlatformService) SetConfigReadOnlyFF(readOnly bool) {
|
||||
ps.configStore.SetReadOnlyFF(readOnly)
|
||||
}
|
||||
|
||||
47
app/platform/config_test.go
Обычный файл
47
app/platform/config_test.go
Обычный файл
@@ -0,0 +1,47 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package platform
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
)
|
||||
|
||||
func TestConfigListener(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
originalSiteName := th.Service.Config().TeamSettings.SiteName
|
||||
|
||||
listenerCalled := false
|
||||
listener := func(oldConfig *model.Config, newConfig *model.Config) {
|
||||
assert.False(t, listenerCalled, "listener called twice")
|
||||
|
||||
assert.Equal(t, *originalSiteName, *oldConfig.TeamSettings.SiteName, "old config contains incorrect site name")
|
||||
assert.Equal(t, "test123", *newConfig.TeamSettings.SiteName, "new config contains incorrect site name")
|
||||
|
||||
listenerCalled = true
|
||||
}
|
||||
listenerId := th.Service.AddConfigListener(listener)
|
||||
defer th.Service.RemoveConfigListener(listenerId)
|
||||
|
||||
listener2Called := false
|
||||
listener2 := func(oldConfig *model.Config, newConfig *model.Config) {
|
||||
assert.False(t, listener2Called, "listener2 called twice")
|
||||
|
||||
listener2Called = true
|
||||
}
|
||||
listener2Id := th.Service.AddConfigListener(listener2)
|
||||
defer th.Service.RemoveConfigListener(listener2Id)
|
||||
|
||||
th.Service.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.TeamSettings.SiteName = "test123"
|
||||
})
|
||||
|
||||
assert.True(t, listenerCalled, "listener should've been called")
|
||||
assert.True(t, listener2Called, "listener 2 should've been called")
|
||||
}
|
||||
118
app/platform/feature_flags.go
Обычный файл
118
app/platform/feature_flags.go
Обычный файл
@@ -0,0 +1,118 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package platform
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/app/featureflag"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
// SetupFeatureFlags called on startup and when the cluster leader changes.
|
||||
// Starts or stops the synchronization of feature flags from upstream management.
|
||||
func (ps *PlatformService) SetupFeatureFlags() {
|
||||
ps.featureFlagSynchronizerMutex.Lock()
|
||||
defer ps.featureFlagSynchronizerMutex.Unlock()
|
||||
splitKey := *ps.Config().ServiceSettings.SplitKey
|
||||
splitConfigured := splitKey != ""
|
||||
syncFeatureFlags := splitConfigured && ps.IsLeader()
|
||||
|
||||
ps.configStore.SetReadOnlyFF(!splitConfigured)
|
||||
|
||||
if syncFeatureFlags {
|
||||
if err := ps.startFeatureFlagUpdateJob(); err != nil {
|
||||
ps.logger.Warn("Unable to setup synchronization with feature flag management. Will fallback to cache.", mlog.Err(err))
|
||||
}
|
||||
} else {
|
||||
ps.StopFeatureFlagUpdateJob()
|
||||
}
|
||||
|
||||
if err := ps.configStore.Load(); err != nil {
|
||||
ps.logger.Warn("Unable to load config store after feature flag setup.", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func (ps *PlatformService) updateFeatureFlagValuesFromManagement() {
|
||||
newCfg := ps.configStore.GetNoEnv().Clone()
|
||||
oldFlags := *newCfg.FeatureFlags
|
||||
newFlags := ps.featureFlagSynchronizer.UpdateFeatureFlagValues(oldFlags)
|
||||
oldFlagsBytes, _ := json.Marshal(oldFlags)
|
||||
newFlagsBytes, _ := json.Marshal(newFlags)
|
||||
ps.logger.Debug("Checking feature flags from management service", mlog.String("old_flags", string(oldFlagsBytes)), mlog.String("new_flags", string(newFlagsBytes)))
|
||||
if oldFlags != newFlags {
|
||||
ps.logger.Debug("Feature flag change detected, updating config")
|
||||
*newCfg.FeatureFlags = newFlags
|
||||
ps.SaveConfig(newCfg, true)
|
||||
}
|
||||
}
|
||||
|
||||
func (ps *PlatformService) startFeatureFlagUpdateJob() error {
|
||||
// Can be run multiple times
|
||||
if ps.featureFlagSynchronizer != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var log *mlog.Logger
|
||||
if *ps.Config().ServiceSettings.DebugSplit {
|
||||
log = ps.logger
|
||||
}
|
||||
|
||||
attributes := map[string]any{}
|
||||
|
||||
// if we are part of a cloud installation, add its installation and group id
|
||||
if installationId := os.Getenv("MM_CLOUD_INSTALLATION_ID"); installationId != "" {
|
||||
attributes["installation_id"] = installationId
|
||||
}
|
||||
if groupId := os.Getenv("MM_CLOUD_GROUP_ID"); groupId != "" {
|
||||
attributes["group_id"] = groupId
|
||||
}
|
||||
|
||||
synchronizer, err := featureflag.NewSynchronizer(featureflag.SyncParams{
|
||||
ServerID: ps.telemetryId,
|
||||
SplitKey: *ps.Config().ServiceSettings.SplitKey,
|
||||
Log: log,
|
||||
Attributes: attributes,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ps.featureFlagStop = make(chan struct{})
|
||||
ps.featureFlagStopped = make(chan struct{})
|
||||
ps.featureFlagSynchronizer = synchronizer
|
||||
syncInterval := *ps.Config().ServiceSettings.FeatureFlagSyncIntervalSeconds
|
||||
|
||||
go func() {
|
||||
ticker := time.NewTicker(time.Duration(syncInterval) * time.Second)
|
||||
defer ticker.Stop()
|
||||
defer close(ps.featureFlagStopped)
|
||||
if err := synchronizer.EnsureReady(); err != nil {
|
||||
ps.logger.Warn("Problem connecting to feature flag management. Will fallback to cloud cache.", mlog.Err(err))
|
||||
return
|
||||
}
|
||||
ps.updateFeatureFlagValuesFromManagement()
|
||||
for {
|
||||
select {
|
||||
case <-ps.featureFlagStop:
|
||||
return
|
||||
case <-ticker.C:
|
||||
ps.updateFeatureFlagValuesFromManagement()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *PlatformService) StopFeatureFlagUpdateJob() {
|
||||
if ps.featureFlagSynchronizer != nil {
|
||||
close(ps.featureFlagStop)
|
||||
<-ps.featureFlagStopped
|
||||
ps.featureFlagSynchronizer.Close()
|
||||
ps.featureFlagSynchronizer = nil
|
||||
}
|
||||
}
|
||||
87
app/platform/helper_test.go
Обычный файл
87
app/platform/helper_test.go
Обычный файл
@@ -0,0 +1,87 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package platform
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/config"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/store"
|
||||
)
|
||||
|
||||
type TestHelper struct {
|
||||
Service *PlatformService
|
||||
}
|
||||
|
||||
func Setup(tb testing.TB) *TestHelper {
|
||||
if testing.Short() {
|
||||
tb.SkipNow()
|
||||
}
|
||||
dbStore := mainHelper.GetStore()
|
||||
dbStore.DropAllTables()
|
||||
dbStore.MarkSystemRanUnitTests()
|
||||
mainHelper.PreloadMigrations()
|
||||
|
||||
return setupTestHelper(dbStore, false, true, tb)
|
||||
}
|
||||
|
||||
func setupTestHelper(dbStore store.Store, enterprise bool, includeCacheLayer bool, tb testing.TB) *TestHelper {
|
||||
tempWorkspace, err := ioutil.TempDir("", "apptest")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
configStore := config.NewTestMemoryStore()
|
||||
|
||||
memoryConfig := configStore.Get()
|
||||
*memoryConfig.PluginSettings.Directory = filepath.Join(tempWorkspace, "plugins")
|
||||
*memoryConfig.PluginSettings.ClientDirectory = filepath.Join(tempWorkspace, "webapp")
|
||||
*memoryConfig.PluginSettings.AutomaticPrepackagedPlugins = false
|
||||
*memoryConfig.LogSettings.EnableSentry = false // disable error reporting during tests
|
||||
*memoryConfig.AnnouncementSettings.AdminNoticesEnabled = false
|
||||
*memoryConfig.AnnouncementSettings.UserNoticesEnabled = false
|
||||
configStore.Set(memoryConfig)
|
||||
|
||||
ps, err := New(ServiceConfig{
|
||||
ConfigStore: configStore,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
th := &TestHelper{
|
||||
Service: ps,
|
||||
}
|
||||
|
||||
// Share same configuration with app.TestHelper
|
||||
th.Service.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.TeamSettings.MaxUsersPerTeam = 50
|
||||
*cfg.RateLimitSettings.Enable = false
|
||||
*cfg.TeamSettings.EnableOpenServer = true
|
||||
})
|
||||
|
||||
// Disable strict password requirements for test
|
||||
th.Service.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PasswordSettings.MinimumLength = 5
|
||||
*cfg.PasswordSettings.Lowercase = false
|
||||
*cfg.PasswordSettings.Uppercase = false
|
||||
*cfg.PasswordSettings.Symbol = false
|
||||
*cfg.PasswordSettings.Number = false
|
||||
})
|
||||
|
||||
if enterprise {
|
||||
th.Service.SetLicense(model.NewTestLicense())
|
||||
} else {
|
||||
th.Service.SetLicense(nil)
|
||||
}
|
||||
|
||||
return th
|
||||
}
|
||||
|
||||
func (th *TestHelper) TearDown() {
|
||||
// Add cleaning code here
|
||||
}
|
||||
32
app/platform/main_test.go
Обычный файл
32
app/platform/main_test.go
Обычный файл
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package platform
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/testlib"
|
||||
)
|
||||
|
||||
var mainHelper *testlib.MainHelper
|
||||
var replicaFlag bool
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if f := flag.Lookup("mysql-replica"); f == nil {
|
||||
flag.BoolVar(&replicaFlag, "mysql-replica", false, "")
|
||||
flag.Parse()
|
||||
}
|
||||
|
||||
var options = testlib.HelperOptions{
|
||||
EnableStore: true,
|
||||
EnableResources: true,
|
||||
WithReadReplica: replicaFlag,
|
||||
}
|
||||
|
||||
mainHelper = testlib.NewMainHelperWithOptions(&options)
|
||||
defer mainHelper.Close()
|
||||
|
||||
mainHelper.Main(m)
|
||||
}
|
||||
19
app/platform/server_license.go
Обычный файл
19
app/platform/server_license.go
Обычный файл
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package platform
|
||||
|
||||
import (
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
)
|
||||
|
||||
// License returns the license stored in the server struct.
|
||||
// This should be removed with MM-45839
|
||||
func (ps *PlatformService) License() *model.License {
|
||||
license, _ := ps.licenseValue.Load().(*model.License)
|
||||
return license
|
||||
}
|
||||
|
||||
func (ps *PlatformService) SetLicense(license *model.License) {
|
||||
ps.licenseValue.Store(license)
|
||||
}
|
||||
@@ -4,6 +4,11 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/app/featureflag"
|
||||
"github.com/mattermost/mattermost-server/v6/config"
|
||||
"github.com/mattermost/mattermost-server/v6/einterfaces"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
@@ -19,6 +24,14 @@ type PlatformService struct {
|
||||
|
||||
metrics *platformMetrics
|
||||
|
||||
featureFlagSynchronizerMutex sync.Mutex
|
||||
featureFlagSynchronizer *featureflag.Synchronizer
|
||||
featureFlagStop chan struct{}
|
||||
featureFlagStopped chan struct{}
|
||||
|
||||
licenseValue atomic.Value
|
||||
telemetryId string
|
||||
|
||||
cluster einterfaces.ClusterInterface
|
||||
}
|
||||
|
||||
@@ -49,3 +62,18 @@ func (ps *PlatformService) ShutdownMetrics() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *PlatformService) ShutdownConfig() error {
|
||||
if ps.configStore != nil {
|
||||
err := ps.configStore.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to close config store: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *PlatformService) SetTelemetryId(id string) {
|
||||
ps.telemetryId = id
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user