Этот коммит содержится в:
Ben Schumacher
2025-01-13 20:23:09 +01:00
коммит произвёл GitHub
родитель 091d1bba8b
Коммит 8d4bf4bae0
50 изменённых файлов: 2429 добавлений и 745 удалений

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

@@ -21,6 +21,7 @@ import (
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/public/utils"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/mattermost/mattermost/server/v8/config"
@@ -40,6 +41,23 @@ func (ps *PlatformService) Config() *model.Config {
return ps.configStore.Get()
}
// getSanitizedConfig gets the configuration without any secrets.
func (ps *PlatformService) getSanitizedConfig(rctx request.CTX) *model.Config {
cfg := ps.Config().Clone()
manifests, err := ps.getPluginManifests()
if err != nil {
// getPluginManifests might error, e.g. when plugins are disabled.
// Sanitize all plugin settings in this case.
rctx.Logger().Warn("Failed to get plugin manifests for config sanitization. Will sanitize all plugin settings.", mlog.Err(err))
cfg.Sanitize(nil)
} else {
cfg.Sanitize(manifests)
}
return cfg
}
// 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.

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

@@ -20,6 +20,12 @@ func RegisterElasticsearchInterface(f func(*PlatformService) searchengine.Search
elasticsearchInterface = f
}
var ldapDiagnosticInterface func(*PlatformService) einterfaces.LdapDiagnosticInterface
func RegisterLdapDiagnosticInterface(f func(*PlatformService) einterfaces.LdapDiagnosticInterface) {
ldapDiagnosticInterface = f
}
var licenseInterface func(*PlatformService) einterfaces.LicenseInterface
func RegisterLicenseInterface(f func(*PlatformService) einterfaces.LicenseInterface) {

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

@@ -10,6 +10,7 @@ import (
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/request"
@@ -143,7 +144,8 @@ func setupTestHelper(dbStore store.Store, enterprise bool, includeCacheLayer boo
*memoryConfig.MetricsSettings.Enable = true
*memoryConfig.ServiceSettings.ListenAddress = "localhost:0"
*memoryConfig.MetricsSettings.ListenAddress = "localhost:0"
configStore.Set(memoryConfig)
_, _, err = configStore.Set(memoryConfig)
require.NoError(tb, err)
options = append(options, ConfigStore(configStore))
@@ -152,7 +154,7 @@ func setupTestHelper(dbStore store.Store, enterprise bool, includeCacheLayer boo
Store: dbStore,
}, options...)
if err != nil {
panic(err)
require.NoError(tb, err)
}
th := &TestHelper{

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

@@ -9,6 +9,7 @@ import (
"io"
"net/http"
"os"
"path"
"time"
"github.com/pkg/errors"
@@ -16,6 +17,7 @@ import (
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/public/utils"
"github.com/mattermost/mattermost/server/v8/config"
)
@@ -246,6 +248,44 @@ func (ps *PlatformService) GetNotificationLogFile(_ request.CTX) (*model.FileDat
}, nil
}
func (ps *PlatformService) GetAdvancedLogs(_ request.CTX) ([]*model.FileData, error) {
advancedLoggingJSON := ps.Config().LogSettings.AdvancedLoggingJSON
if utils.IsEmptyJSON(advancedLoggingJSON) {
return nil, nil
}
cfg := make(mlog.LoggerConfiguration)
err := json.Unmarshal(advancedLoggingJSON, &cfg)
if err != nil {
return nil, errors.Wrap(err, "invalid advanced logging configuration")
}
var ret []*model.FileData
for _, t := range cfg {
if t.Type != "file" {
continue
}
var fileOption struct {
Filename string `json:"filename"`
}
if err := json.Unmarshal(t.Options, &fileOption); err != nil {
return nil, errors.Wrap(err, "error decoding file target options")
}
data, err := os.ReadFile(fileOption.Filename)
if err != nil {
return nil, errors.Wrapf(err, "failed to read notifcation log file at path %s", fileOption.Filename)
}
fileName := path.Base(fileOption.Filename)
ret = append(ret, &model.FileData{
Filename: fileName,
Body: data,
})
}
return ret, nil
}
func isLogFilteredByLevel(logFilter *model.LogFilter, entry *model.LogEntry) bool {
logLevels := logFilter.LogLevels
if len(logLevels) == 0 {

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

@@ -4,10 +4,15 @@
package platform
import (
"bytes"
"encoding/json"
"os"
"path"
"testing"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/v8/channels/testlib"
"github.com/mattermost/mattermost/server/v8/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -104,3 +109,89 @@ func TestGetNotificationLogFile(t *testing.T) {
assert.Equal(t, "notifications.log", fileData.Filename)
assert.Positive(t, len(fileData.Body))
}
func TestGetAdvancedLogs(t *testing.T) {
th := Setup(t)
defer th.TearDown()
t.Run("log messanges from std and LDAP level get returned", func(t *testing.T) {
dir, err := os.MkdirTemp("", "logs")
require.NoError(t, err)
t.Cleanup(func() {
err = os.RemoveAll(dir)
require.NoError(t, err)
})
optLDAP := map[string]string{
"filename": path.Join(dir, "ldap.log"),
}
dataLDAP, err := json.Marshal(optLDAP)
require.NoError(t, err)
optStd := map[string]string{
"filename": path.Join(dir, "std.log"),
}
dataStd, err := json.Marshal(optStd)
require.NoError(t, err)
cfg := mlog.LoggerConfiguration{
"ldap-file": mlog.TargetCfg{
Type: "file",
Format: "json",
Levels: []mlog.Level{
mlog.LvlLDAPError,
mlog.LvlLDAPWarn,
mlog.LvlLDAPInfo,
mlog.LvlLDAPDebug,
},
Options: dataLDAP,
},
"std": mlog.TargetCfg{
Type: "file",
Format: "json",
Levels: []mlog.Level{
mlog.LvlError,
},
Options: dataStd,
},
}
cfgData, err := json.Marshal(cfg)
require.NoError(t, err)
th.Service.UpdateConfig(func(c *model.Config) {
c.LogSettings.AdvancedLoggingJSON = cfgData
})
th.Service.Logger().LogM([]mlog.Level{mlog.LvlLDAPInfo}, "Some LDAP info")
th.Service.Logger().Error("Some Error")
err = th.Service.Logger().Flush()
require.NoError(t, err)
fileDatas, err := th.Service.GetAdvancedLogs(th.Context)
require.NoError(t, err)
require.Len(t, fileDatas, 2)
// Check the order of the log files
var ldapIndex = 0
var stdIndex = 1
if fileDatas[1].Filename == "ldap.log" {
ldapIndex = 1
stdIndex = 0
}
assert.Equal(t, "ldap.log", fileDatas[ldapIndex].Filename)
testlib.AssertLog(t, bytes.NewBuffer(fileDatas[ldapIndex].Body), mlog.LvlLDAPInfo.Name, "Some LDAP info")
assert.Equal(t, "std.log", fileDatas[stdIndex].Filename)
testlib.AssertLog(t, bytes.NewBuffer(fileDatas[stdIndex].Body), mlog.LvlError.Name, "Some Error")
})
// Disable AdvancedLoggingJSON
th.Service.UpdateConfig(func(c *model.Config) {
c.LogSettings.AdvancedLoggingJSON = nil
})
t.Run("No logs returned when AdvancedLoggingJSON is empty", func(t *testing.T) {
// Confirm no logs get returned
fileDatas, err := th.Service.GetAdvancedLogs(th.Context)
require.NoError(t, err)
require.Len(t, fileDatas, 0)
})
}

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

@@ -4,7 +4,6 @@
package platform
import (
"bytes"
"context"
"fmt"
"net"
@@ -12,7 +11,6 @@ import (
"net/http/pprof"
"path"
"runtime"
rpprof "runtime/pprof"
"strings"
"sync"
"text/template"
@@ -25,7 +23,6 @@ import (
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/utils"
"github.com/mattermost/mattermost/server/v8/einterfaces"
)
@@ -259,52 +256,3 @@ func (ps *PlatformService) Metrics() einterfaces.MetricsInterface {
return ps.metricsIFace
}
func (ps *PlatformService) CreateCPUProfile(_ request.CTX) (*model.FileData, error) {
var b bytes.Buffer
err := rpprof.StartCPUProfile(&b)
if err != nil {
return nil, errors.Wrap(err, "failed to start CPU profile")
}
time.Sleep(cpuProfileDuration)
rpprof.StopCPUProfile()
fileData := &model.FileData{
Filename: "cpu.prof",
Body: b.Bytes(),
}
return fileData, nil
}
func (ps *PlatformService) CreateHeapProfile(_ request.CTX) (*model.FileData, error) {
var b bytes.Buffer
err := rpprof.Lookup("heap").WriteTo(&b, 0)
if err != nil {
return nil, errors.Wrap(err, "failed to lookup heap profile")
}
fileData := &model.FileData{
Filename: "heap.prof",
Body: b.Bytes(),
}
return fileData, nil
}
func (ps *PlatformService) CreateGoroutineProfile(_ request.CTX) (*model.FileData, error) {
var b bytes.Buffer
err := rpprof.Lookup("goroutine").WriteTo(&b, 2)
if err != nil {
return nil, errors.Wrap(err, "failed to lookup goroutine profile")
}
fileData := &model.FileData{
Filename: "goroutines",
Body: b.Bytes(),
}
return fileData, nil
}

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

@@ -5,10 +5,12 @@ package platform
import (
"crypto/ecdsa"
"errors"
"fmt"
"hash/maphash"
"net/http"
"runtime"
"strconv"
"sync"
"sync/atomic"
"time"
@@ -88,6 +90,8 @@ type PlatformService struct {
searchConfigListenerId string
searchLicenseListenerId string
ldapDiagnostic einterfaces.LdapDiagnosticInterface
Jobs *jobs.JobServer
hubs []*Hub
@@ -459,6 +463,10 @@ func (ps *PlatformService) initEnterprise() {
ps.SearchEngine.RegisterElasticsearchEngine(elasticsearchInterface(ps))
}
if ldapDiagnosticInterface != nil {
ps.ldapDiagnostic = ldapDiagnosticInterface(ps)
}
if licenseInterface != nil {
ps.licenseManager = licenseInterface(ps)
}
@@ -547,6 +555,29 @@ func (ps *PlatformService) GetPluginStatuses() (model.PluginStatuses, *model.App
return pluginStatuses, nil
}
func (ps *PlatformService) getPluginManifests() ([]*model.Manifest, error) {
if ps.pluginEnv == nil {
return nil, errors.New("plugin environment not initialized")
}
pluginsEnvironment := ps.pluginEnv.GetPluginsEnvironment()
if pluginsEnvironment == nil {
return nil, model.NewAppError("getPluginManifests", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
}
plugins, err := pluginsEnvironment.Available()
if err != nil {
return nil, fmt.Errorf("failed to get list of available plugins: %w", err)
}
manifests := make([]*model.Manifest, len(plugins))
for i := range plugins {
manifests[i] = plugins[i].Manifest
}
return manifests, nil
}
func (ps *PlatformService) FileBackend() filestore.FileBackend {
return ps.filestore
}
@@ -554,3 +585,17 @@ func (ps *PlatformService) FileBackend() filestore.FileBackend {
func (ps *PlatformService) ExportFileBackend() filestore.FileBackend {
return ps.exportFilestore
}
func (ps *PlatformService) LdapDiagnostic() einterfaces.LdapDiagnosticInterface {
return ps.ldapDiagnostic
}
// DatabaseTypeAndSchemaVersion returns the Database type (postgres or mysql) and current version of the schema
func (ps *PlatformService) DatabaseTypeAndSchemaVersion() (string, string, error) {
schemaVersion, err := ps.Store.GetDBSchemaVersion()
if err != nil {
return "", "", err
}
return model.SafeDereference(ps.Config().SqlSettings.DriverName), strconv.Itoa(schemaVersion), nil
}

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

@@ -7,11 +7,13 @@ import (
"math/rand"
"net/http"
"os"
"strconv"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
@@ -231,3 +233,20 @@ func TestSetTelemetryId(t *testing.T) {
require.Equal(t, clientConfig["DiagnosticId"], id)
})
}
func TestDatabaseTypeAndMattermostVersion(t *testing.T) {
th := Setup(t)
defer th.TearDown()
databaseType, schemaVersion, err := th.Service.DatabaseTypeAndSchemaVersion()
require.NoError(t, err)
if *th.Service.Config().SqlSettings.DriverName == model.DatabaseDriverPostgres {
assert.Equal(t, "postgres", databaseType)
} else {
assert.Equal(t, "mysql", databaseType)
}
// It's hard to check wheather the schema version is correct or not.
// So, we just check if it's greater than 1.
assert.GreaterOrEqual(t, schemaVersion, strconv.Itoa(1))
}

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

@@ -0,0 +1,259 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"bytes"
"encoding/json"
"os"
"runtime"
rpprof "runtime/pprof"
"time"
"github.com/hashicorp/go-multierror"
"github.com/pkg/errors"
"gopkg.in/yaml.v3"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
)
const (
envVarInstallType = "MM_INSTALL_TYPE"
unknownDataPoint = "unknown"
)
func (ps *PlatformService) GenerateSupportPacket(rctx request.CTX, options *model.SupportPacketOptions) ([]model.FileData, error) {
functions := map[string]func(request.CTX) (*model.FileData, error){
"diagnostics": ps.getSupportPacketDiagnostics,
"config": ps.getSanitizedConfigFile,
"cpu profile": ps.getCPUProfile,
"heap profile": ps.getHeapProfile,
"goroutines": ps.getGoroutineProfile,
}
if options != nil && options.IncludeLogs {
functions["mattermost log"] = ps.GetLogFile
functions["notification log"] = ps.GetNotificationLogFile
}
var (
fileDatas []model.FileData
rErr *multierror.Error
)
for name, fn := range functions {
fileData, err := fn(rctx)
if err != nil {
rctx.Logger().Error("Failed to generate file for Support Packet",
mlog.String("file", name),
mlog.Err(err),
)
rErr = multierror.Append(rErr, err)
}
if fileData != nil {
fileDatas = append(fileDatas, *fileData)
}
}
if options != nil && options.IncludeLogs {
advancedLogs, err := ps.GetAdvancedLogs(rctx)
if err != nil {
rctx.Logger().Error("Failed to read advanced log files for Support Packet", mlog.Err(err))
rErr = multierror.Append(rErr, err)
}
for _, log := range advancedLogs {
fileDatas = append(fileDatas, *log)
}
}
return fileDatas, rErr.ErrorOrNil()
}
func (ps *PlatformService) getSupportPacketDiagnostics(rctx request.CTX) (*model.FileData, error) {
var (
rErr *multierror.Error
err error
d model.SupportPacketDiagnostics
)
d.Version = model.CurrentSupportPacketVersion
/* License */
if license := ps.License(); license != nil {
d.License.Company = license.Customer.Company
d.License.Users = model.SafeDereference(license.Features.Users)
d.License.SkuShortName = license.SkuShortName
d.License.IsTrial = license.IsTrial
d.License.IsGovSKU = license.IsGovSku
}
/* Server */
d.Server.OS = runtime.GOOS
d.Server.Architecture = runtime.GOARCH
d.Server.Hostname, err = os.Hostname()
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "error while getting hostname"))
}
d.Server.Version = model.CurrentVersion
d.Server.BuildHash = model.BuildHash
installationType := os.Getenv(envVarInstallType)
if installationType == "" {
installationType = unknownDataPoint
}
d.Server.InstallationType = installationType
/* Config */
d.Config.Source = ps.DescribeConfig()
/* DB */
d.Database.Type, d.Database.SchemaVersion, err = ps.DatabaseTypeAndSchemaVersion()
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "error while getting DB type and schema version"))
}
databaseVersion, err := ps.Store.GetDbVersion(false)
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "error while getting DB version"))
} else {
d.Database.Version = databaseVersion
}
d.Database.MasterConnectios = ps.Store.TotalMasterDbConnections()
d.Database.ReplicaConnectios = ps.Store.TotalReadDbConnections()
d.Database.SearchConnections = ps.Store.TotalSearchDbConnections()
/* File store */
d.FileStore.Status = model.StatusOk
err = ps.FileBackend().TestConnection()
if err != nil {
d.FileStore.Status = model.StatusFail
d.FileStore.Error = err.Error()
}
d.FileStore.Driver = ps.FileBackend().DriverName()
/* Websockets */
d.Websocket.Connections = ps.TotalWebsocketConnections()
/* Cluster */
if cluster := ps.Cluster(); cluster != nil {
d.Cluster.ID = cluster.GetClusterId()
clusterInfo := cluster.GetClusterInfos()
d.Cluster.NumberOfNodes = len(clusterInfo)
}
/* LDAP */
if ldap := ps.LdapDiagnostic(); ldap != nil && (*ps.Config().LdapSettings.Enable || *ps.Config().LdapSettings.EnableSync) {
d.LDAP.Status = model.StatusOk
appErr := ldap.RunTest(rctx)
if appErr != nil {
d.LDAP.Status = model.StatusFail
d.LDAP.Error = appErr.Error()
}
severName, serverVersion := unknownDataPoint, unknownDataPoint
// Only if the LDAP test was successful, try to get the LDAP server info
if d.LDAP.Status == model.StatusOk {
severName, serverVersion, err = ldap.GetVendorNameAndVendorVersion(rctx)
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "error while getting LDAP vendor info"))
}
if severName == "" {
severName = unknownDataPoint
}
if serverVersion == "" {
serverVersion = unknownDataPoint
}
}
d.LDAP.ServerName = severName
d.LDAP.ServerVersion = serverVersion
}
/* Elastic Search */
if se := ps.SearchEngine.ElasticsearchEngine; se != nil {
d.ElasticSearch.ServerVersion = se.GetFullVersion()
d.ElasticSearch.ServerPlugins = se.GetPlugins()
}
b, err := yaml.Marshal(&d)
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "failed to marshal Support Packet into yaml"))
}
fileData := &model.FileData{
Filename: "diagnostics.yaml",
Body: b,
}
return fileData, rErr.ErrorOrNil()
}
func (ps *PlatformService) getSanitizedConfigFile(rctx request.CTX) (*model.FileData, error) {
config := ps.getSanitizedConfig(rctx)
spConfig := model.SupportPacketConfig{
Config: config,
FeatureFlags: *config.FeatureFlags,
}
sanitizedConfigPrettyJSON, err := json.MarshalIndent(spConfig, "", " ")
if err != nil {
return nil, errors.Wrap(err, "failed to sanitized config into json")
}
fileData := &model.FileData{
Filename: "sanitized_config.json",
Body: sanitizedConfigPrettyJSON,
}
return fileData, nil
}
func (ps *PlatformService) getCPUProfile(_ request.CTX) (*model.FileData, error) {
var b bytes.Buffer
err := rpprof.StartCPUProfile(&b)
if err != nil {
return nil, errors.Wrap(err, "failed to start CPU profile")
}
time.Sleep(cpuProfileDuration)
rpprof.StopCPUProfile()
fileData := &model.FileData{
Filename: "cpu.prof",
Body: b.Bytes(),
}
return fileData, nil
}
func (ps *PlatformService) getHeapProfile(_ request.CTX) (*model.FileData, error) {
var b bytes.Buffer
err := rpprof.Lookup("heap").WriteTo(&b, 0)
if err != nil {
return nil, errors.Wrap(err, "failed to lookup heap profile")
}
fileData := &model.FileData{
Filename: "heap.prof",
Body: b.Bytes(),
}
return fileData, nil
}
func (ps *PlatformService) getGoroutineProfile(_ request.CTX) (*model.FileData, error) {
var b bytes.Buffer
err := rpprof.Lookup("goroutine").WriteTo(&b, 2)
if err != nil {
return nil, errors.Wrap(err, "failed to lookup goroutine profile")
}
fileData := &model.FileData{
Filename: "goroutines",
Body: b.Bytes(),
}
return fileData, nil
}

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

