mirror of
https://github.com/mattermost/mattermost.git
synced 2026-05-28 04:35:04 -04:00
[MM-54288] Support Packet V2 (#29403)
This commit is contained in:
parent
091d1bba8b
commit
8d4bf4bae0
50 changed files with 2429 additions and 745 deletions
|
|
@ -8,7 +8,7 @@ import (
|
|||
"os"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type DockerCompose struct {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import (
|
|||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost/server/public/utils"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/audit"
|
||||
"github.com/mattermost/mattermost/server/v8/config"
|
||||
"github.com/mattermost/mattermost/server/v8/platform/services/cache"
|
||||
|
|
@ -112,14 +113,10 @@ func generateSupportPacket(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
fileDatas := c.App.GenerateSupportPacket(c.AppContext, supportPacketOptions)
|
||||
|
||||
// Constructing the ZIP file name as per spec (mattermost_support_packet_YYYY-MM-DD-HH-MM.zip)
|
||||
// Note that this filename is also being checked at the webapp, please update the
|
||||
// regex within the commercial_support_modal.tsx file if the naming convention ever changes.
|
||||
now := time.Now()
|
||||
outputZipFilename := fmt.Sprintf("mattermost_support_packet_%s.zip", now.Format("2006-01-02-03-04"))
|
||||
outputZipFilename := supportPacketFileName(now, c.App.License().Customer.Company)
|
||||
|
||||
fileStorageBackend := c.App.FileBackend()
|
||||
|
||||
// We do this incase we get concurrent requests, we will always have a unique directory.
|
||||
// This is to avoid the situation where we try to write to the same directory while we are trying to delete it (further down)
|
||||
outputDirectoryToUse := OutputDirectory + "_" + model.NewId()
|
||||
|
|
@ -147,6 +144,13 @@ func generateSupportPacket(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
web.WriteFileResponse(outputZipFilename, FileMime, 0, now, *c.App.Config().ServiceSettings.WebserverMode, fileBytesReader, true, w, r)
|
||||
}
|
||||
|
||||
// supportPacketFileName returns the ZIP file name in the format mm_support_packet_$CUSTOMER_NAME_YYYY-MM-DDTHH-MM.zip.
|
||||
// Note that this filename is also being checked at the webapp, please update the
|
||||
// regex within the commercial_support_modal.tsx file if the naming convention ever changes.
|
||||
func supportPacketFileName(now time.Time, customerName string) string {
|
||||
return fmt.Sprintf("mm_support_packet_%s_%s.zip", utils.SanitizeFileName(customerName), now.Format("2006-01-02T15-04"))
|
||||
}
|
||||
|
||||
func getSystemPing(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
reqs := c.App.Config().ClientRequirements
|
||||
|
||||
|
|
|
|||
|
|
@ -233,9 +233,14 @@ func TestGenerateSupportPacket(t *testing.T) {
|
|||
th.App.Srv().SetLicense(l)
|
||||
|
||||
th.TestForSystemAdminAndLocal(t, func(t *testing.T, c *model.Client4) {
|
||||
file, _, err := th.SystemAdminClient.GenerateSupportPacket(context.Background())
|
||||
file, filename, _, err := th.SystemAdminClient.GenerateSupportPacket(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.NotZero(t, len(file))
|
||||
|
||||
assert.Contains(t, filename, "mm_support_packet_My_awesome_Company_")
|
||||
|
||||
d, err := io.ReadAll(file)
|
||||
require.NoError(t, err)
|
||||
assert.NotZero(t, len(d))
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -249,20 +254,20 @@ func TestGenerateSupportPacket(t *testing.T) {
|
|||
}()
|
||||
|
||||
th.TestForSystemAdminAndLocal(t, func(t *testing.T, c *model.Client4) {
|
||||
_, resp, err := th.SystemAdminClient.GenerateSupportPacket(context.Background())
|
||||
_, _, resp, err := th.SystemAdminClient.GenerateSupportPacket(context.Background())
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("As a system role, not system admin", func(t *testing.T) {
|
||||
_, resp, err := th.SystemManagerClient.GenerateSupportPacket(context.Background())
|
||||
_, _, resp, err := th.SystemManagerClient.GenerateSupportPacket(context.Background())
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
})
|
||||
|
||||
t.Run("As a Regular User", func(t *testing.T) {
|
||||
_, resp, err := th.Client.GenerateSupportPacket(context.Background())
|
||||
_, _, resp, err := th.Client.GenerateSupportPacket(context.Background())
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
})
|
||||
|
|
@ -271,12 +276,43 @@ func TestGenerateSupportPacket(t *testing.T) {
|
|||
_, err := th.SystemAdminClient.RemoveLicenseFile(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, resp, err := th.SystemAdminClient.GenerateSupportPacket(context.Background())
|
||||
_, _, resp, err := th.SystemAdminClient.GenerateSupportPacket(context.Background())
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSupportPacketFileName(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
now time.Time
|
||||
customerName string
|
||||
expected string
|
||||
}{
|
||||
"standard case": {
|
||||
now: time.Date(2023, 11, 12, 13, 14, 15, 0, time.UTC),
|
||||
customerName: "TestCustomer",
|
||||
expected: "mm_support_packet_TestCustomer_2023-11-12T13-14.zip",
|
||||
},
|
||||
"customer name with special characters": {
|
||||
now: time.Date(2023, 11, 12, 13, 14, 15, 0, time.UTC),
|
||||
customerName: "Test/Customer:Name",
|
||||
expected: "mm_support_packet_Test_Customer_Name_2023-11-12T13-14.zip",
|
||||
},
|
||||
"empty customer name": {
|
||||
now: time.Date(2023, 10, 10, 10, 10, 10, 0, time.UTC),
|
||||
customerName: "",
|
||||
expected: "mm_support_packet__2023-10-10T10-10.zip",
|
||||
},
|
||||
}
|
||||
|
||||
for name, tt := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
result := supportPacketFileName(tt.now, tt.customerName)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSiteURLTest(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
|
|
|||
|
|
@ -87,6 +87,9 @@ func (a *App) SearchEngine() *searchengine.Broker {
|
|||
func (a *App) Ldap() einterfaces.LdapInterface {
|
||||
return a.ch.Ldap
|
||||
}
|
||||
func (a *App) LdapDiagnostic() einterfaces.LdapDiagnosticInterface {
|
||||
return a.ch.srv.platform.LdapDiagnostic()
|
||||
}
|
||||
func (a *App) MessageExport() einterfaces.MessageExportInterface {
|
||||
return a.ch.MessageExport
|
||||
}
|
||||
|
|
|
|||
|
|
@ -582,6 +582,7 @@ type AppIface interface {
|
|||
DeleteReactionForPost(c request.CTX, reaction *model.Reaction) *model.AppError
|
||||
DeleteRemoteCluster(remoteClusterId string) (bool, *model.AppError)
|
||||
DeleteRetentionPolicy(policyID string) *model.AppError
|
||||
DeleteRole(id string) (*model.Role, *model.AppError)
|
||||
DeleteScheduledPost(rctx request.CTX, userId, scheduledPostId, connectionId string) (*model.ScheduledPost, *model.AppError)
|
||||
DeleteScheme(schemeId string) (*model.Scheme, *model.AppError)
|
||||
DeleteSharedChannelRemote(id string) (bool, error)
|
||||
|
|
@ -622,7 +623,7 @@ type AppIface interface {
|
|||
GenerateMfaSecret(userID string) (*model.MfaSecret, *model.AppError)
|
||||
GeneratePresignURLForExport(name string) (*model.PresignURLResponse, *model.AppError)
|
||||
GeneratePublicLink(siteURL string, info *model.FileInfo) string
|
||||
GenerateSupportPacket(c request.CTX, options *model.SupportPacketOptions) []model.FileData
|
||||
GenerateSupportPacket(rctx request.CTX, options *model.SupportPacketOptions) []model.FileData
|
||||
GetAcknowledgementsForPost(postID string) ([]*model.PostAcknowledgement, *model.AppError)
|
||||
GetAcknowledgementsForPostList(postList *model.PostList) (map[string][]*model.PostAcknowledgement, *model.AppError)
|
||||
GetActivePluginManifests() ([]*model.Manifest, *model.AppError)
|
||||
|
|
@ -947,6 +948,7 @@ type AppIface interface {
|
|||
JoinDefaultChannels(c request.CTX, teamID string, user *model.User, shouldBeAdmin bool, userRequestorId string) *model.AppError
|
||||
JoinUserToTeam(c request.CTX, team *model.Team, user *model.User, userRequestorId string) (*model.TeamMember, *model.AppError)
|
||||
Ldap() einterfaces.LdapInterface
|
||||
LdapDiagnostic() einterfaces.LdapDiagnosticInterface
|
||||
LeaveChannel(c request.CTX, channelID string, userID string) *model.AppError
|
||||
LeaveTeam(c request.CTX, team *model.Team, user *model.User, requestorId string) *model.AppError
|
||||
License() *model.License
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ func (a *App) SyncLdap(c request.CTX, includeRemovedMembers bool) {
|
|||
|
||||
func (a *App) TestLdap(rctx request.CTX) *model.AppError {
|
||||
license := a.Srv().License()
|
||||
if ldapI := a.Ldap(); ldapI != nil && license != nil && *license.Features.LDAP && (*a.Config().LdapSettings.Enable || *a.Config().LdapSettings.EnableSync) {
|
||||
if ldapI := a.LdapDiagnostic(); ldapI != nil && license != nil && *license.Features.LDAP && (*a.Config().LdapSettings.Enable || *a.Config().LdapSettings.EnableSync) {
|
||||
if err := ldapI.RunTest(rctx); err != nil {
|
||||
err.StatusCode = 500
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -3771,6 +3771,28 @@ func (a *OpenTracingAppLayer) DeleteRetentionPolicy(policyID string) *model.AppE
|
|||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) DeleteRole(id string) (*model.Role, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteRole")
|
||||
|
||||
a.ctx = newCtx
|
||||
a.app.Srv().Store().SetContext(newCtx)
|
||||
defer func() {
|
||||
a.app.Srv().Store().SetContext(origCtx)
|
||||
a.ctx = origCtx
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
resultVar0, resultVar1 := a.app.DeleteRole(id)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) DeleteScheduledPost(rctx request.CTX, userId string, scheduledPostId string, connectionId string) (*model.ScheduledPost, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteScheduledPost")
|
||||
|
|
@ -4961,7 +4983,7 @@ func (a *OpenTracingAppLayer) GeneratePublicLink(siteURL string, info *model.Fil
|
|||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GenerateSupportPacket(c request.CTX, options *model.SupportPacketOptions) []model.FileData {
|
||||
func (a *OpenTracingAppLayer) GenerateSupportPacket(rctx request.CTX, options *model.SupportPacketOptions) []model.FileData {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GenerateSupportPacket")
|
||||
|
||||
|
|
@ -4973,7 +4995,7 @@ func (a *OpenTracingAppLayer) GenerateSupportPacket(c request.CTX, options *mode
|
|||
}()
|
||||
|
||||
defer span.Finish()
|
||||
resultVar0 := a.app.GenerateSupportPacket(c, options)
|
||||
resultVar0 := a.app.GenerateSupportPacket(rctx, options)
|
||||
|
||||
return resultVar0
|
||||
}
|
||||
|
|
@ -12641,6 +12663,23 @@ func (a *OpenTracingAppLayer) JoinUserToTeam(c request.CTX, team *model.Team, us
|
|||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) LdapDiagnostic() einterfaces.LdapDiagnosticInterface {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.LdapDiagnostic")
|
||||
|
||||
a.ctx = newCtx
|
||||
a.app.Srv().Store().SetContext(newCtx)
|
||||
defer func() {
|
||||
a.app.Srv().Store().SetContext(origCtx)
|
||||
a.ctx = origCtx
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
resultVar0 := a.app.LdapDiagnostic()
|
||||
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) LeaveChannel(c request.CTX, channelID string, userID string) *model.AppError {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.LeaveChannel")
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
259
server/channels/app/platform/support_packet.go
Normal file
259
server/channels/app/platform/support_packet.go
Normal file
|
|
@ -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
|
||||
}
|
||||
398
server/channels/app/platform/support_packet_test.go
Normal file
398
server/channels/app/platform/support_packet_test.go
Normal file
|
|
@ -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))
|
||||
}
|
||||
|
|
@ -90,6 +90,20 @@ func (a *App) GetRolesByNames(names []string) ([]*model.Role, *model.AppError) {
|
|||
return roles, nil
|
||||
}
|
||||
|
||||
func (a *App) DeleteRole(id string) (*model.Role, *model.AppError) {
|
||||
role, err := a.Srv().Store().Role().Delete(id)
|
||||
if err != nil {
|
||||
var nfErr *store.ErrNotFound
|
||||
switch {
|
||||
case errors.As(err, &nfErr):
|
||||
return nil, model.NewAppError("DeleteRole", "app.role.get.app_error", nil, "", http.StatusNotFound).Wrap(err)
|
||||
default:
|
||||
return nil, model.NewAppError("DeleteRole", "app.role.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
}
|
||||
return role, nil
|
||||
}
|
||||
|
||||
// mergeChannelHigherScopedPermissions updates the permissions based on the role type, whether the permission is
|
||||
// moderated, and the value of the permission on the higher-scoped scheme.
|
||||
func (s *Server) mergeChannelHigherScopedPermissions(roles []*model.Role) *model.AppError {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import (
|
|||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
|
@ -592,12 +591,6 @@ func (s *Server) Channels() *Channels {
|
|||
return s.ch
|
||||
}
|
||||
|
||||
// Return Database type (postgres or mysql) and current version of the schema
|
||||
func (s *Server) DatabaseTypeAndSchemaVersion() (string, string) {
|
||||
schemaVersion, _ := s.Store().GetDBSchemaVersion()
|
||||
return *s.platform.Config().SqlSettings.DriverName, strconv.Itoa(schemaVersion)
|
||||
}
|
||||
|
||||
func (s *Server) startInterClusterServices(license *model.License) error {
|
||||
if license == nil {
|
||||
mlog.Debug("No license provided; Remote Cluster services disabled")
|
||||
|
|
|
|||
|
|
@ -164,34 +164,6 @@ func TestStartServerTLSSuccess(t *testing.T) {
|
|||
require.NoError(t, serverErr)
|
||||
}
|
||||
|
||||
func TestDatabaseTypeAndMattermostVersion(t *testing.T) {
|
||||
sqlDrivernameEnvironment := os.Getenv("MM_SQLSETTINGS_DRIVERNAME")
|
||||
|
||||
if sqlDrivernameEnvironment != "" {
|
||||
defer os.Setenv("MM_SQLSETTINGS_DRIVERNAME", sqlDrivernameEnvironment)
|
||||
} else {
|
||||
defer os.Unsetenv("MM_SQLSETTINGS_DRIVERNAME")
|
||||
}
|
||||
|
||||
os.Setenv("MM_SQLSETTINGS_DRIVERNAME", "postgres")
|
||||
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
databaseType, mattermostVersion := th.Server.DatabaseTypeAndSchemaVersion()
|
||||
assert.Equal(t, "postgres", databaseType)
|
||||
assert.GreaterOrEqual(t, mattermostVersion, strconv.Itoa(1))
|
||||
|
||||
os.Setenv("MM_SQLSETTINGS_DRIVERNAME", "mysql")
|
||||
|
||||
th2 := Setup(t)
|
||||
defer th2.TearDown()
|
||||
|
||||
databaseType, mattermostVersion = th2.Server.DatabaseTypeAndSchemaVersion()
|
||||
assert.Equal(t, "mysql", databaseType)
|
||||
assert.GreaterOrEqual(t, mattermostVersion, strconv.Itoa(1))
|
||||
}
|
||||
|
||||
func TestStartServerTLSVersion(t *testing.T) {
|
||||
configStore, _ := config.NewMemoryStore()
|
||||
store, _ := config.NewStoreFromBacking(configStore, nil, false)
|
||||
|
|
|
|||
|
|
@ -5,13 +5,12 @@ package app
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"runtime"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/yaml.v2"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/plugin"
|
||||
|
|
@ -19,39 +18,36 @@ import (
|
|||
"github.com/mattermost/mattermost/server/public/shared/request"
|
||||
)
|
||||
|
||||
func (a *App) GenerateSupportPacket(c request.CTX, options *model.SupportPacketOptions) []model.FileData {
|
||||
// A array of the functions that we can iterate through since they all have the same return value
|
||||
func (a *App) GenerateSupportPacket(rctx request.CTX, options *model.SupportPacketOptions) []model.FileData {
|
||||
functions := map[string]func(c request.CTX) (*model.FileData, error){
|
||||
"support packet": a.generateSupportPacketYaml,
|
||||
"plugins": a.createPluginsFile,
|
||||
"config": a.createSanitizedConfigFile,
|
||||
"cpu profile": a.Srv().Platform().CreateCPUProfile,
|
||||
"heap profile": a.Srv().Platform().CreateHeapProfile,
|
||||
"goroutines": a.Srv().Platform().CreateGoroutineProfile,
|
||||
"metadata": a.createSupportPacketMetadata,
|
||||
"metadata": a.getSupportPacketMetadata,
|
||||
"stats": a.getSupportPacketStats,
|
||||
"jobs": a.getSupportPacketJobList,
|
||||
"permissions": a.getSupportPacketPermissionsInfo,
|
||||
"plugins": a.getPluginsFile,
|
||||
}
|
||||
|
||||
if options.IncludeLogs {
|
||||
functions["mattermost log"] = a.Srv().Platform().GetLogFile
|
||||
functions["notification log"] = a.Srv().Platform().GetNotificationLogFile
|
||||
}
|
||||
|
||||
// If any errors we come across within this function, we will log it in a warning.txt file so that we know why certain files did not get produced if any
|
||||
var warnings *multierror.Error
|
||||
// Creating an array of files that we are going to be adding to our zip file
|
||||
var fileDatas []model.FileData
|
||||
var wg sync.WaitGroup
|
||||
var mut sync.Mutex // Protects warnings and fileDatas
|
||||
var (
|
||||
// If any errors we come across within this function, we will log it in a warning.txt file so that we know why certain files did not get produced if any
|
||||
warnings *multierror.Error
|
||||
// Creating an array of files that we are going to be adding to our zip file
|
||||
fileDatas []model.FileData
|
||||
wg sync.WaitGroup
|
||||
mut sync.Mutex // Protects warnings and fileDatas
|
||||
)
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
for name, fn := range functions {
|
||||
fileData, err := fn(c)
|
||||
fileData, err := fn(rctx)
|
||||
mut.Lock()
|
||||
if err != nil {
|
||||
c.Logger().Error("Failed to generate file for Support Packet", mlog.String("file", name), mlog.Err(err))
|
||||
rctx.Logger().Error("Failed to generate file for Support Packet",
|
||||
mlog.String("file", name),
|
||||
mlog.Err(err),
|
||||
)
|
||||
warnings = multierror.Append(warnings, err)
|
||||
}
|
||||
|
||||
|
|
@ -60,6 +56,17 @@ func (a *App) GenerateSupportPacket(c request.CTX, options *model.SupportPacketO
|
|||
}
|
||||
mut.Unlock()
|
||||
}
|
||||
|
||||
// Generate platform support packet
|
||||
files, err := a.Srv().Platform().GenerateSupportPacket(rctx, options)
|
||||
mut.Lock()
|
||||
if err != nil {
|
||||
warnings = multierror.Append(warnings, err)
|
||||
}
|
||||
if files != nil {
|
||||
fileDatas = append(fileDatas, files...)
|
||||
}
|
||||
mut.Unlock()
|
||||
}()
|
||||
|
||||
// Run the cluster generation in a separate goroutine as CPU profile generation and file upload can take a long time
|
||||
|
|
@ -68,10 +75,10 @@ func (a *App) GenerateSupportPacket(c request.CTX, options *model.SupportPacketO
|
|||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
files, err := cluster.GenerateSupportPacket(c, options)
|
||||
files, err := cluster.GenerateSupportPacket(rctx, options)
|
||||
mut.Lock()
|
||||
if err != nil {
|
||||
c.Logger().Error("Failed to generate Support Packet from cluster nodes", mlog.Err(err))
|
||||
rctx.Logger().Error("Failed to generate Support Packet from cluster nodes", mlog.Err(err))
|
||||
warnings = multierror.Append(warnings, err)
|
||||
}
|
||||
|
||||
|
|
@ -84,7 +91,7 @@ func (a *App) GenerateSupportPacket(c request.CTX, options *model.SupportPacketO
|
|||
|
||||
wg.Wait()
|
||||
|
||||
pluginContext := pluginContext(c)
|
||||
pluginContext := pluginContext(rctx)
|
||||
a.ch.RunMultiHook(func(hooks plugin.Hooks, manifest *model.Manifest) bool {
|
||||
// If the plugin defined the support_packet prop it means there is a UI element to include it in the support packet.
|
||||
// Check if the plugin is in the list of plugins to include in the Support Packet.
|
||||
|
|
@ -97,7 +104,7 @@ func (a *App) GenerateSupportPacket(c request.CTX, options *model.SupportPacketO
|
|||
// Otherwise, just call the hook as the plugin decided to always include it in the Support Packet.
|
||||
pluginData, err := hooks.GenerateSupportData(pluginContext)
|
||||
if err != nil {
|
||||
c.Logger().Warn("Failed to generate plugin file for Support Packet", mlog.String("plugin", manifest.Id), mlog.Err(err))
|
||||
rctx.Logger().Warn("Failed to generate plugin file for Support Packet", mlog.String("plugin", manifest.Id), mlog.Err(err))
|
||||
warnings = multierror.Append(warnings, err)
|
||||
return true
|
||||
}
|
||||
|
|
@ -120,230 +127,212 @@ func (a *App) GenerateSupportPacket(c request.CTX, options *model.SupportPacketO
|
|||
return fileDatas
|
||||
}
|
||||
|
||||
func (a *App) generateSupportPacketYaml(c request.CTX) (*model.FileData, error) {
|
||||
var rErr *multierror.Error
|
||||
|
||||
/* DB */
|
||||
|
||||
databaseType, databaseSchemaVersion := a.Srv().DatabaseTypeAndSchemaVersion()
|
||||
databaseVersion, err := a.Srv().Store().GetDbVersion(false)
|
||||
if err != nil {
|
||||
rErr = multierror.Append(errors.Wrap(err, "error while getting DB version"))
|
||||
}
|
||||
|
||||
/* Cluster */
|
||||
|
||||
var clusterID string
|
||||
if cluster := a.Cluster(); cluster != nil {
|
||||
clusterID = cluster.GetClusterId()
|
||||
}
|
||||
|
||||
/* File store */
|
||||
|
||||
fileDriver := a.Srv().Platform().FileBackend().DriverName()
|
||||
fileStatus := model.StatusOk
|
||||
err = a.Srv().Platform().FileBackend().TestConnection()
|
||||
if err != nil {
|
||||
fileStatus = model.StatusFail + ": " + err.Error()
|
||||
}
|
||||
|
||||
/* LDAP */
|
||||
|
||||
var vendorName, vendorVersion string
|
||||
if ldap := a.Ldap(); ldap != nil && (*a.Config().LdapSettings.Enable || *a.Config().LdapSettings.EnableSync) {
|
||||
vendorName, vendorVersion, err = ldap.GetVendorNameAndVendorVersion(c)
|
||||
if err != nil {
|
||||
rErr = multierror.Append(errors.Wrap(err, "error while getting LDAP vendor info"))
|
||||
}
|
||||
|
||||
if vendorName == "" {
|
||||
vendorName = "unknown"
|
||||
}
|
||||
if vendorVersion == "" {
|
||||
vendorVersion = "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
/* Elastic Search */
|
||||
|
||||
var elasticServerVersion string
|
||||
var elasticServerPlugins []string
|
||||
if se := a.Srv().Platform().SearchEngine.ElasticsearchEngine; se != nil {
|
||||
elasticServerVersion = se.GetFullVersion()
|
||||
elasticServerPlugins = se.GetPlugins()
|
||||
}
|
||||
|
||||
/* License */
|
||||
|
||||
func (a *App) getSupportPacketStats(rctx request.CTX) (*model.FileData, error) {
|
||||
var (
|
||||
licenseTo string
|
||||
supportedUsers int
|
||||
isTrial bool
|
||||
rErr *multierror.Error
|
||||
err error
|
||||
stats model.SupportPacketStats
|
||||
)
|
||||
if license := a.Srv().License(); license != nil {
|
||||
licenseTo = license.Customer.Company
|
||||
supportedUsers = *license.Features.Users
|
||||
isTrial = license.IsTrial
|
||||
}
|
||||
|
||||
/* Server stats */
|
||||
|
||||
uniqueUserCount, err := a.Srv().Store().User().Count(model.UserCountOptions{})
|
||||
stats.RegisteredUsers, err = a.Srv().Store().User().Count(model.UserCountOptions{IncludeDeleted: true})
|
||||
if err != nil {
|
||||
rErr = multierror.Append(errors.Wrap(err, "error while getting user count"))
|
||||
rErr = multierror.Append(errors.Wrap(err, "failed to get registered user count"))
|
||||
}
|
||||
|
||||
var (
|
||||
totalChannels int
|
||||
totalPosts int
|
||||
totalTeams int
|
||||
websocketConnections int
|
||||
masterDbConnections int
|
||||
replicaDbConnections int
|
||||
dailyActiveUsers int
|
||||
monthlyActiveUsers int
|
||||
inactiveUserCount int
|
||||
)
|
||||
analytics, appErr := a.GetAnalyticsForSupportPacket(c)
|
||||
if appErr != nil {
|
||||
rErr = multierror.Append(errors.Wrap(appErr, "error while getting analytics"))
|
||||
} else {
|
||||
if len(analytics) < 11 {
|
||||
rErr = multierror.Append(errors.New("not enough analytics information found"))
|
||||
} else {
|
||||
totalChannels = int(analytics[0].Value) + int(analytics[1].Value)
|
||||
totalPosts = int(analytics[2].Value)
|
||||
totalTeams = int(analytics[4].Value)
|
||||
websocketConnections = int(analytics[5].Value)
|
||||
masterDbConnections = int(analytics[6].Value)
|
||||
replicaDbConnections = int(analytics[7].Value)
|
||||
dailyActiveUsers = int(analytics[8].Value)
|
||||
monthlyActiveUsers = int(analytics[9].Value)
|
||||
inactiveUserCount = int(analytics[10].Value)
|
||||
}
|
||||
}
|
||||
|
||||
/* Jobs */
|
||||
|
||||
dataRetentionJobs, err := a.Srv().Store().Job().GetAllByTypePage(c, model.JobTypeDataRetention, 0, 2)
|
||||
stats.ActiveUsers, err = a.Srv().Store().User().Count(model.UserCountOptions{})
|
||||
if err != nil {
|
||||
rErr = multierror.Append(errors.Wrap(err, "error while getting data retention jobs"))
|
||||
rErr = multierror.Append(errors.Wrap(err, "failed to get active user count"))
|
||||
}
|
||||
messageExportJobs, err := a.Srv().Store().Job().GetAllByTypePage(c, model.JobTypeMessageExport, 0, 2)
|
||||
|
||||
stats.DailyActiveUsers, err = a.Srv().Store().User().AnalyticsActiveCount(DayMilliseconds, model.UserCountOptions{IncludeBotAccounts: false, IncludeDeleted: false})
|
||||
if err != nil {
|
||||
rErr = multierror.Append(errors.Wrap(err, "error while getting message export jobs"))
|
||||
rErr = multierror.Append(errors.Wrap(err, "failed to get daily active user count"))
|
||||
}
|
||||
elasticPostIndexingJobs, err := a.Srv().Store().Job().GetAllByTypePage(c, model.JobTypeElasticsearchPostIndexing, 0, 2)
|
||||
|
||||
stats.MonthlyActiveUsers, err = a.Srv().Store().User().AnalyticsActiveCount(MonthMilliseconds, model.UserCountOptions{IncludeBotAccounts: false, IncludeDeleted: false})
|
||||
if err != nil {
|
||||
rErr = multierror.Append(errors.Wrap(err, "error while getting ES post indexing jobs"))
|
||||
rErr = multierror.Append(errors.Wrap(err, "failed to get monthly active user count"))
|
||||
}
|
||||
elasticPostAggregationJobs, err := a.Srv().Store().Job().GetAllByTypePage(c, model.JobTypeElasticsearchPostAggregation, 0, 2)
|
||||
|
||||
stats.DeactivatedUsers, err = a.Srv().Store().User().AnalyticsGetInactiveUsersCount()
|
||||
if err != nil {
|
||||
rErr = multierror.Append(errors.Wrap(err, "error while getting ES post aggregation jobs"))
|
||||
rErr = multierror.Append(errors.Wrap(err, "failed to get deactivated user count"))
|
||||
}
|
||||
blevePostIndexingJobs, err := a.Srv().Store().Job().GetAllByTypePage(c, model.JobTypeBlevePostIndexing, 0, 2)
|
||||
|
||||
stats.Guests, err = a.Srv().Store().User().AnalyticsGetGuestCount()
|
||||
if err != nil {
|
||||
rErr = multierror.Append(errors.Wrap(err, "error while getting bleve post indexing jobs"))
|
||||
rErr = multierror.Append(errors.Wrap(err, "failed to get guest count"))
|
||||
}
|
||||
ldapSyncJobs, err := a.Srv().Store().Job().GetAllByTypePage(c, model.JobTypeLdapSync, 0, 2)
|
||||
|
||||
stats.BotAccounts, err = a.Srv().Store().User().Count(model.UserCountOptions{IncludeBotAccounts: true, ExcludeRegularUsers: true})
|
||||
if err != nil {
|
||||
rErr = multierror.Append(errors.Wrap(err, "error while getting LDAP sync jobs"))
|
||||
rErr = multierror.Append(errors.Wrap(err, "failed to get bot acount count"))
|
||||
}
|
||||
migrationJobs, err := a.Srv().Store().Job().GetAllByTypePage(c, model.JobTypeMigrations, 0, 2)
|
||||
|
||||
stats.Posts, err = a.Srv().Store().Post().AnalyticsPostCount(&model.PostCountOptions{})
|
||||
if err != nil {
|
||||
rErr = multierror.Append(errors.Wrap(err, "error while getting migration jobs"))
|
||||
rErr = multierror.Append(errors.Wrap(err, "failed to get post count"))
|
||||
}
|
||||
|
||||
// Creating the struct for Support Packet yaml file
|
||||
supportPacket := model.SupportPacket{
|
||||
/* Build information */
|
||||
ServerOS: runtime.GOOS,
|
||||
ServerArchitecture: runtime.GOARCH,
|
||||
ServerVersion: model.CurrentVersion,
|
||||
BuildHash: model.BuildHash,
|
||||
openChannels, err := a.Srv().Store().Channel().AnalyticsTypeCount("", model.ChannelTypeOpen)
|
||||
if err != nil {
|
||||
rErr = multierror.Append(errors.Wrap(err, "failed to get open channels count"))
|
||||
}
|
||||
privateChannels, err := a.Srv().Store().Channel().AnalyticsTypeCount("", model.ChannelTypePrivate)
|
||||
if err != nil {
|
||||
rErr = multierror.Append(errors.Wrap(err, "failed to get private channels count"))
|
||||
}
|
||||
stats.Channels = openChannels + privateChannels
|
||||
|
||||
/* DB */
|
||||
DatabaseType: databaseType,
|
||||
DatabaseVersion: databaseVersion,
|
||||
DatabaseSchemaVersion: databaseSchemaVersion,
|
||||
WebsocketConnections: websocketConnections,
|
||||
MasterDbConnections: masterDbConnections,
|
||||
ReplicaDbConnections: replicaDbConnections,
|
||||
|
||||
/* Cluster */
|
||||
ClusterID: clusterID,
|
||||
|
||||
/* File store */
|
||||
FileDriver: fileDriver,
|
||||
FileStatus: fileStatus,
|
||||
|
||||
/* LDAP */
|
||||
LdapVendorName: vendorName,
|
||||
LdapVendorVersion: vendorVersion,
|
||||
|
||||
/* Elastic Search */
|
||||
ElasticServerVersion: elasticServerVersion,
|
||||
ElasticServerPlugins: elasticServerPlugins,
|
||||
|
||||
/* License */
|
||||
LicenseTo: licenseTo,
|
||||
LicenseSupportedUsers: supportedUsers,
|
||||
LicenseIsTrial: isTrial,
|
||||
|
||||
/* Server stats */
|
||||
ActiveUsers: int(uniqueUserCount),
|
||||
DailyActiveUsers: dailyActiveUsers,
|
||||
MonthlyActiveUsers: monthlyActiveUsers,
|
||||
InactiveUserCount: inactiveUserCount,
|
||||
TotalPosts: totalPosts,
|
||||
TotalChannels: totalChannels,
|
||||
TotalTeams: totalTeams,
|
||||
|
||||
/* Jobs */
|
||||
DataRetentionJobs: dataRetentionJobs,
|
||||
MessageExportJobs: messageExportJobs,
|
||||
ElasticPostIndexingJobs: elasticPostIndexingJobs,
|
||||
ElasticPostAggregationJobs: elasticPostAggregationJobs,
|
||||
BlevePostIndexingJobs: blevePostIndexingJobs,
|
||||
LdapSyncJobs: ldapSyncJobs,
|
||||
MigrationJobs: migrationJobs,
|
||||
stats.Teams, err = a.Srv().Store().Team().AnalyticsTeamCount(nil)
|
||||
if err != nil {
|
||||
rErr = multierror.Append(errors.Wrap(err, "failed to get team count"))
|
||||
}
|
||||
|
||||
// Marshal to a YAML File
|
||||
supportPacketYaml, err := yaml.Marshal(&supportPacket)
|
||||
stats.SlashCommands, err = a.Srv().Store().Command().AnalyticsCommandCount("")
|
||||
if err != nil {
|
||||
rErr = multierror.Append(errors.Wrap(err, "failed to get command count"))
|
||||
}
|
||||
|
||||
stats.IncomingWebhooks, err = a.Srv().Store().Webhook().AnalyticsIncomingCount("", "")
|
||||
if err != nil {
|
||||
rErr = multierror.Append(errors.Wrap(err, "failed to get incoming webhook count"))
|
||||
}
|
||||
|
||||
stats.OutgoingWebhooks, err = a.Srv().Store().Webhook().AnalyticsOutgoingCount("")
|
||||
if err != nil {
|
||||
rErr = multierror.Append(errors.Wrap(err, "failed to get outgoing webhook count"))
|
||||
}
|
||||
|
||||
b, err := yaml.Marshal(&stats)
|
||||
if err != nil {
|
||||
rErr = multierror.Append(errors.Wrap(err, "failed to marshal Support Packet into yaml"))
|
||||
}
|
||||
|
||||
fileData := &model.FileData{
|
||||
Filename: "support_packet.yaml",
|
||||
Body: supportPacketYaml,
|
||||
Filename: "stats.yaml",
|
||||
Body: b,
|
||||
}
|
||||
return fileData, rErr.ErrorOrNil()
|
||||
}
|
||||
|
||||
func (a *App) createSanitizedConfigFile(_ request.CTX) (*model.FileData, error) {
|
||||
// Getting sanitized config, prettifying it, and then adding it to our file data array
|
||||
sanitizedConfigPrettyJSON, err := json.MarshalIndent(a.GetSanitizedConfig(), "", " ")
|
||||
func (a *App) getSupportPacketJobList(rctx request.CTX) (*model.FileData, error) {
|
||||
const numberOfJobsRuns = 5
|
||||
|
||||
var (
|
||||
rErr *multierror.Error
|
||||
err error
|
||||
jobs model.SupportPacketJobList
|
||||
)
|
||||
|
||||
jobs.LDAPSyncJobs, err = a.Srv().Store().Job().GetAllByTypePage(rctx, model.JobTypeLdapSync, 0, numberOfJobsRuns)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to sanitized config into json")
|
||||
rErr = multierror.Append(errors.Wrap(err, "error while getting LDAP sync jobs"))
|
||||
}
|
||||
jobs.DataRetentionJobs, err = a.Srv().Store().Job().GetAllByTypePage(rctx, model.JobTypeDataRetention, 0, numberOfJobsRuns)
|
||||
if err != nil {
|
||||
rErr = multierror.Append(errors.Wrap(err, "error while getting data retention jobs"))
|
||||
}
|
||||
jobs.MessageExportJobs, err = a.Srv().Store().Job().GetAllByTypePage(rctx, model.JobTypeMessageExport, 0, numberOfJobsRuns)
|
||||
if err != nil {
|
||||
rErr = multierror.Append(errors.Wrap(err, "error while getting message export jobs"))
|
||||
}
|
||||
jobs.ElasticPostIndexingJobs, err = a.Srv().Store().Job().GetAllByTypePage(rctx, model.JobTypeElasticsearchPostIndexing, 0, numberOfJobsRuns)
|
||||
if err != nil {
|
||||
rErr = multierror.Append(errors.Wrap(err, "error while getting ES post indexing jobs"))
|
||||
}
|
||||
jobs.ElasticPostAggregationJobs, err = a.Srv().Store().Job().GetAllByTypePage(rctx, model.JobTypeElasticsearchPostAggregation, 0, numberOfJobsRuns)
|
||||
if err != nil {
|
||||
rErr = multierror.Append(errors.Wrap(err, "error while getting ES post aggregation jobs"))
|
||||
}
|
||||
jobs.BlevePostIndexingJobs, err = a.Srv().Store().Job().GetAllByTypePage(rctx, model.JobTypeBlevePostIndexing, 0, numberOfJobsRuns)
|
||||
if err != nil {
|
||||
rErr = multierror.Append(errors.Wrap(err, "error while getting bleve post indexing jobs"))
|
||||
}
|
||||
jobs.MigrationJobs, err = a.Srv().Store().Job().GetAllByTypePage(rctx, model.JobTypeMigrations, 0, numberOfJobsRuns)
|
||||
if err != nil {
|
||||
rErr = multierror.Append(errors.Wrap(err, "error while getting migration jobs"))
|
||||
}
|
||||
|
||||
b, err := yaml.Marshal(&jobs)
|
||||
if err != nil {
|
||||
rErr = multierror.Append(errors.Wrap(err, "failed to marshal jobs list into yaml"))
|
||||
}
|
||||
|
||||
fileData := &model.FileData{
|
||||
Filename: "sanitized_config.json",
|
||||
Body: sanitizedConfigPrettyJSON,
|
||||
Filename: "jobs.yaml",
|
||||
Body: b,
|
||||
}
|
||||
return fileData, nil
|
||||
return fileData, rErr.ErrorOrNil()
|
||||
}
|
||||
|
||||
func (a *App) createPluginsFile(_ request.CTX) (*model.FileData, error) {
|
||||
func (a *App) getSupportPacketPermissionsInfo(_ request.CTX) (*model.FileData, error) {
|
||||
var (
|
||||
rErr *multierror.Error
|
||||
err error
|
||||
permissions model.SupportPacketPermissionInfo
|
||||
)
|
||||
|
||||
var allSchemes []*model.Scheme
|
||||
perPage := 100
|
||||
page := 0
|
||||
for {
|
||||
schemes, appErr := a.GetSchemesPage("", page, perPage)
|
||||
if appErr != nil {
|
||||
rErr = multierror.Append(errors.Wrap(appErr, "failed to get list of schemes"))
|
||||
break
|
||||
}
|
||||
|
||||
allSchemes = append(allSchemes, schemes...)
|
||||
if len(schemes) < perPage {
|
||||
break
|
||||
}
|
||||
page++
|
||||
}
|
||||
|
||||
for _, s := range allSchemes {
|
||||
s.Sanitize()
|
||||
}
|
||||
permissions.Schemes = allSchemes
|
||||
|
||||
roles, appErr := a.GetAllRoles()
|
||||
if appErr != nil {
|
||||
rErr = multierror.Append(errors.Wrap(appErr, "failed to get list of roles"))
|
||||
}
|
||||
|
||||
for _, r := range roles {
|
||||
r.Sanitize()
|
||||
}
|
||||
permissions.Roles = roles
|
||||
|
||||
b, err := yaml.Marshal(&permissions)
|
||||
if err != nil {
|
||||
rErr = multierror.Append(errors.Wrap(err, "failed to marshal permission info into yaml"))
|
||||
}
|
||||
|
||||
fileData := &model.FileData{
|
||||
Filename: "permissions.yaml",
|
||||
Body: b,
|
||||
}
|
||||
return fileData, rErr.ErrorOrNil()
|
||||
}
|
||||
|
||||
func (a *App) getPluginsFile(_ request.CTX) (*model.FileData, error) {
|
||||
// Getting the plugins installed on the server, prettify it, and then add them to the file data array
|
||||
pluginsResponse, appErr := a.GetPlugins()
|
||||
plugins, appErr := a.GetPlugins()
|
||||
if appErr != nil {
|
||||
return nil, errors.Wrap(appErr, "failed to get plugin list for Support Packet")
|
||||
}
|
||||
|
||||
pluginsPrettyJSON, err := json.MarshalIndent(pluginsResponse, "", " ")
|
||||
var pluginList model.SupportPacketPluginList
|
||||
for _, p := range plugins.Active {
|
||||
pluginList.Enabled = append(pluginList.Enabled, p.Manifest)
|
||||
}
|
||||
for _, p := range plugins.Inactive {
|
||||
pluginList.Disabled = append(pluginList.Disabled, p.Manifest)
|
||||
}
|
||||
|
||||
pluginsPrettyJSON, err := json.MarshalIndent(pluginList, "", " ")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to marshal plugin list into json")
|
||||
}
|
||||
|
|
@ -355,7 +344,7 @@ func (a *App) createPluginsFile(_ request.CTX) (*model.FileData, error) {
|
|||
return fileData, nil
|
||||
}
|
||||
|
||||
func (a *App) createSupportPacketMetadata(_ request.CTX) (*model.FileData, error) {
|
||||
func (a *App) getSupportPacketMetadata(_ request.CTX) (*model.FileData, error) {
|
||||
metadata, err := model.GeneratePacketMetadata(model.SupportPacketType, a.TelemetryId(), a.License(), nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to generate Packet metadata")
|
||||
|
|
|
|||
|
|
@ -4,200 +4,29 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/yaml.v2"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/app/platform"
|
||||
smocks "github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/utils/fileutils"
|
||||
"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 TestCreatePluginsFile(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
// Happy path where we have a plugins file with no err
|
||||
fileData, err := th.App.createPluginsFile(th.Context)
|
||||
require.NotNil(t, fileData)
|
||||
assert.Equal(t, "plugins.json", fileData.Filename)
|
||||
assert.Positive(t, len(fileData.Body))
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Turn off plugins so we can get an error
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = false
|
||||
})
|
||||
|
||||
// Plugins off in settings so no fileData and we get a warning instead
|
||||
fileData, err = th.App.createPluginsFile(th.Context)
|
||||
assert.Nil(t, fileData)
|
||||
assert.ErrorContains(t, err, "failed to get plugin list for Support Packet")
|
||||
}
|
||||
|
||||
func TestGenerateSupportPacketYaml(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
licenseUsers := 100
|
||||
license := model.NewTestLicense("ldap")
|
||||
license.Features.Users = model.NewPointer(licenseUsers)
|
||||
th.App.Srv().SetLicense(license)
|
||||
|
||||
generateSupportPacket := func(t *testing.T) *model.SupportPacket {
|
||||
t.Helper()
|
||||
|
||||
fileData, err := th.App.generateSupportPacketYaml(th.Context)
|
||||
require.NotNil(t, fileData)
|
||||
assert.Equal(t, "support_packet.yaml", fileData.Filename)
|
||||
assert.Positive(t, len(fileData.Body))
|
||||
assert.NoError(t, err)
|
||||
|
||||
var packet model.SupportPacket
|
||||
require.NoError(t, yaml.Unmarshal(fileData.Body, &packet))
|
||||
require.NotNil(t, packet)
|
||||
return &packet
|
||||
}
|
||||
|
||||
t.Run("Happy path", func(t *testing.T) {
|
||||
// Happy path where we have a Support Packet yaml file without any warnings
|
||||
packet := generateSupportPacket(t)
|
||||
|
||||
/* Build information */
|
||||
assert.NotEmpty(t, packet.ServerOS)
|
||||
assert.NotEmpty(t, packet.ServerArchitecture)
|
||||
assert.Equal(t, model.CurrentVersion, packet.ServerVersion)
|
||||
// BuildHash is not present in tests
|
||||
|
||||
/* DB */
|
||||
assert.NotEmpty(t, packet.DatabaseType)
|
||||
assert.NotEmpty(t, packet.DatabaseVersion)
|
||||
assert.NotEmpty(t, packet.DatabaseSchemaVersion)
|
||||
assert.Zero(t, packet.WebsocketConnections)
|
||||
assert.NotZero(t, packet.MasterDbConnections)
|
||||
assert.Zero(t, packet.ReplicaDbConnections)
|
||||
|
||||
/* Cluster */
|
||||
assert.Empty(t, packet.ClusterID)
|
||||
|
||||
/* File store */
|
||||
assert.Equal(t, "local", packet.FileDriver)
|
||||
assert.Equal(t, "OK", packet.FileStatus)
|
||||
|
||||
/* LDAP */
|
||||
assert.Empty(t, packet.LdapVendorName)
|
||||
assert.Empty(t, packet.LdapVendorVersion)
|
||||
|
||||
/* Elastic Search */
|
||||
assert.Empty(t, packet.ElasticServerVersion)
|
||||
assert.Empty(t, packet.ElasticServerPlugins)
|
||||
|
||||
/* License */
|
||||
assert.Equal(t, "My awesome Company", packet.LicenseTo)
|
||||
assert.Equal(t, licenseUsers, packet.LicenseSupportedUsers)
|
||||
assert.Equal(t, false, packet.LicenseIsTrial)
|
||||
|
||||
/* Server stats */
|
||||
assert.Equal(t, 3, packet.ActiveUsers) // from InitBasic()
|
||||
assert.Equal(t, 0, packet.DailyActiveUsers)
|
||||
assert.Equal(t, 0, packet.MonthlyActiveUsers)
|
||||
assert.Equal(t, 0, packet.InactiveUserCount)
|
||||
assert.Equal(t, 5, packet.TotalPosts) // from InitBasic()
|
||||
assert.Equal(t, 3, packet.TotalChannels) // from InitBasic()
|
||||
assert.Equal(t, 1, packet.TotalTeams) // from InitBasic()
|
||||
|
||||
/* Jobs */
|
||||
assert.Empty(t, packet.DataRetentionJobs)
|
||||
assert.Empty(t, packet.MessageExportJobs)
|
||||
assert.Empty(t, packet.ElasticPostIndexingJobs)
|
||||
assert.Empty(t, packet.ElasticPostAggregationJobs)
|
||||
assert.Empty(t, packet.BlevePostIndexingJobs)
|
||||
assert.Empty(t, packet.LdapSyncJobs)
|
||||
assert.Empty(t, packet.MigrationJobs)
|
||||
})
|
||||
|
||||
t.Run("post count should be present if number of users extends AnalyticsSettings.MaxUsersForStatistics", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.AnalyticsSettings.MaxUsersForStatistics = model.NewPointer(1)
|
||||
})
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
p := th.CreatePost(th.BasicChannel)
|
||||
require.NotNil(t, p)
|
||||
}
|
||||
|
||||
// InitBasic() already creats 5 posts
|
||||
packet := generateSupportPacket(t)
|
||||
assert.Equal(t, 10, packet.TotalPosts)
|
||||
})
|
||||
|
||||
t.Run("filestore fails", func(t *testing.T) {
|
||||
fb := &fmocks.FileBackend{}
|
||||
platform.SetFileStore(fb)(th.Server.Platform())
|
||||
fb.On("DriverName").Return("mock")
|
||||
fb.On("TestConnection").Return(errors.New("all broken"))
|
||||
|
||||
packet := generateSupportPacket(t)
|
||||
|
||||
assert.Equal(t, "mock", packet.FileDriver)
|
||||
assert.Equal(t, "FAIL: all broken", packet.FileStatus)
|
||||
})
|
||||
|
||||
t.Run("no LDAP info if LDAP sync is disabled", func(t *testing.T) {
|
||||
ldapMock := &emocks.LdapInterface{}
|
||||
th.App.Channels().Ldap = ldapMock
|
||||
|
||||
packet := generateSupportPacket(t)
|
||||
|
||||
assert.Equal(t, "", packet.LdapVendorName)
|
||||
assert.Equal(t, "", packet.LdapVendorVersion)
|
||||
})
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.LdapSettings.EnableSync = model.NewPointer(true)
|
||||
})
|
||||
|
||||
t.Run("no LDAP vendor info found", func(t *testing.T) {
|
||||
ldapMock := &emocks.LdapInterface{}
|
||||
ldapMock.On(
|
||||
"GetVendorNameAndVendorVersion",
|
||||
mock.AnythingOfType("*request.Context"),
|
||||
).Return("", "", nil)
|
||||
th.App.Channels().Ldap = ldapMock
|
||||
|
||||
packet := generateSupportPacket(t)
|
||||
|
||||
assert.Equal(t, "unknown", packet.LdapVendorName)
|
||||
assert.Equal(t, "unknown", packet.LdapVendorVersion)
|
||||
})
|
||||
|
||||
t.Run("found LDAP vendor info", func(t *testing.T) {
|
||||
ldapMock := &emocks.LdapInterface{}
|
||||
ldapMock.On(
|
||||
"GetVendorNameAndVendorVersion",
|
||||
mock.AnythingOfType("*request.Context"),
|
||||
).Return("some vendor", "v1.0.0", nil)
|
||||
th.App.Channels().Ldap = ldapMock
|
||||
|
||||
packet := generateSupportPacket(t)
|
||||
|
||||
assert.Equal(t, "some vendor", packet.LdapVendorName)
|
||||
assert.Equal(t, "v1.0.0", packet.LdapVendorVersion)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGenerateSupportPacket(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
th.App.SetPhase2PermissionsMigrationStatus(true)
|
||||
|
||||
dir, err := os.MkdirTemp("", "")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
|
|
@ -222,29 +51,42 @@ func TestGenerateSupportPacket(t *testing.T) {
|
|||
}
|
||||
genMockLogFiles()
|
||||
|
||||
t.Run("generate Support Packet with logs", func(t *testing.T) {
|
||||
fileDatas := th.App.GenerateSupportPacket(th.Context, &model.SupportPacketOptions{
|
||||
IncludeLogs: true,
|
||||
})
|
||||
getFileNames := func(t *testing.T, fileDatas []model.FileData) []string {
|
||||
var rFileNames []string
|
||||
testFiles := []string{
|
||||
"support_packet.yaml",
|
||||
"metadata.yaml",
|
||||
"plugins.json",
|
||||
"sanitized_config.json",
|
||||
"mattermost.log",
|
||||
"notifications.log",
|
||||
"cpu.prof",
|
||||
"heap.prof",
|
||||
"goroutines",
|
||||
}
|
||||
for _, fileData := range fileDatas {
|
||||
require.NotNil(t, fileData)
|
||||
assert.Positive(t, len(fileData.Body))
|
||||
|
||||
rFileNames = append(rFileNames, fileData.Filename)
|
||||
}
|
||||
assert.ElementsMatch(t, testFiles, rFileNames)
|
||||
return rFileNames
|
||||
}
|
||||
|
||||
expectedFileNames := []string{
|
||||
"metadata.yaml",
|
||||
"stats.yaml",
|
||||
"jobs.yaml",
|
||||
"permissions.yaml",
|
||||
"plugins.json",
|
||||
"sanitized_config.json",
|
||||
"diagnostics.yaml",
|
||||
"cpu.prof",
|
||||
"heap.prof",
|
||||
"goroutines",
|
||||
}
|
||||
|
||||
expectedFileNamesWithLogs := append(expectedFileNames, []string{
|
||||
"mattermost.log",
|
||||
"notifications.log",
|
||||
}...)
|
||||
|
||||
t.Run("generate Support Packet with logs", func(t *testing.T) {
|
||||
fileDatas := th.App.GenerateSupportPacket(th.Context, &model.SupportPacketOptions{
|
||||
IncludeLogs: true,
|
||||
})
|
||||
rFileNames := getFileNames(t, fileDatas)
|
||||
|
||||
assert.ElementsMatch(t, expectedFileNamesWithLogs, rFileNames)
|
||||
})
|
||||
|
||||
t.Run("generate Support Packet without logs", func(t *testing.T) {
|
||||
|
|
@ -252,27 +94,12 @@ func TestGenerateSupportPacket(t *testing.T) {
|
|||
IncludeLogs: false,
|
||||
})
|
||||
|
||||
testFiles := []string{
|
||||
"support_packet.yaml",
|
||||
"metadata.yaml",
|
||||
"plugins.json",
|
||||
"sanitized_config.json",
|
||||
"cpu.prof",
|
||||
"heap.prof",
|
||||
"goroutines",
|
||||
}
|
||||
var rFileNames []string
|
||||
for _, fileData := range fileDatas {
|
||||
require.NotNil(t, fileData)
|
||||
assert.Positive(t, len(fileData.Body))
|
||||
rFileNames := getFileNames(t, fileDatas)
|
||||
|
||||
rFileNames = append(rFileNames, fileData.Filename)
|
||||
}
|
||||
assert.ElementsMatch(t, testFiles, rFileNames)
|
||||
assert.ElementsMatch(t, expectedFileNames, rFileNames)
|
||||
})
|
||||
|
||||
t.Run("remove the log files and ensure that warning.txt file is generated", func(t *testing.T) {
|
||||
// Remove these two files and ensure that warning.txt file is generated
|
||||
err = os.Remove(logLocation)
|
||||
require.NoError(t, err)
|
||||
err = os.Remove(notificationsLogLocation)
|
||||
|
|
@ -282,24 +109,9 @@ func TestGenerateSupportPacket(t *testing.T) {
|
|||
fileDatas := th.App.GenerateSupportPacket(th.Context, &model.SupportPacketOptions{
|
||||
IncludeLogs: true,
|
||||
})
|
||||
testFiles := []string{
|
||||
"support_packet.yaml",
|
||||
"metadata.yaml",
|
||||
"plugins.json",
|
||||
"sanitized_config.json",
|
||||
"cpu.prof",
|
||||
"heap.prof",
|
||||
"warning.txt",
|
||||
"goroutines",
|
||||
}
|
||||
var rFileNames []string
|
||||
for _, fileData := range fileDatas {
|
||||
require.NotNil(t, fileData)
|
||||
assert.Positive(t, len(fileData.Body))
|
||||
rFileNames := getFileNames(t, fileDatas)
|
||||
|
||||
rFileNames = append(rFileNames, fileData.Filename)
|
||||
}
|
||||
assert.ElementsMatch(t, testFiles, rFileNames)
|
||||
assert.ElementsMatch(t, append(expectedFileNames, "warning.txt"), rFileNames)
|
||||
})
|
||||
|
||||
t.Run("steps that generated an error should still return file data", func(t *testing.T) {
|
||||
|
|
@ -320,45 +132,536 @@ func TestGenerateSupportPacket(t *testing.T) {
|
|||
mockStore.On("Webhook").Return(th.App.Srv().Store().Webhook())
|
||||
mockStore.On("System").Return(th.App.Srv().Store().System())
|
||||
mockStore.On("License").Return(th.App.Srv().Store().License())
|
||||
mockStore.On("Command").Return(th.App.Srv().Store().Command())
|
||||
mockStore.On("Role").Return(th.App.Srv().Store().Role())
|
||||
mockStore.On("Scheme").Return(th.App.Srv().Store().Scheme())
|
||||
mockStore.On("Close").Return(nil)
|
||||
mockStore.On("GetDBSchemaVersion").Return(1, nil)
|
||||
mockStore.On("GetDbVersion", false).Return("1.0.0", nil)
|
||||
mockStore.On("TotalMasterDbConnections").Return(30)
|
||||
mockStore.On("TotalReadDbConnections").Return(20)
|
||||
mockStore.On("TotalSearchDbConnections").Return(10)
|
||||
|
||||
oldStore := th.App.Srv().Store()
|
||||
t.Cleanup(func() {
|
||||
th.App.Srv().SetStore(oldStore)
|
||||
})
|
||||
th.App.Srv().SetStore(&mockStore)
|
||||
|
||||
fileDatas := th.App.GenerateSupportPacket(th.Context, &model.SupportPacketOptions{
|
||||
IncludeLogs: false,
|
||||
})
|
||||
rFileNames := getFileNames(t, fileDatas)
|
||||
|
||||
var rFileNames []string
|
||||
for _, fileData := range fileDatas {
|
||||
require.NotNil(t, fileData)
|
||||
assert.Positive(t, len(fileData.Body))
|
||||
|
||||
rFileNames = append(rFileNames, fileData.Filename)
|
||||
}
|
||||
assert.Contains(t, rFileNames, "warning.txt")
|
||||
assert.Contains(t, rFileNames, "support_packet.yaml")
|
||||
assert.Contains(t, rFileNames, "stats.yaml")
|
||||
assert.ElementsMatch(t, append(expectedFileNames, "warning.txt"), rFileNames)
|
||||
})
|
||||
|
||||
pluginID := "testplugin"
|
||||
pluginCode := `
|
||||
package main
|
||||
import (
|
||||
"github.com/mattermost/mattermost/server/public/plugin"
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
type TestPlugin struct {
|
||||
plugin.MattermostPlugin
|
||||
}
|
||||
|
||||
func (p *TestPlugin) GenerateSupportData(c *plugin.Context) ([]*model.FileData, error) {
|
||||
return []*model.FileData{{
|
||||
Filename: "testplugin/diagnostics.yaml",
|
||||
Body: []byte("foo"),
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
plugin.ClientMain(&TestPlugin{})
|
||||
}`
|
||||
|
||||
t.Run("Support Packet always contains plugin data if the plugin doesn't define the support_packet prop", func(t *testing.T) {
|
||||
pluginManifest := `{"id": "testplugin", "server": {"executable": "backend.exe"}}`
|
||||
setupPluginAPITest(t, pluginCode, pluginManifest, pluginID, th.App, th.Context)
|
||||
t.Cleanup(func() {
|
||||
appErr := th.App.ch.RemovePlugin(pluginID)
|
||||
require.Nil(t, appErr)
|
||||
})
|
||||
|
||||
fileDatas := th.App.GenerateSupportPacket(th.Context, &model.SupportPacketOptions{
|
||||
IncludeLogs: false,
|
||||
})
|
||||
rFileNames := getFileNames(t, fileDatas)
|
||||
|
||||
assert.ElementsMatch(t, append(expectedFileNames, "testplugin/diagnostics.yaml"), rFileNames)
|
||||
})
|
||||
|
||||
t.Run("Support Packet contains plugin data if the plugin defines the support_packet prop and it gets queried", func(t *testing.T) {
|
||||
pluginManifest := `{"id": "testplugin", "server": {"executable": "backend.exe"}, "props": {"support_packet": "some text"}}`
|
||||
setupPluginAPITest(t, pluginCode, pluginManifest, pluginID, th.App, th.Context)
|
||||
t.Cleanup(func() {
|
||||
appErr := th.App.ch.RemovePlugin(pluginID)
|
||||
require.Nil(t, appErr)
|
||||
})
|
||||
|
||||
fileDatas := th.App.GenerateSupportPacket(th.Context, &model.SupportPacketOptions{
|
||||
IncludeLogs: false,
|
||||
PluginPackets: []string{pluginID},
|
||||
})
|
||||
rFileNames := getFileNames(t, fileDatas)
|
||||
|
||||
assert.ElementsMatch(t, append(expectedFileNames, "testplugin/diagnostics.yaml"), rFileNames)
|
||||
})
|
||||
|
||||
t.Run("Support Packet doesn't contain plugin data if the plugin defines the support_packet prop and it doesn't get queried", func(t *testing.T) {
|
||||
pluginManifest := `{"id": "testplugin", "server": {"executable": "backend.exe"}, "props": {"support_packet": "some text"}}`
|
||||
setupPluginAPITest(t, pluginCode, pluginManifest, pluginID, th.App, th.Context)
|
||||
t.Cleanup(func() {
|
||||
appErr := th.App.ch.RemovePlugin(pluginID)
|
||||
require.Nil(t, appErr)
|
||||
})
|
||||
|
||||
fileDatas := th.App.GenerateSupportPacket(th.Context, &model.SupportPacketOptions{
|
||||
IncludeLogs: false,
|
||||
})
|
||||
rFileNames := getFileNames(t, fileDatas)
|
||||
|
||||
assert.ElementsMatch(t, expectedFileNames, rFileNames)
|
||||
})
|
||||
|
||||
t.Run("Plugin config values in the Support Packet are obfuscated, if the plugin marks them as secrets", func(t *testing.T) {
|
||||
pluginManifest := `{"id": "testplugin", "server": {"executable": "backend.exe"}, "settings_schema": {"settings": [{"key": "foo", "type": "text"}, {"key": "bar", "type": "text", "secret": true}]}}`
|
||||
setupPluginAPITest(t, pluginCode, pluginManifest, pluginID, th.App, th.Context)
|
||||
t.Cleanup(func() {
|
||||
appErr := th.App.ch.RemovePlugin(pluginID)
|
||||
require.Nil(t, appErr)
|
||||
})
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.PluginSettings.Plugins[pluginID] = map[string]any{
|
||||
"foo": "foo_value",
|
||||
"bar": "bar_value",
|
||||
}
|
||||
})
|
||||
|
||||
fileDatas := th.App.GenerateSupportPacket(th.Context, &model.SupportPacketOptions{
|
||||
IncludeLogs: false,
|
||||
})
|
||||
|
||||
found := false
|
||||
for _, f := range fileDatas {
|
||||
if f.Filename != "sanitized_config.json" {
|
||||
continue
|
||||
}
|
||||
|
||||
var config model.Config
|
||||
err = json.Unmarshal(f.Body, &config)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "foo_value", config.PluginSettings.Plugins[pluginID]["foo"])
|
||||
assert.Equal(t, model.FakeSetting, config.PluginSettings.Plugins[pluginID]["bar"])
|
||||
|
||||
found = true
|
||||
}
|
||||
assert.True(t, found)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateSanitizedConfigFile(t *testing.T) {
|
||||
func TestGetPluginsFile(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
// Happy path where we have a sanitized config file with no err
|
||||
fileData, err := th.App.createSanitizedConfigFile(th.Context)
|
||||
require.NotNil(t, fileData)
|
||||
assert.Equal(t, "sanitized_config.json", fileData.Filename)
|
||||
assert.Positive(t, len(fileData.Body))
|
||||
assert.NoError(t, err)
|
||||
getJobList := func(t *testing.T) *model.SupportPacketPluginList {
|
||||
t.Helper()
|
||||
|
||||
fileData, err := th.App.getPluginsFile(th.Context)
|
||||
assert.NoError(t, err)
|
||||
require.NotNil(t, fileData)
|
||||
assert.Equal(t, "plugins.json", fileData.Filename)
|
||||
assert.Positive(t, len(fileData.Body))
|
||||
|
||||
var pl model.SupportPacketPluginList
|
||||
err = json.Unmarshal(fileData.Body, &pl)
|
||||
require.NoError(t, err)
|
||||
|
||||
return &pl
|
||||
}
|
||||
|
||||
t.Run("no errors if no plugins are installed", func(t *testing.T) {
|
||||
pl := getJobList(t)
|
||||
assert.Len(t, pl.Enabled, 0)
|
||||
assert.Len(t, pl.Disabled, 0)
|
||||
})
|
||||
|
||||
t.Run("two plugins are installed", func(t *testing.T) {
|
||||
path, _ := fileutils.FindDir("tests")
|
||||
|
||||
bundle1, err := os.ReadFile(filepath.Join(path, "testplugin.tar.gz"))
|
||||
require.NoError(t, err)
|
||||
manifest1, appErr := th.App.InstallPlugin(bytes.NewReader(bundle1), false)
|
||||
require.Nil(t, appErr)
|
||||
require.Equal(t, "testplugin", manifest1.Id)
|
||||
appErr = th.App.EnablePlugin(manifest1.Id)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
bundle2, err := os.ReadFile(filepath.Join(path, "testplugin2.tar.gz"))
|
||||
require.NoError(t, err)
|
||||
manifest2, appErr := th.App.InstallPlugin(bytes.NewReader(bundle2), false)
|
||||
require.Nil(t, appErr)
|
||||
require.Equal(t, "testplugin2", manifest2.Id)
|
||||
|
||||
pl := getJobList(t)
|
||||
require.Len(t, pl.Enabled, 1)
|
||||
assert.Equal(t, "testplugin", pl.Enabled[0].Id)
|
||||
require.Len(t, pl.Disabled, 1)
|
||||
assert.Equal(t, "testplugin2", pl.Disabled[0].Id)
|
||||
})
|
||||
|
||||
t.Run("error if plugin are disabled", func(t *testing.T) {
|
||||
// Turn off plugins so we can get an error
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = false
|
||||
})
|
||||
|
||||
// Plugins off in settings so no fileData and we get a warning instead
|
||||
fileData, err := th.App.getPluginsFile(th.Context)
|
||||
assert.Nil(t, fileData)
|
||||
assert.ErrorContains(t, err, "failed to get plugin list for Support Packet")
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateSupportPacketMetadata(t *testing.T) {
|
||||
func TestGetSupportPacketStats(t *testing.T) {
|
||||
th := Setup(t)
|
||||
|
||||
generateStats := func(t *testing.T) *model.SupportPacketStats {
|
||||
t.Helper()
|
||||
|
||||
fileData, err := th.App.getSupportPacketStats(th.Context)
|
||||
require.NotNil(t, fileData)
|
||||
assert.Equal(t, "stats.yaml", fileData.Filename)
|
||||
assert.Positive(t, len(fileData.Body))
|
||||
assert.NoError(t, err)
|
||||
|
||||
var packet model.SupportPacketStats
|
||||
err = yaml.Unmarshal(fileData.Body, &packet)
|
||||
require.NoError(t, err)
|
||||
|
||||
return &packet
|
||||
}
|
||||
|
||||
t.Run("fresh server", func(t *testing.T) {
|
||||
sp := generateStats(t)
|
||||
|
||||
assert.Equal(t, int64(0), sp.RegisteredUsers)
|
||||
assert.Equal(t, int64(0), sp.ActiveUsers)
|
||||
assert.Equal(t, int64(0), sp.DailyActiveUsers)
|
||||
assert.Equal(t, int64(0), sp.MonthlyActiveUsers)
|
||||
assert.Equal(t, int64(0), sp.DeactivatedUsers)
|
||||
assert.Equal(t, int64(0), sp.Guests)
|
||||
assert.Equal(t, int64(0), sp.BotAccounts)
|
||||
assert.Equal(t, int64(0), sp.Posts)
|
||||
assert.Equal(t, int64(0), sp.Channels)
|
||||
assert.Equal(t, int64(0), sp.Teams)
|
||||
assert.Equal(t, int64(0), sp.SlashCommands)
|
||||
assert.Equal(t, int64(0), sp.IncomingWebhooks)
|
||||
assert.Equal(t, int64(0), sp.OutgoingWebhooks)
|
||||
})
|
||||
|
||||
t.Run("Happy path", func(t *testing.T) {
|
||||
var user *model.User
|
||||
for i := 0; i < 4; i++ {
|
||||
user = th.CreateUser()
|
||||
}
|
||||
th.BasicUser = user
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
deactivatedUser := th.CreateUser()
|
||||
require.NotNil(t, deactivatedUser)
|
||||
_, appErr := th.App.UpdateActive(th.Context, deactivatedUser, false)
|
||||
require.Nil(t, appErr)
|
||||
}
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
guest := th.CreateGuest()
|
||||
require.NotNil(t, guest)
|
||||
}
|
||||
|
||||
th.CreateBot()
|
||||
|
||||
team := th.CreateTeam()
|
||||
channel := th.CreateChannel(th.Context, team)
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
p := th.CreatePost(channel)
|
||||
require.NotNil(t, p)
|
||||
}
|
||||
|
||||
cmd, appErr := th.App.CreateCommand(&model.Command{
|
||||
CreatorId: user.Id,
|
||||
TeamId: team.Id,
|
||||
Trigger: "test",
|
||||
Method: model.CommandMethodGet,
|
||||
URL: "http://nowhere.com/",
|
||||
})
|
||||
require.Nil(t, appErr)
|
||||
require.NotNil(t, cmd)
|
||||
|
||||
webhookIn, appErr := th.App.CreateIncomingWebhookForChannel(user.Id, channel, &model.IncomingWebhook{ChannelId: channel.Id})
|
||||
require.Nil(t, appErr)
|
||||
require.NotNil(t, webhookIn)
|
||||
|
||||
webhookOut, appErr := th.App.CreateOutgoingWebhook(&model.OutgoingWebhook{
|
||||
ChannelId: channel.Id,
|
||||
TeamId: channel.TeamId,
|
||||
CreatorId: th.BasicUser.Id,
|
||||
CallbackURLs: []string{"http://nowhere.com/"},
|
||||
})
|
||||
require.Nil(t, appErr)
|
||||
require.NotNil(t, webhookOut)
|
||||
|
||||
sp := generateStats(t)
|
||||
|
||||
assert.Equal(t, int64(9), sp.RegisteredUsers)
|
||||
assert.Equal(t, int64(6), sp.ActiveUsers)
|
||||
assert.Equal(t, int64(0), sp.DailyActiveUsers)
|
||||
assert.Equal(t, int64(0), sp.MonthlyActiveUsers)
|
||||
assert.Equal(t, int64(3), sp.DeactivatedUsers)
|
||||
assert.Equal(t, int64(2), sp.Guests)
|
||||
assert.Equal(t, int64(1), sp.BotAccounts)
|
||||
assert.Equal(t, int64(4), sp.Posts) // 1 from the bot creation and 3 created directly
|
||||
assert.Equal(t, int64(3), sp.Channels) // 2 from the team creation and 1 created directly
|
||||
assert.Equal(t, int64(1), sp.Teams)
|
||||
assert.Equal(t, int64(1), sp.SlashCommands)
|
||||
assert.Equal(t, int64(1), sp.IncomingWebhooks)
|
||||
assert.Equal(t, int64(1), sp.OutgoingWebhooks)
|
||||
})
|
||||
|
||||
// Reset test server
|
||||
th.TearDown()
|
||||
th = Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
t.Run("post count should be present if number of users extends AnalyticsSettings.MaxUsersForStatistics", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.AnalyticsSettings.MaxUsersForStatistics = model.NewPointer(1)
|
||||
})
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
p := th.CreatePost(th.BasicChannel)
|
||||
require.NotNil(t, p)
|
||||
}
|
||||
|
||||
// InitBasic() already creats 5 posts
|
||||
packet := generateStats(t)
|
||||
assert.Equal(t, int64(10), packet.Posts)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetSupportPacketJobList(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
getJobList := func(t *testing.T) *model.SupportPacketJobList {
|
||||
t.Helper()
|
||||
|
||||
fileData, err := th.App.getSupportPacketJobList(th.Context)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, fileData)
|
||||
assert.Equal(t, "jobs.yaml", fileData.Filename)
|
||||
assert.Positive(t, len(fileData.Body))
|
||||
|
||||
var jobs model.SupportPacketJobList
|
||||
err = yaml.Unmarshal(fileData.Body, &jobs)
|
||||
require.NoError(t, err)
|
||||
|
||||
return &jobs
|
||||
}
|
||||
|
||||
t.Run("no jobs run yet", func(t *testing.T) {
|
||||
jobs := getJobList(t)
|
||||
|
||||
assert.Empty(t, jobs.LDAPSyncJobs)
|
||||
assert.Empty(t, jobs.DataRetentionJobs)
|
||||
assert.Empty(t, jobs.MessageExportJobs)
|
||||
assert.Empty(t, jobs.ElasticPostIndexingJobs)
|
||||
assert.Empty(t, jobs.ElasticPostAggregationJobs)
|
||||
assert.Empty(t, jobs.BlevePostIndexingJobs)
|
||||
assert.Empty(t, jobs.MigrationJobs)
|
||||
})
|
||||
|
||||
t.Run("jobs exist", func(t *testing.T) {
|
||||
getJob := func(jobType string) *model.Job {
|
||||
return &model.Job{
|
||||
Id: model.NewId(),
|
||||
Type: jobType,
|
||||
Priority: 1,
|
||||
CreateAt: model.GetMillis() - 1,
|
||||
StartAt: model.GetMillis(),
|
||||
LastActivityAt: model.GetMillis() + 1,
|
||||
Status: model.JobStatusPending,
|
||||
Progress: 51,
|
||||
Data: model.StringMap{"key": "value"},
|
||||
}
|
||||
}
|
||||
// Create some jobs
|
||||
jobsToCreate := []*model.Job{
|
||||
getJob(model.JobTypeLdapSync),
|
||||
getJob(model.JobTypeDataRetention),
|
||||
getJob(model.JobTypeMessageExport),
|
||||
getJob(model.JobTypeElasticsearchPostIndexing),
|
||||
getJob(model.JobTypeElasticsearchPostAggregation),
|
||||
getJob(model.JobTypeBlevePostIndexing),
|
||||
getJob(model.JobTypeMigrations),
|
||||
}
|
||||
|
||||
var expectedJobs []*model.Job
|
||||
for _, job := range jobsToCreate {
|
||||
// Create the job using the store directly.
|
||||
// Creating the job at the job service would error as the workers require enterprise code.
|
||||
rJob, err := th.App.Srv().Store().Job().Save(job)
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedJobs = append(expectedJobs, rJob)
|
||||
}
|
||||
|
||||
jobs := getJobList(t)
|
||||
|
||||
// Helper to verify job content matches
|
||||
verifyJob := func(t *testing.T, expected, actual *model.Job) {
|
||||
t.Helper()
|
||||
assert.Equal(t, expected.Id, actual.Id)
|
||||
assert.Equal(t, expected.Type, actual.Type)
|
||||
assert.Equal(t, expected.Priority, actual.Priority)
|
||||
assert.Equal(t, expected.CreateAt, actual.CreateAt)
|
||||
assert.Equal(t, expected.StartAt, actual.StartAt)
|
||||
assert.Equal(t, expected.LastActivityAt, actual.LastActivityAt)
|
||||
assert.Equal(t, expected.Status, actual.Status)
|
||||
assert.Equal(t, expected.Progress, actual.Progress)
|
||||
assert.Equal(t, expected.Data, actual.Data)
|
||||
}
|
||||
|
||||
// Verify LDAP sync jobs
|
||||
require.Len(t, jobs.LDAPSyncJobs, 1, "Should have 1 LDAP sync job")
|
||||
verifyJob(t, expectedJobs[0], jobs.LDAPSyncJobs[0])
|
||||
|
||||
// Verify data retention jobs
|
||||
require.Len(t, jobs.DataRetentionJobs, 1, "Should have 1 data retention job")
|
||||
verifyJob(t, expectedJobs[1], jobs.DataRetentionJobs[0])
|
||||
|
||||
// Verify message export jobs
|
||||
require.Len(t, jobs.MessageExportJobs, 1, "Should have 1 message export job")
|
||||
verifyJob(t, expectedJobs[2], jobs.MessageExportJobs[0])
|
||||
|
||||
// Verify elasticsearch post indexing jobs
|
||||
require.Len(t, jobs.ElasticPostIndexingJobs, 1, "Should have 1 elasticsearch post indexing job")
|
||||
verifyJob(t, expectedJobs[3], jobs.ElasticPostIndexingJobs[0])
|
||||
|
||||
// Verify elasticsearch post aggregation jobs
|
||||
require.Len(t, jobs.ElasticPostAggregationJobs, 1, "Should have 1 elasticsearch post aggregation job")
|
||||
verifyJob(t, expectedJobs[4], jobs.ElasticPostAggregationJobs[0])
|
||||
|
||||
// Verify bleve post indexing jobs
|
||||
require.Len(t, jobs.BlevePostIndexingJobs, 1, "Should have 1 bleve post indexing job")
|
||||
verifyJob(t, expectedJobs[5], jobs.BlevePostIndexingJobs[0])
|
||||
|
||||
// Verify migration jobs
|
||||
require.Len(t, jobs.MigrationJobs, 1, "Should have 1 migration job")
|
||||
verifyJob(t, expectedJobs[6], jobs.MigrationJobs[0])
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetSupportPacketPermissionsInfo(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.App.SetPhase2PermissionsMigrationStatus(true)
|
||||
|
||||
generatePermissionInfo := func(t *testing.T) *model.SupportPacketPermissionInfo {
|
||||
t.Helper()
|
||||
|
||||
fileData, err := th.App.getSupportPacketPermissionsInfo(th.Context)
|
||||
require.NotNil(t, fileData)
|
||||
assert.Equal(t, "permissions.yaml", fileData.Filename)
|
||||
assert.Positive(t, len(fileData.Body))
|
||||
assert.NoError(t, err)
|
||||
|
||||
var permissions model.SupportPacketPermissionInfo
|
||||
err = yaml.Unmarshal(fileData.Body, &permissions)
|
||||
require.NoError(t, err)
|
||||
|
||||
return &permissions
|
||||
}
|
||||
|
||||
t.Run("No custom permissions", func(t *testing.T) {
|
||||
permissions := generatePermissionInfo(t)
|
||||
|
||||
assert.Len(t, permissions.Roles, 23)
|
||||
assert.Empty(t, permissions.Schemes)
|
||||
})
|
||||
|
||||
scheme, appErr := th.App.CreateScheme(&model.Scheme{
|
||||
Name: "custom_scheme",
|
||||
DisplayName: "Custom Scheme",
|
||||
Scope: model.SchemeScopeTeam,
|
||||
})
|
||||
require.Nil(t, appErr)
|
||||
|
||||
t.Run("with custom scheme", func(t *testing.T) {
|
||||
permissions := generatePermissionInfo(t)
|
||||
|
||||
assert.Len(t, permissions.Roles, 33) // 23 default roles + 10 custom roles from the scheme
|
||||
require.Len(t, permissions.Schemes, 1)
|
||||
assert.Equal(t, scheme.Id, permissions.Schemes[0].Id)
|
||||
assert.Equal(t, model.FakeSetting, permissions.Schemes[0].Name, "Name should be obfuscated")
|
||||
assert.Equal(t, model.FakeSetting, permissions.Schemes[0].DisplayName, "DisplayName should be obfuscated")
|
||||
assert.Equal(t, model.FakeSetting, permissions.Schemes[0].Description, "Description should be obfuscated")
|
||||
})
|
||||
|
||||
t.Run("with custom role", func(t *testing.T) {
|
||||
role, appErr := th.App.CreateRole(&model.Role{
|
||||
Name: "custom_role",
|
||||
DisplayName: "Custom Role",
|
||||
})
|
||||
require.Nil(t, appErr)
|
||||
t.Cleanup(func() {
|
||||
_, appErr := th.App.DeleteRole(role.Id)
|
||||
require.Nil(t, appErr)
|
||||
})
|
||||
|
||||
permissions := generatePermissionInfo(t)
|
||||
|
||||
require.Len(t, permissions.Schemes, 1)
|
||||
require.Len(t, permissions.Roles, 34) // 23 default roles + 10 custom roles from the scheme + 1 custom role
|
||||
found := false
|
||||
for _, r := range permissions.Roles {
|
||||
// Confirm that sensitive fields are obfuscated
|
||||
assert.Equal(t, model.FakeSetting, r.DisplayName, "DisplayName should be obfuscated")
|
||||
assert.Equal(t, model.FakeSetting, r.Description, "Description should be obfuscated")
|
||||
|
||||
// Fine the custom role
|
||||
if r.Id == role.Id {
|
||||
assert.Equal(t, role.Name, r.Name)
|
||||
assert.Equal(t, role.CreateAt, r.CreateAt)
|
||||
assert.Equal(t, role.UpdateAt, r.UpdateAt)
|
||||
assert.Equal(t, role.DeleteAt, r.DeleteAt)
|
||||
assert.Equal(t, role.Permissions, r.Permissions)
|
||||
assert.Equal(t, role.SchemeManaged, r.SchemeManaged)
|
||||
assert.Equal(t, role.BuiltIn, r.BuiltIn)
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, found)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetSupportPacketMetadata(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
t.Run("Happy path", func(t *testing.T) {
|
||||
fileData, err := th.App.createSupportPacketMetadata(th.Context)
|
||||
fileData, err := th.App.getSupportPacketMetadata(th.Context)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, fileData)
|
||||
assert.Equal(t, "metadata.yaml", fileData.Filename)
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ type Client interface {
|
|||
DownloadExport(ctx context.Context, name string, wr io.Writer, offset int64) (int64, *model.Response, error)
|
||||
GeneratePresignedURL(ctx context.Context, name string) (*model.PresignURLResponse, *model.Response, error)
|
||||
ResetSamlAuthDataToEmail(ctx context.Context, includeDeleted bool, dryRun bool, userIDs []string) (int64, *model.Response, error)
|
||||
GenerateSupportPacket(ctx context.Context) ([]byte, *model.Response, error)
|
||||
GenerateSupportPacket(ctx context.Context) (io.ReadCloser, string, *model.Response, error)
|
||||
GetOAuthApps(ctx context.Context, page, perPage int) ([]*model.OAuthApp, *model.Response, error)
|
||||
GetPreferences(ctx context.Context, userId string) (model.Preferences, *model.Response, error)
|
||||
GetPreferencesByCategory(ctx context.Context, userId, category string) (model.Preferences, *model.Response, error)
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ package commands
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
|
|
@ -81,7 +81,7 @@ func init() {
|
|||
SystemSetBusyCmd.Flags().UintP("seconds", "s", 3600, "Number of seconds until server is automatically marked as not busy.")
|
||||
_ = SystemSetBusyCmd.MarkFlagRequired("seconds")
|
||||
|
||||
SystemSupportPacketCmd.Flags().StringP("output-file", "o", "", "Output file name (default \"mattermost_support_packet_YYYY-MM-DD-HH-MM.zip\")")
|
||||
SystemSupportPacketCmd.Flags().StringP("output-file", "o", "", "Define the output file name")
|
||||
|
||||
SystemCmd.AddCommand(
|
||||
SystemGetBusyCmd,
|
||||
|
|
@ -180,23 +180,23 @@ func systemSupportPacketCmdF(c client.Client, cmd *cobra.Command, _ []string) er
|
|||
return err
|
||||
}
|
||||
|
||||
if filename == "" {
|
||||
filename = fmt.Sprintf("mattermost_support_packet_%s.zip", time.Now().Format("2006-01-02-03-04"))
|
||||
}
|
||||
|
||||
printer.Print("Downloading Support Packet")
|
||||
|
||||
data, _, err := c.GenerateSupportPacket(context.TODO())
|
||||
data, rFilename, _, err := c.GenerateSupportPacket(context.TODO())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to fetch Support Packet: %w", err)
|
||||
}
|
||||
|
||||
if filename == "" {
|
||||
filename = rFilename
|
||||
}
|
||||
|
||||
file, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create zip file: %w", err)
|
||||
}
|
||||
|
||||
_, err = file.Write(data)
|
||||
_, err = io.Copy(file, data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write to zip file: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,8 +115,6 @@ func (s *MmctlE2ETestSuite) TestSupportPacketCmdF() {
|
|||
s.Run("Download Support Packet with default filename", func() {
|
||||
printer.Clean()
|
||||
|
||||
s.T().Cleanup(cleanupSupportPacket(s.T()))
|
||||
|
||||
err := systemSupportPacketCmdF(s.th.SystemAdminClient, SystemSupportPacketCmd, []string{})
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(printer.GetLines(), 2)
|
||||
|
|
@ -129,12 +127,17 @@ func (s *MmctlE2ETestSuite) TestSupportPacketCmdF() {
|
|||
entries, err := os.ReadDir(".")
|
||||
s.Require().NoError(err)
|
||||
for _, e := range entries {
|
||||
if strings.HasPrefix(e.Name(), "mattermost_support_packet_") && strings.HasSuffix(e.Name(), ".zip") {
|
||||
if strings.HasPrefix(e.Name(), "mm_support_packet_") && strings.HasSuffix(e.Name(), ".zip") {
|
||||
b, err := os.ReadFile(e.Name())
|
||||
s.NoError(err)
|
||||
|
||||
s.NotEmpty(b, b)
|
||||
|
||||
s.T().Cleanup(func() {
|
||||
err = os.Remove(e.Name())
|
||||
s.Require().NoError(err)
|
||||
})
|
||||
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
|
@ -144,14 +147,16 @@ func (s *MmctlE2ETestSuite) TestSupportPacketCmdF() {
|
|||
s.Run("Download Support Packet with custom filename", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := SystemSupportPacketCmd.ParseFlags([]string{"-o", "foo.zip"})
|
||||
systemSupportPacketCmd := &cobra.Command{}
|
||||
systemSupportPacketCmd.Flags().StringP("output-file", "o", "", "Define the output file name")
|
||||
err := systemSupportPacketCmd.ParseFlags([]string{"-o", "foo.zip"})
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.T().Cleanup(func() {
|
||||
defer func() {
|
||||
s.Require().NoError(os.Remove("foo.zip"))
|
||||
})
|
||||
}()
|
||||
|
||||
err = systemSupportPacketCmdF(s.th.SystemAdminClient, SystemSupportPacketCmd, []string{})
|
||||
err = systemSupportPacketCmdF(s.th.SystemAdminClient, systemSupportPacketCmd, []string{})
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
s.Require().Len(printer.GetLines(), 2)
|
||||
|
|
|
|||
|
|
@ -5,19 +5,17 @@ package commands
|
|||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
)
|
||||
|
||||
|
|
@ -221,19 +219,6 @@ func (s *MmctlUnitTestSuite) TestServerStatusCmd() {
|
|||
})
|
||||
}
|
||||
|
||||
func cleanupSupportPacket(t *testing.T) func() {
|
||||
return func() {
|
||||
entries, err := os.ReadDir(".")
|
||||
require.NoError(t, err)
|
||||
for _, e := range entries {
|
||||
if strings.HasPrefix(e.Name(), "mattermost_support_packet_") && strings.HasSuffix(e.Name(), ".zip") {
|
||||
err = os.Remove(e.Name())
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestSupportPacketCmdF() {
|
||||
printer.SetFormat(printer.FormatPlain)
|
||||
s.T().Cleanup(func() { printer.SetFormat(printer.FormatJSON) })
|
||||
|
|
@ -241,57 +226,51 @@ func (s *MmctlUnitTestSuite) TestSupportPacketCmdF() {
|
|||
s.Run("Download Support Packet with default filename", func() {
|
||||
printer.Clean()
|
||||
|
||||
s.T().Cleanup(cleanupSupportPacket(s.T()))
|
||||
|
||||
data := []byte("some bytes")
|
||||
reader := io.NopCloser(strings.NewReader("some bytes"))
|
||||
s.client.
|
||||
EXPECT().
|
||||
GenerateSupportPacket(context.TODO()).
|
||||
Return(data, &model.Response{}, nil).
|
||||
Return(reader, "mm_support_packet.zip", &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
defer func() {
|
||||
err := os.Remove("mm_support_packet.zip")
|
||||
s.NoError(err)
|
||||
}()
|
||||
|
||||
err := systemSupportPacketCmdF(s.client, SystemSupportPacketCmd, []string{})
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
s.Require().Len(printer.GetLines(), 2)
|
||||
s.Require().Equal(printer.GetLines()[0], "Downloading Support Packet")
|
||||
s.Require().Contains(printer.GetLines()[1], "Downloaded Support Packet to ")
|
||||
s.Require().Equal(printer.GetLines()[1], "Downloaded Support Packet to mm_support_packet.zip")
|
||||
|
||||
var found bool
|
||||
|
||||
entries, err := os.ReadDir(".")
|
||||
s.Require().NoError(err)
|
||||
for _, e := range entries {
|
||||
if strings.HasPrefix(e.Name(), "mattermost_support_packet_") && strings.HasSuffix(e.Name(), ".zip") {
|
||||
b, err := os.ReadFile(e.Name())
|
||||
s.NoError(err)
|
||||
s.Equal(b, data)
|
||||
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
s.True(found)
|
||||
b, err := os.ReadFile("mm_support_packet.zip")
|
||||
s.NoError(err)
|
||||
s.Equal(b, []byte("some bytes"))
|
||||
})
|
||||
|
||||
s.Run("Download Support Packet with custom filename", func() {
|
||||
printer.Clean()
|
||||
|
||||
data := []byte("some bytes")
|
||||
reader := io.NopCloser(strings.NewReader("some bytes"))
|
||||
s.client.
|
||||
EXPECT().
|
||||
GenerateSupportPacket(context.TODO()).
|
||||
Return(data, &model.Response{}, nil).
|
||||
Return(reader, "mm_support_packet.zip", &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := SystemSupportPacketCmd.ParseFlags([]string{"-o", "foo.zip"})
|
||||
systemSupportPacketCmd := &cobra.Command{}
|
||||
systemSupportPacketCmd.Flags().StringP("output-file", "o", "", "Define the output file name")
|
||||
err := systemSupportPacketCmd.ParseFlags([]string{"-o", "foo.zip"})
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.T().Cleanup(func() {
|
||||
s.Require().NoError(os.Remove("foo.zip"))
|
||||
})
|
||||
defer func() {
|
||||
err = os.Remove("foo.zip")
|
||||
s.Require().NoError(err)
|
||||
}()
|
||||
|
||||
err = systemSupportPacketCmdF(s.client, SystemSupportPacketCmd, []string{})
|
||||
err = systemSupportPacketCmdF(s.client, systemSupportPacketCmd, []string{})
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
s.Require().Len(printer.GetLines(), 2)
|
||||
|
|
@ -300,7 +279,7 @@ func (s *MmctlUnitTestSuite) TestSupportPacketCmdF() {
|
|||
|
||||
b, err := os.ReadFile("foo.zip")
|
||||
s.Require().NoError(err)
|
||||
s.Equal(b, data)
|
||||
s.Equal(string(b), "some bytes")
|
||||
})
|
||||
|
||||
s.Run("Request to the server fails", func() {
|
||||
|
|
@ -309,7 +288,7 @@ func (s *MmctlUnitTestSuite) TestSupportPacketCmdF() {
|
|||
s.client.
|
||||
EXPECT().
|
||||
GenerateSupportPacket(context.TODO()).
|
||||
Return(nil, &model.Response{}, errors.New("mock error")).
|
||||
Return(nil, "", &model.Response{}, errors.New("mock error")).
|
||||
Times(1)
|
||||
|
||||
err := systemSupportPacketCmdF(s.client, SystemSupportPacketCmd, []string{})
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ Options
|
|||
::
|
||||
|
||||
-h, --help help for supportpacket
|
||||
-o, --output-file string Output file name (default "mattermost_support_packet_YYYY-MM-DD-HH-MM.zip")
|
||||
-o, --output-file string Define the output file name
|
||||
|
||||
Options inherited from parent commands
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
|
|
|||
|
|
@ -572,13 +572,14 @@ func (mr *MockClientMockRecorder) GeneratePresignedURL(arg0, arg1 interface{}) *
|
|||
}
|
||||
|
||||
// GenerateSupportPacket mocks base method.
|
||||
func (m *MockClient) GenerateSupportPacket(arg0 context.Context) ([]byte, *model.Response, error) {
|
||||
func (m *MockClient) GenerateSupportPacket(arg0 context.Context) (io.ReadCloser, string, *model.Response, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GenerateSupportPacket", arg0)
|
||||
ret0, _ := ret[0].([]byte)
|
||||
ret1, _ := ret[1].(*model.Response)
|
||||
ret2, _ := ret[2].(error)
|
||||
return ret0, ret1, ret2
|
||||
ret0, _ := ret[0].(io.ReadCloser)
|
||||
ret1, _ := ret[1].(string)
|
||||
ret2, _ := ret[2].(*model.Response)
|
||||
ret3, _ := ret[3].(error)
|
||||
return ret0, ret1, ret2, ret3
|
||||
}
|
||||
|
||||
// GenerateSupportPacket indicates an expected call of GenerateSupportPacket.
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ type LdapInterface interface {
|
|||
CheckProviderAttributes(c request.CTX, LS *model.LdapSettings, ouser *model.User, patch *model.UserPatch) string
|
||||
SwitchToLdap(c request.CTX, userID, ldapID, ldapPassword string) *model.AppError
|
||||
StartSynchronizeJob(c request.CTX, waitForJobToFinish bool, includeRemovedMembers bool) (*model.Job, *model.AppError)
|
||||
RunTest(rctx request.CTX) *model.AppError
|
||||
GetAllLdapUsers(c request.CTX) ([]*model.User, *model.AppError)
|
||||
MigrateIDAttribute(c request.CTX, toAttribute string) error
|
||||
GetGroup(rctx request.CTX, groupUID string) (*model.Group, *model.AppError)
|
||||
|
|
@ -26,5 +25,9 @@ type LdapInterface interface {
|
|||
UpdateProfilePictureIfNecessary(request.CTX, model.User, model.Session)
|
||||
GetADLdapIdFromSAMLId(c request.CTX, authData string) string
|
||||
GetSAMLIdFromADLdapId(c request.CTX, authData string) string
|
||||
}
|
||||
|
||||
type LdapDiagnosticInterface interface {
|
||||
RunTest(rctx request.CTX) *model.AppError
|
||||
GetVendorNameAndVendorVersion(rctx request.CTX) (string, string, error)
|
||||
}
|
||||
|
|
|
|||
85
server/einterfaces/mocks/LdapDiagnosticInterface.go
Normal file
85
server/einterfaces/mocks/LdapDiagnosticInterface.go
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
// Code generated by mockery v2.42.2. DO NOT EDIT.
|
||||
|
||||
// Regenerate this file using `make einterfaces-mocks`.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
model "github.com/mattermost/mattermost/server/public/model"
|
||||
request "github.com/mattermost/mattermost/server/public/shared/request"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// LdapDiagnosticInterface is an autogenerated mock type for the LdapDiagnosticInterface type
|
||||
type LdapDiagnosticInterface struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// GetVendorNameAndVendorVersion provides a mock function with given fields: rctx
|
||||
func (_m *LdapDiagnosticInterface) GetVendorNameAndVendorVersion(rctx request.CTX) (string, string, error) {
|
||||
ret := _m.Called(rctx)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetVendorNameAndVendorVersion")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
var r1 string
|
||||
var r2 error
|
||||
if rf, ok := ret.Get(0).(func(request.CTX) (string, string, error)); ok {
|
||||
return rf(rctx)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(request.CTX) string); ok {
|
||||
r0 = rf(rctx)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(request.CTX) string); ok {
|
||||
r1 = rf(rctx)
|
||||
} else {
|
||||
r1 = ret.Get(1).(string)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(2).(func(request.CTX) error); ok {
|
||||
r2 = rf(rctx)
|
||||
} else {
|
||||
r2 = ret.Error(2)
|
||||
}
|
||||
|
||||
return r0, r1, r2
|
||||
}
|
||||
|
||||
// RunTest provides a mock function with given fields: rctx
|
||||
func (_m *LdapDiagnosticInterface) RunTest(rctx request.CTX) *model.AppError {
|
||||
ret := _m.Called(rctx)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RunTest")
|
||||
}
|
||||
|
||||
var r0 *model.AppError
|
||||
if rf, ok := ret.Get(0).(func(request.CTX) *model.AppError); ok {
|
||||
r0 = rf(rctx)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*model.AppError)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// NewLdapDiagnosticInterface creates a new instance of LdapDiagnosticInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewLdapDiagnosticInterface(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *LdapDiagnosticInterface {
|
||||
mock := &LdapDiagnosticInterface{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
|
|
@ -328,41 +328,6 @@ func (_m *LdapInterface) GetUserAttributes(rctx request.CTX, id string, attribut
|
|||
return r0, r1
|
||||
}
|
||||
|
||||
// GetVendorNameAndVendorVersion provides a mock function with given fields: rctx
|
||||
func (_m *LdapInterface) GetVendorNameAndVendorVersion(rctx request.CTX) (string, string, error) {
|
||||
ret := _m.Called(rctx)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetVendorNameAndVendorVersion")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
var r1 string
|
||||
var r2 error
|
||||
if rf, ok := ret.Get(0).(func(request.CTX) (string, string, error)); ok {
|
||||
return rf(rctx)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(request.CTX) string); ok {
|
||||
r0 = rf(rctx)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(request.CTX) string); ok {
|
||||
r1 = rf(rctx)
|
||||
} else {
|
||||
r1 = ret.Get(1).(string)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(2).(func(request.CTX) error); ok {
|
||||
r2 = rf(rctx)
|
||||
} else {
|
||||
r2 = ret.Error(2)
|
||||
}
|
||||
|
||||
return r0, r1, r2
|
||||
}
|
||||
|
||||
// MigrateIDAttribute provides a mock function with given fields: c, toAttribute
|
||||
func (_m *LdapInterface) MigrateIDAttribute(c request.CTX, toAttribute string) error {
|
||||
ret := _m.Called(c, toAttribute)
|
||||
|
|
@ -381,26 +346,6 @@ func (_m *LdapInterface) MigrateIDAttribute(c request.CTX, toAttribute string) e
|
|||
return r0
|
||||
}
|
||||
|
||||
// RunTest provides a mock function with given fields: rctx
|
||||
func (_m *LdapInterface) RunTest(rctx request.CTX) *model.AppError {
|
||||
ret := _m.Called(rctx)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RunTest")
|
||||
}
|
||||
|
||||
var r0 *model.AppError
|
||||
if rf, ok := ret.Get(0).(func(request.CTX) *model.AppError); ok {
|
||||
r0 = rf(rctx)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*model.AppError)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// StartSynchronizeJob provides a mock function with given fields: c, waitForJobToFinish, includeRemovedMembers
|
||||
func (_m *LdapInterface) StartSynchronizeJob(c request.CTX, waitForJobToFinish bool, includeRemovedMembers bool) (*model.Job, *model.AppError) {
|
||||
ret := _m.Called(c, waitForJobToFinish, includeRemovedMembers)
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ require (
|
|||
golang.org/x/term v0.22.0
|
||||
golang.org/x/tools v0.23.0
|
||||
gopkg.in/mail.v2 v2.3.1
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
|
|
@ -234,7 +234,7 @@ require (
|
|||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
modernc.org/gc/v3 v3.0.0-20240722195230-4a140ff9c08e // indirect
|
||||
modernc.org/libc v1.55.3 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
|
|
|
|||
|
|
@ -6586,6 +6586,10 @@
|
|||
"id": "app.role.check_roles_exist.role_not_found",
|
||||
"translation": "The provided role does not exist"
|
||||
},
|
||||
{
|
||||
"id": "app.role.delete.app_error",
|
||||
"translation": "Unable to delete role."
|
||||
},
|
||||
{
|
||||
"id": "app.role.get.app_error",
|
||||
"translation": "Unable to get role."
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ require (
|
|||
golang.org/x/oauth2 v0.21.0
|
||||
golang.org/x/text v0.16.0
|
||||
golang.org/x/tools v0.23.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
|
|
@ -70,7 +70,7 @@ require (
|
|||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
|
||||
// Hack to prevent the willf/bitset module from being upgraded to 1.2.0.
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"net"
|
||||
"net/http"
|
||||
|
|
@ -4778,18 +4779,19 @@ func (c *Client4) GetFileInfosForPostIncludeDeleted(ctx context.Context, postId
|
|||
// General/System Section
|
||||
|
||||
// GenerateSupportPacket generates and downloads a Support Packet.
|
||||
func (c *Client4) GenerateSupportPacket(ctx context.Context) ([]byte, *Response, error) {
|
||||
// It returns a ReadCloser to the packet and the filename. The caller needs to close the ReadCloser.
|
||||
func (c *Client4) GenerateSupportPacket(ctx context.Context) (io.ReadCloser, string, *Response, error) {
|
||||
r, err := c.DoAPIGet(ctx, c.systemRoute()+"/support_packet", "")
|
||||
if err != nil {
|
||||
return nil, BuildResponse(r), err
|
||||
return nil, "", BuildResponse(r), err
|
||||
}
|
||||
defer closeBody(r)
|
||||
|
||||
data, err := io.ReadAll(r.Body)
|
||||
_, params, err := mime.ParseMediaType(r.Header.Get("Content-Disposition"))
|
||||
if err != nil {
|
||||
return nil, BuildResponse(r), NewAppError("GetFile", "model.client.read_job_result_file.app_error", nil, "", r.StatusCode).Wrap(err)
|
||||
return nil, "", BuildResponse(r), fmt.Errorf("could not parse Content-Disposition header: %w", err)
|
||||
}
|
||||
return data, BuildResponse(r), nil
|
||||
|
||||
return r.Body, params["filename"], BuildResponse(r), nil
|
||||
}
|
||||
|
||||
// GetPing will return ok if the running goRoutines are below the threshold and unhealthy for above.
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ package model
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/utils/timeutils"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -103,6 +105,75 @@ func (j *Job) Auditable() map[string]interface{} {
|
|||
}
|
||||
}
|
||||
|
||||
func (j *Job) MarshalYAML() (any, error) {
|
||||
return struct {
|
||||
Id string `yaml:"id"`
|
||||
Type string `yaml:"type"`
|
||||
Priority int64 `yaml:"priority"`
|
||||
CreateAt string `yaml:"create_at"`
|
||||
StartAt string `yaml:"start_at"`
|
||||
LastActivityAt string `yaml:"last_activity_at"`
|
||||
Status string `yaml:"status"`
|
||||
Progress int64 `yaml:"progress"`
|
||||
Data StringMap `yaml:"data"`
|
||||
}{
|
||||
Id: j.Id,
|
||||
Type: j.Type,
|
||||
Priority: j.Priority,
|
||||
CreateAt: timeutils.FormatMillis(j.CreateAt),
|
||||
StartAt: timeutils.FormatMillis(j.StartAt),
|
||||
LastActivityAt: timeutils.FormatMillis(j.LastActivityAt),
|
||||
Status: j.Status,
|
||||
Progress: j.Progress,
|
||||
Data: j.Data,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (j *Job) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
out := struct {
|
||||
Id string `yaml:"id"`
|
||||
Type string `yaml:"type"`
|
||||
Priority int64 `yaml:"priority"`
|
||||
CreateAt string `yaml:"create_at"`
|
||||
StartAt string `yaml:"start_at"`
|
||||
LastActivityAt string `yaml:"last_activity_at"`
|
||||
Status string `yaml:"status"`
|
||||
Progress int64 `yaml:"progress"`
|
||||
Data StringMap `yaml:"data"`
|
||||
}{}
|
||||
|
||||
err := unmarshal(&out)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
createAt, err := timeutils.ParseFormatedMillis(out.CreateAt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
updateAt, err := timeutils.ParseFormatedMillis(out.StartAt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
deleteAt, err := timeutils.ParseFormatedMillis(out.LastActivityAt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*j = Job{
|
||||
Id: out.Id,
|
||||
Type: out.Type,
|
||||
Priority: out.Priority,
|
||||
CreateAt: createAt,
|
||||
StartAt: updateAt,
|
||||
LastActivityAt: deleteAt,
|
||||
Status: out.Status,
|
||||
Progress: out.Progress,
|
||||
Data: out.Data,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *Job) IsValid() *AppError {
|
||||
if !IsValidId(j.Id) {
|
||||
return NewAppError("Job.IsValid", "model.job.is_valid.id.app_error", nil, "id="+j.Id, http.StatusBadRequest)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import (
|
|||
|
||||
"github.com/blang/semver/v4"
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/yaml.v2"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type PluginOption struct {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import (
|
|||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/yaml.v2"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func TestIsValid(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import (
|
|||
"fmt"
|
||||
|
||||
"github.com/blang/semver/v4"
|
||||
"gopkg.in/yaml.v2"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type PacketType string
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/yaml.v2"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func TestPacketMetadataValidate(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ package model
|
|||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/utils/timeutils"
|
||||
)
|
||||
|
||||
// SysconsoleAncillaryPermissions maps the non-sysconsole permissions required by each sysconsole view.
|
||||
|
|
@ -438,6 +440,84 @@ func (r *Role) Auditable() map[string]interface{} {
|
|||
}
|
||||
}
|
||||
|
||||
func (r *Role) Sanitize() {
|
||||
r.DisplayName = FakeSetting
|
||||
r.Description = FakeSetting
|
||||
}
|
||||
|
||||
func (r *Role) MarshalYAML() (any, error) {
|
||||
return struct {
|
||||
Id string `yaml:"id"`
|
||||
Name string `yaml:"name"`
|
||||
DisplayName string `yaml:"display_name"`
|
||||
Description string `yaml:"description"`
|
||||
CreateAt string `yaml:"create_at"`
|
||||
UpdateAt string `yaml:"update_at"`
|
||||
DeleteAt string `yaml:"delete_at"`
|
||||
Permissions []string `yaml:"permissions"`
|
||||
SchemeManaged bool `yaml:"scheme_managed"`
|
||||
BuiltIn bool `yaml:"built_in"`
|
||||
}{
|
||||
Id: r.Id,
|
||||
Name: r.Name,
|
||||
DisplayName: r.DisplayName,
|
||||
Description: r.Description,
|
||||
CreateAt: timeutils.FormatMillis(r.CreateAt),
|
||||
UpdateAt: timeutils.FormatMillis(r.UpdateAt),
|
||||
DeleteAt: timeutils.FormatMillis(r.DeleteAt),
|
||||
Permissions: r.Permissions,
|
||||
SchemeManaged: r.SchemeManaged,
|
||||
BuiltIn: r.BuiltIn,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Role) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
out := struct {
|
||||
Id string `yaml:"id"`
|
||||
Name string `yaml:"name"`
|
||||
DisplayName string `yaml:"display_name"`
|
||||
Description string `yaml:"description"`
|
||||
CreateAt string `yaml:"create_at"`
|
||||
UpdateAt string `yaml:"update_at"`
|
||||
DeleteAt string `yaml:"delete_at"`
|
||||
Permissions []string `yaml:"permissions"`
|
||||
SchemeManaged bool `yaml:"scheme_managed"`
|
||||
BuiltIn bool `yaml:"built_in"`
|
||||
}{}
|
||||
|
||||
err := unmarshal(&out)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
createAt, err := timeutils.ParseFormatedMillis(out.CreateAt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
updateAt, err := timeutils.ParseFormatedMillis(out.UpdateAt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
deleteAt, err := timeutils.ParseFormatedMillis(out.DeleteAt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*r = Role{
|
||||
Id: out.Id,
|
||||
Name: out.Name,
|
||||
DisplayName: out.DisplayName,
|
||||
Description: out.Description,
|
||||
CreateAt: createAt,
|
||||
UpdateAt: updateAt,
|
||||
DeleteAt: deleteAt,
|
||||
Permissions: out.Permissions,
|
||||
SchemeManaged: out.SchemeManaged,
|
||||
BuiltIn: out.BuiltIn,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type RolePatch struct {
|
||||
Permissions *[]string `json:"permissions"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ package model
|
|||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/utils/timeutils"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -62,6 +64,117 @@ func (scheme *Scheme) Auditable() map[string]interface{} {
|
|||
}
|
||||
}
|
||||
|
||||
func (scheme *Scheme) Sanitize() {
|
||||
scheme.Name = FakeSetting
|
||||
scheme.DisplayName = FakeSetting
|
||||
scheme.Description = FakeSetting
|
||||
}
|
||||
|
||||
func (scheme *Scheme) MarshalYAML() (any, error) {
|
||||
return struct {
|
||||
Id string `yaml:"id"`
|
||||
Name string `yaml:"name"`
|
||||
DisplayName string `yaml:"display_name"`
|
||||
Description string `yaml:"description"`
|
||||
CreateAt string `yaml:"create_at"`
|
||||
UpdateAt string `yaml:"update_at"`
|
||||
DeleteAt string `yaml:"delete_at"`
|
||||
Scope string `yaml:"scope"`
|
||||
DefaultTeamAdminRole string `yaml:"default_team_admin_role"`
|
||||
DefaultTeamUserRole string `yaml:"default_team_user_role"`
|
||||
DefaultChannelAdminRole string `yaml:"default_channel_admin_role"`
|
||||
DefaultChannelUserRole string `yaml:"default_channel_user_role"`
|
||||
DefaultTeamGuestRole string `yaml:"default_team_guest_role"`
|
||||
DefaultChannelGuestRole string `yaml:"default_channel_guest_role"`
|
||||
DefaultPlaybookAdminRole string `yaml:"default_playbook_admin_role"`
|
||||
DefaultPlaybookMemberRole string `yaml:"default_playbook_member_role"`
|
||||
DefaultRunAdminRole string `yaml:"default_run_admin_role"`
|
||||
DefaultRunMemberRole string `yaml:"default_run_member_role"`
|
||||
}{
|
||||
Id: scheme.Id,
|
||||
Name: scheme.Name,
|
||||
DisplayName: scheme.DisplayName,
|
||||
Description: scheme.Description,
|
||||
CreateAt: timeutils.FormatMillis(scheme.CreateAt),
|
||||
UpdateAt: timeutils.FormatMillis(scheme.UpdateAt),
|
||||
DeleteAt: timeutils.FormatMillis(scheme.DeleteAt),
|
||||
Scope: scheme.Scope,
|
||||
DefaultTeamAdminRole: scheme.DefaultTeamAdminRole,
|
||||
DefaultTeamUserRole: scheme.DefaultTeamUserRole,
|
||||
DefaultChannelAdminRole: scheme.DefaultChannelAdminRole,
|
||||
DefaultChannelUserRole: scheme.DefaultChannelUserRole,
|
||||
DefaultTeamGuestRole: scheme.DefaultTeamGuestRole,
|
||||
DefaultChannelGuestRole: scheme.DefaultChannelGuestRole,
|
||||
DefaultPlaybookAdminRole: scheme.DefaultPlaybookAdminRole,
|
||||
DefaultPlaybookMemberRole: scheme.DefaultPlaybookMemberRole,
|
||||
DefaultRunAdminRole: scheme.DefaultRunAdminRole,
|
||||
DefaultRunMemberRole: scheme.DefaultRunMemberRole,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (scheme *Scheme) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
out := struct {
|
||||
Id string `yaml:"id"`
|
||||
Name string `yaml:"name"`
|
||||
DisplayName string `yaml:"display_name"`
|
||||
Description string `yaml:"description"`
|
||||
CreateAt string `yaml:"create_at"`
|
||||
UpdateAt string `yaml:"update_at"`
|
||||
DeleteAt string `yaml:"delete_at"`
|
||||
Scope string `yaml:"scope"`
|
||||
DefaultTeamAdminRole string `yaml:"default_team_admin_role"`
|
||||
DefaultTeamUserRole string `yaml:"default_team_user_role"`
|
||||
DefaultChannelAdminRole string `yaml:"default_channel_admin_role"`
|
||||
DefaultChannelUserRole string `yaml:"default_channel_user_role"`
|
||||
DefaultTeamGuestRole string `yaml:"default_team_guest_role"`
|
||||
DefaultChannelGuestRole string `yaml:"default_channel_guest_role"`
|
||||
DefaultPlaybookAdminRole string `yaml:"default_playbook_admin_role"`
|
||||
DefaultPlaybookMemberRole string `yaml:"default_playbook_member_role"`
|
||||
DefaultRunAdminRole string `yaml:"default_run_admin_role"`
|
||||
DefaultRunMemberRole string `yaml:"default_run_member_role"`
|
||||
}{}
|
||||
|
||||
err := unmarshal(&out)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
createAt, err := timeutils.ParseFormatedMillis(out.CreateAt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
updateAt, err := timeutils.ParseFormatedMillis(out.UpdateAt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
deleteAt, err := timeutils.ParseFormatedMillis(out.DeleteAt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*scheme = Scheme{
|
||||
Id: out.Id,
|
||||
Name: out.Name,
|
||||
DisplayName: out.DisplayName,
|
||||
Description: out.Description,
|
||||
CreateAt: createAt,
|
||||
UpdateAt: updateAt,
|
||||
DeleteAt: deleteAt,
|
||||
Scope: out.Scope,
|
||||
DefaultTeamAdminRole: out.DefaultTeamAdminRole,
|
||||
DefaultTeamUserRole: out.DefaultTeamUserRole,
|
||||
DefaultChannelAdminRole: out.DefaultChannelAdminRole,
|
||||
DefaultChannelUserRole: out.DefaultChannelUserRole,
|
||||
DefaultTeamGuestRole: out.DefaultTeamGuestRole,
|
||||
DefaultChannelGuestRole: out.DefaultChannelGuestRole,
|
||||
DefaultPlaybookAdminRole: out.DefaultPlaybookAdminRole,
|
||||
DefaultPlaybookMemberRole: out.DefaultPlaybookMemberRole,
|
||||
DefaultRunAdminRole: out.DefaultRunAdminRole,
|
||||
DefaultRunMemberRole: out.DefaultRunMemberRole,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type SchemePatch struct {
|
||||
Name *string `json:"name"`
|
||||
DisplayName *string `json:"display_name"`
|
||||
|
|
|
|||
|
|
@ -9,72 +9,120 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
SupportPacketErrorFile = "warning.txt"
|
||||
CurrentSupportPacketVersion = 1
|
||||
SupportPacketErrorFile = "warning.txt"
|
||||
)
|
||||
|
||||
type SupportPacket struct {
|
||||
/* Build information */
|
||||
type SupportPacketDiagnostics struct {
|
||||
Version int `yaml:"version"`
|
||||
|
||||
ServerOS string `yaml:"server_os"`
|
||||
ServerArchitecture string `yaml:"server_architecture"`
|
||||
ServerVersion string `yaml:"server_version"`
|
||||
BuildHash string `yaml:"build_hash"`
|
||||
License struct {
|
||||
Company string `yaml:"company"`
|
||||
Users int `yaml:"users"`
|
||||
SkuShortName string `yaml:"sku_short_name"`
|
||||
IsTrial bool `yaml:"is_trial,omitempty"`
|
||||
IsGovSKU bool `yaml:"is_gov_sku,omitempty"`
|
||||
} `yaml:"license"`
|
||||
|
||||
/* DB */
|
||||
Server struct {
|
||||
OS string `yaml:"os"`
|
||||
Architecture string `yaml:"architecture"`
|
||||
Hostname string `yaml:"hostname"`
|
||||
Version string `yaml:"version"`
|
||||
BuildHash string `yaml:"build_hash"`
|
||||
InstallationType string `yaml:"installation_type"`
|
||||
} `yaml:"server"`
|
||||
|
||||
DatabaseType string `yaml:"database_type"`
|
||||
DatabaseVersion string `yaml:"database_version"`
|
||||
DatabaseSchemaVersion string `yaml:"database_schema_version"`
|
||||
WebsocketConnections int `yaml:"websocket_connections"`
|
||||
MasterDbConnections int `yaml:"master_db_connections"`
|
||||
ReplicaDbConnections int `yaml:"read_db_connections"`
|
||||
Config struct {
|
||||
Source string `yaml:"store_type"`
|
||||
} `yaml:"config"`
|
||||
|
||||
/* Cluster */
|
||||
Database struct {
|
||||
Type string `yaml:"type"`
|
||||
Version string `yaml:"version"`
|
||||
SchemaVersion string `yaml:"schema_version"`
|
||||
MasterConnectios int `yaml:"master_connections"`
|
||||
ReplicaConnectios int `yaml:"replica_connections"`
|
||||
SearchConnections int `yaml:"search_connections"`
|
||||
} `yaml:"database"`
|
||||
|
||||
ClusterID string `yaml:"cluster_id"`
|
||||
FileStore struct {
|
||||
Status string `yaml:"file_status"`
|
||||
Error string `yaml:"erorr,omitempty"`
|
||||
Driver string `yaml:"file_driver"`
|
||||
} `yaml:"file_store"`
|
||||
|
||||
/* File store */
|
||||
Websocket struct {
|
||||
Connections int `yaml:"connections"`
|
||||
} `yaml:"websocket"`
|
||||
|
||||
FileDriver string `yaml:"file_driver"`
|
||||
FileStatus string `yaml:"file_status"`
|
||||
Cluster struct {
|
||||
ID string `yaml:"id"`
|
||||
NumberOfNodes int `yaml:"number_of_nodes"`
|
||||
} `yaml:"cluster"`
|
||||
|
||||
/* LDAP */
|
||||
LDAP struct {
|
||||
Status string `yaml:"status,omitempty"`
|
||||
Error string `yaml:"erorr,omitempty"`
|
||||
ServerName string `yaml:"server_name,omitempty"`
|
||||
ServerVersion string `yaml:"server_version,omitempty"`
|
||||
} `yaml:"ldap"`
|
||||
|
||||
LdapVendorName string `yaml:"ldap_vendor_name,omitempty"`
|
||||
LdapVendorVersion string `yaml:"ldap_vendor_version,omitempty"`
|
||||
ElasticSearch struct {
|
||||
ServerVersion string `yaml:"server_version,omitempty"`
|
||||
ServerPlugins []string `yaml:"server_plugins,omitempty"`
|
||||
} `yaml:"elastic"`
|
||||
}
|
||||
|
||||
/* Elastic Search */
|
||||
|
||||
ElasticServerVersion string `yaml:"elastic_server_version,omitempty"`
|
||||
ElasticServerPlugins []string `yaml:"elastic_server_plugins,omitempty"`
|
||||
|
||||
/* License */
|
||||
|
||||
LicenseTo string `yaml:"license_to"`
|
||||
LicenseSupportedUsers int `yaml:"license_supported_users"`
|
||||
LicenseIsTrial bool `yaml:"license_is_trial,omitempty"`
|
||||
|
||||
/* Server stats */
|
||||
|
||||
ActiveUsers int `yaml:"active_users"`
|
||||
DailyActiveUsers int `yaml:"daily_active_users"`
|
||||
MonthlyActiveUsers int `yaml:"monthly_active_users"`
|
||||
InactiveUserCount int `yaml:"inactive_user_count"`
|
||||
TotalPosts int `yaml:"total_posts"`
|
||||
TotalChannels int `yaml:"total_channels"`
|
||||
TotalTeams int `yaml:"total_teams"`
|
||||
|
||||
/* Jobs */
|
||||
type SupportPacketStats struct {
|
||||
RegisteredUsers int64 `yaml:"registered_users"`
|
||||
ActiveUsers int64 `yaml:"active_users"`
|
||||
DailyActiveUsers int64 `yaml:"daily_active_users"`
|
||||
MonthlyActiveUsers int64 `yaml:"monthly_active_users"`
|
||||
DeactivatedUsers int64 `yaml:"deactivated_users"`
|
||||
Guests int64 `yaml:"guests"`
|
||||
BotAccounts int64 `yaml:"bot_accounts"`
|
||||
Posts int64 `yaml:"posts"`
|
||||
Channels int64 `yaml:"channels"`
|
||||
Teams int64 `yaml:"teams"`
|
||||
SlashCommands int64 `yaml:"slash_commands"`
|
||||
IncomingWebhooks int64 `yaml:"incoming_webhooks"`
|
||||
OutgoingWebhooks int64 `yaml:"outgoing_webhooks"`
|
||||
}
|
||||
|
||||
// SupportPacketJobList contains the list of latest run enterprise job runs.
|
||||
// It is included in the Support Packet.
|
||||
type SupportPacketJobList struct {
|
||||
LDAPSyncJobs []*Job `yaml:"ldap_sync_jobs"`
|
||||
DataRetentionJobs []*Job `yaml:"data_retention_jobs"`
|
||||
MessageExportJobs []*Job `yaml:"message_export_jobs"`
|
||||
ElasticPostIndexingJobs []*Job `yaml:"elastic_post_indexing_jobs"`
|
||||
ElasticPostAggregationJobs []*Job `yaml:"elastic_post_aggregation_jobs"`
|
||||
BlevePostIndexingJobs []*Job `yaml:"bleve_post_indexin_jobs"`
|
||||
LdapSyncJobs []*Job `yaml:"ldap_sync_jobs"`
|
||||
MigrationJobs []*Job `yaml:"migration_jobs"`
|
||||
}
|
||||
|
||||
// SupportPacketPermissionInfo contains the list of schemes and the list of roles.
|
||||
// It is included in the Support Packet.
|
||||
type SupportPacketPermissionInfo struct {
|
||||
Roles []*Role `yaml:"roles"`
|
||||
Schemes []*Scheme `yaml:"schemes"`
|
||||
}
|
||||
|
||||
// SupportPacketConfig contains the Mattermost configuration. In contrast to [Config], it also contains the list of Feature Flags.
|
||||
// It is included in the Support Packet.
|
||||
type SupportPacketConfig struct {
|
||||
*Config
|
||||
FeatureFlags FeatureFlags `json:"FeatureFlags"`
|
||||
}
|
||||
|
||||
// SupportPacketPluginList contains the list of enabled and disabled plugins.
|
||||
// It is included in the Support Packet.
|
||||
type SupportPacketPluginList struct {
|
||||
Enabled []Manifest `json:"enabled"`
|
||||
Disabled []Manifest `json:"disabled"`
|
||||
}
|
||||
|
||||
type FileData struct {
|
||||
Filename string
|
||||
Body []byte
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import (
|
|||
|
||||
"github.com/blang/semver/v4"
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/yaml.v2"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/plugin"
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import (
|
|||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/yaml.v2"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/plugin/plugintest"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ package utils
|
|||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func CommonBaseSearchPaths() []string {
|
||||
|
|
@ -111,3 +113,25 @@ func FindDirRelBinary(dir string) (string, bool) {
|
|||
}
|
||||
return found, true
|
||||
}
|
||||
|
||||
// Valid characters are: alphanumeric, dash, underscore
|
||||
var safeFileNameRegex = regexp.MustCompile(`[^\w\-\_]`)
|
||||
|
||||
// SanitizeFileName takes a string and returns a safe file name without an extension.
|
||||
func SanitizeFileName(input string) string {
|
||||
// Trim leading or trailing dots or spaces
|
||||
safeName := strings.Trim(input, ". ")
|
||||
// Replace dots with nothing
|
||||
safeName = strings.ReplaceAll(safeName, ".", "")
|
||||
|
||||
// Replace all invalid characters with an underscore
|
||||
safeName = safeFileNameRegex.ReplaceAllString(safeName, "_")
|
||||
|
||||
// Limit length
|
||||
const maxLength = 100
|
||||
if len(safeName) > maxLength {
|
||||
safeName = safeName[:maxLength]
|
||||
}
|
||||
|
||||
return safeName
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,12 +7,69 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSanitizeFileName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "normal characters",
|
||||
input: "normal-file_name",
|
||||
expected: "normal-file_name",
|
||||
},
|
||||
{
|
||||
name: "special characters",
|
||||
input: "file*name@#$",
|
||||
expected: "file_name___",
|
||||
},
|
||||
{
|
||||
name: "spaces",
|
||||
input: "my file name",
|
||||
expected: "my_file_name",
|
||||
},
|
||||
{
|
||||
name: "leading/trailing dots and spaces",
|
||||
input: " .filename. ",
|
||||
expected: "filename",
|
||||
},
|
||||
{
|
||||
name: "very long filename",
|
||||
input: strings.Repeat("a", 150) + ".txt",
|
||||
expected: strings.Repeat("a", 100),
|
||||
},
|
||||
{
|
||||
name: "unicode characters",
|
||||
input: "résumé",
|
||||
expected: "r_sum_",
|
||||
},
|
||||
{
|
||||
name: "german umlaute",
|
||||
input: "äëïöüß",
|
||||
expected: "______",
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
input: "",
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := SanitizeFileName(tt.input)
|
||||
assert.Equal(t, tt.expected, result, tt.name)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindFile(t *testing.T) {
|
||||
t.Run("files from various paths", func(t *testing.T) {
|
||||
// Create the following directory structure:
|
||||
|
|
|
|||
29
server/public/utils/timeutils/time.go
Normal file
29
server/public/utils/timeutils/time.go
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package timeutils
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
RFC3339Milli = "2006-01-02T15:04:05.999Z07:00"
|
||||
)
|
||||
|
||||
func FormatMillis(millis int64) string {
|
||||
return time.UnixMilli(millis).Format(RFC3339Milli)
|
||||
}
|
||||
|
||||
func ParseFormatedMillis(s string) (millis int64, err error) {
|
||||
if s == "" {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
t, err := time.Parse(RFC3339Milli, s)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return t.UnixMilli(), nil
|
||||
}
|
||||
57
server/public/utils/timeutils/time_test.go
Normal file
57
server/public/utils/timeutils/time_test.go
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package timeutils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFormatMillis(t *testing.T) {
|
||||
t.Run("zero time", func(t *testing.T) {
|
||||
result := FormatMillis(0)
|
||||
// The concrete time depends on the timezone, so we can't test the exact time
|
||||
assert.Contains(t, result, "1970-01-01")
|
||||
assert.Contains(t, result, "00:00")
|
||||
})
|
||||
|
||||
t.Run("positive time", func(t *testing.T) {
|
||||
result := FormatMillis(1609459200000) // 2021-01-01 00:00:00 UTC
|
||||
assert.Contains(t, result, "2021-01-01")
|
||||
assert.Contains(t, result, "00:00")
|
||||
})
|
||||
|
||||
t.Run("negative time", func(t *testing.T) {
|
||||
result := FormatMillis(-1609459200000) // 1919-01-01 00:00:00 UTC
|
||||
assert.Contains(t, result, "1919-01-01")
|
||||
assert.Contains(t, result, "00:00")
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseFormatedMillis(t *testing.T) {
|
||||
t.Run("empty string", func(t *testing.T) {
|
||||
result, err := ParseFormatedMillis("")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(0), result)
|
||||
})
|
||||
|
||||
t.Run("valid timestamp", func(t *testing.T) {
|
||||
result, err := ParseFormatedMillis("2021-01-01T00:00:00.000Z")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(1609459200000), result)
|
||||
})
|
||||
|
||||
t.Run("invalid format", func(t *testing.T) {
|
||||
result, err := ParseFormatedMillis("2021-01-01")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, int64(0), result)
|
||||
})
|
||||
|
||||
t.Run("invalid date", func(t *testing.T) {
|
||||
result, err := ParseFormatedMillis("2021-13-01T00:00:00.000Z")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, int64(0), result)
|
||||
})
|
||||
}
|
||||
|
|
@ -92,8 +92,8 @@ export default class CommercialSupportModal extends React.PureComponent<Props, S
|
|||
|
||||
extractFilename = (input: string | null): string => {
|
||||
// construct the expected filename in case of an error in the header
|
||||
const formattedDate = (moment(new Date())).format('YYYY-MM-DD-HH-mm');
|
||||
const presumedFileName = `mattermost_support_packet_${formattedDate}.zip`;
|
||||
const formattedDate = (moment(new Date())).format('YYYY-MM-DDTHH-mm');
|
||||
const presumedFileName = `mm_support_packet_${formattedDate}.zip`;
|
||||
|
||||
if (input === null) {
|
||||
return presumedFileName;
|
||||
|
|
|
|||
Loading…
Reference in a new issue