diff --git a/server/cmd/mmctl/client/client.go b/server/cmd/mmctl/client/client.go index d2fb5c5aa5..0c6b52a49d 100644 --- a/server/cmd/mmctl/client/client.go +++ b/server/cmd/mmctl/client/client.go @@ -148,4 +148,5 @@ type Client interface { DownloadExport(ctx context.Context, name string, wr io.Writer, offset int64) (int64, *model.Response, error) GeneratePresignedURL(ctx context.Context, name string) (*model.PresignURLResponse, *model.Response, error) ResetSamlAuthDataToEmail(ctx context.Context, includeDeleted bool, dryRun bool, userIDs []string) (int64, *model.Response, error) + GenerateSupportPacket(ctx context.Context) ([]byte, *model.Response, error) } diff --git a/server/cmd/mmctl/commands/system.go b/server/cmd/mmctl/commands/system.go index 93d8d4edcf..3f92403aa5 100644 --- a/server/cmd/mmctl/commands/system.go +++ b/server/cmd/mmctl/commands/system.go @@ -6,6 +6,8 @@ package commands import ( "context" "fmt" + "os" + "time" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -65,15 +67,28 @@ var SystemStatusCmd = &cobra.Command{ RunE: withClient(systemStatusCmdF), } +var SystemSupportPacketCmd = &cobra.Command{ + Use: "supportpacket", + Short: "Download a Support Packet", + Long: "Generate and download a Support Packet of the server to share it with Mattermost Support", + Example: ` system supportpacket`, + Args: cobra.NoArgs, + RunE: withClient(systemSupportPacketCmdF), +} + func init() { SystemSetBusyCmd.Flags().UintP("seconds", "s", 3600, "Number of seconds until server is automatically marked as not busy.") _ = SystemSetBusyCmd.MarkFlagRequired("seconds") + + SystemSupportPacketCmd.Flags().StringP("output-file", "o", "", "Output file name (default \"mattermost_support_packet_YYYY-MM-DD-HH-MM.zip\")") + SystemCmd.AddCommand( SystemGetBusyCmd, SystemSetBusyCmd, SystemClearBusyCmd, SystemVersionCmd, SystemStatusCmd, + SystemSupportPacketCmd, ) RootCmd.AddCommand(SystemCmd) } @@ -152,3 +167,36 @@ Filestore Status: {{.filestore_status}}`, status) return nil } + +func systemSupportPacketCmdF(c client.Client, cmd *cobra.Command, _ []string) error { + printer.SetSingle(true) + + filename, err := cmd.Flags().GetString("output-file") + if err != nil { + return err + } + + if filename == "" { + filename = fmt.Sprintf("mattermost_support_packet_%s.zip", time.Now().Format("2006-01-02-03-04")) + } + + printer.Print("Downloading Support Packet") + + data, _, err := c.GenerateSupportPacket(context.TODO()) + if err != nil { + return fmt.Errorf("unable to fetch Support Packet: %w", err) + } + + file, err := os.Create(filename) + if err != nil { + return fmt.Errorf("failed to create zip file: %w", err) + } + + _, err = file.Write(data) + if err != nil { + return fmt.Errorf("failed to write to zip file: %w", err) + } + + printer.PrintT("Downloaded Support Packet to {{ .filename }}", map[string]string{"filename": filename}) + return nil +} diff --git a/server/cmd/mmctl/commands/system_e2e_test.go b/server/cmd/mmctl/commands/system_e2e_test.go index a9de1f6f8c..64d298b364 100644 --- a/server/cmd/mmctl/commands/system_e2e_test.go +++ b/server/cmd/mmctl/commands/system_e2e_test.go @@ -4,6 +4,8 @@ package commands import ( + "os" + "strings" "time" "github.com/mattermost/mattermost/server/v8/cmd/mmctl/client" @@ -103,3 +105,61 @@ func (s *MmctlE2ETestSuite) TestClearBusyCmd() { s.Require().False(s.th.App.Srv().Platform().Busy.IsBusy()) }) } + +func (s *MmctlE2ETestSuite) TestSupportPacketCmdF() { + s.SetupEnterpriseTestHelper().InitBasic() + + printer.SetFormat(printer.FormatPlain) + s.T().Cleanup(func() { printer.SetFormat(printer.FormatJSON) }) + + s.Run("Download support packet with default filename", func() { + printer.Clean() + + s.T().Cleanup(cleanupSupportPacket(s.T())) + + err := systemSupportPacketCmdF(s.th.SystemAdminClient, SystemSupportPacketCmd, []string{}) + s.Require().NoError(err) + s.Require().Len(printer.GetLines(), 2) + s.Require().Equal(printer.GetLines()[0], "Downloading Support Packet") + s.Require().Contains(printer.GetLines()[1], "Downloaded Support Packet to ") + s.Require().Len(printer.GetErrorLines(), 0) + + var found bool + + entries, err := os.ReadDir(".") + s.Require().NoError(err) + for _, e := range entries { + if strings.HasPrefix(e.Name(), "mattermost_support_packet_") && strings.HasSuffix(e.Name(), ".zip") { + b, err := os.ReadFile(e.Name()) + s.NoError(err) + + s.NotEmpty(b, b) + + found = true + } + } + s.True(found) + }) + + s.Run("Download support packet with custom filename", func() { + printer.Clean() + + err := SystemSupportPacketCmd.ParseFlags([]string{"-o", "foo.zip"}) + s.Require().NoError(err) + + s.T().Cleanup(func() { + s.Require().NoError(os.Remove("foo.zip")) + }) + + err = systemSupportPacketCmdF(s.th.SystemAdminClient, SystemSupportPacketCmd, []string{}) + s.Require().NoError(err) + s.Require().Len(printer.GetErrorLines(), 0) + s.Require().Len(printer.GetLines(), 2) + s.Require().Equal(printer.GetLines()[0], "Downloading Support Packet") + s.Require().Equal(printer.GetLines()[1], "Downloaded Support Packet to foo.zip") + + b, err := os.ReadFile("foo.zip") + s.Require().NoError(err) + s.NotNil(b, b) + }) +} diff --git a/server/cmd/mmctl/commands/system_test.go b/server/cmd/mmctl/commands/system_test.go index 66e72f67e0..734d807059 100644 --- a/server/cmd/mmctl/commands/system_test.go +++ b/server/cmd/mmctl/commands/system_test.go @@ -6,15 +6,19 @@ package commands import ( "context" "net/http" + "os" "strconv" + "strings" + "testing" "time" "github.com/mattermost/mattermost/server/public/model" "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer" - - "github.com/spf13/cobra" ) func (s *MmctlUnitTestSuite) TestGetBusyCmd() { @@ -210,3 +214,102 @@ func (s *MmctlUnitTestSuite) TestServerStatusCmd() { s.Require().Len(printer.GetLines(), 0) }) } + +func cleanupSupportPacket(t *testing.T) func() { + return func() { + entries, err := os.ReadDir(".") + require.NoError(t, err) + for _, e := range entries { + if strings.HasPrefix(e.Name(), "mattermost_support_packet_") && strings.HasSuffix(e.Name(), ".zip") { + err = os.Remove(e.Name()) + assert.NoError(t, err) + } + } + } +} + +func (s *MmctlUnitTestSuite) TestSupportPacketCmdF() { + printer.SetFormat(printer.FormatPlain) + s.T().Cleanup(func() { printer.SetFormat(printer.FormatJSON) }) + + s.Run("Download support packet with default filename", func() { + printer.Clean() + + s.T().Cleanup(cleanupSupportPacket(s.T())) + + data := []byte("some bytes") + s.client. + EXPECT(). + GenerateSupportPacket(context.TODO()). + Return(data, &model.Response{}, nil). + Times(1) + + err := systemSupportPacketCmdF(s.client, SystemSupportPacketCmd, []string{}) + s.Require().NoError(err) + s.Require().Len(printer.GetErrorLines(), 0) + s.Require().Len(printer.GetLines(), 2) + s.Require().Equal(printer.GetLines()[0], "Downloading Support Packet") + s.Require().Contains(printer.GetLines()[1], "Downloaded Support Packet to ") + + var found bool + + entries, err := os.ReadDir(".") + s.Require().NoError(err) + for _, e := range entries { + if strings.HasPrefix(e.Name(), "mattermost_support_packet_") && strings.HasSuffix(e.Name(), ".zip") { + b, err := os.ReadFile(e.Name()) + s.NoError(err) + s.Equal(b, data) + + found = true + } + } + + s.True(found) + }) + + s.Run("Download support packet with custom filename", func() { + printer.Clean() + + data := []byte("some bytes") + s.client. + EXPECT(). + GenerateSupportPacket(context.TODO()). + Return(data, &model.Response{}, nil). + Times(1) + + err := SystemSupportPacketCmd.ParseFlags([]string{"-o", "foo.zip"}) + s.Require().NoError(err) + + s.T().Cleanup(func() { + s.Require().NoError(os.Remove("foo.zip")) + }) + + err = systemSupportPacketCmdF(s.client, SystemSupportPacketCmd, []string{}) + s.Require().NoError(err) + s.Require().Len(printer.GetErrorLines(), 0) + s.Require().Len(printer.GetLines(), 2) + s.Require().Equal(printer.GetLines()[0], "Downloading Support Packet") + s.Require().Equal(printer.GetLines()[1], "Downloaded Support Packet to foo.zip") + + b, err := os.ReadFile("foo.zip") + s.Require().NoError(err) + s.Equal(b, data) + }) + + s.Run("Request to the server fails", func() { + printer.Clean() + + s.client. + EXPECT(). + GenerateSupportPacket(context.TODO()). + Return(nil, &model.Response{}, errors.New("mock error")). + Times(1) + + err := systemSupportPacketCmdF(s.client, SystemSupportPacketCmd, []string{}) + s.Require().Error(err) + s.Require().Len(printer.GetErrorLines(), 0) + s.Require().Len(printer.GetLines(), 1) + s.Require().Equal(printer.GetLines()[0], "Downloading Support Packet") + }) +} diff --git a/server/cmd/mmctl/docs/mmctl_system.rst b/server/cmd/mmctl/docs/mmctl_system.rst index 1cc9a67235..622f9d8284 100644 --- a/server/cmd/mmctl/docs/mmctl_system.rst +++ b/server/cmd/mmctl/docs/mmctl_system.rst @@ -41,5 +41,6 @@ SEE ALSO * `mmctl system getbusy `_ - Get the current busy state * `mmctl system setbusy `_ - Set the busy state to true * `mmctl system status `_ - Prints the status of the server +* `mmctl system supportpacket `_ - Download a Support Packet * `mmctl system version `_ - Prints the remote server version diff --git a/server/cmd/mmctl/docs/mmctl_system_supportpacket.rst b/server/cmd/mmctl/docs/mmctl_system_supportpacket.rst new file mode 100644 index 0000000000..31edd25053 --- /dev/null +++ b/server/cmd/mmctl/docs/mmctl_system_supportpacket.rst @@ -0,0 +1,52 @@ +.. _mmctl_system_supportpacket: + +mmctl system supportpacket +-------------------------- + +Download a Support Packet + +Synopsis +~~~~~~~~ + + +Generate and download a Support Packet of the server to share it with Mattermost Support + +:: + + mmctl system supportpacket [flags] + +Examples +~~~~~~~~ + +:: + + system supportpacket + +Options +~~~~~~~ + +:: + + -h, --help help for supportpacket + -o, --output-file string Output file name (default "mattermost_support_packet_YYYY-MM-DD-HH-MM.zip") + +Options inherited from parent commands +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:: + + --config string path to the configuration file (default "$XDG_CONFIG_HOME/mmctl/config") + --disable-pager disables paged output + --insecure-sha1-intermediate allows to use insecure TLS protocols, such as SHA-1 + --insecure-tls-version allows to use TLS versions 1.0 and 1.1 + --json the output format will be in json format + --local allows communicating with the server through a unix socket + --quiet prevent mmctl to generate output for the commands + --strict will only run commands if the mmctl version matches the server one + --suppress-warnings disables printing warning messages + +SEE ALSO +~~~~~~~~ + +* `mmctl system `_ - System management + diff --git a/server/cmd/mmctl/mocks/client_mock.go b/server/cmd/mmctl/mocks/client_mock.go index 467b5093ed..5870286e3d 100644 --- a/server/cmd/mmctl/mocks/client_mock.go +++ b/server/cmd/mmctl/mocks/client_mock.go @@ -541,6 +541,22 @@ func (mr *MockClientMockRecorder) GeneratePresignedURL(arg0, arg1 interface{}) * return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GeneratePresignedURL", reflect.TypeOf((*MockClient)(nil).GeneratePresignedURL), arg0, arg1) } +// GenerateSupportPacket mocks base method. +func (m *MockClient) GenerateSupportPacket(arg0 context.Context) ([]byte, *model.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GenerateSupportPacket", arg0) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(*model.Response) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GenerateSupportPacket indicates an expected call of GenerateSupportPacket. +func (mr *MockClientMockRecorder) GenerateSupportPacket(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateSupportPacket", reflect.TypeOf((*MockClient)(nil).GenerateSupportPacket), arg0) +} + // GetAllTeams mocks base method. func (m *MockClient) GetAllTeams(arg0 context.Context, arg1 string, arg2, arg3 int) ([]*model.Team, *model.Response, error) { m.ctrl.T.Helper()