@@ -0,0 +1,398 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"bytes"
"encoding/json"
"errors"
"os"
"path"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/v8/channels/testlib"
"github.com/mattermost/mattermost/server/v8/config"
emocks "github.com/mattermost/mattermost/server/v8/einterfaces/mocks"
fmocks "github.com/mattermost/mattermost/server/v8/platform/shared/filestore/mocks"
)
func TestGenerateSupportPacket(t *testing.T) {
th := Setup(t)
defer th.TearDown()
dir, err := os.MkdirTemp("", "")
require.NoError(t, err)
t.Cleanup(func() {
err = os.RemoveAll(dir)
assert.NoError(t, err)
})
th.Service.UpdateConfig(func(cfg *model.Config) {
*cfg.LogSettings.FileLocation = dir
*cfg.NotificationLogSettings.FileLocation = dir
})
logLocation := config.GetLogFileLocation(dir)
notificationsLogLocation := config.GetNotificationsLogFileLocation(dir)
genMockLogFiles := func() {
d1 := []byte("hello\ngo\n")
genErr := os.WriteFile(logLocation, d1, 0600)
require.NoError(t, genErr)
genErr = os.WriteFile(notificationsLogLocation, d1, 0600)
require.NoError(t, genErr)
}
genMockLogFiles()
getFileNames := func(t *testing.T, fileDatas []model.FileData) []string {
var rFileNames []string
for _, fileData := range fileDatas {
require.NotNil(t, fileData)
assert.Positive(t, len(fileData.Body))
rFileNames = append(rFileNames, fileData.Filename)
}
return rFileNames
}
expectedFileNames := []string{
"diagnostics.yaml",
"sanitized_config.json",
"cpu.prof",
"heap.prof",
"goroutines",
}
expectedFileNamesWithLogs := append(expectedFileNames, []string{
"mattermost.log",
"notifications.log",
}...)
var fileDatas []model.FileData
t.Run("generate Support Packet with logs", func(t *testing.T) {
fileDatas, err = th.Service.GenerateSupportPacket(th.Context, &model.SupportPacketOptions{
IncludeLogs: true,
})
require.NoError(t, err)
rFileNames := getFileNames(t, fileDatas)
assert.ElementsMatch(t, expectedFileNamesWithLogs, rFileNames)
})
t.Run("generate Support Packet without logs", func(t *testing.T) {
fileDatas, err = th.Service.GenerateSupportPacket(th.Context, &model.SupportPacketOptions{
IncludeLogs: false,
})
require.NoError(t, err)
rFileNames := getFileNames(t, fileDatas)
assert.ElementsMatch(t, expectedFileNames, rFileNames)
})
t.Run("remove the log files and ensure that an error is returned", func(t *testing.T) {
err = os.Remove(logLocation)
require.NoError(t, err)
err = os.Remove(notificationsLogLocation)
require.NoError(t, err)
t.Cleanup(genMockLogFiles)
fileDatas, err = th.Service.GenerateSupportPacket(th.Context, &model.SupportPacketOptions{
IncludeLogs: true,
})
require.Error(t, err)
assert.Contains(t, err.Error(), "failed read mattermost log file")
rFileNames := getFileNames(t, fileDatas)
assert.ElementsMatch(t, expectedFileNames, rFileNames)
})
t.Run("with advanced logs", func(t *testing.T) {
optLDAP := map[string]string{
"filename": path.Join(dir, "ldap.log"),
}
dataLDAP, err := json.Marshal(optLDAP)
require.NoError(t, err)
cfg := mlog.LoggerConfiguration{
"ldap-file": mlog.TargetCfg{
Type: "file",
Format: "json",
Levels: []mlog.Level{
mlog.LvlLDAPError,
mlog.LvlLDAPWarn,
mlog.LvlLDAPInfo,
mlog.LvlLDAPDebug,
},
Options: dataLDAP,
},
}
cfgData, err := json.Marshal(cfg)
require.NoError(t, err)
th.Service.UpdateConfig(func(c *model.Config) {
c.LogSettings.AdvancedLoggingJSON = cfgData
})
th.Service.Logger().LogM([]mlog.Level{mlog.LvlLDAPInfo}, "Some LDAP info")
err = th.Service.Logger().Flush()
require.NoError(t, err)
fileDatas, err = th.Service.GenerateSupportPacket(th.Context, &model.SupportPacketOptions{
IncludeLogs: true,
})
require.NoError(t, err)
rFileNames := getFileNames(t, fileDatas)
assert.ElementsMatch(t, append(expectedFileNamesWithLogs, "ldap.log"), rFileNames)
found := false
for _, fileData := range fileDatas {
if fileData.Filename == "ldap.log" {
testlib.AssertLog(t, bytes.NewBuffer(fileData.Body), mlog.LvlLDAPInfo.Name, "Some LDAP info")
found = true
break
}
}
assert.True(t, found)
})
}
func TestGetSupportPacketDiagnostics(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
t.Setenv(envVarInstallType, "docker")
licenseUsers := 100
license := model.NewTestLicense("ldap")
license.SkuShortName = model.LicenseShortSkuEnterprise
license.Features.Users = model.NewPointer(licenseUsers)
ok := th.Service.SetLicense(license)
require.True(t, ok)
getDiagnostics := func(t *testing.T) *model.SupportPacketDiagnostics {
t.Helper()
fileData, err := th.Service.getSupportPacketDiagnostics(th.Context)
require.NotNil(t, fileData)
assert.Equal(t, "diagnostics.yaml", fileData.Filename)
assert.Positive(t, len(fileData.Body))
assert.NoError(t, err)
var d model.SupportPacketDiagnostics
require.NoError(t, yaml.Unmarshal(fileData.Body, &d))
return &d
}
t.Run("Happy path", func(t *testing.T) {
d := getDiagnostics(t)
assert.Equal(t, 1, d.Version)
/* License */
assert.Equal(t, "My awesome Company", d.License.Company)
assert.Equal(t, licenseUsers, d.License.Users)
assert.Equal(t, model.LicenseShortSkuEnterprise, d.License.SkuShortName)
assert.Equal(t, false, d.License.IsTrial)
assert.Equal(t, false, d.License.IsGovSKU)
/* Server information */
assert.NotEmpty(t, d.Server.OS)
assert.NotEmpty(t, d.Server.Architecture)
assert.NotEmpty(t, d.Server.Hostname)
assert.Equal(t, model.CurrentVersion, d.Server.Version)
// BuildHash is not present in tests
assert.Equal(t, "docker", d.Server.InstallationType)
/* Config */
assert.Equal(t, "memory://", d.Config.Source)
/* DB */
assert.NotEmpty(t, d.Database.Type)
assert.NotEmpty(t, d.Database.Version)
assert.NotEmpty(t, d.Database.SchemaVersion)
assert.NotZero(t, d.Database.MasterConnectios)
assert.Zero(t, d.Database.ReplicaConnectios)
assert.Zero(t, d.Database.SearchConnections)
/* File store */
assert.Equal(t, "OK", d.FileStore.Status)
assert.Empty(t, d.FileStore.Error)
assert.Equal(t, "local", d.FileStore.Driver)
/* Websockets */
assert.Zero(t, d.Websocket.Connections)
/* Cluster */
assert.Empty(t, d.Cluster.ID)
assert.Zero(t, d.Cluster.NumberOfNodes)
/* LDAP */
assert.Empty(t, d.LDAP.Status)
assert.Empty(t, d.LDAP.Error)
assert.Empty(t, d.LDAP.ServerName)
assert.Empty(t, d.LDAP.ServerVersion)
/* Elastic Search */
assert.Empty(t, d.ElasticSearch.ServerVersion)
assert.Empty(t, d.ElasticSearch.ServerPlugins)
})
t.Run("filestore fails", func(t *testing.T) {
fb := &fmocks.FileBackend{}
err := SetFileStore(fb)(th.Service)
require.NoError(t, err)
fb.On("DriverName").Return("mock")
fb.On("TestConnection").Return(errors.New("all broken"))
packet := getDiagnostics(t)
assert.Equal(t, "FAIL", packet.FileStore.Status)
assert.Equal(t, "all broken", packet.FileStore.Error)
assert.Equal(t, "mock", packet.FileStore.Driver)
})
t.Run("no LDAP info if LDAP sync is disabled", func(t *testing.T) {
ldapMock := &emocks.LdapDiagnosticInterface{}
th.Service.ldapDiagnostic = ldapMock
packet := getDiagnostics(t)
assert.Equal(t, "", packet.LDAP.ServerName)
assert.Equal(t, "", packet.LDAP.ServerVersion)
})
th.Service.UpdateConfig(func(cfg *model.Config) {
cfg.LdapSettings.EnableSync = model.NewPointer(true)
})
t.Run("no LDAP vendor info found", func(t *testing.T) {
ldapMock := &emocks.LdapDiagnosticInterface{}
ldapMock.On(
"GetVendorNameAndVendorVersion",
mock.AnythingOfType("*request.Context"),
).Return("", "", nil)
ldapMock.On(
"RunTest",
mock.AnythingOfType("*request.Context"),
).Return(nil)
th.Service.ldapDiagnostic = ldapMock
packet := getDiagnostics(t)
assert.Equal(t, "OK", packet.LDAP.Status)
assert.Empty(t, packet.LDAP.Error)
assert.Equal(t, "unknown", packet.LDAP.ServerName)
assert.Equal(t, "unknown", packet.LDAP.ServerVersion)
})
t.Run("found LDAP vendor info", func(t *testing.T) {
ldapMock := &emocks.LdapDiagnosticInterface{}
ldapMock.On(
"GetVendorNameAndVendorVersion",
mock.AnythingOfType("*request.Context"),
).Return("some vendor", "v1.0.0", nil)
ldapMock.On(
"RunTest",
mock.AnythingOfType("*request.Context"),
).Return(nil)
th.Service.ldapDiagnostic = ldapMock
packet := getDiagnostics(t)
assert.Equal(t, "OK", packet.LDAP.Status)
assert.Empty(t, packet.LDAP.Error)
assert.Equal(t, "some vendor", packet.LDAP.ServerName)
assert.Equal(t, "v1.0.0", packet.LDAP.ServerVersion)
})
t.Run("LDAP test fails", func(t *testing.T) {
ldapMock := &emocks.LdapDiagnosticInterface{}
ldapMock.On(
"GetVendorNameAndVendorVersion",
mock.AnythingOfType("*request.Context"),
).Return("some vendor", "v1.0.0", nil)
ldapMock.On(
"RunTest",
mock.AnythingOfType("*request.Context"),
).Return(model.NewAppError("", "some error", nil, "", 0))
th.Service.ldapDiagnostic = ldapMock
packet := getDiagnostics(t)
assert.Equal(t, "FAIL", packet.LDAP.Status)
assert.Equal(t, "some error", packet.LDAP.Error)
assert.Equal(t, "unknown", packet.LDAP.ServerName)
assert.Equal(t, "unknown", packet.LDAP.ServerVersion)
})
}
func TestGetSanitizedConfigFile(t *testing.T) {
t.Setenv("MM_FEATUREFLAGS_TestFeature", "true")
th := Setup(t)
defer th.TearDown()
th.Service.UpdateConfig(func(cfg *model.Config) {
cfg.ServiceSettings.AllowedUntrustedInternalConnections = model.NewPointer("example.com")
})
// Happy path where we have a sanitized config file with no err
fileData, err := th.Service.getSanitizedConfigFile(th.Context)
require.NotNil(t, fileData)
assert.Equal(t, "sanitized_config.json", fileData.Filename)
assert.Positive(t, len(fileData.Body))
assert.NoError(t, err)
var config model.Config
err = json.Unmarshal(fileData.Body, &config)
require.NoError(t, err)
// Ensure sensitive fields are redacted
assert.Equal(t, model.FakeSetting, *config.SqlSettings.DataSource)
// Ensure non-sensitive fields are present
assert.Equal(t, "example.com", *config.ServiceSettings.AllowedUntrustedInternalConnections)
// Ensure feature flags are present
assert.Equal(t, "true", config.FeatureFlags.TestFeature)
}
func TestGetCPUProfile(t *testing.T) {
th := Setup(t)
defer th.TearDown()
fileData, err := th.Service.getCPUProfile(th.Context)
require.NoError(t, err)
assert.Equal(t, "cpu.prof", fileData.Filename)
assert.Positive(t, len(fileData.Body))
}
func TestGetHeapProfile(t *testing.T) {
th := Setup(t)
defer th.TearDown()
fileData, err := th.Service.getHeapProfile(th.Context)
require.NoError(t, err)
assert.Equal(t, "heap.prof", fileData.Filename)
assert.Positive(t, len(fileData.Body))
}
func TestGetGoroutineProfile(t *testing.T) {
th := Setup(t)
defer th.TearDown()
fileData, err := th.Service.getGoroutineProfile(th.Context)
require.NoError(t, err)
assert.Equal(t, "goroutines", fileData.Filename)
assert.Positive(t, len(fileData.Body))
}