diff --git a/app/admin.go b/app/admin.go index 890a0d7d89..8b901b1b5d 100644 --- a/app/admin.go +++ b/app/admin.go @@ -29,7 +29,7 @@ func (s *Server) GetLogs(page, perPage int) ([]string, *model.AppError) { var lines []string license := s.License() - if license != nil && *license.Features.Cluster && s.Cluster != nil && *s.Config().ClusterSettings.Enable { + if license != nil && *license.Features.Cluster && s.Cluster != nil && *s.platform.Config().ClusterSettings.Enable { if info := s.Cluster.GetMyClusterInfo(); info != nil { lines = append(lines, "-----------------------------------------------------------------------------------------------------------") lines = append(lines, "-----------------------------------------------------------------------------------------------------------") @@ -48,7 +48,7 @@ func (s *Server) GetLogs(page, perPage int) ([]string, *model.AppError) { lines = append(lines, melines...) - if s.Cluster != nil && *s.Config().ClusterSettings.Enable { + if s.Cluster != nil && *s.platform.Config().ClusterSettings.Enable { clines, err := s.Cluster.GetLogs(page, perPage) if err != nil { return nil, err @@ -67,9 +67,9 @@ func (a *App) GetLogs(page, perPage int) ([]string, *model.AppError) { func (s *Server) GetLogsSkipSend(page, perPage int) ([]string, *model.AppError) { var lines []string - if *s.Config().LogSettings.EnableFile { + if *s.platform.Config().LogSettings.EnableFile { s.Log.Flush() - logFile := config.GetLogFileLocation(*s.Config().LogSettings.FileLocation) + logFile := config.GetLogFileLocation(*s.platform.Config().LogSettings.FileLocation) file, err := os.Open(logFile) if err != nil { return nil, model.NewAppError("getLogs", "api.admin.file_read_error", nil, err.Error(), http.StatusInternalServerError) diff --git a/app/app_iface.go b/app/app_iface.go index f2466ad4c2..7b54096e61 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -283,6 +283,8 @@ type AppIface interface { // PromoteGuestToUser Convert user's roles and all his membership's roles from // guest roles to regular user roles. PromoteGuestToUser(c *request.Context, user *model.User, requestorId string) *model.AppError + // Removes a listener function by the unique ID returned when AddConfigListener was called + RemoveConfigListener(id string) // RenameChannel is used to rename the channel Name and the DisplayName fields RenameChannel(c request.CTX, channel *model.Channel, newChannelName string, newDisplayName string) (*model.Channel, *model.AppError) // RenameTeam is used to rename the team Name and the DisplayName fields @@ -941,7 +943,6 @@ type AppIface interface { ReloadConfig() error RemoveAllDeactivatedMembersFromChannel(c request.CTX, channel *model.Channel) *model.AppError RemoveChannelsFromRetentionPolicy(policyID string, channelIDs []string) *model.AppError - RemoveConfigListener(id string) RemoveCustomStatus(c request.CTX, userID string) *model.AppError RemoveDirectory(path string) *model.AppError RemoveFile(path string) *model.AppError diff --git a/app/audit.go b/app/audit.go index 9f6e727235..7b250c769e 100644 --- a/app/audit.go +++ b/app/audit.go @@ -109,10 +109,10 @@ func (s *Server) configureAudit(adt *audit.Audit, bAllowAdvancedLogging bool) er adt.OnError = s.onAuditError var logConfigSrc config.LogConfigSrc - dsn := *s.Config().ExperimentalAuditSettings.AdvancedLoggingConfig + dsn := *s.platform.Config().ExperimentalAuditSettings.AdvancedLoggingConfig if bAllowAdvancedLogging && dsn != "" { var err error - logConfigSrc, err = config.NewLogConfigSrc(dsn, s.configStore.Store) + logConfigSrc, err = config.NewLogConfigSrc(dsn, s.platform.GetConfigStore()) if err != nil { return fmt.Errorf("invalid config source for audit, %w", err) } @@ -120,7 +120,7 @@ func (s *Server) configureAudit(adt *audit.Audit, bAllowAdvancedLogging bool) er } // ExperimentalAuditSettings provides basic file audit (E0, E10); logConfigSrc provides advanced config (E20). - cfg, err := config.MloggerConfigFromAuditConfig(s.Config().ExperimentalAuditSettings, logConfigSrc) + cfg, err := config.MloggerConfigFromAuditConfig(s.platform.Config().ExperimentalAuditSettings, logConfigSrc) if err != nil { return fmt.Errorf("invalid config for audit, %w", err) } diff --git a/app/channel_test.go b/app/channel_test.go index 459423ab29..72dc0db3e5 100644 --- a/app/channel_test.go +++ b/app/channel_test.go @@ -2056,7 +2056,7 @@ func TestMarkChannelsAsViewedPanic(t *testing.T) { UserStore: &mockUserStore, SessionStore: &mockSessionStore, OAuthStore: &mockOAuthStore, - ConfigFn: th.App.ch.srv.Config, + ConfigFn: th.App.ch.srv.platform.Config, LicenseFn: th.App.ch.srv.License, }) require.NoError(t, err) diff --git a/app/channels.go b/app/channels.go index 48aba3297c..ed3dea6c44 100644 --- a/app/channels.go +++ b/app/channels.go @@ -31,12 +31,6 @@ type licenseSvc interface { RequestTrialLicense(requesterID string, users int, termsAccepted bool, receiveEmailsAccepted bool) *model.AppError } -// namer is an interface which enforces that -// all services can return their names. -type namer interface { - Name() ServiceKey -} - // Channels contains all channels related state. type Channels struct { srv *Server @@ -107,7 +101,7 @@ func init() { func NewChannels(s *Server, services map[ServiceKey]any) (*Channels, error) { ch := &Channels{ srv: s, - imageProxy: imageproxy.MakeImageProxy(s, s.httpService, s.Log), + imageProxy: imageproxy.MakeImageProxy(s.platform, s.httpService, s.Log), uploadLockMap: map[string]bool{}, } @@ -133,10 +127,6 @@ func NewChannels(s *Server, services map[ServiceKey]any) (*Channels, error) { if !ok { return nil, errors.New("Config service did not satisfy ConfigSvc interface") } - _, ok = svc.(namer) - if !ok { - return nil, errors.New("Config service does not contain Name method") - } ch.cfgSvc = cfgSvc case FilestoreKey: filestore, ok := svc.(filestore.FileBackend) @@ -149,10 +139,6 @@ func NewChannels(s *Server, services map[ServiceKey]any) (*Channels, error) { if !ok { return nil, errors.New("License service did not satisfy licenseSvc interface") } - _, ok = svc.(namer) - if !ok { - return nil, errors.New("License service does not contain Name method") - } ch.licenseSvc = svc } } diff --git a/app/cluster_discovery.go b/app/cluster_discovery.go index 0a003e06a0..9ae26038d4 100644 --- a/app/cluster_discovery.go +++ b/app/cluster_discovery.go @@ -83,7 +83,7 @@ func (cds *ClusterDiscoveryService) Stop() { } func (s *Server) IsLeader() bool { - if s.License() != nil && *s.Config().ClusterSettings.Enable && s.Cluster != nil { + if s.License() != nil && *s.platform.Config().ClusterSettings.Enable && s.Cluster != nil { return s.Cluster.IsLeader() } return true diff --git a/app/config.go b/app/config.go index 6aacab9405..1c66654039 100644 --- a/app/config.go +++ b/app/config.go @@ -12,7 +12,6 @@ import ( "encoding/base64" "encoding/json" "fmt" - "net/http" "net/url" "reflect" "strconv" @@ -22,7 +21,6 @@ import ( "github.com/mattermost/mattermost-server/v6/config" "github.com/mattermost/mattermost-server/v6/model" - "github.com/mattermost/mattermost-server/v6/product" "github.com/mattermost/mattermost-server/v6/shared/mail" "github.com/mattermost/mattermost-server/v6/shared/mlog" "github.com/mattermost/mattermost-server/v6/utils" @@ -32,113 +30,24 @@ const ( ErrorTermsOfServiceNoRowsFound = "app.terms_of_service.get.no_rows.app_error" ) -// ensure the config wrapper implements `product.ConfigService` -var _ product.ConfigService = (*configWrapper)(nil) - -// configWrapper is an adapter struct that only exposes the -// config related functionality to be passed down to other products. -type configWrapper struct { - srv *Server - *config.Store -} - -func (w *configWrapper) Name() ServiceKey { - return ConfigKey -} - -func (w *configWrapper) Config() *model.Config { - return w.Store.Get() -} - -func (w *configWrapper) AddConfigListener(listener func(*model.Config, *model.Config)) string { - return w.Store.AddListener(listener) -} - -func (w *configWrapper) RemoveConfigListener(id string) { - w.Store.RemoveListener(id) -} - -func (w *configWrapper) UpdateConfig(f func(*model.Config)) { - if w.Store.IsReadOnly() { - return - } - old := w.Config() - updated := old.Clone() - f(updated) - if _, _, err := w.Store.Set(updated); err != nil { - mlog.Error("Failed to update config", mlog.Err(err)) - } -} - -func (w *configWrapper) SaveConfig(newCfg *model.Config, sendConfigChangeClusterMessage bool) (*model.Config, *model.Config, *model.AppError) { - oldCfg, newCfg, err := w.Store.Set(newCfg) - if errors.Cause(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 w.srv.startMetrics && *w.Config().MetricsSettings.Enable { - if w.srv.GetMetrics() != nil { - w.srv.GetMetrics().Register() - } - w.srv.platform.RestartMetrics() // TODO: remove when this moved to the platform service - } else { - w.srv.platform.ShutdownMetrics() // TODO: remove when this moved to the platform service - } - - if w.srv.Cluster != nil { - err := w.srv.Cluster.ConfigChanged(w.Store.RemoveEnvironmentOverrides(oldCfg), - w.Store.RemoveEnvironmentOverrides(newCfg), sendConfigChangeClusterMessage) - if err != nil { - return nil, nil, err - } - } - - return oldCfg, newCfg, nil -} - -func (w *configWrapper) ReloadConfig() error { - if err := w.Store.Load(); err != nil { - return err - } - return nil -} - func (s *Server) Config() *model.Config { - return s.configStore.Config() -} - -func (s *Server) ConfigStore() *configWrapper { - return s.configStore + return s.platform.Config() } func (a *App) Config() *model.Config { return a.ch.cfgSvc.Config() } -func (s *Server) EnvironmentConfig(filter func(reflect.StructField) bool) map[string]any { - return s.configStore.GetEnvironmentOverridesWithFilter(filter) -} - func (a *App) EnvironmentConfig(filter func(reflect.StructField) bool) map[string]any { - return a.Srv().EnvironmentConfig(filter) -} - -func (s *Server) UpdateConfig(f func(*model.Config)) { - s.configStore.UpdateConfig(f) + return a.Srv().platform.GetEnvironmentOverridesWithFilter(filter) } func (a *App) UpdateConfig(f func(*model.Config)) { - a.Srv().UpdateConfig(f) -} - -func (s *Server) ReloadConfig() error { - return s.configStore.ReloadConfig() + a.Srv().platform.UpdateConfig(f) } func (a *App) ReloadConfig() error { - return a.Srv().ReloadConfig() + return a.Srv().platform.ReloadConfig() } func (a *App) ClientConfig() map[string]string { @@ -153,24 +62,13 @@ func (a *App) LimitedClientConfig() map[string]string { return a.ch.limitedClientConfig.Load().(map[string]string) } -// 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 (s *Server) AddConfigListener(listener func(*model.Config, *model.Config)) string { - return s.configStore.AddConfigListener(listener) -} - func (a *App) AddConfigListener(listener func(*model.Config, *model.Config)) string { - return a.Srv().AddConfigListener(listener) + return a.Srv().platform.AddConfigListener(listener) } // Removes a listener function by the unique ID returned when AddConfigListener was called -func (s *Server) RemoveConfigListener(id string) { - s.configStore.RemoveConfigListener(id) -} - func (a *App) RemoveConfigListener(id string) { - a.Srv().RemoveConfigListener(id) + a.Srv().platform.RemoveConfigListener(id) } // ensurePostActionCookieSecret ensures that the key for encrypting PostActionCookie exists @@ -449,7 +347,7 @@ func (a *App) LimitedClientConfigWithComputed() map[string]string { // GetConfigFile proxies access to the given configuration file to the underlying config store. func (a *App) GetConfigFile(name string) ([]byte, error) { - data, err := a.Srv().configStore.GetFile(name) + data, err := a.Srv().platform.GetConfigFile(name) if err != nil { return nil, errors.Wrapf(err, "failed to get config file %s", name) } @@ -471,15 +369,9 @@ func (a *App) GetEnvironmentConfig(filter func(reflect.StructField) bool) map[st return a.EnvironmentConfig(filter) } -// SaveConfig replaces the active configuration, optionally notifying cluster peers. -// It returns both the previous and current configs. -func (s *Server) SaveConfig(newCfg *model.Config, sendConfigChangeClusterMessage bool) (*model.Config, *model.Config, *model.AppError) { - return s.configStore.SaveConfig(newCfg, sendConfigChangeClusterMessage) -} - // SaveConfig replaces the active configuration, optionally notifying cluster peers. func (a *App) SaveConfig(newCfg *model.Config, sendConfigChangeClusterMessage bool) (*model.Config, *model.Config, *model.AppError) { - return a.Srv().SaveConfig(newCfg, sendConfigChangeClusterMessage) + return a.Srv().platform.SaveConfig(newCfg, sendConfigChangeClusterMessage) } func (a *App) HandleMessageExportConfig(cfg *model.Config, appCfg *model.Config) { @@ -499,8 +391,8 @@ func (a *App) HandleMessageExportConfig(cfg *model.Config, appCfg *model.Config) } func (s *Server) MailServiceConfig() *mail.SMTPConfig { - emailSettings := s.Config().EmailSettings - hostname := utils.GetHostnameFromSiteURL(*s.Config().ServiceSettings.SiteURL) + emailSettings := s.platform.Config().EmailSettings + hostname := utils.GetHostnameFromSiteURL(*s.platform.Config().ServiceSettings.SiteURL) cfg := mail.SMTPConfig{ Hostname: hostname, ConnectionSecurity: *emailSettings.ConnectionSecurity, diff --git a/app/config_test.go b/app/config_test.go index fcca2889a4..aa019e4281 100644 --- a/app/config_test.go +++ b/app/config_test.go @@ -16,41 +16,6 @@ import ( "github.com/mattermost/mattermost-server/v6/utils" ) -func TestConfigListener(t *testing.T) { - th := Setup(t) - defer th.TearDown() - - originalSiteName := th.App.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.App.AddConfigListener(listener) - defer th.App.RemoveConfigListener(listenerId) - - listener2Called := false - listener2 := func(oldConfig *model.Config, newConfig *model.Config) { - assert.False(t, listener2Called, "listener2 called twice") - - listener2Called = true - } - listener2Id := th.App.AddConfigListener(listener2) - defer th.App.RemoveConfigListener(listener2Id) - - th.App.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") -} - func TestAsymmetricSigningKey(t *testing.T) { th := SetupWithStoreMock(t) defer th.TearDown() diff --git a/app/download.go b/app/download.go index ac9ac68466..d83299d483 100644 --- a/app/download.go +++ b/app/download.go @@ -35,7 +35,7 @@ func (s *Server) downloadFromURL(downloadURL string) ([]byte, error) { if err != nil { return nil, errors.Errorf("failed to parse url %s", downloadURL) } - if !*s.Config().PluginSettings.AllowInsecureDownloadURL && u.Scheme != "https" { + if !*s.platform.Config().PluginSettings.AllowInsecureDownloadURL && u.Scheme != "https" { return nil, errors.Errorf("insecure url not allowed %s", downloadURL) } diff --git a/app/enterprise.go b/app/enterprise.go index bb34bb1d8e..db826a873f 100644 --- a/app/enterprise.go +++ b/app/enterprise.go @@ -87,9 +87,9 @@ func RegisterCloudInterface(f func(*Server) einterfaces.CloudInterface) { cloudInterface = f } -var metricsInterface func(*Server) einterfaces.MetricsInterface +var metricsInterface func(*Server, string, string) einterfaces.MetricsInterface -func RegisterMetricsInterface(f func(*Server) einterfaces.MetricsInterface) { +func RegisterMetricsInterface(f func(*Server, string, string) einterfaces.MetricsInterface) { metricsInterface = f } diff --git a/app/feature_flags.go b/app/feature_flags.go deleted file mode 100644 index efb517eca3..0000000000 --- a/app/feature_flags.go +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -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 (s *Server) setupFeatureFlags() { - s.featureFlagSynchronizerMutex.Lock() - defer s.featureFlagSynchronizerMutex.Unlock() - splitKey := *s.Config().ServiceSettings.SplitKey - splitConfigured := splitKey != "" - syncFeatureFlags := splitConfigured && s.IsLeader() - - s.configStore.SetReadOnlyFF(!splitConfigured) - - if syncFeatureFlags { - if err := s.startFeatureFlagUpdateJob(); err != nil { - s.Log.Warn("Unable to setup synchronization with feature flag management. Will fallback to cache.", mlog.Err(err)) - } - } else { - s.stopFeatureFlagUpdateJob() - } - - if err := s.configStore.Load(); err != nil { - s.Log.Warn("Unable to load config store after feature flag setup.", mlog.Err(err)) - } -} - -func (s *Server) updateFeatureFlagValuesFromManagement() { - newCfg := s.configStore.GetNoEnv().Clone() - oldFlags := *newCfg.FeatureFlags - newFlags := s.featureFlagSynchronizer.UpdateFeatureFlagValues(oldFlags) - oldFlagsBytes, _ := json.Marshal(oldFlags) - newFlagsBytes, _ := json.Marshal(newFlags) - s.Log.Debug("Checking feature flags from management service", mlog.String("old_flags", string(oldFlagsBytes)), mlog.String("new_flags", string(newFlagsBytes))) - if oldFlags != newFlags { - s.Log.Debug("Feature flag change detected, updating config") - *newCfg.FeatureFlags = newFlags - s.SaveConfig(newCfg, true) - } -} - -func (s *Server) startFeatureFlagUpdateJob() error { - // Can be run multiple times - if s.featureFlagSynchronizer != nil { - return nil - } - - var log *mlog.Logger - if *s.Config().ServiceSettings.DebugSplit { - log = s.Log - } - - 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: s.TelemetryId(), - SplitKey: *s.Config().ServiceSettings.SplitKey, - Log: log, - Attributes: attributes, - }) - if err != nil { - return err - } - - s.featureFlagStop = make(chan struct{}) - s.featureFlagStopped = make(chan struct{}) - s.featureFlagSynchronizer = synchronizer - syncInterval := *s.Config().ServiceSettings.FeatureFlagSyncIntervalSeconds - - go func() { - ticker := time.NewTicker(time.Duration(syncInterval) * time.Second) - defer ticker.Stop() - defer close(s.featureFlagStopped) - if err := synchronizer.EnsureReady(); err != nil { - s.Log.Warn("Problem connecting to feature flag management. Will fallback to cloud cache.", mlog.Err(err)) - return - } - s.updateFeatureFlagValuesFromManagement() - for { - select { - case <-s.featureFlagStop: - return - case <-ticker.C: - s.updateFeatureFlagValuesFromManagement() - } - } - }() - - return nil -} - -func (s *Server) stopFeatureFlagUpdateJob() { - if s.featureFlagSynchronizer != nil { - close(s.featureFlagStop) - <-s.featureFlagStopped - s.featureFlagSynchronizer.Close() - s.featureFlagSynchronizer = nil - } -} diff --git a/app/ldap.go b/app/ldap.go index 20f4fdf752..40399f50f4 100644 --- a/app/ldap.go +++ b/app/ldap.go @@ -191,7 +191,7 @@ func (a *App) writeLdapFile(filename string, fileData *multipart.FileHeader) *mo return model.NewAppError("AddLdapCertificate", "api.admin.add_certificate.saving.app_error", nil, err.Error(), http.StatusInternalServerError) } - err = a.Srv().configStore.SetFile(filename, data) + err = a.Srv().platform.SetConfigFile(filename, data) if err != nil { return model.NewAppError("AddLdapCertificate", "api.admin.add_certificate.saving.app_error", nil, err.Error(), http.StatusInternalServerError) } @@ -234,7 +234,7 @@ func (a *App) AddLdapPrivateCertificate(fileData *multipart.FileHeader) *model.A } func (a *App) removeLdapFile(filename string) *model.AppError { - if err := a.Srv().configStore.RemoveFile(filename); err != nil { + if err := a.Srv().platform.RemoveConfigFile(filename); err != nil { return model.NewAppError("RemoveLdapFile", "api.admin.remove_certificate.delete.app_error", map[string]any{"Filename": filename}, err.Error(), http.StatusInternalServerError) } return nil diff --git a/app/license.go b/app/license.go index c6617bd76f..ff6b2dff9a 100644 --- a/app/license.go +++ b/app/license.go @@ -48,7 +48,7 @@ func (w *licenseWrapper) GetLicense() *model.License { } func (w *licenseWrapper) RequestTrialLicense(requesterID string, users int, termsAccepted bool, receiveEmailsAccepted bool) *model.AppError { - if *w.srv.Config().ExperimentalSettings.RestrictSystemAdmin { + if *w.srv.platform.Config().ExperimentalSettings.RestrictSystemAdmin { return model.NewAppError("RequestTrialLicense", "api.restricted_system_admin", nil, "", http.StatusForbidden) } @@ -75,8 +75,8 @@ func (w *licenseWrapper) RequestTrialLicense(requesterID string, users int, term ServerID: w.srv.TelemetryId(), Name: requester.GetDisplayName(model.ShowFullName), Email: requester.Email, - SiteName: *w.srv.Config().TeamSettings.SiteName, - SiteURL: *w.srv.Config().ServiceSettings.SiteURL, + SiteName: *w.srv.platform.Config().TeamSettings.SiteName, + SiteURL: *w.srv.platform.Config().ServiceSettings.SiteURL, Users: users, TermsAccepted: termsAccepted, ReceiveEmailsAccepted: receiveEmailsAccepted, @@ -93,6 +93,11 @@ type JWTClaims struct { jwt.StandardClaims } +func (s *Server) License() *model.License { + license, _ := s.licenseValue.Load().(*model.License) + return license +} + func (s *Server) LoadLicense() { // ENV var overrides all other sources of license. licenseStr := os.Getenv(LicenseEnv) @@ -131,7 +136,7 @@ func (s *Server) LoadLicense() { if !model.IsValidId(licenseId) { // Lets attempt to load the file from disk since it was missing from the DB - license, licenseBytes := utils.GetAndValidateLicenseFileFromDisk(*s.Config().ServiceSettings.LicenseFileLocation) + license, licenseBytes := utils.GetAndValidateLicenseFileFromDisk(*s.platform.Config().ServiceSettings.LicenseFileLocation) if license != nil { if _, err := s.SaveLicense(licenseBytes); err != nil { @@ -177,13 +182,13 @@ func (s *Server) SaveLicense(licenseBytes []byte) (*model.License, *model.AppErr return nil, model.NewAppError("addLicense", model.ExpiredLicenseError, nil, "", http.StatusBadRequest) } - if *s.Config().JobSettings.RunJobs && s.Jobs != nil { + if *s.platform.Config().JobSettings.RunJobs && s.Jobs != nil { if err := s.Jobs.StopWorkers(); err != nil && !errors.Is(err, jobs.ErrWorkersNotRunning) { mlog.Warn("Stopping job server workers failed", mlog.Err(err)) } } - if *s.Config().JobSettings.RunScheduler && s.Jobs != nil { + if *s.platform.Config().JobSettings.RunScheduler && s.Jobs != nil { if err := s.Jobs.StopSchedulers(); err != nil && !errors.Is(err, jobs.ErrSchedulersNotRunning) { mlog.Error("Stopping job server schedulers failed", mlog.Err(err)) } @@ -193,12 +198,12 @@ func (s *Server) SaveLicense(licenseBytes []byte) (*model.License, *model.AppErr // restart job server workers - this handles the edge case where a license file is uploaded, but the job server // doesn't start until the server is restarted, which prevents the 'run job now' buttons in system console from // functioning as expected - if *s.Config().JobSettings.RunJobs && s.Jobs != nil { + if *s.platform.Config().JobSettings.RunJobs && s.Jobs != nil { if err := s.Jobs.StartWorkers(); err != nil { mlog.Error("Starting job server workers failed", mlog.Err(err)) } } - if *s.Config().JobSettings.RunScheduler && s.Jobs != nil { + if *s.platform.Config().JobSettings.RunScheduler && s.Jobs != nil { if err := s.Jobs.StartSchedulers(); err != nil && !errors.Is(err, jobs.ErrSchedulersRunning) { mlog.Error("Starting job server schedulers failed", mlog.Err(err)) } @@ -233,7 +238,7 @@ func (s *Server) SaveLicense(licenseBytes []byte) (*model.License, *model.AppErr return nil, model.NewAppError("addLicense", "api.license.add_license.save_active.app_error", nil, "", http.StatusInternalServerError) } - s.ReloadConfig() + s.platform.ReloadConfig() s.InvalidateAllCaches() return &license, nil @@ -256,12 +261,20 @@ func (s *Server) SetLicense(license *model.License) bool { license.Features.SetDefaults() s.licenseValue.Store(license) + if s.platform != nil { + s.platform.SetLicense(license) + } + s.clientLicenseValue.Store(utils.GetClientLicense(license)) return true } s.licenseValue.Store((*model.License)(nil)) s.clientLicenseValue.Store(map[string]string(nil)) + if s.platform != nil { + s.platform.SetLicense((*model.License)(nil)) + } + return false } @@ -307,7 +320,7 @@ func (s *Server) RemoveLicense() *model.AppError { } s.SetLicense(nil) - s.ReloadConfig() + s.platform.ReloadConfig() s.InvalidateAllCaches() return nil @@ -360,7 +373,7 @@ func (s *Server) RequestTrialLicense(trialRequest *model.TrialLicenseRequest) *m return err } - s.ReloadConfig() + s.platform.ReloadConfig() s.InvalidateAllCaches() return nil diff --git a/app/migrations.go b/app/migrations.go index aba545c443..36d12302c1 100644 --- a/app/migrations.go +++ b/app/migrations.go @@ -69,9 +69,9 @@ func (s *Server) doAdvancedPermissionsMigration() { return } - config := s.Config() + config := s.platform.Config() *config.ServiceSettings.PostEditTimeLimit = -1 - if _, _, err := s.SaveConfig(config, true); err != nil { + if _, _, err := s.platform.SaveConfig(config, true); err != nil { mlog.Error("Failed to update config in Advanced Permissions Phase 1 Migration.", mlog.Err(err)) } @@ -327,7 +327,7 @@ func (s *Server) doContentExtractionConfigDefaultTrueMigration() { return } - s.UpdateConfig(func(config *model.Config) { + s.platform.UpdateConfig(func(config *model.Config) { config.FileSettings.ExtractContent = model.NewBool(true) }) @@ -474,7 +474,7 @@ const existingInstallationPostsThreshold = 10 func (s *Server) doFirstAdminSetupCompleteMigration() { // Don't run the migration until the flag is turned on. - if !s.Config().FeatureFlags.UseCaseOnboarding { + if !s.platform.Config().FeatureFlags.UseCaseOnboarding { return } diff --git a/app/notification_push.go b/app/notification_push.go index e10b716c7d..b0643754a5 100644 --- a/app/notification_push.go +++ b/app/notification_push.go @@ -290,7 +290,7 @@ func (a *App) UpdateMobileAppBadge(userID string) { } func (s *Server) createPushNotificationsHub(c request.CTX) { - buffer := *s.Config().EmailSettings.PushNotificationBuffer + buffer := *s.platform.Config().EmailSettings.PushNotificationBuffer hub := PushNotificationsHub{ notificationsChan: make(chan PushNotification, buffer), app: New(ServerConnector(s.Channels())), diff --git a/app/notification_push_test.go b/app/notification_push_test.go index 53ac88db13..0f3e8a157f 100644 --- a/app/notification_push_test.go +++ b/app/notification_push_test.go @@ -17,6 +17,7 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "github.com/mattermost/mattermost-server/v6/app/platform" "github.com/mattermost/mattermost-server/v6/config" "github.com/mattermost/mattermost-server/v6/model" fmocks "github.com/mattermost/mattermost-server/v6/shared/filestore/mocks" @@ -1443,9 +1444,13 @@ func TestPushNotificationRace(t *testing.T) { Router: mux.NewRouter(), filestore: &fmocks.FileBackend{}, } - s.configStore = &configWrapper{srv: s, Store: memoryStore} + var err error + s.platform, err = platform.New(platform.ServiceConfig{ + ConfigStore: memoryStore, + }) + require.NoError(t, err) serviceMap := map[ServiceKey]any{ - ConfigKey: s.configStore, + ConfigKey: s.platform, LicenseKey: &licenseWrapper{s}, FilestoreKey: s.filestore, } diff --git a/app/options.go b/app/options.go index 4c35cd0478..6452fd9092 100644 --- a/app/options.go +++ b/app/options.go @@ -6,6 +6,7 @@ package app import ( "github.com/pkg/errors" + "github.com/mattermost/mattermost-server/v6/app/platform" "github.com/mattermost/mattermost-server/v6/config" "github.com/mattermost/mattermost-server/v6/einterfaces" "github.com/mattermost/mattermost-server/v6/model" @@ -52,7 +53,22 @@ func Config(dsn string, readOnly bool, configDefaults *model.Config) Option { return errors.Wrap(err, "failed to apply Config option") } - s.configStore = &configWrapper{srv: s, Store: configStore} + platformCfg := platform.ServiceConfig{ + ConfigStore: configStore, + Logger: s.Log, + StartMetrics: s.startMetrics, + Cluster: s.Cluster, + } + if metricsInterface != nil { + platformCfg.Metrics = metricsInterface(s, *configStore.Get().SqlSettings.DriverName, *configStore.Get().SqlSettings.DataSource) + } + + ps, sErr := platform.New(platformCfg) + if sErr != nil { + return errors.Wrap(sErr, "failed to initialize platform") + } + s.platform = ps + return nil } } @@ -60,7 +76,21 @@ func Config(dsn string, readOnly bool, configDefaults *model.Config) Option { // ConfigStore applies the given config store, typically to replace the traditional sources with a memory store for testing. func ConfigStore(configStore *config.Store) Option { return func(s *Server) error { - s.configStore = &configWrapper{srv: s, Store: configStore} + platformCfg := platform.ServiceConfig{ + ConfigStore: configStore, + Logger: s.Log, + StartMetrics: s.startMetrics, + Cluster: s.Cluster, + } + if metricsInterface != nil { + platformCfg.Metrics = metricsInterface(s, *configStore.Get().SqlSettings.DriverName, *configStore.Get().SqlSettings.DataSource) + } + + ps, sErr := platform.New(platformCfg) + if sErr != nil { + return errors.Wrap(sErr, "failed to initialize platform") + } + s.platform = ps return nil } diff --git a/app/platform/cluster.go b/app/platform/cluster.go new file mode 100644 index 0000000000..75fe329cea --- /dev/null +++ b/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 +} diff --git a/app/platform/config.go b/app/platform/config.go index 20c5a823d5..fc13aa3761 100644 --- a/app/platform/config.go +++ b/app/platform/config.go @@ -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) +} diff --git a/app/platform/config_test.go b/app/platform/config_test.go new file mode 100644 index 0000000000..62ecbce482 --- /dev/null +++ b/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") +} diff --git a/app/platform/feature_flags.go b/app/platform/feature_flags.go new file mode 100644 index 0000000000..8bcc8843d5 --- /dev/null +++ b/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 + } +} diff --git a/app/platform/helper_test.go b/app/platform/helper_test.go new file mode 100644 index 0000000000..17b053093f --- /dev/null +++ b/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 +} diff --git a/app/platform/main_test.go b/app/platform/main_test.go new file mode 100644 index 0000000000..e2e91836f5 --- /dev/null +++ b/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) +} diff --git a/app/platform/server_license.go b/app/platform/server_license.go new file mode 100644 index 0000000000..3ccc95999b --- /dev/null +++ b/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) +} diff --git a/app/platform/service.go b/app/platform/service.go index baa4b1362e..2db592adc2 100644 --- a/app/platform/service.go +++ b/app/platform/service.go @@ -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 +} diff --git a/app/plugin_signature.go b/app/plugin_signature.go index 6dab66efb8..9724080aed 100644 --- a/app/plugin_signature.go +++ b/app/plugin_signature.go @@ -25,7 +25,7 @@ func (a *App) GetPublicKey(name string) ([]byte, *model.AppError) { } func (s *Server) getPublicKey(name string) ([]byte, *model.AppError) { - data, err := s.configStore.GetFile(name) + data, err := s.platform.GetConfigFile(name) if err != nil { return nil, model.NewAppError("GetPublicKey", "app.plugin.get_public_key.get_file.app_error", nil, err.Error(), http.StatusInternalServerError) } @@ -41,7 +41,7 @@ func (a *App) AddPublicKey(name string, key io.Reader) *model.AppError { if err != nil { return model.NewAppError("AddPublicKey", "app.plugin.write_file.read.app_error", nil, err.Error(), http.StatusInternalServerError) } - err = a.Srv().configStore.SetFile(name, data) + err = a.Srv().platform.SetConfigFile(name, data) if err != nil { return model.NewAppError("AddPublicKey", "app.plugin.write_file.saving.app_error", nil, err.Error(), http.StatusInternalServerError) } @@ -61,7 +61,7 @@ func (a *App) DeletePublicKey(name string) *model.AppError { return model.NewAppError("AddPublicKey", "app.plugin.modify_saml.app_error", nil, "", http.StatusInternalServerError) } filename := filepath.Base(name) - if err := a.Srv().configStore.RemoveFile(filename); err != nil { + if err := a.Srv().platform.RemoveConfigFile(filename); err != nil { return model.NewAppError("DeletePublicKey", "app.plugin.delete_public_key.delete.app_error", nil, err.Error(), http.StatusInternalServerError) } diff --git a/app/post_metadata.go b/app/post_metadata.go index 05996e9b0a..f7155e0a7c 100644 --- a/app/post_metadata.go +++ b/app/post_metadata.go @@ -41,7 +41,7 @@ var linkCache = cache.NewLRU(cache.LRUOptions{ func (s *Server) initPostMetadata() { // Dump any cached links if the proxy settings have changed so image URLs can be updated - s.AddConfigListener(func(before, after *model.Config) { + s.platform.AddConfigListener(func(before, after *model.Config) { if (before.ImageProxySettings.Enable != after.ImageProxySettings.Enable) || (before.ImageProxySettings.ImageProxyType != after.ImageProxySettings.ImageProxyType) || (before.ImageProxySettings.RemoteImageProxyURL != after.ImageProxySettings.RemoteImageProxyURL) || diff --git a/app/post_metadata_test.go b/app/post_metadata_test.go index 3bf8ebf334..63ee5c99d6 100644 --- a/app/post_metadata_test.go +++ b/app/post_metadata_test.go @@ -796,7 +796,7 @@ func TestPreparePostForClientWithImageProxy(t *testing.T) { *cfg.ImageProxySettings.RemoteImageProxyOptions = "foo" }) - th.App.ch.imageProxy = imageproxy.MakeImageProxy(th.Server, th.Server.HTTPService(), th.Server.Log) + th.App.ch.imageProxy = imageproxy.MakeImageProxy(th.Server.platform, th.Server.HTTPService(), th.Server.Log) return th } diff --git a/app/post_test.go b/app/post_test.go index 64c203e8f2..3e5429ffc0 100644 --- a/app/post_test.go +++ b/app/post_test.go @@ -472,7 +472,7 @@ func TestImageProxy(t *testing.T) { *cfg.ServiceSettings.SiteURL = "http://mymattermost.com" }) - th.App.ch.imageProxy = imageproxy.MakeImageProxy(th.Server, th.Server.HTTPService(), th.Server.Log) + th.App.ch.imageProxy = imageproxy.MakeImageProxy(th.Server.platform, th.Server.HTTPService(), th.Server.Log) for name, tc := range map[string]struct { ProxyType string @@ -686,7 +686,7 @@ func TestCreatePost(t *testing.T) { *cfg.ImageProxySettings.RemoteImageProxyOptions = "foo" }) - th.App.ch.imageProxy = imageproxy.MakeImageProxy(th.Server, th.Server.HTTPService(), th.Server.Log) + th.App.ch.imageProxy = imageproxy.MakeImageProxy(th.Server.platform, th.Server.HTTPService(), th.Server.Log) imageURL := "http://mydomain.com/myimage" proxiedImageURL := "http://mymattermost.com/api/v4/image?url=http%3A%2F%2Fmydomain.com%2Fmyimage" @@ -956,7 +956,7 @@ func TestPatchPost(t *testing.T) { *cfg.ImageProxySettings.RemoteImageProxyOptions = "foo" }) - th.App.ch.imageProxy = imageproxy.MakeImageProxy(th.Server, th.Server.HTTPService(), th.Server.Log) + th.App.ch.imageProxy = imageproxy.MakeImageProxy(th.Server.platform, th.Server.HTTPService(), th.Server.Log) imageURL := "http://mydomain.com/myimage" proxiedImageURL := "http://mymattermost.com/api/v4/image?url=http%3A%2F%2Fmydomain.com%2Fmyimage" @@ -1252,7 +1252,7 @@ func TestUpdatePost(t *testing.T) { *cfg.ImageProxySettings.RemoteImageProxyOptions = "foo" }) - th.App.ch.imageProxy = imageproxy.MakeImageProxy(th.Server, th.Server.HTTPService(), th.Server.Log) + th.App.ch.imageProxy = imageproxy.MakeImageProxy(th.Server.platform, th.Server.HTTPService(), th.Server.Log) imageURL := "http://mydomain.com/myimage" proxiedImageURL := "http://mymattermost.com/api/v4/image?url=http%3A%2F%2Fmydomain.com%2Fmyimage" diff --git a/app/reaction_test.go b/app/reaction_test.go index e0d536d775..3e60208b84 100644 --- a/app/reaction_test.go +++ b/app/reaction_test.go @@ -90,8 +90,8 @@ func TestGetTopReactionsForTeamSince(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() - th.Server.configStore.SetReadOnlyFF(false) - defer th.Server.configStore.SetReadOnlyFF(true) + th.Server.platform.SetConfigReadOnlyFF(false) + defer th.Server.platform.SetConfigReadOnlyFF(true) userId := th.BasicUser.Id user2Id := th.BasicUser2.Id @@ -261,8 +261,8 @@ func TestGetTopReactionsForUserSince(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() - th.Server.configStore.SetReadOnlyFF(false) - defer th.Server.configStore.SetReadOnlyFF(true) + th.Server.platform.SetConfigReadOnlyFF(false) + defer th.Server.platform.SetConfigReadOnlyFF(true) userId := th.BasicUser.Id diff --git a/app/saml.go b/app/saml.go index 58787bf08a..260d601cee 100644 --- a/app/saml.go +++ b/app/saml.go @@ -47,7 +47,7 @@ func (a *App) writeSamlFile(filename string, fileData *multipart.FileHeader) *mo return model.NewAppError("AddSamlCertificate", "api.admin.add_certificate.saving.app_error", nil, err.Error(), http.StatusInternalServerError) } - err = a.Srv().configStore.SetFile(filename, data) + err = a.Srv().platform.SetConfigFile(filename, data) if err != nil { return model.NewAppError("AddSamlCertificate", "api.admin.add_certificate.saving.app_error", nil, err.Error(), http.StatusInternalServerError) } @@ -107,7 +107,7 @@ func (a *App) AddSamlIdpCertificate(fileData *multipart.FileHeader) *model.AppEr } func (a *App) removeSamlFile(filename string) *model.AppError { - if err := a.Srv().configStore.RemoveFile(filename); err != nil { + if err := a.Srv().platform.RemoveConfigFile(filename); err != nil { return model.NewAppError("RemoveSamlFile", "api.admin.remove_certificate.delete.app_error", map[string]any{"Filename": filename}, err.Error(), http.StatusInternalServerError) } @@ -171,9 +171,9 @@ func (a *App) RemoveSamlIdpCertificate() *model.AppError { func (a *App) GetSamlCertificateStatus() *model.SamlCertificateStatus { status := &model.SamlCertificateStatus{} - status.IdpCertificateFile, _ = a.Srv().configStore.HasFile(*a.Config().SamlSettings.IdpCertificateFile) - status.PrivateKeyFile, _ = a.Srv().configStore.HasFile(*a.Config().SamlSettings.PrivateKeyFile) - status.PublicCertificateFile, _ = a.Srv().configStore.HasFile(*a.Config().SamlSettings.PublicCertificateFile) + status.IdpCertificateFile, _ = a.Srv().platform.HasConfigFile(*a.Config().SamlSettings.IdpCertificateFile) + status.PrivateKeyFile, _ = a.Srv().platform.HasConfigFile(*a.Config().SamlSettings.PrivateKeyFile) + status.PublicCertificateFile, _ = a.Srv().platform.HasConfigFile(*a.Config().SamlSettings.PublicCertificateFile) return status } @@ -267,7 +267,7 @@ func (a *App) SetSamlIdpCertificateFromMetadata(data []byte) *model.AppError { Bytes: block.Bytes, }) - if err := a.Srv().configStore.SetFile(SamlIdpCertificateName, data); err != nil { + if err := a.Srv().platform.SetConfigFile(SamlIdpCertificateName, data); err != nil { return model.NewAppError("SetSamlIdpCertificateFromMetadata", "api.admin.saml.failure_save_idp_certificate_file.app_error", nil, err.Error(), http.StatusInternalServerError) } diff --git a/app/security_update_check.go b/app/security_update_check.go index 3712465ed7..273f520e23 100644 --- a/app/security_update_check.go +++ b/app/security_update_check.go @@ -33,7 +33,7 @@ const ( ) func (s *Server) DoSecurityUpdateCheck() { - if !*s.Config().ServiceSettings.EnableSecurityFixAlert { + if !*s.platform.Config().ServiceSettings.EnableSecurityFixAlert { return } @@ -53,7 +53,7 @@ func (s *Server) DoSecurityUpdateCheck() { v.Set(PropSecurityID, s.TelemetryId()) v.Set(PropSecurityBuild, model.CurrentVersion+"."+model.BuildNumber) v.Set(PropSecurityEnterpriseReady, model.BuildEnterpriseReady) - v.Set(PropSecurityDatabase, *s.Config().SqlSettings.DriverName) + v.Set(PropSecurityDatabase, *s.platform.Config().SqlSettings.DriverName) v.Set(PropSecurityOS, runtime.GOOS) if props[model.SystemRanUnitTests] != "" { diff --git a/app/server.go b/app/server.go index 48b99eea35..8f641e8c7c 100644 --- a/app/server.go +++ b/app/server.go @@ -31,7 +31,6 @@ import ( "golang.org/x/crypto/acme/autocert" "github.com/mattermost/mattermost-server/v6/app/email" - "github.com/mattermost/mattermost-server/v6/app/featureflag" "github.com/mattermost/mattermost-server/v6/app/platform" "github.com/mattermost/mattermost-server/v6/app/request" "github.com/mattermost/mattermost-server/v6/app/teams" @@ -168,7 +167,6 @@ type Server struct { searchConfigListenerId string searchLicenseListenerId string loggerLicenseListenerId string - configStore *configWrapper filestore filestore.FileBackend platform *platform.PlatformService @@ -201,11 +199,6 @@ type Server struct { tracer *tracing.Tracer - featureFlagSynchronizer *featureflag.Synchronizer - featureFlagStop chan struct{} - featureFlagStopped chan struct{} - featureFlagSynchronizerMutex sync.Mutex - products map[string]Product } @@ -237,7 +230,7 @@ func NewServer(options ...Option) (*Server, error) { // and has dependency requirements with the previous step. // // Step 1: Config. - if s.configStore == nil { + if s.platform == nil { innerStore, err := config.NewFileStore("config.json", true) if err != nil { return nil, errors.Wrap(err, "failed to load config") @@ -247,7 +240,25 @@ func NewServer(options ...Option) (*Server, error) { return nil, errors.Wrap(err, "failed to load config") } - s.configStore = &configWrapper{srv: s, Store: configStore} + platformCfg := platform.ServiceConfig{ + ConfigStore: configStore, + Logger: s.Log, + StartMetrics: s.startMetrics, + Cluster: s.Cluster, + } + if metricsInterface != nil { + platformCfg.Metrics = metricsInterface(s, *configStore.Get().SqlSettings.DriverName, *configStore.Get().SqlSettings.DataSource) + } + + ps, sErr := platform.New(platformCfg) + if sErr != nil { + return nil, errors.Wrap(sErr, "failed to initialize platform") + } + s.platform = ps + + if s.licenseValue.Load() != nil { + ps.SetLicense(s.licenseValue.Load().(*model.License)) // in case license is set in server options + } } // Step 2: Logging @@ -255,7 +266,7 @@ func NewServer(options ...Option) (*Server, error) { mlog.Error("Could not initiate logging", mlog.Err(err)) } - subpath, err := utils.GetSubpathFromConfig(s.Config()) + subpath, err := utils.GetSubpathFromConfig(s.platform.Config()) if err != nil { return nil, errors.Wrap(err, "failed to parse SiteURL subpath") } @@ -264,12 +275,12 @@ func NewServer(options ...Option) (*Server, error) { // This is called after initLogging() to avoid a race condition. mlog.Info("Server is initializing...", mlog.String("go_version", runtime.Version())) - s.httpService = httpservice.MakeHTTPService(s) + s.httpService = httpservice.MakeHTTPService(s.platform) // Step 3: Search Engine // Depends on Step 1 (config). - searchEngine := searchengine.NewBroker(s.Config()) - bleveEngine := bleveengine.NewBleveEngine(s.Config()) + searchEngine := searchengine.NewBroker(s.platform.Config()) + bleveEngine := bleveengine.NewBleveEngine(s.platform.Config()) if err := bleveEngine.Start(); err != nil { return nil, err } @@ -280,22 +291,6 @@ func NewServer(options ...Option) (*Server, error) { // Depends on step 3 (s.SearchEngine must be non-nil) s.initEnterprise() - platformCfg := platform.ServiceConfig{ - ConfigStore: s.configStore.Store, - Logger: s.Log, - StartMetrics: s.startMetrics, - Cluster: s.Cluster, - } - if metricsInterface != nil { - platformCfg.Metrics = metricsInterface(s) - } - - ps, sErr := platform.New(platformCfg) - if sErr != nil { - return nil, errors.Wrap(sErr, "failed to initialize platform") - } - s.platform = ps - // Step 5: Cache provider. // At the moment we only have this implementation // in the future the cache provider will be built based on the loaded config @@ -308,7 +303,7 @@ func NewServer(options ...Option) (*Server, error) { // Depends on Step 1 (config), 4 (metrics, cluster) and 5 (cacheProvider). if s.newStore == nil { s.newStore = func() (store.Store, error) { - s.sqlStore = sqlstore.New(s.Config().SqlSettings, s.GetMetrics()) + s.sqlStore = sqlstore.New(s.platform.Config().SqlSettings, s.GetMetrics()) lcl, err2 := localcachelayer.NewLocalCacheLayer( retrylayer.New(s.sqlStore), @@ -323,10 +318,10 @@ func NewServer(options ...Option) (*Server, error) { searchStore := searchlayer.NewSearchLayer( lcl, s.SearchEngine, - s.Config(), + s.platform.Config(), ) - s.AddConfigListener(func(prevCfg, cfg *model.Config) { + s.platform.AddConfigListener(func(prevCfg, cfg *model.Config) { searchStore.UpdateConfig(cfg) }) @@ -352,7 +347,7 @@ func NewServer(options ...Option) (*Server, error) { UserStore: s.Store.User(), SessionStore: s.Store.Session(), OAuthStore: s.Store.OAuth(), - ConfigFn: s.Config, + ConfigFn: s.platform.Config, Metrics: s.GetMetrics(), Cluster: s.Cluster, LicenseFn: s.License, @@ -376,9 +371,9 @@ func NewServer(options ...Option) (*Server, error) { } license := s.License() - insecure := s.Config().ServiceSettings.EnableInsecureOutgoingConnections + insecure := s.platform.Config().ServiceSettings.EnableInsecureOutgoingConnections // Step 7: Initialize filestore - backend, err := filestore.NewFileBackend(s.Config().FileSettings.ToFileBackendSettings(license != nil && *license.Features.Compliance, insecure != nil && *insecure)) + backend, err := filestore.NewFileBackend(s.platform.Config().FileSettings.ToFileBackendSettings(license != nil && *license.Features.Compliance, insecure != nil && *insecure)) if err != nil { return nil, errors.Wrap(err, "failed to initialize filebackend") } @@ -398,7 +393,7 @@ func NewServer(options ...Option) (*Server, error) { GroupStore: s.Store.Group(), Users: s.userService, WebHub: s, - ConfigFn: s.Config, + ConfigFn: s.platform.Config, LicenseFn: s.License, }) if err != nil { @@ -410,7 +405,7 @@ func NewServer(options ...Option) (*Server, error) { serviceMap := map[ServiceKey]any{ ChannelKey: &channelsWrapper{srv: s}, - ConfigKey: s.configStore, + ConfigKey: s.platform, LicenseKey: s.licenseWrapper, FilestoreKey: s.filestore, FileInfoStoreKey: &fileInfoWrapper{srv: s}, @@ -441,7 +436,7 @@ func NewServer(options ...Option) (*Server, error) { // below this. Otherwise, please add it to Channels struct in app/channels.go. // ------------------------------------------------------------------------- - if *s.Config().LogSettings.EnableDiagnostics && *s.Config().LogSettings.EnableSentry { + if *s.platform.Config().LogSettings.EnableDiagnostics && *s.platform.Config().LogSettings.EnableSentry { if strings.Contains(SentryDSN, "placeholder") { mlog.Warn("Sentry reporting is enabled, but SENTRY_DSN is not set. Disabling reporting.") } else { @@ -468,7 +463,7 @@ func NewServer(options ...Option) (*Server, error) { } } - if *s.Config().ServiceSettings.EnableOpenTracing { + if *s.platform.Config().ServiceSettings.EnableOpenTracing { tracer, err2 := tracing.New() if err2 != nil { return nil, err2 @@ -496,7 +491,7 @@ func NewServer(options ...Option) (*Server, error) { s.createPushNotificationsHub(request.EmptyContext(s.GetLogger())) - if err2 := i18n.InitTranslations(*s.Config().LocalizationSettings.DefaultServerLocale, *s.Config().LocalizationSettings.DefaultClientLocale); err2 != nil { + if err2 := i18n.InitTranslations(*s.platform.Config().LocalizationSettings.DefaultServerLocale, *s.platform.Config().LocalizationSettings.DefaultClientLocale); err2 != nil { return nil, errors.Wrapf(err2, "unable to load Mattermost translation files") } @@ -515,7 +510,7 @@ func NewServer(options ...Option) (*Server, error) { }) s.htmlTemplateWatcher = htmlTemplateWatcher - s.configListenerId = s.AddConfigListener(func(_, _ *model.Config) { + s.configListenerId = s.platform.AddConfigListener(func(_, _ *model.Config) { ch := s.Channels() ch.regenerateClientConfig() @@ -544,9 +539,10 @@ func NewServer(options ...Option) (*Server, error) { }) s.telemetryService = telemetry.New(New(ServerConnector(s.Channels())), s.Store, s.SearchEngine, s.Log) + s.platform.SetTelemetryId(s.TelemetryId()) // TODO: move this into platform once telemetry service moved to platform. emailService, err := email.NewService(email.ServiceConfig{ - ConfigFn: s.Config, + ConfigFn: s.platform.Config, LicenseFn: s.License, GoFn: s.Go, TemplatesContainer: s.TemplatesContainer(), @@ -558,7 +554,7 @@ func NewServer(options ...Option) (*Server, error) { } s.EmailService = emailService - s.setupFeatureFlags() + s.platform.SetupFeatureFlags() s.initJobs() @@ -567,7 +563,7 @@ func NewServer(options ...Option) (*Server, error) { if s.Jobs != nil { s.Jobs.HandleClusterLeaderChange(s.IsLeader()) } - s.setupFeatureFlags() + s.platform.SetupFeatureFlags() }) // If configured with a subpath, redirect 404s at the root back into the subpath. @@ -578,12 +574,12 @@ func NewServer(options ...Option) (*Server, error) { }) } - if _, err = url.ParseRequestURI(*s.Config().ServiceSettings.SiteURL); err != nil { + if _, err = url.ParseRequestURI(*s.platform.Config().ServiceSettings.SiteURL); err != nil { mlog.Error("SiteURL must be set. Some features will operate incorrectly if the SiteURL is not set. See documentation for details: https://docs.mattermost.com/configure/configuration-settings.html#site-url") } // Start email batching because it's not like the other jobs - s.AddConfigListener(func(_, _ *model.Config) { + s.platform.AddConfigListener(func(_, _ *model.Config) { s.EmailService.InitEmailBatching() }) @@ -604,7 +600,7 @@ func NewServer(options ...Option) (*Server, error) { pwd, _ := os.Getwd() mlog.Info("Printing current working", mlog.String("directory", pwd)) - mlog.Info("Loaded config", mlog.String("source", s.configStore.String())) + mlog.Info("Loaded config", mlog.String("source", s.platform.DescribeConfig())) allowAdvancedLogging := license != nil && *license.Features.AdvancedLogging @@ -626,7 +622,7 @@ func NewServer(options ...Option) (*Server, error) { // Enable developer settings if this is a "dev" build if model.BuildNumber == "dev" { - s.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableDeveloper = true }) + s.platform.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableDeveloper = true }) } if s.startMetrics { @@ -649,13 +645,13 @@ func NewServer(options ...Option) (*Server, error) { } }) - s.SearchEngine.UpdateConfig(s.Config()) + s.SearchEngine.UpdateConfig(s.platform.Config()) searchConfigListenerId, searchLicenseListenerId := s.StartSearchEngine() s.searchConfigListenerId = searchConfigListenerId s.searchLicenseListenerId = searchLicenseListenerId // if enabled - perform initial product notices fetch - if *s.Config().AnnouncementSettings.AdminNoticesEnabled || *s.Config().AnnouncementSettings.UserNoticesEnabled { + if *s.platform.Config().AnnouncementSettings.AdminNoticesEnabled || *s.platform.Config().AnnouncementSettings.UserNoticesEnabled { go func() { appInstance := New(ServerConnector(s.Channels())) if err := appInstance.UpdateProductNotices(); err != nil { @@ -668,7 +664,7 @@ func NewServer(options ...Option) (*Server, error) { return s, nil } - s.AddConfigListener(func(old, new *model.Config) { + s.platform.AddConfigListener(func(old, new *model.Config) { appInstance := New(ServerConnector(s.Channels())) if *old.GuestAccountsSettings.Enable && !*new.GuestAccountsSettings.Enable { c := request.EmptyContext(s.GetLogger()) @@ -679,7 +675,7 @@ func NewServer(options ...Option) (*Server, error) { }) // Disable active guest accounts on first run if guest accounts are disabled - if !*s.Config().GuestAccountsSettings.Enable { + if !*s.platform.Config().GuestAccountsSettings.Enable { appInstance := New(ServerConnector(s.Channels())) c := request.EmptyContext(s.GetLogger()) if appErr := appInstance.DeactivateGuests(c); appErr != nil { @@ -703,7 +699,7 @@ func NewServer(options ...Option) (*Server, error) { s.initPostMetadata() // Dump the image cache if the proxy settings have changed. (need switch URLs to the correct proxy) - s.AddConfigListener(func(oldCfg, newCfg *model.Config) { + s.platform.AddConfigListener(func(oldCfg, newCfg *model.Config) { if (oldCfg.ImageProxySettings.Enable != newCfg.ImageProxySettings.Enable) || (oldCfg.ImageProxySettings.ImageProxyType != newCfg.ImageProxySettings.ImageProxyType) || (oldCfg.ImageProxySettings.RemoteImageProxyURL != newCfg.ImageProxySettings.RemoteImageProxyURL) || @@ -755,18 +751,18 @@ func (s *Server) runJobs() { complianceI.StartComplianceDailyJob() } - if *s.Config().JobSettings.RunJobs && s.Jobs != nil { + if *s.platform.Config().JobSettings.RunJobs && s.Jobs != nil { if err := s.Jobs.StartWorkers(); err != nil { mlog.Error("Failed to start job server workers", mlog.Err(err)) } } - if *s.Config().JobSettings.RunScheduler && s.Jobs != nil { + if *s.platform.Config().JobSettings.RunScheduler && s.Jobs != nil { if err := s.Jobs.StartSchedulers(); err != nil { mlog.Error("Failed to start job server schedulers", mlog.Err(err)) } } - if *s.Config().ServiceSettings.EnableAWSMetering { + if *s.platform.Config().ServiceSettings.EnableAWSMetering { runReportToAWSMeterJob(s) } } @@ -786,7 +782,7 @@ func (s *Server) Channels() *Channels { // Return Database type (postgres or mysql) and current version of the schema func (s *Server) DatabaseTypeAndSchemaVersion() (string, string) { schemaVersion, _ := s.Store.GetDBSchemaVersion() - return *s.Config().SqlSettings.DriverName, strconv.Itoa(schemaVersion) + return *s.platform.Config().SqlSettings.DriverName, strconv.Itoa(schemaVersion) } // initLogging initializes and configures the logger(s). This may be called more than once. @@ -809,7 +805,7 @@ func (s *Server) initLogging() error { s.NotificationsLog = l.With(mlog.String("logSource", "notifications")) } - if err := s.configureLogger("logging", s.Log, &s.Config().LogSettings, s.configStore.Store, config.GetLogFileLocation); err != nil { + if err := s.platform.ConfigureLogger("logging", s.Log, &s.platform.Config().LogSettings, config.GetLogFileLocation); err != nil { // if the config is locked then a unit test has already configured and locked the logger; not an error. if !errors.Is(err, mlog.ErrConfigurationLock) { // revert to default logger if the config is invalid @@ -824,8 +820,8 @@ func (s *Server) initLogging() error { // Use the app logger as the global logger (eventually remove all instances of global logging). mlog.InitGlobalLogger(s.Log) - notificationLogSettings := config.GetLogSettingsFromNotificationsLogSettings(&s.Config().NotificationLogSettings) - if err := s.configureLogger("notification logging", s.NotificationsLog, notificationLogSettings, s.configStore.Store, config.GetNotificationsLogFileLocation); err != nil { + notificationLogSettings := config.GetLogSettingsFromNotificationsLogSettings(&s.platform.Config().NotificationLogSettings) + if err := s.platform.ConfigureLogger("notification logging", s.NotificationsLog, notificationLogSettings, config.GetNotificationsLogFileLocation); err != nil { if !errors.Is(err, mlog.ErrConfigurationLock) { mlog.Error("Error configuring notification logger", mlog.Err(err)) return err @@ -834,33 +830,6 @@ func (s *Server) initLogging() error { return nil } -// configureLogger applies the specified configuration to a logger. -func (s *Server) configureLogger(name string, logger *mlog.Logger, logSettings *model.LogSettings, configStore *config.Store, 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, configStore) - if err != nil { - return fmt.Errorf("invalid config source for %s, %w", name, err) - } - mlog.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 -} - // removeUnlicensedLogTargets removes any unlicensed log target types. func (s *Server) removeUnlicensedLogTargets(license *model.License) { if license != nil && *license.Features.AdvancedLogging { @@ -895,7 +864,7 @@ func (s *Server) startInterClusterServices(license *model.License) error { } // Config check - if !*s.Config().ExperimentalSettings.EnableRemoteClusterService { + if !*s.platform.Config().ExperimentalSettings.EnableRemoteClusterService { mlog.Debug("Remote Cluster Service disabled via config") return nil } @@ -924,7 +893,7 @@ func (s *Server) startInterClusterServices(license *model.License) error { } // Config check - if !*s.Config().ExperimentalSettings.EnableSharedChannels { + if !*s.platform.Config().ExperimentalSettings.EnableSharedChannels { mlog.Debug("Shared Channels Service disabled via config") return nil } @@ -1029,14 +998,16 @@ func (s *Server) Shutdown() { s.WaitForGoroutines() - s.RemoveConfigListener(s.configListenerId) + s.platform.RemoveConfigListener(s.configListenerId) s.stopSearchEngine() s.Audit.Shutdown() - s.stopFeatureFlagUpdateJob() + s.platform.StopFeatureFlagUpdateJob() - s.configStore.Close() + if err = s.platform.ShutdownConfig(); err != nil { + s.Log.Warn("Failed to shut down config store", mlog.Err(err)) + } if s.Cluster != nil { s.Cluster.StopInterNodeCommunication() @@ -1239,23 +1210,23 @@ func (s *Server) Start() error { s.checkPushNotificationServerURL() - s.ReloadConfig() + s.platform.ReloadConfig() mlog.Info("Starting Server...") var handler http.Handler = s.RootRouter - if *s.Config().LogSettings.EnableDiagnostics && *s.Config().LogSettings.EnableSentry && !strings.Contains(SentryDSN, "placeholder") { + if *s.platform.Config().LogSettings.EnableDiagnostics && *s.platform.Config().LogSettings.EnableSentry && !strings.Contains(SentryDSN, "placeholder") { sentryHandler := sentryhttp.New(sentryhttp.Options{ Repanic: true, }) handler = sentryHandler.Handle(handler) } - if allowedOrigins := *s.Config().ServiceSettings.AllowCorsFrom; allowedOrigins != "" { - exposedCorsHeaders := *s.Config().ServiceSettings.CorsExposedHeaders - allowCredentials := *s.Config().ServiceSettings.CorsAllowCredentials - debug := *s.Config().ServiceSettings.CorsDebug + if allowedOrigins := *s.platform.Config().ServiceSettings.AllowCorsFrom; allowedOrigins != "" { + exposedCorsHeaders := *s.platform.Config().ServiceSettings.CorsExposedHeaders + allowCredentials := *s.platform.Config().ServiceSettings.CorsAllowCredentials + debug := *s.platform.Config().ServiceSettings.CorsDebug corsWrapper := cors.New(cors.Options{ AllowedOrigins: strings.Fields(allowedOrigins), AllowedMethods: corsAllowedMethods, @@ -1274,10 +1245,10 @@ func (s *Server) Start() error { handler = corsWrapper.Handler(handler) } - if *s.Config().RateLimitSettings.Enable { + if *s.platform.Config().RateLimitSettings.Enable { mlog.Info("RateLimiter is enabled") - rateLimiter, err2 := NewRateLimiter(&s.Config().RateLimitSettings, s.Config().ServiceSettings.TrustedProxyIPHeader) + rateLimiter, err2 := NewRateLimiter(&s.platform.Config().RateLimitSettings, s.platform.Config().ServiceSettings.TrustedProxyIPHeader) if err2 != nil { return err2 } @@ -1292,15 +1263,15 @@ func (s *Server) Start() error { s.Server = &http.Server{ Handler: handler, - ReadTimeout: time.Duration(*s.Config().ServiceSettings.ReadTimeout) * time.Second, - WriteTimeout: time.Duration(*s.Config().ServiceSettings.WriteTimeout) * time.Second, - IdleTimeout: time.Duration(*s.Config().ServiceSettings.IdleTimeout) * time.Second, + ReadTimeout: time.Duration(*s.platform.Config().ServiceSettings.ReadTimeout) * time.Second, + WriteTimeout: time.Duration(*s.platform.Config().ServiceSettings.WriteTimeout) * time.Second, + IdleTimeout: time.Duration(*s.platform.Config().ServiceSettings.IdleTimeout) * time.Second, ErrorLog: errStdLog, } - addr := *s.Config().ServiceSettings.ListenAddress + addr := *s.platform.Config().ServiceSettings.ListenAddress if addr == "" { - if *s.Config().ServiceSettings.ConnectionSecurity == model.ConnSecurityTLS { + if *s.platform.Config().ServiceSettings.ConnectionSecurity == model.ConnSecurityTLS { addr = ":https" } else { addr = ":http" @@ -1317,11 +1288,11 @@ func (s *Server) Start() error { mlog.Info(logListeningPort, mlog.String("address", listener.Addr().String())) m := &autocert.Manager{ - Cache: autocert.DirCache(*s.Config().ServiceSettings.LetsEncryptCertificateCacheFile), + Cache: autocert.DirCache(*s.platform.Config().ServiceSettings.LetsEncryptCertificateCacheFile), Prompt: autocert.AcceptTOS, } - if *s.Config().ServiceSettings.Forward80To443 { + if *s.platform.Config().ServiceSettings.Forward80To443 { if host, port, err := net.SplitHostPort(addr); err != nil { mlog.Error("Unable to setup forwarding", mlog.Err(err)) } else if port != "443" { @@ -1329,7 +1300,7 @@ func (s *Server) Start() error { } else { httpListenAddress := net.JoinHostPort(host, "http") - if *s.Config().ServiceSettings.UseLetsEncrypt { + if *s.platform.Config().ServiceSettings.UseLetsEncrypt { server := &http.Server{ Addr: httpListenAddress, Handler: m.HTTPHandler(nil), @@ -1353,21 +1324,21 @@ func (s *Server) Start() error { }() } } - } else if *s.Config().ServiceSettings.UseLetsEncrypt { + } else if *s.platform.Config().ServiceSettings.UseLetsEncrypt { return errors.New(i18n.T("api.server.start_server.forward80to443.disabled_while_using_lets_encrypt")) } s.didFinishListen = make(chan struct{}) go func() { var err error - if *s.Config().ServiceSettings.ConnectionSecurity == model.ConnSecurityTLS { + if *s.platform.Config().ServiceSettings.ConnectionSecurity == model.ConnSecurityTLS { tlsConfig := &tls.Config{ PreferServerCipherSuites: true, CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256}, } - switch *s.Config().ServiceSettings.TLSMinVer { + switch *s.platform.Config().ServiceSettings.TLSMinVer { case "1.0": tlsConfig.MinVersion = tls.VersionTLS10 case "1.1": @@ -1385,11 +1356,11 @@ func (s *Server) Start() error { tls.TLS_RSA_WITH_AES_256_GCM_SHA384, } - if len(s.Config().ServiceSettings.TLSOverwriteCiphers) == 0 { + if len(s.platform.Config().ServiceSettings.TLSOverwriteCiphers) == 0 { tlsConfig.CipherSuites = defaultCiphers } else { var cipherSuites []uint16 - for _, cipher := range s.Config().ServiceSettings.TLSOverwriteCiphers { + for _, cipher := range s.platform.Config().ServiceSettings.TLSOverwriteCiphers { value, ok := model.ServerTLSSupportedCiphers[cipher] if !ok { @@ -1411,12 +1382,12 @@ func (s *Server) Start() error { certFile := "" keyFile := "" - if *s.Config().ServiceSettings.UseLetsEncrypt { + if *s.platform.Config().ServiceSettings.UseLetsEncrypt { tlsConfig.GetCertificate = m.GetCertificate tlsConfig.NextProtos = append(tlsConfig.NextProtos, "h2") } else { - certFile = *s.Config().ServiceSettings.TLSCertFile - keyFile = *s.Config().ServiceSettings.TLSKeyFile + certFile = *s.platform.Config().ServiceSettings.TLSCertFile + keyFile = *s.platform.Config().ServiceSettings.TLSKeyFile } s.Server.TLSConfig = tlsConfig @@ -1433,7 +1404,7 @@ func (s *Server) Start() error { close(s.didFinishListen) }() - if *s.Config().ServiceSettings.EnableLocalMode { + if *s.platform.Config().ServiceSettings.EnableLocalMode { if err := s.startLocalModeServer(); err != nil { mlog.Critical(err.Error()) } @@ -1451,7 +1422,7 @@ func (s *Server) startLocalModeServer() error { Handler: s.LocalRouter, } - socket := *s.configStore.Get().ServiceSettings.LocalModeSocketLocation + socket := *s.platform.Config().ServiceSettings.LocalModeSocketLocation if err := os.RemoveAll(socket); err != nil { return errors.Wrapf(err, i18n.T("api.server.start_server.starting.critical"), err) } @@ -1495,7 +1466,7 @@ func (a *App) OriginChecker() func(*http.Request) bool { } func (s *Server) checkPushNotificationServerURL() { - notificationServer := *s.Config().EmailSettings.PushNotificationServer + notificationServer := *s.platform.Config().EmailSettings.PushNotificationServer if strings.HasPrefix(notificationServer, "http://") { mlog.Warn("Your push notification server is configured with HTTP. For improved security, update to HTTPS in your configuration.") } @@ -1563,7 +1534,7 @@ func runReportToAWSMeterJob(s *Server) { } func doReportUsageToAWSMeteringService(s *Server) { - awsMeter := awsmeter.New(s.Store, s.Config()) + awsMeter := awsmeter.New(s.Store, s.platform.Config()) if awsMeter == nil { mlog.Error("Cannot obtain instance of AWS Metering Service.") return @@ -1604,12 +1575,12 @@ func doSessionCleanup(s *Server) { } func doJobsCleanup(s *Server) { - if *s.Config().JobSettings.CleanupJobsThresholdDays < 0 { + if *s.platform.Config().JobSettings.CleanupJobsThresholdDays < 0 { return } mlog.Debug("Cleaning up jobs store.") - dur := time.Duration(*s.Config().JobSettings.CleanupJobsThresholdDays) * time.Hour * 24 + dur := time.Duration(*s.platform.Config().JobSettings.CleanupJobsThresholdDays) * time.Hour * 24 expiry := model.GetMillisForTime(time.Now().Add(-dur)) err := s.Store.Job().Cleanup(expiry, jobsCleanupBatchSize) if err != nil { @@ -1618,12 +1589,12 @@ func doJobsCleanup(s *Server) { } func doConfigCleanup(s *Server) { - if *s.Config().JobSettings.CleanupConfigThresholdDays < 0 || !config.IsDatabaseDSN(s.ConfigStore().Store.String()) { + if *s.platform.Config().JobSettings.CleanupConfigThresholdDays < 0 || !config.IsDatabaseDSN(s.platform.DescribeConfig()) { return } mlog.Info("Cleaning up configuration store.") - if err := s.ConfigStore().Store.CleanUp(); err != nil { + if err := s.platform.CleanUpConfig(); err != nil { mlog.Warn("Error while cleaning up configurations", mlog.Err(err)) } } @@ -1654,7 +1625,7 @@ func (s *Server) sendLicenseUpForRenewalEmail(users map[string]*model.User, lice if name == "" { name = user.Username } - if err := s.EmailService.SendLicenseUpForRenewalEmail(user.Email, name, user.Locale, *s.Config().ServiceSettings.SiteURL, renewalLink, daysToExpiration); err != nil { + if err := s.EmailService.SendLicenseUpForRenewalEmail(user.Email, name, user.Locale, *s.platform.Config().ServiceSettings.SiteURL, renewalLink, daysToExpiration); err != nil { mlog.Error("Error sending license up for renewal email to", mlog.String("user_email", user.Email), mlog.Err(err)) countNotOks++ } @@ -1735,7 +1706,7 @@ func (s *Server) doLicenseExpirationCheck() { mlog.Debug("Sending license expired email.", mlog.String("user_email", user.Email)) s.Go(func() { - if err := s.SendRemoveExpiredLicenseEmail(user.Email, renewalLink, user.Locale, *s.Config().ServiceSettings.SiteURL); err != nil { + if err := s.SendRemoveExpiredLicenseEmail(user.Email, renewalLink, user.Locale, *s.platform.Config().ServiceSettings.SiteURL); err != nil { mlog.Error("Error while sending the license expired email.", mlog.String("user_email", user.Email), mlog.Err(err)) } }) @@ -1765,7 +1736,7 @@ func (s *Server) StartSearchEngine() (string, string) { }) } - configListenerId := s.AddConfigListener(func(oldConfig *model.Config, newConfig *model.Config) { + configListenerId := s.platform.AddConfigListener(func(oldConfig *model.Config, newConfig *model.Config) { if s.SearchEngine == nil { return } @@ -1824,7 +1795,7 @@ func (s *Server) StartSearchEngine() (string, string) { } func (s *Server) stopSearchEngine() { - s.RemoveConfigListener(s.searchConfigListenerId) + s.platform.RemoveConfigListener(s.searchConfigListenerId) s.RemoveLicenseListener(s.searchLicenseListenerId) if s.SearchEngine != nil && s.SearchEngine.ElasticsearchEngine != nil && s.SearchEngine.ElasticsearchEngine.IsActive() { s.SearchEngine.ElasticsearchEngine.Stop() @@ -1858,7 +1829,7 @@ func (ch *Channels) ClientConfigHash() string { } func (s *Server) initJobs() { - s.Jobs = jobs.NewJobServer(s, s.Store, s.GetMetrics()) + s.Jobs = jobs.NewJobServer(s.platform, s.Store, s.GetMetrics()) if jobsDataRetentionJobInterface != nil { builder := jobsDataRetentionJobInterface(s) @@ -2031,7 +2002,7 @@ func (s *Server) SetSharedChannelSyncService(sharedChannelService SharedChannelS } func (s *Server) GetProfileImage(user *model.User) ([]byte, bool, *model.AppError) { - if *s.Config().FileSettings.DriverName == "" { + if *s.platform.Config().FileSettings.DriverName == "" { img, appErr := s.GetDefaultProfileImage(user) if appErr != nil { return nil, false, appErr @@ -2141,3 +2112,8 @@ func (a *App) GetAppliedSchemaMigrations() ([]model.AppliedMigration, *model.App } return table, nil } + +// Expose platform service from server, this should be replaced with server itself in time. +func (s *Server) Platform() *platform.PlatformService { + return s.platform +} diff --git a/app/server_inactivity.go b/app/server_inactivity.go index 3bb83e7f0c..4c5b0b7a01 100644 --- a/app/server_inactivity.go +++ b/app/server_inactivity.go @@ -17,17 +17,17 @@ const inactivityEmailSent = "INACTIVITY" func (s *Server) doInactivityCheck() { - if *s.Config().ServiceSettings.EnableDeveloper { + if *s.platform.Config().ServiceSettings.EnableDeveloper { mlog.Info("No activity check because developer mode is enabled") return } - if !*s.Config().EmailSettings.EnableInactivityEmail { + if !*s.platform.Config().EmailSettings.EnableInactivityEmail { mlog.Info("No activity check because EnableInactivityEmail is false") return } - if !s.Config().FeatureFlags.EnableInactivityCheckJob { + if !s.platform.Config().FeatureFlags.EnableInactivityCheckJob { mlog.Info("No activity check because EnableInactivityCheckJob feature flag is disabled") return } @@ -70,7 +70,7 @@ func (s *Server) doInactivityCheck() { } func (s *Server) takeInactivityAction() { - siteURL := *s.Config().ServiceSettings.SiteURL + siteURL := *s.platform.Config().ServiceSettings.SiteURL if siteURL == "" { mlog.Warn("No SiteURL configured") } diff --git a/app/server_license.go b/app/server_license.go deleted file mode 100644 index ffb0dc68ca..0000000000 --- a/app/server_license.go +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "github.com/mattermost/mattermost-server/v6/model" -) - -func (s *Server) License() *model.License { - license, _ := s.licenseValue.Load().(*model.License) - return license -} diff --git a/app/server_test.go b/app/server_test.go index 710aa302a1..c1e0cd8fec 100644 --- a/app/server_test.go +++ b/app/server_test.go @@ -23,6 +23,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/mattermost/mattermost-server/v6/app/platform" "github.com/mattermost/mattermost-server/v6/config" "github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/mattermost-server/v6/shared/filestore" @@ -83,54 +84,70 @@ func TestReadReplicaDisabledBasedOnLicense(t *testing.T) { s, err := NewServer(func(server *Server) error { configStore := config.NewTestMemoryStore() configStore.Set(&cfg) - server.configStore = &configWrapper{srv: server, Store: configStore} + var err error + server.platform, err = platform.New(platform.ServiceConfig{ + ConfigStore: configStore, + }) + require.NoError(t, err) return nil }) require.NoError(t, err) defer s.Shutdown() require.Same(t, s.sqlStore.GetMasterX(), s.sqlStore.GetReplicaX()) - require.Len(t, s.Config().SqlSettings.DataSourceReplicas, 1) + require.Len(t, s.platform.Config().SqlSettings.DataSourceReplicas, 1) }) t.Run("Read Replicas With License", func(t *testing.T) { s, err := NewServer(func(server *Server) error { configStore := config.NewTestMemoryStore() configStore.Set(&cfg) - server.configStore = &configWrapper{srv: server, Store: configStore} + var err error + server.platform, err = platform.New(platform.ServiceConfig{ + ConfigStore: configStore, + }) + require.NoError(t, err) server.licenseValue.Store(model.NewTestLicense()) return nil }) require.NoError(t, err) defer s.Shutdown() require.NotSame(t, s.sqlStore.GetMasterX(), s.sqlStore.GetReplicaX()) - require.Len(t, s.Config().SqlSettings.DataSourceReplicas, 1) + require.Len(t, s.platform.Config().SqlSettings.DataSourceReplicas, 1) }) t.Run("Search Replicas with no License", func(t *testing.T) { s, err := NewServer(func(server *Server) error { configStore := config.NewTestMemoryStore() configStore.Set(&cfg) - server.configStore = &configWrapper{srv: server, Store: configStore} + var err error + server.platform, err = platform.New(platform.ServiceConfig{ + ConfigStore: configStore, + }) + require.NoError(t, err) return nil }) require.NoError(t, err) defer s.Shutdown() require.Same(t, s.sqlStore.GetMasterX(), s.sqlStore.GetSearchReplicaX()) - require.Len(t, s.Config().SqlSettings.DataSourceSearchReplicas, 1) + require.Len(t, s.platform.Config().SqlSettings.DataSourceSearchReplicas, 1) }) t.Run("Search Replicas With License", func(t *testing.T) { s, err := NewServer(func(server *Server) error { configStore := config.NewTestMemoryStore() configStore.Set(&cfg) - server.configStore = &configWrapper{srv: server, Store: configStore} + var err error + server.platform, err = platform.New(platform.ServiceConfig{ + ConfigStore: configStore, + }) + require.NoError(t, err) server.licenseValue.Store(model.NewTestLicense()) return nil }) require.NoError(t, err) defer s.Shutdown() require.NotSame(t, s.sqlStore.GetMasterX(), s.sqlStore.GetSearchReplicaX()) - require.Len(t, s.Config().SqlSettings.DataSourceSearchReplicas, 1) + require.Len(t, s.platform.Config().SqlSettings.DataSourceSearchReplicas, 1) }) } @@ -143,7 +160,7 @@ func TestStartServerPortUnavailable(t *testing.T) { require.NoError(t, err) // Attempt to listen on the port used above. - s.UpdateConfig(func(cfg *model.Config) { + s.platform.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = listener.Addr().String() }) @@ -168,8 +185,12 @@ func TestStartServerNoS3Bucket(t *testing.T) { s, err := NewServer(func(server *Server) error { configStore, _ := config.NewFileStore("config.json", true) store, _ := config.NewStoreFromBacking(configStore, nil, false) - server.configStore = &configWrapper{srv: server, Store: store} - server.UpdateConfig(func(cfg *model.Config) { + var err error + server.platform, err = platform.New(platform.ServiceConfig{ + ConfigStore: store, + }) + require.NoError(t, err) + server.platform.UpdateConfig(func(cfg *model.Config) { cfg.FileSettings = model.FileSettings{ DriverName: model.NewString(model.ImageDriverS3), AmazonS3AccessKeyId: model.NewString(model.MinioAccessKey), @@ -393,7 +414,7 @@ func TestPanicLog(t *testing.T) { }) testDir, _ := fileutils.FindDir("tests") - s.UpdateConfig(func(cfg *model.Config) { + s.platform.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = ":0" *cfg.ServiceSettings.ConnectionSecurity = "TLS" *cfg.ServiceSettings.TLSKeyFile = path.Join(testDir, "tls_test_key.pem") diff --git a/app/status_test.go b/app/status_test.go index e56ef60728..81ef4cebc4 100644 --- a/app/status_test.go +++ b/app/status_test.go @@ -104,7 +104,7 @@ func TestCustomStatusErrors(t *testing.T) { UserStore: &mockUserStore, SessionStore: &mockSessionStore, OAuthStore: &mockOAuthStore, - ConfigFn: th.App.ch.srv.Config, + ConfigFn: th.App.ch.srv.platform.Config, LicenseFn: th.App.ch.srv.License, }) require.NoError(t, err) diff --git a/app/team_test.go b/app/team_test.go index bb69aa762c..3938aaacff 100644 --- a/app/team_test.go +++ b/app/team_test.go @@ -1044,7 +1044,7 @@ func TestLeaveTeamPanic(t *testing.T) { UserStore: &mockUserStore, SessionStore: &mocks.SessionStore{}, OAuthStore: &mocks.OAuthStore{}, - ConfigFn: th.App.ch.srv.Config, + ConfigFn: th.App.ch.srv.platform.Config, LicenseFn: th.App.ch.srv.License, }) require.NoError(t, err) @@ -1088,7 +1088,7 @@ func TestLeaveTeamPanic(t *testing.T) { GroupStore: &mocks.GroupStore{}, Users: th.App.ch.srv.userService, WebHub: th.App.ch.srv, - ConfigFn: th.App.ch.srv.Config, + ConfigFn: th.App.ch.srv.platform.Config, LicenseFn: th.App.ch.srv.License, }) require.NoError(t, err) diff --git a/app/user_test.go b/app/user_test.go index 4d545f894f..75d5fa80f3 100644 --- a/app/user_test.go +++ b/app/user_test.go @@ -1712,7 +1712,7 @@ func TestUpdateThreadReadForUser(t *testing.T) { UserStore: &mockUserStore, SessionStore: &storemocks.SessionStore{}, OAuthStore: &storemocks.OAuthStore{}, - ConfigFn: th.App.ch.srv.Config, + ConfigFn: th.App.ch.srv.platform.Config, LicenseFn: th.App.ch.srv.License, }) require.NoError(t, err) @@ -1749,8 +1749,8 @@ func TestCreateUserWithInitialPreferences(t *testing.T) { }) t.Run("successfully create a user with insights feature flag disabled", func(t *testing.T) { - th.Server.configStore.SetReadOnlyFF(false) - defer th.Server.configStore.SetReadOnlyFF(true) + th.Server.platform.SetConfigReadOnlyFF(false) + defer th.Server.platform.SetConfigReadOnlyFF(true) th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.InsightsEnabled = false }) defer th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.InsightsEnabled = true }) testUser := th.CreateUser() @@ -1769,8 +1769,8 @@ func TestCreateUserWithInitialPreferences(t *testing.T) { }) t.Run("successfully create a guest user with initial tutorial, insights and recommended steps preferences", func(t *testing.T) { - th.Server.configStore.SetReadOnlyFF(false) - defer th.Server.configStore.SetReadOnlyFF(true) + th.Server.platform.SetConfigReadOnlyFF(false) + defer th.Server.platform.SetConfigReadOnlyFF(true) th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.InsightsEnabled = true }) testUser := th.CreateGuest() defer th.App.PermanentDeleteUser(th.Context, testUser) diff --git a/app/web_hub_test.go b/app/web_hub_test.go index 10dd13c695..ee14d43fd2 100644 --- a/app/web_hub_test.go +++ b/app/web_hub_test.go @@ -167,7 +167,7 @@ func TestHubSessionRevokeRace(t *testing.T) { UserStore: &mockUserStore, SessionStore: &mockSessionStore, OAuthStore: &mockOAuthStore, - ConfigFn: th.App.ch.srv.Config, + ConfigFn: th.App.ch.srv.platform.Config, Metrics: th.App.Metrics(), Cluster: th.App.Cluster(), LicenseFn: th.App.ch.srv.License, diff --git a/cmd/mattermost/commands/init.go b/cmd/mattermost/commands/init.go index a4c58285fa..dfcb54b2ed 100644 --- a/cmd/mattermost/commands/init.go +++ b/cmd/mattermost/commands/init.go @@ -42,9 +42,10 @@ func initDBCommandContext(configDSN string, readOnlyConfigStore bool) (*app.App, model.AppErrorInit(i18n.T) s, err := app.NewServer( + // The option order is important as app.Config option reads app.StartMetrics option. + app.StartMetrics, app.Config(configDSN, readOnlyConfigStore, nil), app.StartSearchEngine, - app.StartMetrics, ) if err != nil { return nil, err diff --git a/cmd/mattermost/commands/server.go b/cmd/mattermost/commands/server.go index 3d0c430255..20309b31be 100644 --- a/cmd/mattermost/commands/server.go +++ b/cmd/mattermost/commands/server.go @@ -65,11 +65,12 @@ func runServer(configStore *config.Store, interruptChan chan os.Signal) error { debug.SetTraceback("crash") options := []app.Option{ + // The option order is important as app.Config option reads app.StartMetrics option. + app.StartMetrics, app.ConfigStore(configStore), app.RunEssentialJobs, app.JoinCluster, app.StartSearchEngine, - app.StartMetrics, } server, err := app.NewServer(options...) if err != nil { diff --git a/web/web_test.go b/web/web_test.go index 8c79c4cd33..d31503560e 100644 --- a/web/web_test.go +++ b/web/web_test.go @@ -102,16 +102,17 @@ func setupTestHelper(tb testing.TB, includeCacheLayer bool) *TestHelper { } } + a := app.New(app.ServerConnector(s.Channels())) prevListenAddress := *s.Config().ServiceSettings.ListenAddress - s.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = ":0" }) + a.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = ":0" }) serverErr := s.Start() if serverErr != nil { panic(serverErr) } - s.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = prevListenAddress }) + a.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = prevListenAddress }) // Disable strict password requirements for test - s.UpdateConfig(func(cfg *model.Config) { + a.UpdateConfig(func(cfg *model.Config) { *cfg.PasswordSettings.MinimumLength = 5 *cfg.PasswordSettings.Lowercase = false *cfg.PasswordSettings.Uppercase = false @@ -119,15 +120,13 @@ func setupTestHelper(tb testing.TB, includeCacheLayer bool) *TestHelper { *cfg.PasswordSettings.Number = false }) - a := app.New(app.ServerConnector(s.Channels())) - web := New(s) URL = fmt.Sprintf("http://localhost:%v", s.ListenAddr.Port) apiClient = model.NewAPIv4Client(URL) s.Store.MarkSystemRanUnitTests() - s.UpdateConfig(func(cfg *model.Config) { + a.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.EnableOpenServer = true })