Doug Lauder
2023-03-22 17:22:27 -04:00
коммит произвёл GitHub
родитель b61c096497
Коммит c943ed6859
13276 изменённых файлов: 1695615 добавлений и 223189 удалений

228
server/cmd/mattermost/commands/cmdtestlib.go Обычный файл
Просмотреть файл

@@ -0,0 +1,228 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package commands
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/api4"
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest/mocks"
"github.com/mattermost/mattermost-server/v6/server/channels/testlib"
)
var coverprofileCounters map[string]int = make(map[string]int)
var mainHelper *testlib.MainHelper
type testHelper struct {
*api4.TestHelper
config *model.Config
tempDir string
configFilePath string
disableAutoConfig bool
}
// Setup creates an instance of testHelper.
func Setup(t testing.TB) *testHelper {
dir, err := testlib.SetupTestResources()
if err != nil {
panic("failed to create temporary directory: " + err.Error())
}
api4TestHelper := api4.Setup(t)
testHelper := &testHelper{
TestHelper: api4TestHelper,
tempDir: dir,
configFilePath: filepath.Join(dir, "config-helper.json"),
}
config := &model.Config{}
config.SetDefaults()
testHelper.SetConfig(config)
return testHelper
}
// Setup creates an instance of testHelper.
func SetupWithStoreMock(t testing.TB) *testHelper {
dir, err := testlib.SetupTestResources()
if err != nil {
panic("failed to create temporary directory: " + err.Error())
}
api4TestHelper := api4.SetupWithStoreMock(t)
systemStore := mocks.SystemStore{}
systemStore.On("Get").Return(make(model.StringMap), nil)
licenseStore := mocks.LicenseStore{}
licenseStore.On("Get", "").Return(&model.LicenseRecord{}, nil)
api4TestHelper.App.Srv().Store().(*mocks.Store).On("System").Return(&systemStore)
api4TestHelper.App.Srv().Store().(*mocks.Store).On("License").Return(&licenseStore)
testHelper := &testHelper{
TestHelper: api4TestHelper,
tempDir: dir,
configFilePath: filepath.Join(dir, "config-helper.json"),
}
config := &model.Config{}
config.SetDefaults()
testHelper.SetConfig(config)
return testHelper
}
// InitBasic simply proxies to api4.InitBasic, while still returning a testHelper.
func (h *testHelper) InitBasic() *testHelper {
h.TestHelper.InitBasic()
return h
}
// TemporaryDirectory returns the temporary directory created for user by the test helper.
func (h *testHelper) TemporaryDirectory() string {
return h.tempDir
}
// Config returns the configuration passed to a running command.
func (h *testHelper) Config() *model.Config {
return h.config.Clone()
}
// ConfigPath returns the path to the temporary config file passed to a running command.
func (h *testHelper) ConfigPath() string {
return h.configFilePath
}
// SetConfig replaces the configuration passed to a running command.
func (h *testHelper) SetConfig(config *model.Config) {
if !testing.Short() {
config.SqlSettings = *mainHelper.GetSQLSettings()
}
// Disable strict password requirements for test
*config.PasswordSettings.MinimumLength = 5
*config.PasswordSettings.Lowercase = false
*config.PasswordSettings.Uppercase = false
*config.PasswordSettings.Symbol = false
*config.PasswordSettings.Number = false
h.config = config
buf, err := json.Marshal(config)
if err != nil {
panic("failed to marshal config: " + err.Error())
}
if err := os.WriteFile(h.configFilePath, buf, 0600); err != nil {
panic("failed to write file " + h.configFilePath + ": " + err.Error())
}
}
// SetAutoConfig configures whether the --config flag is automatically passed to a running command.
func (h *testHelper) SetAutoConfig(autoConfig bool) {
h.disableAutoConfig = !autoConfig
}
// TearDown cleans up temporary files and assets created during the life of the test helper.
func (h *testHelper) TearDown() {
h.TestHelper.TearDown()
os.RemoveAll(h.tempDir)
}
func (h *testHelper) execArgs(t *testing.T, args []string) []string {
ret := []string{"-test.v", "-test.run", "ExecCommand"}
if coverprofile := flag.Lookup("test.coverprofile").Value.String(); coverprofile != "" {
dir := filepath.Dir(coverprofile)
base := filepath.Base(coverprofile)
baseParts := strings.SplitN(base, ".", 2)
name := strings.Replace(t.Name(), "/", "_", -1)
coverprofileCounters[name] = coverprofileCounters[name] + 1
baseParts[0] = fmt.Sprintf("%v-%v-%v", baseParts[0], name, coverprofileCounters[name])
ret = append(ret, "-test.coverprofile", filepath.Join(dir, strings.Join(baseParts, ".")))
}
ret = append(ret, "--")
// Unless the test passes a `--config` of its own, create a temporary one from the default
// configuration with the current test database applied.
hasConfig := h.disableAutoConfig
for _, arg := range args {
if arg == "--config" {
hasConfig = true
break
}
}
if !hasConfig {
ret = append(ret, "--config", h.configFilePath)
}
ret = append(ret, args...)
return ret
}
func (h *testHelper) cmd(t *testing.T, args []string) *exec.Cmd {
path, err := os.Executable()
require.NoError(t, err)
cmd := exec.Command(path, h.execArgs(t, args)...)
cmd.Env = []string{}
for _, env := range os.Environ() {
// Ignore MM_SQLSETTINGS_DATASOURCE from the environment, since we override.
if strings.HasPrefix(env, "MM_SQLSETTINGS_DATASOURCE=") {
continue
}
cmd.Env = append(cmd.Env, env)
}
return cmd
}
// CheckCommand invokes the test binary, returning the output modified for assertion testing.
func (h *testHelper) CheckCommand(t *testing.T, args ...string) string {
output, err := h.cmd(t, args).CombinedOutput()
require.NoError(t, err, string(output))
return strings.TrimSpace(strings.TrimSuffix(strings.TrimSpace(string(output)), "PASS"))
}
// RunCommand invokes the test binary, returning only any error.
func (h *testHelper) RunCommand(t *testing.T, args ...string) error {
return h.cmd(t, args).Run()
}
// RunCommandWithOutput is a variant of RunCommand that returns the unmodified output and any error.
func (h *testHelper) RunCommandWithOutput(t *testing.T, args ...string) (string, error) {
cmd := h.cmd(t, args)
var buf bytes.Buffer
reader, writer := io.Pipe()
cmd.Stdout = writer
cmd.Stderr = writer
done := make(chan bool)
go func() {
io.Copy(&buf, reader)
close(done)
}()
err := cmd.Run()
writer.Close()
<-done
return buf.String(), err
}

