Mono repo -> Master (#22553)
Combines the following repositories into one: https://github.com/mattermost/mattermost-server https://github.com/mattermost/mattermost-webapp https://github.com/mattermost/focalboard https://github.com/mattermost/mattermost-plugin-playbooks
Этот коммит содержится в:
228
server/cmd/mattermost/commands/cmdtestlib.go
Обычный файл
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
Обычный файл
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
|
||||
}
|
||||
19
server/cmd/mattermost/commands/exec_command_test.go
Обычный файл
19
server/cmd/mattermost/commands/exec_command_test.go
Обычный файл
@@ -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
Обычный файл
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
|
||||
}
|
||||
74
server/cmd/mattermost/commands/export_test.go
Обычный файл
74
server/cmd/mattermost/commands/export_test.go
Обычный файл
@@ -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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
|
||||
}
|
||||
73
server/cmd/mattermost/commands/utils_test.go
Обычный файл
73
server/cmd/mattermost/commands/utils_test.go
Обычный файл
@@ -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
Обычный файл
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
|
||||
}
|
||||
19
server/cmd/mattermost/commands/version_test.go
Обычный файл
19
server/cmd/mattermost/commands/version_test.go
Обычный файл
@@ -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
Обычный файл
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
Обычный файл
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()
|
||||
}
|
||||
Ссылка в новой задаче
Block a user