[MM-54593] HA aware Support Packet (#27598)
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
3f4b8e8137
Коммит
1158e6358c
@@ -1138,7 +1138,7 @@
|
|||||||
- name: plugin_packets
|
- name: plugin_packets
|
||||||
in: query
|
in: query
|
||||||
description: |
|
description: |
|
||||||
Specifies plugin identifiers whose content should be included in the support packet.
|
Specifies plugin identifiers whose content should be included in the Support Packet.
|
||||||
|
|
||||||
__Minimum server version__: 9.8.0
|
__Minimum server version__: 9.8.0
|
||||||
required: false
|
required: false
|
||||||
|
|||||||
@@ -49,6 +49,6 @@ const goToSupportPacketGenerationModal = () => {
|
|||||||
cy.findByRole('button', {name: 'Menu Icon'}).should('exist').click();
|
cy.findByRole('button', {name: 'Menu Icon'}).should('exist').click();
|
||||||
cy.findByRole('button', {name: 'Commercial Support dialog'}).click();
|
cy.findByRole('button', {name: 'Commercial Support dialog'}).click();
|
||||||
|
|
||||||
// * Ensure the download support packet button exist and that text regarding setting the proper settings exist
|
// * Ensure the download Support Packet button exist and that text regarding setting the proper settings exist
|
||||||
cy.get('a.DownloadSupportPacket').should('exist');
|
cy.get('a.DownloadSupportPacket').should('exist');
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import (
|
|||||||
"github.com/mattermost/mattermost/server/public/model"
|
"github.com/mattermost/mattermost/server/public/model"
|
||||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||||
"github.com/mattermost/mattermost/server/v8/channels/audit"
|
"github.com/mattermost/mattermost/server/v8/channels/audit"
|
||||||
|
"github.com/mattermost/mattermost/server/v8/config"
|
||||||
"github.com/mattermost/mattermost/server/v8/platform/services/cache"
|
"github.com/mattermost/mattermost/server/v8/platform/services/cache"
|
||||||
"github.com/mattermost/mattermost/server/v8/platform/services/upgrader"
|
"github.com/mattermost/mattermost/server/v8/platform/services/upgrader"
|
||||||
"github.com/mattermost/mattermost/server/v8/platform/shared/web"
|
"github.com/mattermost/mattermost/server/v8/platform/shared/web"
|
||||||
@@ -84,7 +85,7 @@ func generateSupportPacket(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Support packet generation is limited to system admins (MM-42271).
|
// Support Packet generation is limited to system admins (MM-42271).
|
||||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
||||||
c.SetPermissionError(model.PermissionManageSystem)
|
c.SetPermissionError(model.PermissionManageSystem)
|
||||||
return
|
return
|
||||||
@@ -427,14 +428,14 @@ func downloadLogs(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fileData, err := c.App.GetMattermostLog(c.AppContext)
|
fileData, err := c.App.Srv().Platform().GetLogFile(c.AppContext)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Err = model.NewAppError("downloadLogs", "api.system.logs.download_bytes_buffer.app_error", nil, err.Error(), http.StatusInternalServerError)
|
c.Err = model.NewAppError("downloadLogs", "api.system.logs.download_bytes_buffer.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
reader := bytes.NewReader(fileData.Body)
|
reader := bytes.NewReader(fileData.Body)
|
||||||
web.WriteFileResponse("mattermost.log",
|
web.WriteFileResponse(config.LogFilename,
|
||||||
"text/plain",
|
"text/plain",
|
||||||
int64(len(fileData.Body)),
|
int64(len(fileData.Body)),
|
||||||
time.Now(),
|
time.Now(),
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ func TestGenerateSupportPacket(t *testing.T) {
|
|||||||
th.LoginSystemManager()
|
th.LoginSystemManager()
|
||||||
defer th.TearDown()
|
defer th.TearDown()
|
||||||
|
|
||||||
t.Run("system admin and local client can generate support packet", func(t *testing.T) {
|
t.Run("system admin and local client can generate Support Packet", func(t *testing.T) {
|
||||||
l := model.NewTestLicense()
|
l := model.NewTestLicense()
|
||||||
th.App.Srv().SetLicense(l)
|
th.App.Srv().SetLicense(l)
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ func (a *App) getAnalytics(rctx request.CTX, name string, teamID string, forSupp
|
|||||||
}
|
}
|
||||||
|
|
||||||
skipIntensiveQueries := false
|
skipIntensiveQueries := false
|
||||||
// When generating a Support packet, always run intensive queries.
|
// When generating a Support Packet, always run intensive queries.
|
||||||
if !forSupportPacket {
|
if !forSupportPacket {
|
||||||
if systemUserCount > int64(*a.Config().AnalyticsSettings.MaxUsersForStatistics) {
|
if systemUserCount > int64(*a.Config().AnalyticsSettings.MaxUsersForStatistics) {
|
||||||
rctx.Logger().Debug("More than limit users are on the system, intensive queries skipped", mlog.Int("limit", *a.Config().AnalyticsSettings.MaxUsersForStatistics))
|
rctx.Logger().Debug("More than limit users are on the system, intensive queries skipped", mlog.Int("limit", *a.Config().AnalyticsSettings.MaxUsersForStatistics))
|
||||||
|
|||||||
@@ -731,7 +731,6 @@ type AppIface interface {
|
|||||||
GetLatestVersion(rctx request.CTX, latestVersionUrl string) (*model.GithubReleaseInfo, *model.AppError)
|
GetLatestVersion(rctx request.CTX, latestVersionUrl string) (*model.GithubReleaseInfo, *model.AppError)
|
||||||
GetLogs(rctx request.CTX, page, perPage int) ([]string, *model.AppError)
|
GetLogs(rctx request.CTX, page, perPage int) ([]string, *model.AppError)
|
||||||
GetLogsSkipSend(rctx request.CTX, page, perPage int, logFilter *model.LogFilter) ([]string, *model.AppError)
|
GetLogsSkipSend(rctx request.CTX, page, perPage int, logFilter *model.LogFilter) ([]string, *model.AppError)
|
||||||
GetMattermostLog(ctx request.CTX) (*model.FileData, error)
|
|
||||||
GetMemberCountsByGroup(rctx request.CTX, channelID string, includeTimezones bool) ([]*model.ChannelMemberCountByGroup, *model.AppError)
|
GetMemberCountsByGroup(rctx request.CTX, channelID string, includeTimezones bool) ([]*model.ChannelMemberCountByGroup, *model.AppError)
|
||||||
GetMessageForNotification(post *model.Post, teamName, siteUrl string, translateFunc i18n.TranslateFunc) string
|
GetMessageForNotification(post *model.Post, teamName, siteUrl string, translateFunc i18n.TranslateFunc) string
|
||||||
GetMultipleEmojiByName(c request.CTX, names []string) ([]*model.Emoji, *model.AppError)
|
GetMultipleEmojiByName(c request.CTX, names []string) ([]*model.Emoji, *model.AppError)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
"github.com/mattermost/mattermost/server/public/model"
|
||||||
|
"github.com/mattermost/mattermost/server/public/shared/request"
|
||||||
"github.com/mattermost/mattermost/server/v8/einterfaces"
|
"github.com/mattermost/mattermost/server/v8/einterfaces"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -146,6 +147,9 @@ func (c *ClusterMock) GetLogs(page, perPage int) ([]string, *model.AppError) {
|
|||||||
func (c *ClusterMock) QueryLogs(page, perPage int) (map[string][]string, *model.AppError) {
|
func (c *ClusterMock) QueryLogs(page, perPage int) (map[string][]string, *model.AppError) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
func (c *ClusterMock) GenerateSupportPacket(rctx request.CTX, options *model.SupportPacketOptions) (map[string][]model.FileData, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
func (c *ClusterMock) GetPluginStatuses() (model.PluginStatuses, *model.AppError) { return nil, nil }
|
func (c *ClusterMock) GetPluginStatuses() (model.PluginStatuses, *model.AppError) { return nil, nil }
|
||||||
func (c *ClusterMock) ConfigChanged(previousConfig *model.Config, newConfig *model.Config, sendToOtherServer bool) *model.AppError {
|
func (c *ClusterMock) ConfigChanged(previousConfig *model.Config, newConfig *model.Config, sendToOtherServer bool) *model.AppError {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -7660,28 +7660,6 @@ func (a *OpenTracingAppLayer) GetMarketplacePlugins(rctx request.CTX, filter *mo
|
|||||||
return resultVar0, resultVar1
|
return resultVar0, resultVar1
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *OpenTracingAppLayer) GetMattermostLog(ctx request.CTX) (*model.FileData, error) {
|
|
||||||
origCtx := a.ctx
|
|
||||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetMattermostLog")
|
|
||||||
|
|
||||||
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.GetMattermostLog(ctx)
|
|
||||||
|
|
||||||
if resultVar1 != nil {
|
|
||||||
span.LogFields(spanlog.Error(resultVar1))
|
|
||||||
ext.Error.Set(span, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
return resultVar0, resultVar1
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *OpenTracingAppLayer) GetMemberCountsByGroup(rctx request.CTX, channelID string, includeTimezones bool) ([]*model.ChannelMemberCountByGroup, *model.AppError) {
|
func (a *OpenTracingAppLayer) GetMemberCountsByGroup(rctx request.CTX, channelID string, includeTimezones bool) ([]*model.ChannelMemberCountByGroup, *model.AppError) {
|
||||||
origCtx := a.ctx
|
origCtx := a.ctx
|
||||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetMemberCountsByGroup")
|
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetMemberCountsByGroup")
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
"github.com/mattermost/mattermost/server/public/model"
|
||||||
|
"github.com/mattermost/mattermost/server/public/shared/request"
|
||||||
"github.com/mattermost/mattermost/server/v8/einterfaces"
|
"github.com/mattermost/mattermost/server/v8/einterfaces"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -144,6 +145,9 @@ func (c *ClusterMock) GetLogs(page, perPage int) ([]string, *model.AppError)
|
|||||||
func (c *ClusterMock) QueryLogs(page, perPage int) (map[string][]string, *model.AppError) {
|
func (c *ClusterMock) QueryLogs(page, perPage int) (map[string][]string, *model.AppError) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
func (c *ClusterMock) GenerateSupportPacket(rctx request.CTX, options *model.SupportPacketOptions) (map[string][]model.FileData, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
func (c *ClusterMock) GetPluginStatuses() (model.PluginStatuses, *model.AppError) { return nil, nil }
|
func (c *ClusterMock) GetPluginStatuses() (model.PluginStatuses, *model.AppError) { return nil, nil }
|
||||||
func (c *ClusterMock) ConfigChanged(previousConfig *model.Config, newConfig *model.Config, sendToOtherServer bool) *model.AppError {
|
func (c *ClusterMock) ConfigChanged(previousConfig *model.Config, newConfig *model.Config, sendToOtherServer bool) *model.AppError {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -6,14 +6,16 @@ package platform
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
"github.com/mattermost/mattermost/server/public/model"
|
||||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||||
|
"github.com/mattermost/mattermost/server/public/shared/request"
|
||||||
"github.com/mattermost/mattermost/server/v8/config"
|
"github.com/mattermost/mattermost/server/v8/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -206,6 +208,40 @@ func (ps *PlatformService) GetLogsSkipSend(page, perPage int, logFilter *model.L
|
|||||||
return lines, nil
|
return lines, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ps *PlatformService) GetLogFile(_ request.CTX) (*model.FileData, error) {
|
||||||
|
if !*ps.Config().LogSettings.EnableFile {
|
||||||
|
return nil, errors.New("Unable to retrieve mattermost logs because LogSettings.EnableFile is set to false")
|
||||||
|
}
|
||||||
|
|
||||||
|
mattermostLog := config.GetLogFileLocation(*ps.Config().LogSettings.FileLocation)
|
||||||
|
mattermostLogFileData, err := os.ReadFile(mattermostLog)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "failed read mattermost log file at path %s", mattermostLog)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &model.FileData{
|
||||||
|
Filename: config.LogFilename,
|
||||||
|
Body: mattermostLogFileData,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ps *PlatformService) GetNotificationLogFile(_ request.CTX) (*model.FileData, error) {
|
||||||
|
if !*ps.Config().NotificationLogSettings.EnableFile {
|
||||||
|
return nil, errors.New("Unable to retrieve notifications logs because NotificationLogSettings.EnableFile is set to false")
|
||||||
|
}
|
||||||
|
|
||||||
|
notificationsLog := config.GetNotificationsLogFileLocation(*ps.Config().LogSettings.FileLocation)
|
||||||
|
notificationsLogFileData, err := os.ReadFile(notificationsLog)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "failed read notifcation log file at path %s", notificationsLog)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &model.FileData{
|
||||||
|
Filename: config.LogNotificationFilename,
|
||||||
|
Body: notificationsLogFileData,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func isLogFilteredByLevel(logFilter *model.LogFilter, entry *model.LogEntry) bool {
|
func isLogFilteredByLevel(logFilter *model.LogFilter, entry *model.LogEntry) bool {
|
||||||
logLevels := logFilter.LogLevels
|
logLevels := logFilter.LogLevels
|
||||||
if len(logLevels) == 0 {
|
if len(logLevels) == 0 {
|
||||||
|
|||||||
104
server/channels/app/platform/log_test.go
Обычный файл
104
server/channels/app/platform/log_test.go
Обычный файл
@@ -0,0 +1,104 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
package platform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mattermost/mattermost/server/public/model"
|
||||||
|
"github.com/mattermost/mattermost/server/v8/config"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetMattermostLog(t *testing.T) {
|
||||||
|
th := Setup(t)
|
||||||
|
defer th.TearDown()
|
||||||
|
|
||||||
|
// disable mattermost log file setting in config so we should get an warning
|
||||||
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
||||||
|
*cfg.LogSettings.EnableFile = false
|
||||||
|
})
|
||||||
|
|
||||||
|
fileData, err := th.Service.GetLogFile(th.Context)
|
||||||
|
assert.Nil(t, fileData)
|
||||||
|
assert.ErrorContains(t, err, "Unable to retrieve mattermost logs because LogSettings.EnableFile is set to false")
|
||||||
|
|
||||||
|
dir, err := os.MkdirTemp("", "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
err = os.RemoveAll(dir)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Enable log file but point to an empty directory to get an error trying to read the file
|
||||||
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
||||||
|
*cfg.LogSettings.EnableFile = true
|
||||||
|
*cfg.LogSettings.FileLocation = dir
|
||||||
|
})
|
||||||
|
|
||||||
|
logLocation := config.GetLogFileLocation(dir)
|
||||||
|
|
||||||
|
// There is no mattermost.log file yet, so this fails
|
||||||
|
fileData, err = th.Service.GetLogFile(th.Context)
|
||||||
|
assert.Nil(t, fileData)
|
||||||
|
assert.ErrorContains(t, err, "failed read mattermost log file at path "+logLocation)
|
||||||
|
|
||||||
|
// Happy path where we get a log file and no warning
|
||||||
|
d1 := []byte("hello\ngo\n")
|
||||||
|
err = os.WriteFile(logLocation, d1, 0777)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
fileData, err = th.Service.GetLogFile(th.Context)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, fileData)
|
||||||
|
assert.Equal(t, "mattermost.log", fileData.Filename)
|
||||||
|
assert.Positive(t, len(fileData.Body))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetNotificationLogFile(t *testing.T) {
|
||||||
|
th := Setup(t)
|
||||||
|
defer th.TearDown()
|
||||||
|
|
||||||
|
// Disable notifications file setting in config so we should get an warning
|
||||||
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
||||||
|
*cfg.NotificationLogSettings.EnableFile = false
|
||||||
|
})
|
||||||
|
|
||||||
|
fileData, err := th.Service.GetNotificationLogFile(th.Context)
|
||||||
|
assert.Nil(t, fileData)
|
||||||
|
assert.ErrorContains(t, err, "Unable to retrieve notifications logs because NotificationLogSettings.EnableFile is set to false")
|
||||||
|
|
||||||
|
dir, err := os.MkdirTemp("", "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
err = os.RemoveAll(dir)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Enable notifications file but point to an empty directory to get an error trying to read the file
|
||||||
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
||||||
|
*cfg.NotificationLogSettings.EnableFile = true
|
||||||
|
*cfg.LogSettings.FileLocation = dir
|
||||||
|
})
|
||||||
|
|
||||||
|
logLocation := config.GetNotificationsLogFileLocation(dir)
|
||||||
|
|
||||||
|
// There is no notifications.log file yet, so this fails
|
||||||
|
fileData, err = th.Service.GetNotificationLogFile(th.Context)
|
||||||
|
assert.Nil(t, fileData)
|
||||||
|
assert.ErrorContains(t, err, "failed read notifcation log file at path "+logLocation)
|
||||||
|
|
||||||
|
// Happy path where we have file and no error
|
||||||
|
d1 := []byte("hello\ngo\n")
|
||||||
|
err = os.WriteFile(logLocation, d1, 0777)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
fileData, err = th.Service.GetNotificationLogFile(th.Context)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
require.NotNil(t, fileData)
|
||||||
|
assert.Equal(t, "notifications.log", fileData.Filename)
|
||||||
|
assert.Positive(t, len(fileData.Body))
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
package platform
|
package platform
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
@@ -11,6 +12,7 @@ import (
|
|||||||
"net/http/pprof"
|
"net/http/pprof"
|
||||||
"path"
|
"path"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
rpprof "runtime/pprof"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"text/template"
|
"text/template"
|
||||||
@@ -23,11 +25,15 @@ import (
|
|||||||
"github.com/mattermost/mattermost/server/public/model"
|
"github.com/mattermost/mattermost/server/public/model"
|
||||||
"github.com/mattermost/mattermost/server/public/plugin"
|
"github.com/mattermost/mattermost/server/public/plugin"
|
||||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
"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/channels/utils"
|
||||||
"github.com/mattermost/mattermost/server/v8/einterfaces"
|
"github.com/mattermost/mattermost/server/v8/einterfaces"
|
||||||
)
|
)
|
||||||
|
|
||||||
const TimeToWaitForConnectionsToCloseOnServerShutdown = time.Second
|
const (
|
||||||
|
TimeToWaitForConnectionsToCloseOnServerShutdown = time.Second
|
||||||
|
cpuProfileDuration = 5 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
type platformMetrics struct {
|
type platformMetrics struct {
|
||||||
server *http.Server
|
server *http.Server
|
||||||
@@ -247,3 +253,52 @@ func (ps *PlatformService) Metrics() einterfaces.MetricsInterface {
|
|||||||
|
|
||||||
return ps.metricsIFace
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,13 +4,9 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"os"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"runtime/pprof"
|
"sync"
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/hashicorp/go-multierror"
|
"github.com/hashicorp/go-multierror"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
@@ -19,61 +15,86 @@ import (
|
|||||||
"github.com/mattermost/mattermost/server/public/model"
|
"github.com/mattermost/mattermost/server/public/model"
|
||||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||||
"github.com/mattermost/mattermost/server/public/shared/request"
|
"github.com/mattermost/mattermost/server/public/shared/request"
|
||||||
"github.com/mattermost/mattermost/server/v8/config"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
cpuProfileDuration = 5 * time.Second
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a *App) GenerateSupportPacket(c request.CTX, options *model.SupportPacketOptions) []model.FileData {
|
func (a *App) GenerateSupportPacket(c request.CTX, options *model.SupportPacketOptions) []model.FileData {
|
||||||
// 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 []string
|
|
||||||
|
|
||||||
// Creating an array of files that we are going to be adding to our zip file
|
|
||||||
fileDatas := []model.FileData{}
|
|
||||||
|
|
||||||
// A array of the functions that we can iterate through since they all have the same return value
|
// A array of the functions that we can iterate through since they all have the same return value
|
||||||
functions := map[string]func(c request.CTX) (*model.FileData, error){
|
functions := map[string]func(c request.CTX) (*model.FileData, error){
|
||||||
"support package": a.generateSupportPacketYaml,
|
"support packet": a.generateSupportPacketYaml,
|
||||||
"plugins": a.createPluginsFile,
|
"plugins": a.createPluginsFile,
|
||||||
"config": a.createSanitizedConfigFile,
|
"config": a.createSanitizedConfigFile,
|
||||||
"cpu profile": a.createCPUProfile,
|
"cpu profile": a.Srv().Platform().CreateCPUProfile,
|
||||||
"heap profile": a.createHeapProfile,
|
"heap profile": a.Srv().Platform().CreateHeapProfile,
|
||||||
"goroutines": a.createGoroutineProfile,
|
"goroutines": a.Srv().Platform().CreateGoroutineProfile,
|
||||||
"metadata": a.createSupportPacketMetadata,
|
"metadata": a.createSupportPacketMetadata,
|
||||||
}
|
}
|
||||||
|
|
||||||
if options.IncludeLogs {
|
if options.IncludeLogs {
|
||||||
functions["mattermost log"] = a.GetMattermostLog
|
functions["mattermost log"] = a.Srv().Platform().GetLogFile
|
||||||
functions["notification log"] = a.getNotificationsLog
|
functions["notification log"] = a.Srv().Platform().GetNotificationLogFile
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, fn := range functions {
|
// 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
|
||||||
fileData, err := fn(c)
|
var warnings *multierror.Error
|
||||||
if err != nil {
|
// Creating an array of files that we are going to be adding to our zip file
|
||||||
c.Logger().Error("Failed to generate file for support package", mlog.Err(err), mlog.String("file", name))
|
var fileDatas []model.FileData
|
||||||
warnings = append(warnings, err.Error())
|
var wg sync.WaitGroup
|
||||||
}
|
var mut sync.Mutex // Protects warnings and fileDatas
|
||||||
|
|
||||||
if fileData != nil {
|
wg.Add(1)
|
||||||
fileDatas = append(fileDatas, *fileData)
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
for name, fn := range functions {
|
||||||
|
fileData, err := fn(c)
|
||||||
|
mut.Lock()
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error("Failed to generate file for Support Packet", mlog.String("file", name), mlog.Err(err))
|
||||||
|
warnings = multierror.Append(warnings, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fileData != nil {
|
||||||
|
fileDatas = append(fileDatas, *fileData)
|
||||||
|
}
|
||||||
|
mut.Unlock()
|
||||||
}
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Run the cluster generation in a separate goroutine as CPU profile generation and file upload can take a long time
|
||||||
|
if cluster := a.Cluster(); cluster != nil && *a.Config().ClusterSettings.Enable {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
files, err := cluster.GenerateSupportPacket(c, options)
|
||||||
|
mut.Lock()
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error("Failed to generate Support Packet from cluster nodes", mlog.Err(err))
|
||||||
|
warnings = multierror.Append(warnings, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, node := range files {
|
||||||
|
fileDatas = append(fileDatas, node...)
|
||||||
|
}
|
||||||
|
mut.Unlock()
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
if pluginsEnvironment := a.GetPluginsEnvironment(); pluginsEnvironment != nil {
|
if pluginsEnvironment := a.GetPluginsEnvironment(); pluginsEnvironment != nil {
|
||||||
pluginContext := pluginContext(c)
|
pluginContext := pluginContext(c)
|
||||||
for _, id := range options.PluginPackets {
|
for _, id := range options.PluginPackets {
|
||||||
hooks, err := pluginsEnvironment.HooksForPlugin(id)
|
hooks, err := pluginsEnvironment.HooksForPlugin(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Logger().Error("Failed to call hooks for plugin", mlog.Err(err), mlog.String("plugin", id))
|
c.Logger().Error("Failed to call hooks for plugin", mlog.Err(err), mlog.String("plugin", id))
|
||||||
warnings = append(warnings, err.Error())
|
warnings = multierror.Append(warnings, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
pluginData, err := hooks.GenerateSupportData(pluginContext)
|
pluginData, err := hooks.GenerateSupportData(pluginContext)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Logger().Warn("Failed to generate plugin file for support package", mlog.Err(err), mlog.String("plugin", id))
|
c.Logger().Warn("Failed to generate plugin file for Support Packet", mlog.Err(err), mlog.String("plugin", id))
|
||||||
warnings = append(warnings, err.Error())
|
warnings = multierror.Append(warnings, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, data := range pluginData {
|
for _, data := range pluginData {
|
||||||
@@ -83,11 +104,10 @@ func (a *App) GenerateSupportPacket(c request.CTX, options *model.SupportPacketO
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Adding a warning.txt file to the fileDatas if any warning
|
// Adding a warning.txt file to the fileDatas if any warning
|
||||||
if len(warnings) > 0 {
|
if warnings != nil {
|
||||||
finalWarning := strings.Join(warnings, "\n")
|
|
||||||
fileDatas = append(fileDatas, model.FileData{
|
fileDatas = append(fileDatas, model.FileData{
|
||||||
Filename: "warning.txt",
|
Filename: model.SupportPacketErrorFile,
|
||||||
Body: []byte(finalWarning),
|
Body: []byte(warnings.Error()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,8 +128,8 @@ func (a *App) generateSupportPacketYaml(c request.CTX) (*model.FileData, error)
|
|||||||
/* Cluster */
|
/* Cluster */
|
||||||
|
|
||||||
var clusterID string
|
var clusterID string
|
||||||
if a.Cluster() != nil {
|
if cluster := a.Cluster(); cluster != nil {
|
||||||
clusterID = a.Cluster().GetClusterId()
|
clusterID = cluster.GetClusterId()
|
||||||
}
|
}
|
||||||
|
|
||||||
/* File store */
|
/* File store */
|
||||||
@@ -124,8 +144,7 @@ func (a *App) generateSupportPacketYaml(c request.CTX) (*model.FileData, error)
|
|||||||
/* LDAP */
|
/* LDAP */
|
||||||
|
|
||||||
var vendorName, vendorVersion string
|
var vendorName, vendorVersion string
|
||||||
ldap := a.Ldap()
|
if ldap := a.Ldap(); ldap != nil {
|
||||||
if ldap != nil {
|
|
||||||
vendorName, vendorVersion, err = ldap.GetVendorNameAndVendorVersion(c)
|
vendorName, vendorVersion, err = ldap.GetVendorNameAndVendorVersion(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rErr = multierror.Append(errors.Wrap(err, "error while getting LDAP vendor info"))
|
rErr = multierror.Append(errors.Wrap(err, "error while getting LDAP vendor info"))
|
||||||
@@ -143,9 +162,9 @@ func (a *App) generateSupportPacketYaml(c request.CTX) (*model.FileData, error)
|
|||||||
|
|
||||||
var elasticServerVersion string
|
var elasticServerVersion string
|
||||||
var elasticServerPlugins []string
|
var elasticServerPlugins []string
|
||||||
if a.Srv().Platform().SearchEngine.ElasticsearchEngine != nil {
|
if se := a.Srv().Platform().SearchEngine.ElasticsearchEngine; se != nil {
|
||||||
elasticServerVersion = a.Srv().Platform().SearchEngine.ElasticsearchEngine.GetFullVersion()
|
elasticServerVersion = se.GetFullVersion()
|
||||||
elasticServerPlugins = a.Srv().Platform().SearchEngine.ElasticsearchEngine.GetPlugins()
|
elasticServerPlugins = se.GetPlugins()
|
||||||
}
|
}
|
||||||
|
|
||||||
/* License */
|
/* License */
|
||||||
@@ -182,19 +201,20 @@ func (a *App) generateSupportPacketYaml(c request.CTX) (*model.FileData, error)
|
|||||||
analytics, appErr := a.GetAnalyticsForSupportPacket(c)
|
analytics, appErr := a.GetAnalyticsForSupportPacket(c)
|
||||||
if appErr != nil {
|
if appErr != nil {
|
||||||
rErr = multierror.Append(errors.Wrap(appErr, "error while getting analytics"))
|
rErr = multierror.Append(errors.Wrap(appErr, "error while getting analytics"))
|
||||||
}
|
|
||||||
if len(analytics) < 11 {
|
|
||||||
rErr = multierror.Append(errors.New("not enought analytics information found"))
|
|
||||||
} else {
|
} else {
|
||||||
totalChannels = int(analytics[0].Value) + int(analytics[1].Value)
|
if len(analytics) < 11 {
|
||||||
totalPosts = int(analytics[2].Value)
|
rErr = multierror.Append(errors.New("not enough analytics information found"))
|
||||||
totalTeams = int(analytics[4].Value)
|
} else {
|
||||||
websocketConnections = int(analytics[5].Value)
|
totalChannels = int(analytics[0].Value) + int(analytics[1].Value)
|
||||||
masterDbConnections = int(analytics[6].Value)
|
totalPosts = int(analytics[2].Value)
|
||||||
replicaDbConnections = int(analytics[7].Value)
|
totalTeams = int(analytics[4].Value)
|
||||||
dailyActiveUsers = int(analytics[8].Value)
|
websocketConnections = int(analytics[5].Value)
|
||||||
monthlyActiveUsers = int(analytics[9].Value)
|
masterDbConnections = int(analytics[6].Value)
|
||||||
inactiveUserCount = int(analytics[10].Value)
|
replicaDbConnections = int(analytics[7].Value)
|
||||||
|
dailyActiveUsers = int(analytics[8].Value)
|
||||||
|
monthlyActiveUsers = int(analytics[9].Value)
|
||||||
|
inactiveUserCount = int(analytics[10].Value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Jobs */
|
/* Jobs */
|
||||||
@@ -228,7 +248,7 @@ func (a *App) generateSupportPacketYaml(c request.CTX) (*model.FileData, error)
|
|||||||
rErr = multierror.Append(errors.Wrap(err, "error while getting migration jobs"))
|
rErr = multierror.Append(errors.Wrap(err, "error while getting migration jobs"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creating the struct for support packet yaml file
|
// Creating the struct for Support Packet yaml file
|
||||||
supportPacket := model.SupportPacket{
|
supportPacket := model.SupportPacket{
|
||||||
/* Build information */
|
/* Build information */
|
||||||
ServerOS: runtime.GOOS,
|
ServerOS: runtime.GOOS,
|
||||||
@@ -283,10 +303,10 @@ func (a *App) generateSupportPacketYaml(c request.CTX) (*model.FileData, error)
|
|||||||
MigrationJobs: migrationJobs,
|
MigrationJobs: migrationJobs,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Marshal to a Yaml File
|
// Marshal to a YAML File
|
||||||
supportPacketYaml, err := yaml.Marshal(&supportPacket)
|
supportPacketYaml, err := yaml.Marshal(&supportPacket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rErr = multierror.Append(errors.Wrap(err, "failed to marshal support package into yaml"))
|
rErr = multierror.Append(errors.Wrap(err, "failed to marshal Support Packet into yaml"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fileData := &model.FileData{
|
fileData := &model.FileData{
|
||||||
@@ -296,61 +316,6 @@ func (a *App) generateSupportPacketYaml(c request.CTX) (*model.FileData, error)
|
|||||||
return fileData, rErr.ErrorOrNil()
|
return fileData, rErr.ErrorOrNil()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) createPluginsFile(_ 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()
|
|
||||||
if appErr != nil {
|
|
||||||
return nil, errors.Wrap(appErr, "failed to get plugin list for support package")
|
|
||||||
}
|
|
||||||
|
|
||||||
pluginsPrettyJSON, err := json.MarshalIndent(pluginsResponse, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "failed to marshal plugin list into json")
|
|
||||||
}
|
|
||||||
|
|
||||||
fileData := &model.FileData{
|
|
||||||
Filename: "plugins.json",
|
|
||||||
Body: pluginsPrettyJSON,
|
|
||||||
}
|
|
||||||
return fileData, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) getNotificationsLog(_ request.CTX) (*model.FileData, error) {
|
|
||||||
if !*a.Config().NotificationLogSettings.EnableFile {
|
|
||||||
return nil, errors.New("Unable to retrieve notifications.log because LogSettings: EnableFile is set to false")
|
|
||||||
}
|
|
||||||
|
|
||||||
notificationsLog := config.GetNotificationsLogFileLocation(*a.Config().LogSettings.FileLocation)
|
|
||||||
notificationsLogFileData, err := os.ReadFile(notificationsLog)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrapf(err, "failed read notifcation log file at path %s", notificationsLog)
|
|
||||||
}
|
|
||||||
|
|
||||||
fileData := &model.FileData{
|
|
||||||
Filename: "notifications.log",
|
|
||||||
Body: notificationsLogFileData,
|
|
||||||
}
|
|
||||||
return fileData, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) GetMattermostLog(ctx request.CTX) (*model.FileData, error) {
|
|
||||||
if !*a.Config().LogSettings.EnableFile {
|
|
||||||
return nil, errors.New("Unable to retrieve mattermost.log because LogSettings: EnableFile is set to false")
|
|
||||||
}
|
|
||||||
|
|
||||||
mattermostLog := config.GetLogFileLocation(*a.Config().LogSettings.FileLocation)
|
|
||||||
mattermostLogFileData, err := os.ReadFile(mattermostLog)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrapf(err, "failed read mattermost log file at path %s", mattermostLog)
|
|
||||||
}
|
|
||||||
|
|
||||||
fileData := &model.FileData{
|
|
||||||
Filename: "mattermost.log",
|
|
||||||
Body: mattermostLogFileData,
|
|
||||||
}
|
|
||||||
return fileData, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) createSanitizedConfigFile(_ request.CTX) (*model.FileData, error) {
|
func (a *App) createSanitizedConfigFile(_ request.CTX) (*model.FileData, error) {
|
||||||
// Getting sanitized config, prettifying it, and then adding it to our file data array
|
// Getting sanitized config, prettifying it, and then adding it to our file data array
|
||||||
sanitizedConfigPrettyJSON, err := json.MarshalIndent(a.GetSanitizedConfig(), "", " ")
|
sanitizedConfigPrettyJSON, err := json.MarshalIndent(a.GetSanitizedConfig(), "", " ")
|
||||||
@@ -365,51 +330,21 @@ func (a *App) createSanitizedConfigFile(_ request.CTX) (*model.FileData, error)
|
|||||||
return fileData, nil
|
return fileData, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) createCPUProfile(_ request.CTX) (*model.FileData, error) {
|
func (a *App) createPluginsFile(_ request.CTX) (*model.FileData, error) {
|
||||||
var b bytes.Buffer
|
// Getting the plugins installed on the server, prettify it, and then add them to the file data array
|
||||||
|
pluginsResponse, appErr := a.GetPlugins()
|
||||||
err := pprof.StartCPUProfile(&b)
|
if appErr != nil {
|
||||||
if err != nil {
|
return nil, errors.Wrap(appErr, "failed to get plugin list for Support Packet")
|
||||||
return nil, errors.Wrap(err, "failed to start CPU profile")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(cpuProfileDuration)
|
pluginsPrettyJSON, err := json.MarshalIndent(pluginsResponse, "", " ")
|
||||||
|
|
||||||
pprof.StopCPUProfile()
|
|
||||||
|
|
||||||
fileData := &model.FileData{
|
|
||||||
Filename: "cpu.prof",
|
|
||||||
Body: b.Bytes(),
|
|
||||||
}
|
|
||||||
return fileData, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) createHeapProfile(request.CTX) (*model.FileData, error) {
|
|
||||||
var b bytes.Buffer
|
|
||||||
|
|
||||||
err := pprof.Lookup("heap").WriteTo(&b, 0)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to lookup heap profile")
|
return nil, errors.Wrap(err, "failed to marshal plugin list into json")
|
||||||
}
|
}
|
||||||
|
|
||||||
fileData := &model.FileData{
|
fileData := &model.FileData{
|
||||||
Filename: "heap.prof",
|
Filename: "plugins.json",
|
||||||
Body: b.Bytes(),
|
Body: pluginsPrettyJSON,
|
||||||
}
|
|
||||||
return fileData, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) createGoroutineProfile(_ request.CTX) (*model.FileData, error) {
|
|
||||||
var b bytes.Buffer
|
|
||||||
|
|
||||||
err := pprof.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
|
return fileData, nil
|
||||||
}
|
}
|
||||||
@@ -417,12 +352,12 @@ func (a *App) createGoroutineProfile(_ request.CTX) (*model.FileData, error) {
|
|||||||
func (a *App) createSupportPacketMetadata(_ request.CTX) (*model.FileData, error) {
|
func (a *App) createSupportPacketMetadata(_ request.CTX) (*model.FileData, error) {
|
||||||
metadata, err := model.GeneratePacketMetadata(model.SupportPacketType, a.TelemetryId(), a.License(), nil)
|
metadata, err := model.GeneratePacketMetadata(model.SupportPacketType, a.TelemetryId(), a.License(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to generate packet metadata")
|
return nil, errors.Wrap(err, "failed to generate Packet metadata")
|
||||||
}
|
}
|
||||||
|
|
||||||
b, err := yaml.Marshal(metadata)
|
b, err := yaml.Marshal(metadata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to marshal packet metadata into yaml")
|
return nil, errors.Wrap(err, "failed to marshal Packet metadata into yaml")
|
||||||
}
|
}
|
||||||
|
|
||||||
fileData := &model.FileData{
|
fileData := &model.FileData{
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ func TestCreatePluginsFile(t *testing.T) {
|
|||||||
// Plugins off in settings so no fileData and we get a warning instead
|
// Plugins off in settings so no fileData and we get a warning instead
|
||||||
fileData, err = th.App.createPluginsFile(th.Context)
|
fileData, err = th.App.createPluginsFile(th.Context)
|
||||||
assert.Nil(t, fileData)
|
assert.Nil(t, fileData)
|
||||||
assert.ErrorContains(t, err, "failed to get plugin list for support package")
|
assert.ErrorContains(t, err, "failed to get plugin list for Support Packet")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGenerateSupportPacketYaml(t *testing.T) {
|
func TestGenerateSupportPacketYaml(t *testing.T) {
|
||||||
@@ -68,7 +68,7 @@ func TestGenerateSupportPacketYaml(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
t.Run("Happy path", func(t *testing.T) {
|
t.Run("Happy path", func(t *testing.T) {
|
||||||
// Happy path where we have a support packet yaml file without any warnings
|
// Happy path where we have a Support Packet yaml file without any warnings
|
||||||
packet := generateSupportPacket(t)
|
packet := generateSupportPacket(t)
|
||||||
|
|
||||||
/* Build information */
|
/* Build information */
|
||||||
@@ -207,7 +207,7 @@ func TestGenerateSupportPacket(t *testing.T) {
|
|||||||
}
|
}
|
||||||
genMockLogFiles()
|
genMockLogFiles()
|
||||||
|
|
||||||
t.Run("generate support packet with logs", func(t *testing.T) {
|
t.Run("generate Support Packet with logs", func(t *testing.T) {
|
||||||
fileDatas := th.App.GenerateSupportPacket(th.Context, &model.SupportPacketOptions{
|
fileDatas := th.App.GenerateSupportPacket(th.Context, &model.SupportPacketOptions{
|
||||||
IncludeLogs: true,
|
IncludeLogs: true,
|
||||||
})
|
})
|
||||||
@@ -232,7 +232,7 @@ func TestGenerateSupportPacket(t *testing.T) {
|
|||||||
assert.ElementsMatch(t, testFiles, rFileNames)
|
assert.ElementsMatch(t, testFiles, rFileNames)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("generate support packet without logs", func(t *testing.T) {
|
t.Run("generate Support Packet without logs", func(t *testing.T) {
|
||||||
fileDatas := th.App.GenerateSupportPacket(th.Context, &model.SupportPacketOptions{
|
fileDatas := th.App.GenerateSupportPacket(th.Context, &model.SupportPacketOptions{
|
||||||
IncludeLogs: false,
|
IncludeLogs: false,
|
||||||
})
|
})
|
||||||
@@ -326,96 +326,6 @@ func TestGenerateSupportPacket(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetNotificationsLog(t *testing.T) {
|
|
||||||
th := Setup(t)
|
|
||||||
defer th.TearDown()
|
|
||||||
|
|
||||||
// Disable notifications file setting in config so we should get an warning
|
|
||||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
||||||
*cfg.NotificationLogSettings.EnableFile = false
|
|
||||||
})
|
|
||||||
|
|
||||||
fileData, err := th.App.getNotificationsLog(th.Context)
|
|
||||||
assert.Nil(t, fileData)
|
|
||||||
assert.ErrorContains(t, err, "Unable to retrieve notifications.log because LogSettings: EnableFile is set to false")
|
|
||||||
|
|
||||||
dir, err := os.MkdirTemp("", "")
|
|
||||||
require.NoError(t, err)
|
|
||||||
t.Cleanup(func() {
|
|
||||||
err = os.RemoveAll(dir)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Enable notifications file but point to an empty directory to get an error trying to read the file
|
|
||||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
||||||
*cfg.NotificationLogSettings.EnableFile = true
|
|
||||||
*cfg.LogSettings.FileLocation = dir
|
|
||||||
})
|
|
||||||
|
|
||||||
logLocation := config.GetNotificationsLogFileLocation(dir)
|
|
||||||
|
|
||||||
// There is no notifications.log file yet, so this fails
|
|
||||||
fileData, err = th.App.getNotificationsLog(th.Context)
|
|
||||||
assert.Nil(t, fileData)
|
|
||||||
assert.ErrorContains(t, err, "failed read notifcation log file at path "+logLocation)
|
|
||||||
|
|
||||||
// Happy path where we have file and no error
|
|
||||||
d1 := []byte("hello\ngo\n")
|
|
||||||
err = os.WriteFile(logLocation, d1, 0777)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
fileData, err = th.App.getNotificationsLog(th.Context)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
require.NotNil(t, fileData)
|
|
||||||
assert.Equal(t, "notifications.log", fileData.Filename)
|
|
||||||
assert.Positive(t, len(fileData.Body))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetMattermostLog(t *testing.T) {
|
|
||||||
th := Setup(t)
|
|
||||||
defer th.TearDown()
|
|
||||||
|
|
||||||
// disable mattermost log file setting in config so we should get an warning
|
|
||||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
||||||
*cfg.LogSettings.EnableFile = false
|
|
||||||
})
|
|
||||||
|
|
||||||
fileData, err := th.App.GetMattermostLog(th.Context)
|
|
||||||
assert.Nil(t, fileData)
|
|
||||||
assert.ErrorContains(t, err, "Unable to retrieve mattermost.log because LogSettings: EnableFile is set to false")
|
|
||||||
|
|
||||||
dir, err := os.MkdirTemp("", "")
|
|
||||||
require.NoError(t, err)
|
|
||||||
t.Cleanup(func() {
|
|
||||||
err = os.RemoveAll(dir)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Enable log file but point to an empty directory to get an error trying to read the file
|
|
||||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
||||||
*cfg.LogSettings.EnableFile = true
|
|
||||||
*cfg.LogSettings.FileLocation = dir
|
|
||||||
})
|
|
||||||
|
|
||||||
logLocation := config.GetLogFileLocation(dir)
|
|
||||||
|
|
||||||
// There is no mattermost.log file yet, so this fails
|
|
||||||
fileData, err = th.App.GetMattermostLog(th.Context)
|
|
||||||
assert.Nil(t, fileData)
|
|
||||||
assert.ErrorContains(t, err, "failed read mattermost log file at path "+logLocation)
|
|
||||||
|
|
||||||
// Happy path where we get a log file and no warning
|
|
||||||
d1 := []byte("hello\ngo\n")
|
|
||||||
err = os.WriteFile(logLocation, d1, 0777)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
fileData, err = th.App.GetMattermostLog(th.Context)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, fileData)
|
|
||||||
assert.Equal(t, "mattermost.log", fileData.Filename)
|
|
||||||
assert.Positive(t, len(fileData.Body))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCreateSanitizedConfigFile(t *testing.T) {
|
func TestCreateSanitizedConfigFile(t *testing.T) {
|
||||||
th := Setup(t)
|
th := Setup(t)
|
||||||
defer th.TearDown()
|
defer th.TearDown()
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
"github.com/mattermost/mattermost/server/public/model"
|
||||||
|
"github.com/mattermost/mattermost/server/public/shared/request"
|
||||||
"github.com/mattermost/mattermost/server/v8/einterfaces"
|
"github.com/mattermost/mattermost/server/v8/einterfaces"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -63,6 +64,10 @@ func (c *FakeClusterInterface) QueryLogs(page, perPage int) (map[string][]string
|
|||||||
return make(map[string][]string), nil
|
return make(map[string][]string), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *FakeClusterInterface) GenerateSupportPacket(rctx request.CTX, options *model.SupportPacketOptions) (map[string][]model.FileData, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *FakeClusterInterface) ConfigChanged(previousConfig *model.Config, newConfig *model.Config, sendToOtherServer bool) *model.AppError {
|
func (c *FakeClusterInterface) ConfigChanged(previousConfig *model.Config, newConfig *model.Config, sendToOtherServer bool) *model.AppError {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ func (s *MmctlE2ETestSuite) TestSupportPacketCmdF() {
|
|||||||
printer.SetFormat(printer.FormatPlain)
|
printer.SetFormat(printer.FormatPlain)
|
||||||
s.T().Cleanup(func() { printer.SetFormat(printer.FormatJSON) })
|
s.T().Cleanup(func() { printer.SetFormat(printer.FormatJSON) })
|
||||||
|
|
||||||
s.Run("Download support packet with default filename", func() {
|
s.Run("Download Support Packet with default filename", func() {
|
||||||
printer.Clean()
|
printer.Clean()
|
||||||
|
|
||||||
s.T().Cleanup(cleanupSupportPacket(s.T()))
|
s.T().Cleanup(cleanupSupportPacket(s.T()))
|
||||||
@@ -141,7 +141,7 @@ func (s *MmctlE2ETestSuite) TestSupportPacketCmdF() {
|
|||||||
s.True(found)
|
s.True(found)
|
||||||
})
|
})
|
||||||
|
|
||||||
s.Run("Download support packet with custom filename", func() {
|
s.Run("Download Support Packet with custom filename", func() {
|
||||||
printer.Clean()
|
printer.Clean()
|
||||||
|
|
||||||
err := SystemSupportPacketCmd.ParseFlags([]string{"-o", "foo.zip"})
|
err := SystemSupportPacketCmd.ParseFlags([]string{"-o", "foo.zip"})
|
||||||
|
|||||||
@@ -238,7 +238,7 @@ func (s *MmctlUnitTestSuite) TestSupportPacketCmdF() {
|
|||||||
printer.SetFormat(printer.FormatPlain)
|
printer.SetFormat(printer.FormatPlain)
|
||||||
s.T().Cleanup(func() { printer.SetFormat(printer.FormatJSON) })
|
s.T().Cleanup(func() { printer.SetFormat(printer.FormatJSON) })
|
||||||
|
|
||||||
s.Run("Download support packet with default filename", func() {
|
s.Run("Download Support Packet with default filename", func() {
|
||||||
printer.Clean()
|
printer.Clean()
|
||||||
|
|
||||||
s.T().Cleanup(cleanupSupportPacket(s.T()))
|
s.T().Cleanup(cleanupSupportPacket(s.T()))
|
||||||
@@ -274,7 +274,7 @@ func (s *MmctlUnitTestSuite) TestSupportPacketCmdF() {
|
|||||||
s.True(found)
|
s.True(found)
|
||||||
})
|
})
|
||||||
|
|
||||||
s.Run("Download support packet with custom filename", func() {
|
s.Run("Download Support Packet with custom filename", func() {
|
||||||
printer.Clean()
|
printer.Clean()
|
||||||
|
|
||||||
data := []byte("some bytes")
|
data := []byte("some bytes")
|
||||||
|
|||||||
@@ -133,6 +133,8 @@ services:
|
|||||||
- "RUN_SERVER_IN_BACKGROUND=false"
|
- "RUN_SERVER_IN_BACKGROUND=false"
|
||||||
- "MM_CLUSTERSETTINGS_ENABLE=true"
|
- "MM_CLUSTERSETTINGS_ENABLE=true"
|
||||||
- "MM_CLUSTERSETTINGS_CLUSTERNAME=mm_dev_cluster"
|
- "MM_CLUSTERSETTINGS_CLUSTERNAME=mm_dev_cluster"
|
||||||
|
- "MM_LOGSETTINGS_FILELOCATION=./logs/leader"
|
||||||
|
- "MM_NOTIFICATIONLOGSETTINGSS_FILELOCATION=./logs/leader"
|
||||||
networks:
|
networks:
|
||||||
- mm-test
|
- mm-test
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -169,6 +171,8 @@ services:
|
|||||||
- "RUN_SERVER_IN_BACKGROUND=false"
|
- "RUN_SERVER_IN_BACKGROUND=false"
|
||||||
- "MM_CLUSTERSETTINGS_ENABLE=true"
|
- "MM_CLUSTERSETTINGS_ENABLE=true"
|
||||||
- "MM_CLUSTERSETTINGS_CLUSTERNAME=mm_dev_cluster"
|
- "MM_CLUSTERSETTINGS_CLUSTERNAME=mm_dev_cluster"
|
||||||
|
- "MM_LOGSETTINGS_FILELOCATION=./logs/follower"
|
||||||
|
- "MM_NOTIFICATIONLOGSETTINGSS_FILELOCATION=./logs/follower"
|
||||||
networks:
|
networks:
|
||||||
- mm-test
|
- mm-test
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -205,6 +209,8 @@ services:
|
|||||||
- "RUN_SERVER_IN_BACKGROUND=false"
|
- "RUN_SERVER_IN_BACKGROUND=false"
|
||||||
- "MM_CLUSTERSETTINGS_ENABLE=true"
|
- "MM_CLUSTERSETTINGS_ENABLE=true"
|
||||||
- "MM_CLUSTERSETTINGS_CLUSTERNAME=mm_dev_cluster"
|
- "MM_CLUSTERSETTINGS_CLUSTERNAME=mm_dev_cluster"
|
||||||
|
- "MM_LOGSETTINGS_FILELOCATION=./logs/follower2"
|
||||||
|
- "MM_NOTIFICATIONLOGSETTINGSS_FILELOCATION=./logs/follower2"
|
||||||
networks:
|
networks:
|
||||||
- mm-test
|
- mm-test
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package einterfaces
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
"github.com/mattermost/mattermost/server/public/model"
|
||||||
|
"github.com/mattermost/mattermost/server/public/shared/request"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ClusterMessageHandler func(msg *model.ClusterMessage)
|
type ClusterMessageHandler func(msg *model.ClusterMessage)
|
||||||
@@ -27,6 +28,7 @@ type ClusterInterface interface {
|
|||||||
GetClusterStats() ([]*model.ClusterStats, *model.AppError)
|
GetClusterStats() ([]*model.ClusterStats, *model.AppError)
|
||||||
GetLogs(page, perPage int) ([]string, *model.AppError)
|
GetLogs(page, perPage int) ([]string, *model.AppError)
|
||||||
QueryLogs(page, perPage int) (map[string][]string, *model.AppError)
|
QueryLogs(page, perPage int) (map[string][]string, *model.AppError)
|
||||||
|
GenerateSupportPacket(rctx request.CTX, options *model.SupportPacketOptions) (map[string][]model.FileData, error)
|
||||||
GetPluginStatuses() (model.PluginStatuses, *model.AppError)
|
GetPluginStatuses() (model.PluginStatuses, *model.AppError)
|
||||||
ConfigChanged(previousConfig *model.Config, newConfig *model.Config, sendToOtherServer bool) *model.AppError
|
ConfigChanged(previousConfig *model.Config, newConfig *model.Config, sendToOtherServer bool) *model.AppError
|
||||||
// WebConnCountForUser returns the number of active webconn connections
|
// WebConnCountForUser returns the number of active webconn connections
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import (
|
|||||||
mock "github.com/stretchr/testify/mock"
|
mock "github.com/stretchr/testify/mock"
|
||||||
|
|
||||||
model "github.com/mattermost/mattermost/server/public/model"
|
model "github.com/mattermost/mattermost/server/public/model"
|
||||||
|
|
||||||
|
request "github.com/mattermost/mattermost/server/public/shared/request"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ClusterInterface is an autogenerated mock type for the ClusterInterface type
|
// ClusterInterface is an autogenerated mock type for the ClusterInterface type
|
||||||
@@ -36,6 +38,36 @@ func (_m *ClusterInterface) ConfigChanged(previousConfig *model.Config, newConfi
|
|||||||
return r0
|
return r0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GenerateSupportPacket provides a mock function with given fields: rctx, options
|
||||||
|
func (_m *ClusterInterface) GenerateSupportPacket(rctx request.CTX, options *model.SupportPacketOptions) (map[string][]model.FileData, error) {
|
||||||
|
ret := _m.Called(rctx, options)
|
||||||
|
|
||||||
|
if len(ret) == 0 {
|
||||||
|
panic("no return value specified for GenerateSupportPacket")
|
||||||
|
}
|
||||||
|
|
||||||
|
var r0 map[string][]model.FileData
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(0).(func(request.CTX, *model.SupportPacketOptions) (map[string][]model.FileData, error)); ok {
|
||||||
|
return rf(rctx, options)
|
||||||
|
}
|
||||||
|
if rf, ok := ret.Get(0).(func(request.CTX, *model.SupportPacketOptions) map[string][]model.FileData); ok {
|
||||||
|
r0 = rf(rctx, options)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(map[string][]model.FileData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rf, ok := ret.Get(1).(func(request.CTX, *model.SupportPacketOptions) error); ok {
|
||||||
|
r1 = rf(rctx, options)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
// GetClusterId provides a mock function with given fields:
|
// GetClusterId provides a mock function with given fields:
|
||||||
func (_m *ClusterInterface) GetClusterId() string {
|
func (_m *ClusterInterface) GetClusterId() string {
|
||||||
ret := _m.Called()
|
ret := _m.Called()
|
||||||
|
|||||||
@@ -4636,7 +4636,7 @@ func (c *Client4) GetFileInfosForPostIncludeDeleted(ctx context.Context, postId
|
|||||||
|
|
||||||
// General/System Section
|
// General/System Section
|
||||||
|
|
||||||
// GenerateSupportPacket downloads the generated support packet
|
// GenerateSupportPacket generates and downloads a Support Packet.
|
||||||
func (c *Client4) GenerateSupportPacket(ctx context.Context) ([]byte, *Response, error) {
|
func (c *Client4) GenerateSupportPacket(ctx context.Context) ([]byte, *Response, error) {
|
||||||
r, err := c.DoAPIGet(ctx, c.systemRoute()+"/support_packet", "")
|
r, err := c.DoAPIGet(ctx, c.systemRoute()+"/support_packet", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -45,16 +45,18 @@ const (
|
|||||||
// m.ClusterEventMap in metrics/metrics.go file.
|
// m.ClusterEventMap in metrics/metrics.go file.
|
||||||
|
|
||||||
// Gossip communication
|
// Gossip communication
|
||||||
ClusterGossipEventRequestGetLogs = "gossip_request_get_logs"
|
ClusterGossipEventRequestGetLogs = "gossip_request_get_logs"
|
||||||
ClusterGossipEventResponseGetLogs = "gossip_response_get_logs"
|
ClusterGossipEventResponseGetLogs = "gossip_response_get_logs"
|
||||||
ClusterGossipEventRequestGetClusterStats = "gossip_request_cluster_stats"
|
ClusterGossipEventRequestGenerateSupportPacket = "gossip_request_generate_support_packet"
|
||||||
ClusterGossipEventResponseGetClusterStats = "gossip_response_cluster_stats"
|
ClusterGossipEventResponseGenerateSupportPacket = "gossip_response_generate_support_packet"
|
||||||
ClusterGossipEventRequestGetPluginStatuses = "gossip_request_plugin_statuses"
|
ClusterGossipEventRequestGetClusterStats = "gossip_request_cluster_stats"
|
||||||
ClusterGossipEventResponseGetPluginStatuses = "gossip_response_plugin_statuses"
|
ClusterGossipEventResponseGetClusterStats = "gossip_response_cluster_stats"
|
||||||
ClusterGossipEventRequestSaveConfig = "gossip_request_save_config"
|
ClusterGossipEventRequestGetPluginStatuses = "gossip_request_plugin_statuses"
|
||||||
ClusterGossipEventResponseSaveConfig = "gossip_response_save_config"
|
ClusterGossipEventResponseGetPluginStatuses = "gossip_response_plugin_statuses"
|
||||||
ClusterGossipEventRequestWebConnCount = "gossip_request_webconn_count"
|
ClusterGossipEventRequestSaveConfig = "gossip_request_save_config"
|
||||||
ClusterGossipEventResponseWebConnCount = "gossip_response_webconn_count"
|
ClusterGossipEventResponseSaveConfig = "gossip_response_save_config"
|
||||||
|
ClusterGossipEventRequestWebConnCount = "gossip_request_webconn_count"
|
||||||
|
ClusterGossipEventResponseWebConnCount = "gossip_response_webconn_count"
|
||||||
|
|
||||||
// SendTypes for ClusterMessage.
|
// SendTypes for ClusterMessage.
|
||||||
ClusterSendBestEffort = "best_effort"
|
ClusterSendBestEffort = "best_effort"
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
SupportPacketErrorFile = "warning.txt"
|
||||||
|
)
|
||||||
|
|
||||||
type SupportPacket struct {
|
type SupportPacket struct {
|
||||||
/* Build information */
|
/* Build information */
|
||||||
|
|
||||||
|
|||||||
@@ -3310,7 +3310,7 @@
|
|||||||
"combined_system_message.removed_from_team.one_you": "You were **removed from the team**.",
|
"combined_system_message.removed_from_team.one_you": "You were **removed from the team**.",
|
||||||
"combined_system_message.removed_from_team.two": "{firstUser} and {secondUser} were **removed from the team**.",
|
"combined_system_message.removed_from_team.two": "{firstUser} and {secondUser} were **removed from the team**.",
|
||||||
"combined_system_message.you": "You",
|
"combined_system_message.you": "You",
|
||||||
"commercial_support.description": "If you're experiencing issues, [submit a support ticket](!{supportLink}). To help with troubleshooting, it's recommended to download the support packet below that includes more details about your Mattermost environment.",
|
"commercial_support.description": "If you're experiencing issues, [submit a support ticket](!{supportLink}). To help with troubleshooting, it's recommended to download the Support Packet below that includes more details about your Mattermost environment.",
|
||||||
"commercial_support.download_contents": "**Select your Support Packet contents to download**",
|
"commercial_support.download_contents": "**Select your Support Packet contents to download**",
|
||||||
"commercial_support.download_support_packet": "Download Support Packet",
|
"commercial_support.download_support_packet": "Download Support Packet",
|
||||||
"commercial_support.title": "Commercial Support",
|
"commercial_support.title": "Commercial Support",
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user