181
server/cmd/mattermost/commands/db.go Обычный файл
Просмотреть файл

@@ -0,0 +1,181 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package commands
import (
"fmt"
"strconv"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/channels/store/sqlstore"
"github.com/mattermost/mattermost-server/v6/server/config"
)
var DbCmd = &cobra.Command{
Use: "db",
Short: "Commands related to the database",
}
var InitDbCmd = &cobra.Command{
Use: "init",
Short: "Initialize the database",
Long: `Initialize the database for a given DSN, executing the migrations and loading the custom defaults if any.
This command should be run using a database configuration DSN.`,
Example: ` # you can use the config flag to pass the DSN
$ mattermost db init --config postgres://localhost/mattermost
# or you can use the MM_CONFIG environment variable
$ MM_CONFIG=postgres://localhost/mattermost mattermost db init
# and you can set a custom defaults file to be loaded into the database
$ MM_CUSTOM_DEFAULTS_PATH=custom.json MM_CONFIG=postgres://localhost/mattermost mattermost db init`,
Args: cobra.NoArgs,
RunE: initDbCmdF,
}
var ResetCmd = &cobra.Command{
Use: "reset",
Short: "Reset the database to initial state",
Long: "Completely erases the database causing the loss of all data. This will reset Mattermost to its initial state.",
RunE: resetCmdF,
}
var MigrateCmd = &cobra.Command{
Use: "migrate",
Short: "Migrate the database if there are any unapplied migrations",
Long: "Run the missing migrations from the migrations table.",
RunE: migrateCmdF,
}
var DBVersionCmd = &cobra.Command{
Use: "version",
Short: "Returns the recent applied version number",
RunE: dbVersionCmdF,
}
func init() {
ResetCmd.Flags().Bool("confirm", false, "Confirm you really want to delete everything and a DB backup has been performed.")
DBVersionCmd.Flags().Bool("all", false, "Returns all applied migrations")
DbCmd.AddCommand(
InitDbCmd,
ResetCmd,
MigrateCmd,
DBVersionCmd,
)
RootCmd.AddCommand(
DbCmd,
)
}
func initDbCmdF(command *cobra.Command, _ []string) error {
dsn := getConfigDSN(command, config.GetEnvironment())
if !config.IsDatabaseDSN(dsn) {
return errors.New("this command should be run using a database configuration DSN")
}
customDefaults, err := loadCustomDefaults()
if err != nil {
return errors.Wrap(err, "error loading custom configuration defaults")
}
configStore, err := config.NewStoreFromDSN(getConfigDSN(command, config.GetEnvironment()), false, customDefaults, true)
if err != nil {
return errors.Wrap(err, "failed to load configuration")
}
defer configStore.Close()
sqlStore := sqlstore.New(configStore.Get().SqlSettings, nil)
defer sqlStore.Close()
fmt.Println("Database store correctly initialised")
return nil
}
func resetCmdF(command *cobra.Command, args []string) error {
a, err := InitDBCommandContextCobra(command, app.SkipPostInitialization())
if err != nil {
return err
}
defer a.Srv().Shutdown()
confirmFlag, _ := command.Flags().GetBool("confirm")
if !confirmFlag {
var confirm string
CommandPrettyPrintln("Have you performed a database backup? (YES/NO): ")
fmt.Scanln(&confirm)
if confirm != "YES" {
return errors.New("ABORTED: You did not answer YES exactly, in all capitals.")
}
CommandPrettyPrintln("Are you sure you want to delete everything? All data will be permanently deleted? (YES/NO): ")
fmt.Scanln(&confirm)
if confirm != "YES" {
return errors.New("ABORTED: You did not answer YES exactly, in all capitals.")
}
}
a.Srv().Store().DropAllTables()
CommandPrettyPrintln("Database successfully reset")
auditRec := a.MakeAuditRecord("reset", audit.Success)
a.LogAuditRec(auditRec, nil)
return nil
}
func migrateCmdF(command *cobra.Command, args []string) error {
cfgDSN := getConfigDSN(command, config.GetEnvironment())
cfgStore, err := config.NewStoreFromDSN(cfgDSN, true, nil, true)
if err != nil {
return errors.Wrap(err, "failed to load configuration")
}
config := cfgStore.Get()
store := sqlstore.New(config.SqlSettings, nil)
defer store.Close()
CommandPrettyPrintln("Database successfully migrated")
return nil
}
func dbVersionCmdF(command *cobra.Command, args []string) error {
cfgDSN := getConfigDSN(command, config.GetEnvironment())
cfgStore, err := config.NewStoreFromDSN(cfgDSN, true, nil, true)
if err != nil {
return errors.Wrap(err, "failed to load configuration")
}
config := cfgStore.Get()
store := sqlstore.New(config.SqlSettings, nil)
defer store.Close()
allFlag, _ := command.Flags().GetBool("all")
if allFlag {
applied, err2 := store.GetAppliedMigrations()
if err2 != nil {
return errors.Wrap(err2, "failed to get applied migrations")
}
for _, migration := range applied {
CommandPrettyPrintln(fmt.Sprintf("Varsion: %d, Name: %s", migration.Version, migration.Name))
}
return nil
}
v, err := store.GetDBSchemaVersion()
if err != nil {
return errors.Wrap(err, "failed to get schema version")
}
CommandPrettyPrintln("Current database schema version is: " + strconv.Itoa(v))
return nil
}

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

@@ -0,0 +1,19 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package commands
import (
"flag"
"testing"
"github.com/stretchr/testify/require"
)
func TestExecCommand(t *testing.T) {
if filter := flag.Lookup("test.run").Value.String(); filter != "ExecCommand" {
t.Skip("use -run ExecCommand to execute a command via the test executable")
}
RootCmd.SetArgs(flag.Args())
require.NoError(t, RootCmd.Execute())
}

254
server/cmd/mattermost/commands/export.go Обычный файл
Просмотреть файл

