[MM-31790] Support Packet Generation BACKEND (#16667)
* init commit * clean up the code * make mocks * fix translations * mocks and lint fixes * add tests * little fixes * Update i18n/en.json Co-authored-by: Scott Bishel <scott.bishel@mattermost.com> * Update i18n/en.json Co-authored-by: Scott Bishel <scott.bishel@mattermost.com> * Update i18n/en.json Co-authored-by: Scott Bishel <scott.bishel@mattermost.com> * Update i18n/en.json Co-authored-by: Scott Bishel <scott.bishel@mattermost.com> * Update i18n/en.json Co-authored-by: Scott Bishel <scott.bishel@mattermost.com> * Address Comments * fix i18n * update api endpoint * add enable file and file level for conditional show of banner * Address Comments * Make it more clear about returns * Create zip file utility function * update en.json * address comments * write tests * check for data in test * remove warning string * Correct expected and actual * set database through environment variables * reset environment variable at end of test Co-authored-by: Mattermod <mattermod@users.noreply.github.com> Co-authored-by: Scott Bishel <scott.bishel@mattermost.com>
Этот коммит содержится в:
@@ -4,11 +4,13 @@
|
||||
package api4
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"path"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -64,6 +66,64 @@ func (api *API) InitSystem() {
|
||||
api.BaseRoutes.ApiRoot.Handle("/warn_metrics/trial-license-ack/{warn_metric_id:[A-Za-z0-9-_]+}", api.ApiHandler(requestTrialLicenseAndAckWarnMetric)).Methods("POST")
|
||||
api.BaseRoutes.System.Handle("/notices/{team_id:[A-Za-z0-9]+}", api.ApiSessionRequired(getProductNotices)).Methods("GET")
|
||||
api.BaseRoutes.System.Handle("/notices/view", api.ApiSessionRequired(updateViewedProductNotices)).Methods("PUT")
|
||||
|
||||
api.BaseRoutes.System.Handle("/support_packet", api.ApiSessionRequired(generateSupportPacket)).Methods("GET")
|
||||
}
|
||||
|
||||
func generateSupportPacket(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
const FileMime = "application/zip"
|
||||
const OutputDirectory = "support_packet"
|
||||
|
||||
// Checking to see if the user is a admin of any sort or not
|
||||
// If they are a admin, they should theoritcally have access to one or more of the system console read permissions
|
||||
if !c.App.SessionHasPermissionToAny(*c.App.Session(), model.SysconsoleReadPermissions) {
|
||||
c.SetPermissionError(model.SysconsoleReadPermissions...)
|
||||
return
|
||||
}
|
||||
|
||||
// Checking to see if the server has a e10 or e20 license (this feature is only permitted for servers with licenses)
|
||||
if c.App.Srv().License() == nil {
|
||||
c.Err = model.NewAppError("Api4.generateSupportPacket", "api.no_license", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
fileDatas := c.App.GenerateSupportPacket()
|
||||
|
||||
// Constructing the ZIP file name as per spec (mattermost_support_packet_YYYY-MM-DD-HH-MM.zip)
|
||||
now := time.Now()
|
||||
outputZipFilename := fmt.Sprintf("mattermost_support_packet_%s.zip", now.Format("2006-01-02-03-04"))
|
||||
|
||||
fileStorageBackend, fileBackendErr := c.App.FileBackend()
|
||||
if fileBackendErr != nil {
|
||||
c.Err = fileBackendErr
|
||||
return
|
||||
}
|
||||
|
||||
// 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()
|
||||
err := c.App.CreateZipFileAndAddFiles(fileStorageBackend, fileDatas, outputZipFilename, outputDirectoryToUse)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.generateSupportPacket", "api.unable_to_create_zip_file", nil, err.Error(), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
fileBytes, err := fileStorageBackend.ReadFile(path.Join(outputDirectoryToUse, outputZipFilename))
|
||||
defer fileStorageBackend.RemoveDirectory(outputDirectoryToUse)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.generateSupportPacket", "api.unable_to_read_file_from_backend", nil, err.Error(), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
fileBytesReader := bytes.NewReader(fileBytes)
|
||||
|
||||
// Send the zip file back to client
|
||||
// We are able to pass 0 for content size due to the fact that Golang's serveContent (https://golang.org/src/net/http/fs.go)
|
||||
// already sets that for us
|
||||
writeFileResponseErr := writeFileResponse(outputZipFilename, FileMime, 0, now, *c.App.Config().ServiceSettings.WebserverMode, fileBytesReader, true, w, r)
|
||||
if writeFileResponseErr != nil {
|
||||
c.Err = model.NewAppError("generateSupportPacket", "api.unable_write_file_response", nil, writeFileResponseErr.Error(), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func getSystemPing(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -187,6 +187,34 @@ func TestEmailTest(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestGenerateSupportPacket(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
t.Run("As a System Administrator", func(t *testing.T) {
|
||||
l := model.NewTestLicense()
|
||||
th.App.Srv().SetLicense(l)
|
||||
|
||||
file, resp := th.SystemAdminClient.GenerateSupportPacket()
|
||||
require.Nil(t, resp.Error)
|
||||
require.NotZero(t, len(file))
|
||||
})
|
||||
|
||||
t.Run("As a Regular User", func(t *testing.T) {
|
||||
_, resp := th.Client.GenerateSupportPacket()
|
||||
CheckForbiddenStatus(t, resp)
|
||||
})
|
||||
|
||||
t.Run("Server with no License", func(t *testing.T) {
|
||||
ok, resp := th.SystemAdminClient.RemoveLicenseFile()
|
||||
CheckNoError(t, resp)
|
||||
require.True(t, ok)
|
||||
|
||||
_, resp = th.SystemAdminClient.GenerateSupportPacket()
|
||||
CheckForbiddenStatus(t, resp)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSiteURLTest(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
@@ -308,6 +308,9 @@ type AppIface interface {
|
||||
TeamMembersMinusGroupMembers(teamID string, groupIDs []string, page, perPage int) ([]*model.UserWithGroups, int64, *model.AppError)
|
||||
// This function migrates the default built in roles from code/config to the database.
|
||||
DoAdvancedPermissionsMigration()
|
||||
// This function zip's up all the files in fileDatas array and then saves it to the directory specified with the specified zip file name
|
||||
// Ensure the zip file name ends with a .zip
|
||||
CreateZipFileAndAddFiles(fileBackend filesstore.FileBackend, fileDatas []model.FileData, zipFileName, directory string) error
|
||||
// This to be used for places we check the users password when they are already logged in
|
||||
DoubleCheckPassword(user *model.User, password string) *model.AppError
|
||||
// UpdateBotActive marks a bot as active or inactive, along with its corresponding user.
|
||||
@@ -506,6 +509,7 @@ type AppIface interface {
|
||||
FindTeamByName(name string) bool
|
||||
GenerateMfaSecret(userId string) (*model.MfaSecret, *model.AppError)
|
||||
GeneratePublicLink(siteURL string, info *model.FileInfo) string
|
||||
GenerateSupportPacket() []model.FileData
|
||||
GetActivePluginManifests() ([]*model.Manifest, *model.AppError)
|
||||
GetAllChannels(page, perPage int, opts model.ChannelSearchOpts) (*model.ChannelListWithTeamData, *model.AppError)
|
||||
GetAllChannelsCount(opts model.ChannelSearchOpts) (int64, *model.AppError)
|
||||
|
||||
49
app/file.go
49
app/file.go
@@ -4,6 +4,7 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
@@ -18,6 +19,8 @@ import (
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
@@ -1258,3 +1261,49 @@ func (a *App) CopyFileInfos(userId string, fileIds []string) ([]string, *model.A
|
||||
|
||||
return newFileIds, nil
|
||||
}
|
||||
|
||||
// This function zip's up all the files in fileDatas array and then saves it to the directory specified with the specified zip file name
|
||||
// Ensure the zip file name ends with a .zip
|
||||
func (a *App) CreateZipFileAndAddFiles(fileBackend filesstore.FileBackend, fileDatas []model.FileData, zipFileName, directory string) error {
|
||||
// Create Zip File (temporarily stored on disk)
|
||||
conglomerateZipFile, err := os.Create(zipFileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(zipFileName)
|
||||
|
||||
// Create a new zip archive.
|
||||
zipFileWriter := zip.NewWriter(conglomerateZipFile)
|
||||
|
||||
// Populate Zip file with File Datas array
|
||||
err = populateZipfile(zipFileWriter, fileDatas)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conglomerateZipFile.Seek(0, 0)
|
||||
_, err = fileBackend.WriteFile(conglomerateZipFile, path.Join(directory, zipFileName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// This is a implementation of Go's example of writing files to zip (with slight modification)
|
||||
// https://golang.org/src/archive/zip/example_test.go
|
||||
func populateZipfile(w *zip.Writer, fileDatas []model.FileData) error {
|
||||
defer w.Close()
|
||||
for _, fd := range fileDatas {
|
||||
f, err := w.Create(fd.Filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = f.Write(fd.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -14,6 +15,8 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
"github.com/mattermost/mattermost-server/v5/plugin/plugintest/mock"
|
||||
"github.com/mattermost/mattermost-server/v5/services/filesstore/mocks"
|
||||
"github.com/mattermost/mattermost-server/v5/utils/fileutils"
|
||||
)
|
||||
|
||||
@@ -269,6 +272,23 @@ func TestMigrateFilenamesToFileInfos(t *testing.T) {
|
||||
assert.Equal(t, 0, len(infos))
|
||||
}
|
||||
|
||||
func TestCreateZipFileAndAddFiles(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
mockBackend := mocks.FileBackend{}
|
||||
mockBackend.On("WriteFile", mock.Anything, "directory-to-heaven/zip-file-name-to-heaven.zip").Return(int64(666), errors.New("Only those who dare to fail greatly can ever achieve greatly"))
|
||||
|
||||
err := th.App.CreateZipFileAndAddFiles(&mockBackend, []model.FileData{}, "zip-file-name-to-heaven.zip", "directory-to-heaven")
|
||||
require.NotNil(t, err)
|
||||
require.Equal(t, err.Error(), "Only those who dare to fail greatly can ever achieve greatly")
|
||||
|
||||
mockBackend = mocks.FileBackend{}
|
||||
mockBackend.On("WriteFile", mock.Anything, "directory-to-heaven/zip-file-name-to-heaven.zip").Return(int64(666), nil)
|
||||
err = th.App.CreateZipFileAndAddFiles(&mockBackend, []model.FileData{}, "zip-file-name-to-heaven.zip", "directory-to-heaven")
|
||||
require.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestCopyFileInfos(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
@@ -2416,6 +2416,28 @@ func (a *OpenTracingAppLayer) CreateWebhookPost(userId string, channel *model.Ch
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) CreateZipFileAndAddFiles(fileBackend filesstore.FileBackend, fileDatas []model.FileData, zipFileName string, directory string) error {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateZipFileAndAddFiles")
|
||||
|
||||
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.CreateZipFileAndAddFiles(fileBackend, fileDatas, zipFileName, directory)
|
||||
|
||||
if resultVar0 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar0))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) DBHealthCheckDelete() error {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DBHealthCheckDelete")
|
||||
@@ -3943,6 +3965,23 @@ func (a *OpenTracingAppLayer) GeneratePublicLink(siteURL string, info *model.Fil
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GenerateSupportPacket() []model.FileData {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GenerateSupportPacket")
|
||||
|
||||
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.GenerateSupportPacket()
|
||||
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetActivePluginManifests() ([]*model.Manifest, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetActivePluginManifests")
|
||||
|
||||
185
app/server.go
185
app/server.go
@@ -6,8 +6,10 @@ package app
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"hash/maphash"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -23,6 +25,8 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
sentryhttp "github.com/getsentry/sentry-go/http"
|
||||
"github.com/gorilla/mux"
|
||||
@@ -636,6 +640,12 @@ func (s *Server) AppOptions() []AppOption {
|
||||
}
|
||||
}
|
||||
|
||||
// Return Database type (postgres or mysql) and current version of Mattermost
|
||||
func (s *Server) DatabaseTypeAndMattermostVersion() (string, string) {
|
||||
mattermostVersion, _ := s.Store.System().GetByName("Version")
|
||||
return *s.Config().SqlSettings.DriverName, mattermostVersion.Value
|
||||
}
|
||||
|
||||
// initLogging initializes and configures the logger. This may be called more than once.
|
||||
func (s *Server) initLogging() error {
|
||||
if s.Log == nil {
|
||||
@@ -1611,3 +1621,178 @@ func (s *Server) HttpService() httpservice.HTTPService {
|
||||
func (s *Server) SetLog(l *mlog.Logger) {
|
||||
s.Log = l
|
||||
}
|
||||
|
||||
func (a *App) GenerateSupportPacket() []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
|
||||
functions := []func() (*model.FileData, string){
|
||||
a.generateSupportPacketYaml,
|
||||
a.createPluginsFile,
|
||||
a.createSanitizedConfigFile,
|
||||
a.getMattermostLog,
|
||||
a.getNotificationsLog,
|
||||
}
|
||||
|
||||
for _, fn := range functions {
|
||||
fileData, warning := fn()
|
||||
|
||||
if fileData != nil {
|
||||
fileDatas = append(fileDatas, *fileData)
|
||||
} else {
|
||||
warnings = append(warnings, warning)
|
||||
}
|
||||
}
|
||||
|
||||
// Adding a warning.txt file to the fileDatas if any warning
|
||||
if len(warnings) > 0 {
|
||||
finalWarning := strings.Join(warnings, "\n")
|
||||
fileDatas = append(fileDatas, model.FileData{
|
||||
Filename: "warning.txt",
|
||||
Body: []byte(finalWarning),
|
||||
})
|
||||
}
|
||||
|
||||
return fileDatas
|
||||
}
|
||||
|
||||
func (a *App) getNotificationsLog() (*model.FileData, string) {
|
||||
var warning string
|
||||
|
||||
// Getting notifications.log
|
||||
if *a.Srv().Config().NotificationLogSettings.EnableFile {
|
||||
// notifications.log
|
||||
notificationsLog := utils.GetNotificationsLogFileLocation(*a.Srv().Config().LogSettings.FileLocation)
|
||||
|
||||
notificationsLogFileData, notificationsLogFileDataErr := ioutil.ReadFile(notificationsLog)
|
||||
|
||||
if notificationsLogFileDataErr == nil {
|
||||
fileData := model.FileData{
|
||||
Filename: "notifications.log",
|
||||
Body: notificationsLogFileData,
|
||||
}
|
||||
return &fileData, ""
|
||||
}
|
||||
|
||||
warning = fmt.Sprintf("ioutil.ReadFile(notificationsLog) Error: %s", notificationsLogFileDataErr.Error())
|
||||
|
||||
} else {
|
||||
warning = "Unable to retrieve notifications.log because LogSettings: EnableFile is false in config.json"
|
||||
}
|
||||
|
||||
return nil, warning
|
||||
}
|
||||
|
||||
func (a *App) getMattermostLog() (*model.FileData, string) {
|
||||
var warning string
|
||||
|
||||
// Getting mattermost.log
|
||||
if *a.Srv().Config().LogSettings.EnableFile {
|
||||
// mattermost.log
|
||||
mattermostLog := utils.GetLogFileLocation(*a.Srv().Config().LogSettings.FileLocation)
|
||||
|
||||
mattermostLogFileData, mattermostLogFileDataErr := ioutil.ReadFile(mattermostLog)
|
||||
|
||||
if mattermostLogFileDataErr == nil {
|
||||
fileData := model.FileData{
|
||||
Filename: "mattermost.log",
|
||||
Body: mattermostLogFileData,
|
||||
}
|
||||
return &fileData, ""
|
||||
}
|
||||
warning = fmt.Sprintf("ioutil.ReadFile(mattermostLog) Error: %s", mattermostLogFileDataErr.Error())
|
||||
|
||||
} else {
|
||||
warning = "Unable to retrieve mattermost.log because LogSettings: EnableFile is false in config.json"
|
||||
}
|
||||
|
||||
return nil, warning
|
||||
}
|
||||
|
||||
func (a *App) createSanitizedConfigFile() (*model.FileData, string) {
|
||||
// Getting sanitized config, prettifying it, and then adding it to our file data array
|
||||
sanitizedConfigPrettyJSON, err := json.MarshalIndent(a.GetSanitizedConfig(), "", " ")
|
||||
if err == nil {
|
||||
fileData := model.FileData{
|
||||
Filename: "sanitized_config.json",
|
||||
Body: sanitizedConfigPrettyJSON,
|
||||
}
|
||||
return &fileData, ""
|
||||
}
|
||||
|
||||
warning := fmt.Sprintf("json.MarshalIndent(c.App.GetSanitizedConfig()) Error: %s", err.Error())
|
||||
return nil, warning
|
||||
}
|
||||
|
||||
func (a *App) createPluginsFile() (*model.FileData, string) {
|
||||
var warning string
|
||||
|
||||
// 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 {
|
||||
pluginsPrettyJSON, err := json.MarshalIndent(pluginsResponse, "", " ")
|
||||
if err == nil {
|
||||
fileData := model.FileData{
|
||||
Filename: "plugins.json",
|
||||
Body: pluginsPrettyJSON,
|
||||
}
|
||||
|
||||
return &fileData, ""
|
||||
}
|
||||
|
||||
warning = fmt.Sprintf("json.MarshalIndent(pluginsResponse) Error: %s", err.Error())
|
||||
} else {
|
||||
warning = fmt.Sprintf("c.App.GetPlugins() Error: %s", appErr.Error())
|
||||
}
|
||||
|
||||
return nil, warning
|
||||
}
|
||||
|
||||
func (a *App) generateSupportPacketYaml() (*model.FileData, string) {
|
||||
// Here we are getting information regarding Elastic Search
|
||||
var elasticServerVersion string
|
||||
var elasticServerPlugins []string
|
||||
if a.Srv().SearchEngine.ElasticsearchEngine != nil {
|
||||
elasticServerVersion = a.Srv().SearchEngine.ElasticsearchEngine.GetFullVersion()
|
||||
elasticServerPlugins = a.Srv().SearchEngine.ElasticsearchEngine.GetPlugins()
|
||||
}
|
||||
|
||||
// Here we are getting information regarding LDAP
|
||||
ldapInterface := a.Srv().Ldap
|
||||
var vendorName, vendorVersion string
|
||||
if ldapInterface != nil {
|
||||
vendorName, vendorVersion = ldapInterface.GetVendorNameAndVendorVersion()
|
||||
}
|
||||
|
||||
// Here we are getting information regarding the database (mysql/postgres + current Mattermost version)
|
||||
databaseType, databaseVersion := a.Srv().DatabaseTypeAndMattermostVersion()
|
||||
|
||||
// Creating the struct for support packet yaml file
|
||||
supportPacket := model.SupportPacket{
|
||||
ServerOS: runtime.GOOS,
|
||||
ServerArchitecture: runtime.GOARCH,
|
||||
DatabaseType: databaseType,
|
||||
DatabaseVersion: databaseVersion,
|
||||
LdapVendorName: vendorName,
|
||||
LdapVendorVersion: vendorVersion,
|
||||
ElasticServerVersion: elasticServerVersion,
|
||||
ElasticServerPlugins: elasticServerPlugins,
|
||||
}
|
||||
|
||||
// Marshal to a Yaml File
|
||||
supportPacketYaml, err := yaml.Marshal(&supportPacket)
|
||||
if err == nil {
|
||||
fileData := model.FileData{
|
||||
Filename: "support_packet.yaml",
|
||||
Body: supportPacketYaml,
|
||||
}
|
||||
return &fileData, ""
|
||||
}
|
||||
|
||||
warning := fmt.Sprintf("yaml.Marshal(&supportPacket) Error: %s", err.Error())
|
||||
return nil, warning
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/config"
|
||||
@@ -161,6 +162,184 @@ func TestStartServerTLSSuccess(t *testing.T) {
|
||||
require.NoError(t, serverErr)
|
||||
}
|
||||
|
||||
func TestDatabaseTypeAndMattermostVersion(t *testing.T) {
|
||||
sqlDrivernameEnvironment := os.Getenv("MM_SQLSETTINGS_DRIVERNAME")
|
||||
defer os.Setenv("MM_SQLSETTINGS_DRIVERNAME", sqlDrivernameEnvironment)
|
||||
|
||||
os.Setenv("MM_SQLSETTINGS_DRIVERNAME", "postgres")
|
||||
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
databaseType, mattermostVersion := th.Server.DatabaseTypeAndMattermostVersion()
|
||||
assert.Equal(t, "postgres", databaseType)
|
||||
assert.Equal(t, "5.31.0", mattermostVersion)
|
||||
|
||||
os.Setenv("MM_SQLSETTINGS_DRIVERNAME", "mysql")
|
||||
|
||||
th2 := Setup(t)
|
||||
defer th2.TearDown()
|
||||
|
||||
databaseType, mattermostVersion = th2.Server.DatabaseTypeAndMattermostVersion()
|
||||
assert.Equal(t, "mysql", databaseType)
|
||||
assert.Equal(t, "5.31.0", mattermostVersion)
|
||||
}
|
||||
|
||||
func TestGenerateSupportPacket(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
d1 := []byte("hello\ngo\n")
|
||||
err := ioutil.WriteFile("mattermost.log", d1, 0777)
|
||||
require.Nil(t, err)
|
||||
err = ioutil.WriteFile("notifications.log", d1, 0777)
|
||||
require.Nil(t, err)
|
||||
|
||||
fileDatas := th.App.GenerateSupportPacket()
|
||||
testFiles := []string{"support_packet.yaml", "plugins.json", "sanitized_config.json", "mattermost.log", "notifications.log"}
|
||||
for i, fileData := range fileDatas {
|
||||
require.NotNil(t, fileData)
|
||||
assert.Equal(t, testFiles[i], fileData.Filename)
|
||||
assert.Positive(t, len(fileData.Body))
|
||||
}
|
||||
|
||||
// Remove these two files and ensure that warning.txt file is generated
|
||||
err = os.Remove("notifications.log")
|
||||
require.Nil(t, err)
|
||||
err = os.Remove("mattermost.log")
|
||||
require.Nil(t, err)
|
||||
fileDatas = th.App.GenerateSupportPacket()
|
||||
testFiles = []string{"support_packet.yaml", "plugins.json", "sanitized_config.json", "warning.txt"}
|
||||
for i, fileData := range fileDatas {
|
||||
require.NotNil(t, fileData)
|
||||
assert.Equal(t, testFiles[i], fileData.Filename)
|
||||
assert.Positive(t, len(fileData.Body))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetNotificationsLog(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
// Disable notifications file to get an error
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.NotificationLogSettings.EnableFile = false
|
||||
})
|
||||
|
||||
fileData, warning := th.App.getNotificationsLog()
|
||||
assert.Nil(t, fileData)
|
||||
assert.Equal(t, warning, "Unable to retrieve notifications.log because LogSettings: EnableFile is false in config.json")
|
||||
|
||||
// Enable notifications file but delete any notifications file to get an error trying to read the file
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.NotificationLogSettings.EnableFile = true
|
||||
})
|
||||
|
||||
// If any previous notifications.log file, lets delete it
|
||||
os.Remove("notifications.log")
|
||||
|
||||
fileData, warning = th.App.getNotificationsLog()
|
||||
assert.Nil(t, fileData)
|
||||
assert.Contains(t, warning, "ioutil.ReadFile(notificationsLog) Error:")
|
||||
|
||||
// Happy path where we have file and no warning
|
||||
d1 := []byte("hello\ngo\n")
|
||||
err := ioutil.WriteFile("notifications.log", d1, 0777)
|
||||
defer os.Remove("notifications.log")
|
||||
require.Nil(t, err)
|
||||
|
||||
fileData, warning = th.App.getNotificationsLog()
|
||||
require.NotNil(t, fileData)
|
||||
assert.Equal(t, "notifications.log", fileData.Filename)
|
||||
assert.Positive(t, len(fileData.Body))
|
||||
assert.Empty(t, warning)
|
||||
}
|
||||
|
||||
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, warning := th.App.getMattermostLog()
|
||||
assert.Nil(t, fileData)
|
||||
assert.Equal(t, "Unable to retrieve mattermost.log because LogSettings: EnableFile is false in config.json", warning)
|
||||
|
||||
// We enable the setting but delete any mattermost log file
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.LogSettings.EnableFile = true
|
||||
})
|
||||
|
||||
// If any previous mattermost.log file, lets delete it
|
||||
os.Remove("mattermost.log")
|
||||
|
||||
fileData, warning = th.App.getMattermostLog()
|
||||
assert.Nil(t, fileData)
|
||||
assert.Contains(t, warning, "ioutil.ReadFile(mattermostLog) Error:")
|
||||
|
||||
// Happy path where we get a log file and no warning
|
||||
d1 := []byte("hello\ngo\n")
|
||||
err := ioutil.WriteFile("mattermost.log", d1, 0777)
|
||||
defer os.Remove("mattermost.log")
|
||||
require.Nil(t, err)
|
||||
|
||||
fileData, warning = th.App.getMattermostLog()
|
||||
require.NotNil(t, fileData)
|
||||
assert.Equal(t, "mattermost.log", fileData.Filename)
|
||||
assert.Positive(t, len(fileData.Body))
|
||||
assert.Empty(t, warning)
|
||||
}
|
||||
|
||||
func TestCreateSanitizedConfigFile(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
// Happy path where we have a sanitized config file with no warning
|
||||
fileData, warning := th.App.createSanitizedConfigFile()
|
||||
require.NotNil(t, fileData)
|
||||
assert.Equal(t, "sanitized_config.json", fileData.Filename)
|
||||
assert.Positive(t, len(fileData.Body))
|
||||
assert.Empty(t, warning)
|
||||
}
|
||||
|
||||
func TestCreatePluginsFile(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
// Happy path where we have a plugins file with no warning
|
||||
fileData, warning := th.App.createPluginsFile()
|
||||
require.NotNil(t, fileData)
|
||||
assert.Equal(t, "plugins.json", fileData.Filename)
|
||||
assert.Positive(t, len(fileData.Body))
|
||||
assert.Empty(t, warning)
|
||||
|
||||
// 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, warning = th.App.createPluginsFile()
|
||||
assert.Nil(t, fileData)
|
||||
assert.Contains(t, warning, "c.App.GetPlugins() Error:")
|
||||
}
|
||||
|
||||
func TestGenerateSupportPacketYaml(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
// Happy path where we have a support packet yaml file without any warnings
|
||||
fileData, warning := th.App.generateSupportPacketYaml()
|
||||
require.NotNil(t, fileData)
|
||||
assert.Equal(t, "support_packet.yaml", fileData.Filename)
|
||||
assert.Positive(t, len(fileData.Body))
|
||||
assert.Empty(t, warning)
|
||||
|
||||
}
|
||||
|
||||
func TestStartServerTLSVersion(t *testing.T) {
|
||||
s, err := NewServer()
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -222,6 +222,8 @@ func GenerateLimitedClientConfig(c *model.Config, telemetryID string, license *m
|
||||
props["BuildEnterpriseReady"] = model.BuildEnterpriseReady
|
||||
|
||||
props["EnableBotAccountCreation"] = strconv.FormatBool(*c.ServiceSettings.EnableBotAccountCreation)
|
||||
props["EnableFile"] = strconv.FormatBool(*c.LogSettings.EnableFile)
|
||||
props["FileLevel"] = *c.LogSettings.FileLevel
|
||||
|
||||
props["SiteName"] = *c.TeamSettings.SiteName
|
||||
props["WebsocketURL"] = strings.TrimRight(*c.ServiceSettings.WebsocketURL, "/")
|
||||
|
||||
@@ -23,4 +23,5 @@ type LdapInterface interface {
|
||||
FirstLoginSync(user *model.User, userAuthService, userAuthData, email string) *model.AppError
|
||||
UpdateProfilePictureIfNecessary(model.User, model.Session)
|
||||
GetADLdapIdFromSAMLId(authData string) string
|
||||
GetVendorNameAndVendorVersion() (string, string)
|
||||
}
|
||||
|
||||
@@ -233,6 +233,27 @@ func (_m *LdapInterface) GetUserAttributes(id string, attributes []string) (map[
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetVendorNameAndVendorVersion provides a mock function with given fields:
|
||||
func (_m *LdapInterface) GetVendorNameAndVendorVersion() (string, string) {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func() string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
var r1 string
|
||||
if rf, ok := ret.Get(1).(func() string); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Get(1).(string)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MigrateIDAttribute provides a mock function with given fields: toAttribute
|
||||
func (_m *LdapInterface) MigrateIDAttribute(toAttribute string) error {
|
||||
ret := _m.Called(toAttribute)
|
||||
|
||||
16
i18n/en.json
16
i18n/en.json
@@ -1580,6 +1580,10 @@
|
||||
"id": "api.migrate_to_saml.error",
|
||||
"translation": "Unable to migrate SAML."
|
||||
},
|
||||
{
|
||||
"id": "api.no_license",
|
||||
"translation": "E10 or E20 license required to use this endpoint."
|
||||
},
|
||||
{
|
||||
"id": "api.oauth.allow_oauth.redirect_callback.app_error",
|
||||
"translation": "invalid_request: Supplied redirect_uri did not match registered callback_url."
|
||||
@@ -3050,6 +3054,18 @@
|
||||
"id": "api.templates.welcome_subject",
|
||||
"translation": "[{{ .SiteName }}] You joined {{ .ServerURL }}"
|
||||
},
|
||||
{
|
||||
"id": "api.unable_to_create_zip_file",
|
||||
"translation": "Error creating zip file."
|
||||
},
|
||||
{
|
||||
"id": "api.unable_to_read_file_from_backend",
|
||||
"translation": "Error reading file from backend"
|
||||
},
|
||||
{
|
||||
"id": "api.unable_write_file_response",
|
||||
"translation": "Error downloading support packet file."
|
||||
},
|
||||
{
|
||||
"id": "api.upgrade_to_enterprise.already-done.app_error",
|
||||
"translation": "You have already upgraded to Mattermost Enterprise Edition. Please restart the server to finish the upgrade."
|
||||
|
||||
@@ -3278,6 +3278,21 @@ func (c *Client4) GetFileInfosForPost(postId string, etag string) ([]*FileInfo,
|
||||
|
||||
// General/System Section
|
||||
|
||||
// GenerateSupportPacket downloads the generated support packet
|
||||
func (c *Client4) GenerateSupportPacket() ([]byte, *Response) {
|
||||
r, appErr := c.DoApiGet(c.GetSystemRoute()+"/support_packet", "")
|
||||
if appErr != nil {
|
||||
return nil, BuildErrorResponse(r, appErr)
|
||||
}
|
||||
defer closeBody(r)
|
||||
|
||||
data, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return nil, BuildErrorResponse(r, NewAppError("GetFile", "model.client.read_job_result_file.app_error", nil, err.Error(), r.StatusCode))
|
||||
}
|
||||
return data, BuildResponse(r)
|
||||
}
|
||||
|
||||
// GetPing will return ok if the running goRoutines are below the threshold and unhealthy for above.
|
||||
func (c *Client4) GetPing() (string, *Response) {
|
||||
r, err := c.DoApiGet(c.GetSystemRoute()+"/ping", "")
|
||||
|
||||
@@ -87,6 +87,22 @@ type ServerBusyState struct {
|
||||
Expires_ts string `json:"expires_ts,omitempty"`
|
||||
}
|
||||
|
||||
type SupportPacket struct {
|
||||
ServerOS string `yaml:"server_os"`
|
||||
ServerArchitecture string `yaml:"server_architecture"`
|
||||
DatabaseType string `yaml:"database_type"`
|
||||
DatabaseVersion string `yaml:"database_version"`
|
||||
LdapVendorName string `yaml:"ldap_vendor_name,omitempty"`
|
||||
LdapVendorVersion string `yaml:"ldap_vendor_version,omitempty"`
|
||||
ElasticServerVersion string `yaml:"elastic_server_version,omitempty"`
|
||||
ElasticServerPlugins []string `yaml:"elastic_server_plugins,omitempty"`
|
||||
}
|
||||
|
||||
type FileData struct {
|
||||
Filename string
|
||||
Body []byte
|
||||
}
|
||||
|
||||
func (sbs *ServerBusyState) ToJson() string {
|
||||
b, _ := json.Marshal(sbs)
|
||||
return string(b)
|
||||
|
||||
@@ -231,6 +231,14 @@ func (b *BleveEngine) GetVersion() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (b *BleveEngine) GetFullVersion() string {
|
||||
return "0"
|
||||
}
|
||||
|
||||
func (b *BleveEngine) GetPlugins() []string {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func (b *BleveEngine) GetName() string {
|
||||
return EngineName
|
||||
}
|
||||
|
||||
@@ -12,7 +12,9 @@ import (
|
||||
type SearchEngineInterface interface {
|
||||
Start() *model.AppError
|
||||
Stop() *model.AppError
|
||||
GetFullVersion() string
|
||||
GetVersion() int
|
||||
GetPlugins() []string
|
||||
UpdateConfig(cfg *model.Config)
|
||||
GetName() string
|
||||
IsActive() bool
|
||||
|
||||
@@ -176,6 +176,20 @@ func (_m *SearchEngineInterface) DeleteUserPosts(userID string) *model.AppError
|
||||
return r0
|
||||
}
|
||||
|
||||
// GetFullVersion provides a mock function with given fields:
|
||||
func (_m *SearchEngineInterface) GetFullVersion() string {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func() string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// GetName provides a mock function with given fields:
|
||||
func (_m *SearchEngineInterface) GetName() string {
|
||||
ret := _m.Called()
|
||||
@@ -190,6 +204,22 @@ func (_m *SearchEngineInterface) GetName() string {
|
||||
return r0
|
||||
}
|
||||
|
||||
// GetPlugins provides a mock function with given fields:
|
||||
func (_m *SearchEngineInterface) GetPlugins() []string {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 []string
|
||||
if rf, ok := ret.Get(0).(func() []string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]string)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// GetVersion provides a mock function with given fields:
|
||||
func (_m *SearchEngineInterface) GetVersion() int {
|
||||
ret := _m.Called()
|
||||
|
||||
1
testlib/testdata/mysql_migration_warmup.sql
поставляемый
1
testlib/testdata/mysql_migration_warmup.sql
поставляемый
@@ -35,6 +35,7 @@ INSERT INTO `Systems` VALUES ('add_use_group_mentions_permission', 'true');
|
||||
INSERT INTO `Systems` VALUES ('add_system_console_permissions', 'true');
|
||||
INSERT INTO `Systems` VALUES ('add_convert_channel_permissions', 'true');
|
||||
INSERT INTO `Systems` VALUES ('manage_shared_channel_permissions', 'true');
|
||||
INSERT INTO `Systems` VALUES ('Version', '5.31.0');
|
||||
|
||||
|
||||
/*!40000 ALTER TABLE `Systems` ENABLE KEYS */;
|
||||
|
||||
1
testlib/testdata/postgres_migration_warmup.sql
поставляемый
1
testlib/testdata/postgres_migration_warmup.sql
поставляемый
@@ -43,6 +43,7 @@ INSERT INTO public.systems VALUES ('add_use_group_mentions_permission', 'true');
|
||||
INSERT INTO public.systems VALUES ('add_system_console_permissions', 'true');
|
||||
INSERT INTO public.systems VALUES ('add_convert_channel_permissions', 'true');
|
||||
INSERT INTO public.systems VALUES ('manage_shared_channel_permissions', 'true');
|
||||
INSERT INTO public.systems VALUES ('Version', '5.31.0');
|
||||
|
||||
--
|
||||
-- PostgreSQL database dump complete
|
||||
|
||||
Ссылка в новой задаче
Block a user