@@ -0,0 +1,254 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package commands
import (
"context"
"fmt"
"os"
"path/filepath"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
var ExportCmd = &cobra.Command{
Use: "export",
Short: "Export data from Mattermost",
Long: "Export data from Mattermost in a format suitable for import into a third-party application or another Mattermost instance",
}
var ScheduleExportCmd = &cobra.Command{
Use: "schedule",
Short: "Schedule an export data job in Mattermost",
Long: "Schedule an export data job in Mattermost (this will run asynchronously via a background worker)",
Example: "export schedule --format=actiance --exportFrom=12345 --timeoutSeconds=12345",
RunE: scheduleExportCmdF,
}
var CsvExportCmd = &cobra.Command{
Use: "csv",
Short: "Export data from Mattermost in CSV format",
Long: "Export data from Mattermost in CSV format",
Example: "export csv --exportFrom=12345",
RunE: buildExportCmdF("csv"),
}
var ActianceExportCmd = &cobra.Command{
Use: "actiance",
Short: "Export data from Mattermost in Actiance format",
Long: "Export data from Mattermost in Actiance format",
Example: "export actiance --exportFrom=12345",
RunE: buildExportCmdF("actiance"),
}
var GlobalRelayZipExportCmd = &cobra.Command{
Use: "global-relay-zip",
Short: "Export data from Mattermost into a zip file containing emails to send to Global Relay for debug and testing purposes only.",
Long: "Export data from Mattermost into a zip file containing emails to send to Global Relay for debug and testing purposes only. This does not archive any information in Global Relay.",
Example: "export global-relay-zip --exportFrom=12345",
RunE: buildExportCmdF("globalrelay-zip"),
}
var BulkExportCmd = &cobra.Command{
Use: "bulk [file]",
Short: "Export bulk data.",
Long: "Export data to a file compatible with the Mattermost Bulk Import format.",
Example: "export bulk bulk_data.json",
RunE: bulkExportCmdF,
Args: cobra.ExactArgs(1),
}
func init() {
ScheduleExportCmd.Flags().String("format", "actiance", "The format to export data")
ScheduleExportCmd.Flags().Int64("exportFrom", -1, "The timestamp of the earliest post to export, expressed in seconds since the unix epoch.")
ScheduleExportCmd.Flags().Int("timeoutSeconds", -1, "The maximum number of seconds to wait for the job to complete before timing out.")
CsvExportCmd.Flags().Int64("exportFrom", -1, "The timestamp of the earliest post to export, expressed in seconds since the unix epoch.")
CsvExportCmd.Flags().Int("limit", -1, "The number of posts to export. The default of -1 means no limit.")
ActianceExportCmd.Flags().Int64("exportFrom", -1, "The timestamp of the earliest post to export, expressed in seconds since the unix epoch.")
ActianceExportCmd.Flags().Int("limit", -1, "The number of posts to export. The default of -1 means no limit.")
GlobalRelayZipExportCmd.Flags().Int64("exportFrom", -1, "The timestamp of the earliest post to export, expressed in seconds since the unix epoch.")
GlobalRelayZipExportCmd.Flags().Int("limit", -1, "The number of posts to export. The default of -1 means no limit.")
BulkExportCmd.Flags().Bool("all-teams", true, "Export all teams from the server.")
BulkExportCmd.Flags().Bool("attachments", false, "Also export file attachments.")
BulkExportCmd.Flags().Bool("archive", false, "Outputs a single archive file.")
ExportCmd.AddCommand(ScheduleExportCmd)
ExportCmd.AddCommand(CsvExportCmd)
ExportCmd.AddCommand(ActianceExportCmd)
ExportCmd.AddCommand(GlobalRelayZipExportCmd)
ExportCmd.AddCommand(BulkExportCmd)
RootCmd.AddCommand(ExportCmd)
}
func scheduleExportCmdF(command *cobra.Command, args []string) error {
a, err := InitDBCommandContextCobra(command, app.SkipPostInitialization())
if err != nil {
return err
}
defer a.Srv().Shutdown()
if !*a.Config().MessageExportSettings.EnableExport {
return errors.New("ERROR: The message export feature is not enabled")
}
// for now, format is hard-coded to actiance. In time, we'll have to support other formats and inject them into job data
format, err := command.Flags().GetString("format")
if err != nil {
return errors.New("format flag error")
}
if format != "actiance" {
return errors.New("unsupported export format")
}
startTime, err := command.Flags().GetInt64("exportFrom")
if err != nil {
return errors.New("exportFrom flag error")
}
if startTime < 0 {
return errors.New("exportFrom must be a positive integer")
}
timeoutSeconds, err := command.Flags().GetInt("timeoutSeconds")
if err != nil {
return errors.New("timeoutSeconds error")
}
if timeoutSeconds < 0 {
return errors.New("timeoutSeconds must be a positive integer")
}
if messageExportI := a.MessageExport(); messageExportI != nil {
ctx := context.Background()
if timeoutSeconds > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, time.Second*time.Duration(timeoutSeconds))
defer cancel()
}
job, err := messageExportI.StartSynchronizeJob(ctx, startTime)
if err != nil || job.Status == model.JobStatusError || job.Status == model.JobStatusCanceled {
CommandPrintErrorln("ERROR: Message export job failed. Please check the server logs")
} else {
CommandPrettyPrintln("SUCCESS: Message export job complete")
auditRec := a.MakeAuditRecord("scheduleExport", audit.Success)
auditRec.AddMeta("format", format)
auditRec.AddMeta("start", startTime)
a.LogAuditRec(auditRec, nil)
}
}
return nil
}
func buildExportCmdF(format string) func(command *cobra.Command, args []string) error {
return func(command *cobra.Command, args []string) error {
a, err := InitDBCommandContextCobra(command, app.SkipPostInitialization())
license := a.Srv().License()
if err != nil {
return err
}
defer a.Srv().Shutdown()
startTime, err := command.Flags().GetInt64("exportFrom")
if err != nil {
return errors.New("exportFrom flag error")
}
if startTime < 0 {
return errors.New("exportFrom must be a positive integer")
}
limit, err := command.Flags().GetInt("limit")
if err != nil {
return errors.New("limit flag error")
}
if a.MessageExport() == nil || license == nil || !*license.Features.MessageExport {
return errors.New("message export feature not available")
}
warningsCount, appErr := a.MessageExport().RunExport(format, startTime, limit)
if appErr != nil {
return appErr
}
if warningsCount == 0 {
CommandPrettyPrintln("SUCCESS: Your data was exported.")
} else {
if format == model.ComplianceExportTypeGlobalrelay || format == model.ComplianceExportTypeGlobalrelayZip {
CommandPrettyPrintln(fmt.Sprintf("WARNING: %d warnings encountered, see logs for details.", warningsCount))
} else {
CommandPrettyPrintln(fmt.Sprintf("WARNING: %d warnings encountered, see warning.txt for details.", warningsCount))
}
}
auditRec := a.MakeAuditRecord("buildExport", audit.Success)
auditRec.AddMeta("format", format)
auditRec.AddMeta("start", startTime)
a.LogAuditRec(auditRec, nil)
return nil
}
}
func bulkExportCmdF(command *cobra.Command, args []string) error {
a, err := InitDBCommandContextCobra(command, app.SkipPostInitialization())
if err != nil {
return err
}
defer a.Srv().Shutdown()
allTeams, err := command.Flags().GetBool("all-teams")
if err != nil {
return errors.Wrap(err, "all-teams flag error")
}
if !allTeams {
return errors.New("Nothing to export. Please specify the --all-teams flag to export all teams.")
}
attachments, err := command.Flags().GetBool("attachments")
if err != nil {
return errors.Wrap(err, "attachments flag error")
}
archive, err := command.Flags().GetBool("archive")
if err != nil {
return errors.Wrap(err, "archive flag error")
}
fileWriter, err := os.Create(args[0])
if err != nil {
return err
}
defer fileWriter.Close()
outPath, err := filepath.Abs(args[0])
if err != nil {
return err
}
var opts model.BulkExportOpts
opts.IncludeAttachments = attachments
opts.CreateArchive = archive
if err := a.BulkExport(request.EmptyContext(a.Log()), fileWriter, filepath.Dir(outPath), nil /* nil job since it's spawned from CLI */, opts); err != nil {
CommandPrintErrorln(err.Error())
return err
}
auditRec := a.MakeAuditRecord("bulkExport", audit.Success)
auditRec.AddMeta("all_teams", allTeams)
auditRec.AddMeta("file", args[0])
a.LogAuditRec(auditRec, nil)
return nil
}

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

@@ -0,0 +1,74 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package commands
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
)
// There are no tests that actually run the Message Export job, because it can take a long time to complete depending
// on the size of the database that the config is pointing to. As such, these tests just ensure that the CLI command
// fails fast if invalid flags are supplied
func TestMessageExportNotEnabled(t *testing.T) {
th := Setup(t)
defer th.TearDown()
config := th.Config()
config.MessageExportSettings.EnableExport = model.NewBool(false)
th.SetConfig(config)
// should fail fast because the feature isn't enabled
require.Error(t, th.RunCommand(t, "export", "schedule"))
}
func TestMessageExportInvalidFormat(t *testing.T) {
th := Setup(t)
defer th.TearDown()
config := th.Config()
config.MessageExportSettings.EnableExport = model.NewBool(true)
th.SetConfig(config)
// should fail fast because format isn't supported
require.Error(t, th.RunCommand(t, "--format", "not_actiance", "export", "schedule"))
}
func TestMessageExportNegativeExportFrom(t *testing.T) {
th := Setup(t)
defer th.TearDown()
config := th.Config()
config.MessageExportSettings.EnableExport = model.NewBool(true)
th.SetConfig(config)
// should fail fast because export from must be a valid timestamp
require.Error(t, th.RunCommand(t, "--format", "actiance", "--exportFrom", "-1", "export", "schedule"))
}
func TestMessageExportNegativeTimeoutSeconds(t *testing.T) {
th := Setup(t)
defer th.TearDown()
config := th.Config()
config.MessageExportSettings.EnableExport = model.NewBool(true)
th.SetConfig(config)
// should fail fast because timeout seconds must be a positive int
require.Error(t, th.RunCommand(t, "--format", "actiance", "--exportFrom", "0", "--timeoutSeconds", "-1", "export", "schedule"))
}
func TestMessageExportFailsWithoutLicense(t *testing.T) {
th := Setup(t)
defer th.TearDown()
actual, _ := th.RunCommandWithOutput(t, "export", "csv", "export.csv", "--exportFrom", "0")
// should fail fast because timeout seconds must be a positive int
require.Contains(t, actual, "message export feature not available")
}

186
server/cmd/mattermost/commands/import.go Обычный файл
Просмотреть файл

@@ -0,0 +1,186 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package commands
import (
"errors"
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
)
var ImportCmd = &cobra.Command{
Use: "import",
Short: "Import data.",
}
var SlackImportCmd = &cobra.Command{
Use: "slack [team] [file]",
Short: "Import a team from Slack.",
Long: "Import a team from a Slack export zip file.",
Example: " import slack myteam slack_export.zip",
RunE: slackImportCmdF,
}
var BulkImportCmd = &cobra.Command{
Use: "bulk [file]",
Short: "Import bulk data.",
Long: "Import data from a Mattermost Bulk Import File.",
Example: " import bulk bulk_data.json",
RunE: bulkImportCmdF,
}
func init() {
BulkImportCmd.Flags().Bool("apply", false, "Save the import data to the database. Use with caution - this cannot be reverted.")
BulkImportCmd.Flags().Bool("validate", false, "Validate the import data without making any changes to the system.")
BulkImportCmd.Flags().Int("workers", 2, "How many workers to run whilst doing the import.")
BulkImportCmd.Flags().String("import-path", "", "A path to the data directory to import files from.")
ImportCmd.AddCommand(
BulkImportCmd,
SlackImportCmd,
)
RootCmd.AddCommand(ImportCmd)
}
func slackImportCmdF(command *cobra.Command, args []string) error {
a, err := InitDBCommandContextCobra(command)
if err != nil {
return err
}
defer a.Srv().Shutdown()
if len(args) != 2 {
return errors.New("Incorrect number of arguments.")
}
team := getTeamFromTeamArg(a, args[0])
if team == nil {
return errors.New("Unable to find team '" + args[0] + "'")
}
fileReader, err := os.Open(args[1])
if err != nil {
return err
}
defer fileReader.Close()
fileInfo, err := fileReader.Stat()
if err != nil {
return err
}
CommandPrettyPrintln("Running Slack Import. This may take a long time for large teams or teams with many messages.")
importErr, log := a.SlackImport(request.EmptyContext(a.Log()), fileReader, fileInfo.Size(), team.Id)
if importErr != nil {
return err
}
CommandPrettyPrintln("")
CommandPrintln(log.String())
CommandPrettyPrintln("")
CommandPrettyPrintln("Finished Slack Import.")
CommandPrettyPrintln("")
auditRec := a.MakeAuditRecord("slackImport", audit.Success)
auditRec.AddMeta("team", team)
auditRec.AddMeta("file", args[1])
a.LogAuditRec(auditRec, nil)
return nil
}
func bulkImportCmdF(command *cobra.Command, args []string) error {
a, err := InitDBCommandContextCobra(command)
if err != nil {
return err
}
defer a.Srv().Shutdown()
apply, err := command.Flags().GetBool("apply")
if err != nil {
return errors.New("Apply flag error")
}
validate, err := command.Flags().GetBool("validate")
if err != nil {
return errors.New("Validate flag error")
}
workers, err := command.Flags().GetInt("workers")
if err != nil {
return errors.New("Workers flag error")
}
importPath, err := command.Flags().GetString("import-path")
if err != nil {
return errors.New("import-path flag error")
}
if len(args) != 1 {
return errors.New("Incorrect number of arguments.")
}
fileReader, err := os.Open(args[0])
if err != nil {
return err
}
defer fileReader.Close()
if apply && validate {
CommandPrettyPrintln("Use only one of --apply or --validate.")
return nil
}
if apply && !validate {
CommandPrettyPrintln("Running Bulk Import. This may take a long time.")
} else {
CommandPrettyPrintln("Running Bulk Import Data Validation.")
CommandPrettyPrintln("** This checks the validity of the entities in the data file, but does not persist any changes **")
CommandPrettyPrintln("Use the --apply flag to perform the actual data import.")
}
CommandPrettyPrintln("")
if err, lineNumber := a.BulkImportWithPath(request.EmptyContext(a.Log()), fileReader, nil, !apply, workers, importPath); err != nil {
CommandPrintErrorln(err.Error())
if lineNumber != 0 {
CommandPrintErrorln(fmt.Sprintf("Error occurred on data file line %v", lineNumber))
}
return err
}
if apply {
CommandPrettyPrintln("Finished Bulk Import.")
auditRec := a.MakeAuditRecord("bulkImport", audit.Success)
auditRec.AddMeta("file", args[0])
a.LogAuditRec(auditRec, nil)
} else {
CommandPrettyPrintln("Validation complete. You can now perform the import by rerunning this command with the --apply flag.")
}
return nil
}
func getTeamFromTeamArg(a *app.App, teamArg string) *model.Team {
var team *model.Team
team, err := a.Srv().Store().Team().GetByName(teamArg)
if err != nil {
var t *model.Team
if t, err = a.Srv().Store().Team().Get(teamArg); err == nil {
team = t
}
}
return team
}

54
server/cmd/mattermost/commands/init.go Обычный файл
Просмотреть файл

@@ -0,0 +1,54 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package commands
import (
"github.com/spf13/cobra"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/config"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
func initDBCommandContextCobra(command *cobra.Command, readOnlyConfigStore bool, options ...app.Option) (*app.App, error) {
a, err := initDBCommandContext(getConfigDSN(command, config.GetEnvironment()), readOnlyConfigStore, options...)
if err != nil {
// Returning an error just prints the usage message, so actually panic
panic(err)
}
a.InitPlugins(request.EmptyContext(a.Log()), *a.Config().PluginSettings.Directory, *a.Config().PluginSettings.ClientDirectory)
a.DoAppMigrations()
return a, nil
}
func InitDBCommandContextCobra(command *cobra.Command, options ...app.Option) (*app.App, error) {
return initDBCommandContextCobra(command, true, options...)
}
func initDBCommandContext(configDSN string, readOnlyConfigStore bool, options ...app.Option) (*app.App, error) {
if err := utils.TranslationsPreInit(); err != nil {
return nil, err
}
model.AppErrorInit(i18n.T)
// The option order is important as app.Config option reads app.StartMetrics option.
options = append(options, app.Config(configDSN, readOnlyConfigStore, nil))
s, err := app.NewServer(options...)
if err != nil {
return nil, err
}
a := app.New(app.ServerConnector(s.Channels()))
if model.BuildEnterpriseReady == "true" {
a.Srv().LoadLicense()
}
return a, nil
}

72
server/cmd/mattermost/commands/jobserver.go Обычный файл
Просмотреть файл

@@ -0,0 +1,72 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package commands
import (
"os"
"os/signal"
"syscall"
"github.com/spf13/cobra"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/config"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var JobserverCmd = &cobra.Command{
Use: "jobserver",
Short: "Start the Mattermost job server",
RunE: jobserverCmdF,
}
func init() {
JobserverCmd.Flags().Bool("nojobs", false, "Do not run jobs on this jobserver.")
JobserverCmd.Flags().Bool("noschedule", false, "Do not schedule jobs from this jobserver.")
RootCmd.AddCommand(JobserverCmd)
}
func jobserverCmdF(command *cobra.Command, args []string) error {
// Options
noJobs, _ := command.Flags().GetBool("nojobs")
noSchedule, _ := command.Flags().GetBool("noschedule")
// Initialize
a, err := initDBCommandContext(getConfigDSN(command, config.GetEnvironment()), false, app.StartMetrics)
if err != nil {
return err
}
defer a.Srv().Shutdown()
a.Srv().LoadLicense()
// Run jobs
mlog.Info("Starting Mattermost job server")
defer mlog.Info("Stopped Mattermost job server")
if !noJobs {
a.Srv().Jobs.StartWorkers()
defer a.Srv().Jobs.StopWorkers()
}
if !noSchedule {
a.Srv().Jobs.StartSchedulers()
defer a.Srv().Jobs.StopSchedulers()
}
if !noJobs || !noSchedule {
auditRec := a.MakeAuditRecord("jobServer", audit.Success)
a.LogAuditRec(auditRec, nil)
}
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
<-signalChan
// Cleanup anything that isn't handled by a defer statement
mlog.Info("Stopping Mattermost job server")
return nil
}

100
server/cmd/mattermost/commands/main_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,100 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package commands
import (
"flag"
"os"
"testing"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/api4"
"github.com/mattermost/mattermost-server/v6/server/channels/testlib"
)
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
type TestConfig struct {
TestServiceSettings TestServiceSettings
TestTeamSettings TestTeamSettings
TestClientRequirements TestClientRequirements
TestMessageExportSettings TestMessageExportSettings
}
type TestMessageExportSettings struct {
Enableexport bool
Exportformat string
TestGlobalRelaySettings TestGlobalRelaySettings
}
type TestGlobalRelaySettings struct {
Customertype string
Smtpusername string
Smtppassword string
}
type TestServiceSettings struct {
Siteurl string
Websocketurl string
Licensedfieldlocation string
}
type TestTeamSettings struct {
Sitename string
Maxuserperteam int
}
type TestClientRequirements struct {
Androidlatestversion string
Androidminversion string
Desktoplatestversion string
}
type TestNewConfig struct {
TestNewServiceSettings TestNewServiceSettings
TestNewTeamSettings TestNewTeamSettings
}
type TestNewServiceSettings struct {
SiteUrl *string
UseLetsEncrypt *bool
TLSStrictTransportMaxAge *int64
AllowedThemes []string
}
type TestNewTeamSettings struct {
SiteName *string
MaxUserPerTeam *int
}
type TestPluginSettings struct {
Enable *bool
Directory *string `restricted:"true"`
Plugins map[string]map[string]any
PluginStates map[string]*model.PluginState
SignaturePublicKeyFiles []string
}
func TestMain(m *testing.M) {
// Command tests are run by re-invoking the test binary in question, so avoid creating
// another container when we detect same.
flag.Parse()
if filter := flag.Lookup("test.run").Value.String(); filter == "ExecCommand" {
status := m.Run()
os.Exit(status)
return
}
var options = testlib.HelperOptions{
EnableStore: true,
EnableResources: true,
}
mainHelper = testlib.NewMainHelperWithOptions(&options)
defer mainHelper.Close()
api4.SetMainHelper(mainHelper)
mainHelper.Main(m)
}

21
server/cmd/mattermost/commands/output.go Обычный файл
Просмотреть файл

@@ -0,0 +1,21 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package commands
import (
"fmt"
"os"
)
func CommandPrintln(a ...any) (int, error) {
return fmt.Println(a...)
}
func CommandPrintErrorln(a ...any) (int, error) {
return fmt.Fprintln(os.Stderr, a...)
}
func CommandPrettyPrintln(a ...any) (int, error) {
return fmt.Fprintln(os.Stdout, a...)
}

25
server/cmd/mattermost/commands/root.go Обычный файл
Просмотреть файл

@@ -0,0 +1,25 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package commands
import (
"github.com/spf13/cobra"
)
type Command = cobra.Command
func Run(args []string) error {
RootCmd.SetArgs(args)
return RootCmd.Execute()
}
var RootCmd = &cobra.Command{
Use: "mattermost",
Short: "Open source, self-hosted Slack-alternative",
Long: `Mattermost offers workplace messaging across web, PC and phones with archiving, search and integration with your existing systems. Documentation available at https://docs.mattermost.com`,
}
func init() {
RootCmd.PersistentFlags().StringP("config", "c", "", "Configuration file to use.")
}

152
server/cmd/mattermost/commands/server.go Обычный файл
Просмотреть файл

@@ -0,0 +1,152 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package commands
import (
"bytes"
"net"
"os"
"os/signal"
"runtime/debug"
"runtime/pprof"
"syscall"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/mattermost/mattermost-server/v6/server/channels/api4"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/manualtesting"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/channels/web"
"github.com/mattermost/mattermost-server/v6/server/channels/wsapi"
"github.com/mattermost/mattermost-server/v6/server/config"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var serverCmd = &cobra.Command{
Use: "server",
Short: "Run the Mattermost server",
RunE: serverCmdF,
SilenceUsage: true,
}
func init() {
RootCmd.AddCommand(serverCmd)
RootCmd.RunE = serverCmdF
}
func serverCmdF(command *cobra.Command, args []string) error {
interruptChan := make(chan os.Signal, 1)
if err := utils.TranslationsPreInit(); err != nil {
return errors.Wrap(err, "unable to load Mattermost translation files")
}
customDefaults, err := loadCustomDefaults()
if err != nil {
mlog.Warn("Error loading custom configuration defaults: " + err.Error())
}
configStore, err := config.NewStoreFromDSN(getConfigDSN(command, config.GetEnvironment()), false, customDefaults, true)
if err != nil {
return errors.Wrap(err, "failed to load configuration")
}
defer configStore.Close()
return runServer(configStore, interruptChan)
}
func runServer(configStore *config.Store, interruptChan chan os.Signal) error {
// Setting the highest traceback level from the code.
// This is done to print goroutines from all threads (see golang.org/issue/13161)
// and also preserve a crash dump for later investigation.
debug.SetTraceback("crash")
options := []app.Option{
// The option order is important as app.Config option reads app.StartMetrics option.
app.StartMetrics,
app.ConfigStore(configStore),
app.RunEssentialJobs,
app.JoinCluster,
}
server, err := app.NewServer(options...)
if err != nil {
mlog.Error(err.Error())
return err
}
defer server.Shutdown()
// We add this after shutdown so that it can be called
// before server shutdown happens as it can close
// the advanced logger and prevent the mlog call from working properly.
defer func() {
// A panic pass-through layer which just logs it
// and sends it upwards.
if x := recover(); x != nil {
var buf bytes.Buffer
pprof.Lookup("goroutine").WriteTo(&buf, 2)
mlog.Error("A panic occurred",
mlog.Any("error", x),
mlog.String("stack", buf.String()))
panic(x)
}
}()
api, err := api4.Init(server)
if err != nil {
mlog.Error(err.Error())
return err
}
wsapi.Init(server)
web.New(server)
err = server.Start()
if err != nil {
mlog.Error(err.Error())
return err
}
// If we allow testing then listen for manual testing URL hits
if *server.Config().ServiceSettings.EnableTesting {
manualtesting.Init(api)
}
notifyReady()
// wait for kill signal before attempting to gracefully shutdown
// the running service
signal.Notify(interruptChan, syscall.SIGINT, syscall.SIGTERM)
<-interruptChan
return nil
}
func notifyReady() {
// If the environment vars provide a systemd notification socket,
// notify systemd that the server is ready.
systemdSocket := os.Getenv("NOTIFY_SOCKET")
if systemdSocket != "" {
mlog.Info("Sending systemd READY notification.")
err := sendSystemdReadyNotification(systemdSocket)
if err != nil {
mlog.Error(err.Error())
}
}
}
func sendSystemdReadyNotification(socketPath string) error {
msg := "READY=1"
addr := &net.UnixAddr{
Name: socketPath,
Net: "unixgram",
}
conn, err := net.DialUnix(addr.Net, nil, addr)
if err != nil {
return err
}
defer conn.Close()
_, err = conn.Write([]byte(msg))
return err
}

149
server/cmd/mattermost/commands/server_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,149 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package commands
import (
"net"
"os"
"syscall"
"testing"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
"github.com/mattermost/mattermost-server/v6/server/config"
)
const (
unitTestListeningPort = ":0"
)
//nolint:golint,unused
type ServerTestHelper struct {
disableConfigWatch bool
interruptChan chan os.Signal
originalInterval int
}
//nolint:golint,unused
func SetupServerTest(t testing.TB) *ServerTestHelper {
if testing.Short() {
t.SkipNow()
}
// Build a channel that will be used by the server to receive system signals...
interruptChan := make(chan os.Signal, 1)
// ...and sent it immediately a SIGINT value.
// This will make the server loop stop as soon as it started successfully.
interruptChan <- syscall.SIGINT
// Let jobs poll for termination every 0.2s (instead of every 15s by default)
// Otherwise we would have to wait the whole polling duration before the test
// terminates.
originalInterval := jobs.DefaultWatcherPollingInterval
jobs.DefaultWatcherPollingInterval = 200
th := &ServerTestHelper{
disableConfigWatch: true,
interruptChan: interruptChan,
originalInterval: originalInterval,
}
return th
}
//nolint:golint,unused
func (th *ServerTestHelper) TearDownServerTest() {
jobs.DefaultWatcherPollingInterval = th.originalInterval
}
func TestRunServerSuccess(t *testing.T) {
th := SetupServerTest(t)
defer th.TearDownServerTest()
configStore := config.NewTestMemoryStore()
// Use non-default listening port in case another server instance is already running.
cfg := configStore.Get()
*cfg.ServiceSettings.ListenAddress = unitTestListeningPort
configStore.Set(cfg)
err := runServer(configStore, th.interruptChan)
require.NoError(t, err)
}
func TestRunServerSystemdNotification(t *testing.T) {
th := SetupServerTest(t)
defer th.TearDownServerTest()
// Get a random temporary filename for using as a mock systemd socket
socketFile, err := os.CreateTemp("", "mattermost-systemd-mock-socket-")
if err != nil {
panic(err)
}
socketPath := socketFile.Name()
os.Remove(socketPath)
// Set the socket path in the process environment
originalSocket := os.Getenv("NOTIFY_SOCKET")
os.Setenv("NOTIFY_SOCKET", socketPath)
defer os.Setenv("NOTIFY_SOCKET", originalSocket)
// Open the socket connection
addr := &net.UnixAddr{
Name: socketPath,
Net: "unixgram",
}
connection, err := net.ListenUnixgram("unixgram", addr)
if err != nil {
panic(err)
}
defer connection.Close()
defer os.Remove(socketPath)
// Listen for socket data
socketReader := make(chan string)
go func(ch chan string) {
buffer := make([]byte, 512)
count, readErr := connection.Read(buffer)
if readErr != nil {
panic(readErr)
}
data := buffer[0:count]
ch <- string(data)
}(socketReader)
configStore := config.NewTestMemoryStore()
// Use non-default listening port in case another server instance is already running.
cfg := configStore.Get()
*cfg.ServiceSettings.ListenAddress = unitTestListeningPort
configStore.Set(cfg)
// Start and stop the server
err = runServer(configStore, th.interruptChan)
require.NoError(t, err)
// Ensure the notification has been sent on the socket and is correct
notification := <-socketReader
require.Equal(t, notification, "READY=1")
}
func TestRunServerNoSystemd(t *testing.T) {
th := SetupServerTest(t)
defer th.TearDownServerTest()
// Temporarily remove any Systemd socket defined in the environment
originalSocket := os.Getenv("NOTIFY_SOCKET")
os.Unsetenv("NOTIFY_SOCKET")
defer os.Setenv("NOTIFY_SOCKET", originalSocket)
configStore := config.NewTestMemoryStore()
// Use non-default listening port in case another server instance is already running.
cfg := configStore.Get()
*cfg.ServiceSettings.ListenAddress = unitTestListeningPort
configStore.Set(cfg)
err := runServer(configStore, th.interruptChan)
require.NoError(t, err)
}

153
server/cmd/mattermost/commands/test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,153 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package commands
import (
"bufio"
"fmt"
"os"
"os/exec"
"os/signal"
"syscall"
"github.com/spf13/cobra"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/api4"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/wsapi"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
var TestCmd = &cobra.Command{
Use: "test",
Short: "Testing Commands",
Hidden: true,
}
var RunWebClientTestsCmd = &cobra.Command{
Use: "web_client_tests",
Short: "Run the web client tests",
RunE: webClientTestsCmdF,
}
var RunServerForWebClientTestsCmd = &cobra.Command{
Use: "web_client_tests_server",
Short: "Run the server configured for running the web client tests against it",
RunE: serverForWebClientTestsCmdF,
}
func init() {
TestCmd.AddCommand(
RunWebClientTestsCmd,
RunServerForWebClientTestsCmd,
)
RootCmd.AddCommand(TestCmd)
}
func webClientTestsCmdF(command *cobra.Command, args []string) error {
a, err := InitDBCommandContextCobra(command, app.StartMetrics)
if err != nil {
return err
}
defer a.Srv().Shutdown()
i18n.InitTranslations(*a.Config().LocalizationSettings.DefaultServerLocale, *a.Config().LocalizationSettings.DefaultClientLocale)
serverErr := a.Srv().Start()
if serverErr != nil {
return serverErr
}
_, err = api4.Init(a.Srv())
if err != nil {
return err
}
wsapi.Init(a.Srv())
a.UpdateConfig(setupClientTests)
runWebClientTests()
return nil
}
func serverForWebClientTestsCmdF(command *cobra.Command, args []string) error {
a, err := InitDBCommandContextCobra(command, app.StartMetrics)
if err != nil {
return err
}
defer a.Srv().Shutdown()
i18n.InitTranslations(*a.Config().LocalizationSettings.DefaultServerLocale, *a.Config().LocalizationSettings.DefaultClientLocale)
serverErr := a.Srv().Start()
if serverErr != nil {
return serverErr
}
_, err = api4.Init(a.Srv())
if err != nil {
return err
}
wsapi.Init(a.Srv())
a.UpdateConfig(setupClientTests)
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
<-c
return nil
}
func setupClientTests(cfg *model.Config) {
*cfg.TeamSettings.EnableOpenServer = true
*cfg.ServiceSettings.EnableCommands = false
*cfg.ServiceSettings.EnableCustomEmoji = true
*cfg.ServiceSettings.EnableIncomingWebhooks = false
*cfg.ServiceSettings.EnableOutgoingWebhooks = false
}
func executeTestCommand(command *exec.Cmd) {
cmdOutPipe, err := command.StdoutPipe()
if err != nil {
CommandPrintErrorln("Failed to run tests")
os.Exit(1)
return
}
cmdErrOutPipe, err := command.StderrPipe()
if err != nil {
CommandPrintErrorln("Failed to run tests")
os.Exit(1)
return
}
cmdOutReader := bufio.NewScanner(cmdOutPipe)
cmdErrOutReader := bufio.NewScanner(cmdErrOutPipe)
go func() {
for cmdOutReader.Scan() {
fmt.Println(cmdOutReader.Text())
}
}()
go func() {
for cmdErrOutReader.Scan() {
fmt.Println(cmdErrOutReader.Text())
}
}()
if err := command.Run(); err != nil {
CommandPrintErrorln("Client Tests failed")
os.Exit(1)
return
}
}
func runWebClientTests() {
if webappDir := os.Getenv("WEBAPP_DIR"); webappDir != "" {
os.Chdir(webappDir)
} else {
os.Chdir("../mattermost-webapp")
}
cmd := exec.Command("npm", "test")
executeTestCommand(cmd)
}

91
server/cmd/mattermost/commands/utils.go Обычный файл
Просмотреть файл

@@ -0,0 +1,91 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package commands
import (
"bytes"
"encoding/json"
"fmt"
"os"
"reflect"
"sort"
"strings"
"github.com/spf13/cobra"
"github.com/mattermost/mattermost-server/v6/model"
)
const CustomDefaultsEnvVar = "MM_CUSTOM_DEFAULTS_PATH"
// printStringMap takes a reflect.Value and prints it out alphabetically based on key values, which must be strings.
// This is done recursively if it's a map, and uses the given tab settings.
func printStringMap(value reflect.Value, tabVal int) string {
out := &bytes.Buffer{}
var sortedKeys []string
stringToKeyMap := make(map[string]reflect.Value)
for _, k := range value.MapKeys() {
sortedKeys = append(sortedKeys, k.String())
stringToKeyMap[k.String()] = k
}
sort.Strings(sortedKeys)
for _, keyString := range sortedKeys {
key := stringToKeyMap[keyString]
val := value.MapIndex(key)
if newVal, ok := val.Interface().(map[string]any); !ok {
fmt.Fprintf(out, "%s", strings.Repeat("\t", tabVal))
fmt.Fprintf(out, "%v: \"%v\"\n", key.Interface(), val.Interface())
} else {
fmt.Fprintf(out, "%s", strings.Repeat("\t", tabVal))
fmt.Fprintf(out, "%v:\n", key.Interface())
// going one level in, increase the tab
tabVal++
fmt.Fprintf(out, "%s", printStringMap(reflect.ValueOf(newVal), tabVal))
// coming back one level, decrease the tab
tabVal--
}
}
return out.String()
}
func getConfigDSN(command *cobra.Command, env map[string]string) string {
configDSN, _ := command.Flags().GetString("config")
// Config not supplied in flag, check env
if configDSN == "" {
configDSN = env["MM_CONFIG"]
}
// Config not supplied in env or flag use default
if configDSN == "" {
configDSN = "config.json"
}
return configDSN
}
func loadCustomDefaults() (*model.Config, error) {
customDefaultsPath := os.Getenv(CustomDefaultsEnvVar)
if customDefaultsPath == "" {
return nil, nil
}
file, err := os.Open(customDefaultsPath)
if err != nil {
return nil, fmt.Errorf("unable to open custom defaults file at %q: %w", customDefaultsPath, err)
}
defer file.Close()
var customDefaults *model.Config
err = json.NewDecoder(file).Decode(&customDefaults)
if err != nil {
return nil, fmt.Errorf("unable to decode custom defaults configuration: %w", err)
}
return customDefaults, nil
}

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

@@ -0,0 +1,73 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package commands
import (
"reflect"
"sort"
"strings"
"testing"
)
func TestPrintMap(t *testing.T) {
inputCases := []any{
map[string]any{
"CustomerType": "A9",
"SmtpUsername": "",
"SmtpPassword": "",
"EmailAddress": "",
},
map[string]any{
"EnableExport": false,
"ExportFormat": "actiance",
"DailyRunTime": "01:00",
"GlobalRelaySettings": map[string]any{
"CustomerType": "A9",
"SmtpUsername": "",
"SmtpPassword": "",
"EmailAddress": "",
},
},
}
outputCases := []string{
"CustomerType: \"A9\"\nSmtpUsername: \"\"\nSmtpPassword: \"\"\nEmailAddress: \"\"\n",
"EnableExport: \"false\"\nExportFormat: \"actiance\"\nDailyRunTime: \"01:00\"\nGlobalRelaySettings:\n\t CustomerType: \"A9\"\n\tSmtpUsername: \"\"\n\tSmtpPassword: \"\"\n\tEmailAddress: \"\"\n",
}
cases := []struct {
Name string
Input reflect.Value
Expected string
}{
{
Name: "Basic print",
Input: reflect.ValueOf(inputCases[0]),
Expected: outputCases[0],
},
{
Name: "Complex print",
Input: reflect.ValueOf(inputCases[1]),
Expected: outputCases[1],
},
}
for _, test := range cases {
t.Run(test.Name, func(t *testing.T) {
res := printStringMap(test.Input, 0)
// create two slice of string formed by splitting our strings on \n
slice1 := strings.Split(res, "\n")
slice2 := strings.Split(res, "\n")
sort.Strings(slice1)
sort.Strings(slice2)
if !reflect.DeepEqual(slice1, slice2) {
t.Errorf("got '%#v' want '%#v", slice1, slice2)
}
})
}
}

32
server/cmd/mattermost/commands/version.go Обычный файл
Просмотреть файл

@@ -0,0 +1,32 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package commands
import (
"github.com/spf13/cobra"
"github.com/mattermost/mattermost-server/v6/model"
)
var VersionCmd = &cobra.Command{
Use: "version",
Short: "Display version information",
RunE: versionCmdF,
}
func init() {
VersionCmd.Flags().Bool("skip-server-start", false, "Skip the server initialization and return the Mattermost version without the DB version.")
VersionCmd.Flags().MarkDeprecated("skip-server-start", "This flag is not necessary anymore and the flag will be removed in the future releases. Consider removing it from your scripts.")
RootCmd.AddCommand(VersionCmd)
}
func versionCmdF(command *cobra.Command, args []string) error {
CommandPrintln("Version: " + model.CurrentVersion)
CommandPrintln("Build Number: " + model.BuildNumber)
CommandPrintln("Build Date: " + model.BuildDate)
CommandPrintln("Build Hash: " + model.BuildHash)
CommandPrintln("Build Enterprise Ready: " + model.BuildEnterpriseReady)
return nil
}

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

@@ -0,0 +1,19 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package commands
import (
"testing"
)
func TestVersion(t *testing.T) {
if testing.Short() {
t.Skip("skipping version test in short mode")
}
th := SetupWithStoreMock(t)
defer th.TearDown()
th.CheckCommand(t, "version")
}

23
server/cmd/mattermost/main.go Обычный файл
Просмотреть файл

@@ -0,0 +1,23 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package main
import (
"os"
"github.com/mattermost/mattermost-server/v6/server/cmd/mattermost/commands"
// Import and register app layer slash commands
_ "github.com/mattermost/mattermost-server/v6/server/channels/app/slashcommands"
// Plugins
_ "github.com/mattermost/mattermost-server/v6/model/oauthproviders/gitlab"
// Enterprise Imports
_ "github.com/mattermost/mattermost-server/v6/server/channels/imports"
)
func main() {
if err := commands.Run(os.Args[1:]); err != nil {
os.Exit(1)
}
}

20
server/cmd/mattermost/main_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,20 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
//go:build maincoverage
// +build maincoverage
package main
import (
"testing"
)
// TestRunMain can be used to track code coverage in integration tests.
// To run this:
// go test -coverpkg="<>" -ldflags '<>' -tags maincoverage -c ./cmd/mattermost/
// ./mattermost.test -test.run="^TestRunMain$" -test.coverprofile=coverage.out
// And then run your integration tests.
func TestRunMain(t *testing.T) {
main()
}