diff --git a/server/cmd/mmctl/commands/compliance_export.go b/server/cmd/mmctl/commands/compliance_export.go new file mode 100644 index 0000000000..301714ad40 --- /dev/null +++ b/server/cmd/mmctl/commands/compliance_export.go @@ -0,0 +1,60 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package commands + +import ( + "context" + "fmt" + + "github.com/mattermost/mattermost/server/v8/cmd/mmctl/client" + "github.com/spf13/cobra" +) + +var ComplianceExportCmd = &cobra.Command{ + Use: "compliance_export", + Short: "Management of compliance exports", +} + +var ComplianceExportListCmd = &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "List compliance export jobs, sorted by creation date descending (newest first)", + Args: cobra.NoArgs, + RunE: withClient(complianceExportListCmdF), +} + +var ComplianceExportShowCmd = &cobra.Command{ + Use: "show [complianceExportJobID]", + Example: " compliance_export show o98rj3ur83dp5dppfyk5yk6osy", + Short: "Show compliance export job", + Args: cobra.ExactArgs(1), + RunE: withClient(complianceExportShowCmdF), +} + +func init() { + ComplianceExportListCmd.Flags().Int("page", 0, "Page number to fetch for the list of compliance export jobs") + ComplianceExportListCmd.Flags().Int("per-page", DefaultPageSize, "Number of compliance export jobs to be fetched") + ComplianceExportListCmd.Flags().Bool("all", false, "Fetch all compliance export jobs. --page flag will be ignored if provided") + + ComplianceExportCmd.AddCommand( + ComplianceExportListCmd, + ComplianceExportShowCmd, + ) + RootCmd.AddCommand(ComplianceExportCmd) +} + +func complianceExportListCmdF(c client.Client, command *cobra.Command, args []string) error { + return jobListCmdF(c, command, "message_export", "") +} + +func complianceExportShowCmdF(c client.Client, command *cobra.Command, args []string) error { + job, _, err := c.GetJob(context.TODO(), args[0]) + if err != nil { + return fmt.Errorf("failed to get compliance export job: %w", err) + } + + printJob(job) + + return nil +} diff --git a/server/cmd/mmctl/commands/compliance_export_e2e_test.go b/server/cmd/mmctl/commands/compliance_export_e2e_test.go new file mode 100644 index 0000000000..7e9b15cd91 --- /dev/null +++ b/server/cmd/mmctl/commands/compliance_export_e2e_test.go @@ -0,0 +1,200 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package commands + +import ( + "context" + + "github.com/mattermost/mattermost/server/public/model" + st "github.com/mattermost/mattermost/server/v8/channels/store/storetest" + "github.com/mattermost/mattermost/server/v8/cmd/mmctl/client" + "github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer" +) + +func (s *MmctlE2ETestSuite) TestComplianceExportListCmdE2E() { + s.SetupMessageExportTestHelper() + + s.Run("no permissions", func() { + printer.Clean() + + cmd := makeCmd() + err := complianceExportListCmdF(s.th.Client, cmd, nil) + s.Require().EqualError(err, "failed to get jobs: You do not have the appropriate permissions.") + s.Require().Empty(printer.GetLines()) + s.Require().Empty(printer.GetErrorLines()) + }) + + jobType := model.JobTypeMessageExport + + s.RunForSystemAdminAndLocal("List with no compliance export jobs", func(c client.Client) { + // Ensure no jobs exist + jobs, _, err := s.th.SystemAdminClient.GetJobsByType(context.Background(), jobType, 0, 1000) + s.Require().NoError(err) + + for _, job := range jobs { + var result string + result, err = s.th.App.Srv().Store().Job().Delete(job.Id) + s.Require().NoError(err, "Failed to delete job (result: %v)", result) + } + + cmd := makeCmd() + // Test default pagination + printer.Clean() + err = complianceExportListCmdF(c, cmd, nil) + s.Require().NoError(err) + s.Require().Len(printer.GetLines(), 1) + s.Require().Equal("No jobs found", printer.GetLines()[0]) + + // Test with 1 per page + printer.Clean() + cmd = makeCmd() + _ = cmd.Flags().Set("page", "0") + _ = cmd.Flags().Set("per-page", "1") + err = complianceExportListCmdF(c, cmd, nil) + s.Require().NoError(err) + s.Require().Len(printer.GetLines(), 1) + s.Require().Equal("No jobs found", printer.GetLines()[0]) + + // Test with all items + printer.Clean() + cmd = makeCmd() + _ = cmd.Flags().Set("all", "true") + err = complianceExportListCmdF(c, cmd, nil) + s.Require().NoError(err) + s.Require().Len(printer.GetLines(), 1) + s.Require().Equal("No jobs found", printer.GetLines()[0]) + }) + + s.RunForSystemAdminAndLocal("List compliance export jobs", func(c client.Client) { + now := model.GetMillis() + // Create 2 jobs + job, _, err := s.th.SystemAdminClient.CreateJob(context.Background(), &model.Job{ + Id: st.NewTestID(), + CreateAt: now - 1000, + Status: model.JobStatusSuccess, + Type: model.JobTypeMessageExport, + StartAt: now - 1000, + LastActivityAt: now - 1000, + }) + s.Require().NoError(err) + + job2, _, err := s.th.SystemAdminClient.CreateJob(context.Background(), &model.Job{ + Id: st.NewTestID(), + CreateAt: now - 100, + Status: model.JobStatusSuccess, + Type: model.JobTypeMessageExport, + StartAt: now - 100, + LastActivityAt: now - 100, + }) + s.Require().NoError(err) + defer func() { + // Ensure jobs are deleted from the database + var result string + result, err = s.th.App.Srv().Store().Job().Delete(job.Id) + s.Require().NoError(err, "Failed to delete job (result: %v)", result) + result, err = s.th.App.Srv().Store().Job().Delete(job2.Id) + s.Require().NoError(err, "Failed to delete job (result: %v)", result) + }() + + // Test default pagination + printer.Clean() + cmd := makeCmd() + err = complianceExportListCmdF(c, cmd, nil) + s.Require().NoError(err) + s.Require().Len(printer.GetLines(), 2) + s.Require().Equal(job2.Id, printer.GetLines()[0].(*model.Job).Id) + s.Require().Equal(job.Id, printer.GetLines()[1].(*model.Job).Id) + + // Test with 1 per page + printer.Clean() + cmd = makeCmd() + _ = cmd.Flags().Set("page", "0") + _ = cmd.Flags().Set("per-page", "1") + err = complianceExportListCmdF(c, cmd, nil) + s.Require().NoError(err) + s.Require().Len(printer.GetLines(), 1) + s.Require().Equal(job2.Id, printer.GetLines()[0].(*model.Job).Id) + + // Test with all items + printer.Clean() + cmd = makeCmd() + _ = cmd.Flags().Set("all", "true") + err = complianceExportListCmdF(c, cmd, nil) + s.Require().NoError(err) + s.Require().Len(printer.GetLines(), 2) + s.Require().Equal(job2.Id, printer.GetLines()[0].(*model.Job).Id) + s.Require().Equal(job.Id, printer.GetLines()[1].(*model.Job).Id) + }) +} + +func (s *MmctlE2ETestSuite) TestComplianceExportShowCmdE2E() { + s.SetupMessageExportTestHelper() + + now := model.GetMillis() + + // Create a job + job, _, err := s.th.SystemAdminClient.CreateJob(context.Background(), &model.Job{ + Id: st.NewTestID(), + CreateAt: now - 1000, + Status: model.JobStatusSuccess, + Type: model.JobTypeMessageExport, + StartAt: now - 1000, + LastActivityAt: now - 1000, + }) + s.Require().NoError(err) + defer func() { + // Ensure job is deleted from the database + var result string + result, err = s.th.App.Srv().Store().Job().Delete(job.Id) + s.Require().NoError(err, "Failed to delete job (result: %v)", result) + }() + + s.Run("no permissions", func() { + printer.Clean() + + cmd := makeCmd() + err := complianceExportShowCmdF(s.th.Client, cmd, []string{job.Id}) + s.Require().EqualError(err, "failed to get compliance export job: You do not have the appropriate permissions.") + s.Require().Empty(printer.GetLines()) + s.Require().Empty(printer.GetErrorLines()) + }) + + s.RunForSystemAdminAndLocal("Show non-existent job", func(c client.Client) { + printer.Clean() + + cmd := makeCmd() + err := complianceExportShowCmdF(c, cmd, []string{"non-existent-job-id"}) + s.Require().EqualError(err, "failed to get compliance export job: Sorry, we could not find the page., There doesn't appear to be an api call for the url='/api/v4/jobs/non-existent-job-id'. Typo? are you missing a team_id or user_id as part of the url?") + s.Require().Empty(printer.GetLines()) + s.Require().Empty(printer.GetErrorLines()) + }) + + s.RunForSystemAdminAndLocal("Show existing job", func(c client.Client) { + now := model.GetMillis() + // Create a job + job, _, err := s.th.SystemAdminClient.CreateJob(context.Background(), &model.Job{ + Id: st.NewTestID(), + CreateAt: now - 1000, + Status: model.JobStatusSuccess, + Type: model.JobTypeMessageExport, + StartAt: now - 1000, + LastActivityAt: now - 1000, + }) + s.Require().NoError(err) + defer func() { + // Ensure job is deleted from the database + var result string + result, err = s.th.App.Srv().Store().Job().Delete(job.Id) + s.Require().NoError(err, "Failed to delete job (result: %v)", result) + }() + + printer.Clean() + cmd := makeCmd() + err = complianceExportShowCmdF(c, cmd, []string{job.Id}) + s.Require().NoError(err) + s.Require().Len(printer.GetLines(), 1) + s.Require().Empty(printer.GetErrorLines()) + s.Require().Equal(job.Id, printer.GetLines()[0].(*model.Job).Id) + }) +} diff --git a/server/cmd/mmctl/commands/compliance_export_test.go b/server/cmd/mmctl/commands/compliance_export_test.go new file mode 100644 index 0000000000..7a0e744a52 --- /dev/null +++ b/server/cmd/mmctl/commands/compliance_export_test.go @@ -0,0 +1,166 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package commands + +import ( + "context" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer" + "github.com/spf13/cobra" +) + +func (s *MmctlUnitTestSuite) TestComplianceExportListCmdF() { + s.Run("list default pagination", func() { + printer.Clean() + var mockJobs []*model.Job + + // Test with default pagination + s.client. + EXPECT(). + GetJobs(context.TODO(), "message_export", "", 0, DefaultPageSize). + Return(mockJobs, &model.Response{}, nil). + Times(1) + + cmd := makeCmd() + err := complianceExportListCmdF(s.client, cmd, nil) + s.Require().Nil(err) + s.Len(printer.GetLines(), 1) + s.Len(printer.GetErrorLines(), 0) + s.Equal("No jobs found", printer.GetLines()[0]) + + // Test with 10 per page + printer.Clean() + cmd = makeCmd() + _ = cmd.Flags().Set("per-page", "10") + s.client. + EXPECT(). + GetJobs(context.TODO(), "message_export", "", 0, 10). + Return(mockJobs, &model.Response{}, nil). + Times(1) + + err = complianceExportListCmdF(s.client, cmd, nil) + s.Require().Nil(err) + s.Len(printer.GetLines(), 1) + s.Len(printer.GetErrorLines(), 0) + s.Equal("No jobs found", printer.GetLines()[0]) + + // Test with all items + printer.Clean() + cmd = makeCmd() + _ = cmd.Flags().Set("all", "true") + s.client. + EXPECT(). + GetJobs(context.TODO(), "message_export", "", 0, DefaultPageSize). + Return(mockJobs, &model.Response{}, nil). + Times(1) + + err = complianceExportListCmdF(s.client, cmd, nil) + s.Require().Nil(err) + s.Len(printer.GetLines(), 1) + s.Len(printer.GetErrorLines(), 0) + s.Equal("No jobs found", printer.GetLines()[0]) + }) + + s.Run("list with paging", func() { + // Create 5 mock jobs + mockJobs := make([]*model.Job, 5) + for i := range 5 { + mockJobs[i] = &model.Job{ + Id: model.NewId(), + CreateAt: model.GetMillis() - int64(i*1000), + } + } + + // Test paging with 2 jobs per page + printer.Clean() + cmd := makeCmd() + _ = cmd.Flags().Set("all", "true") + _ = cmd.Flags().Set("per-page", "2") + + // Expect 4 API calls (2 jobs each for first 2 pages, 1 job for last page, then a call with 0 jobs) + s.client. + EXPECT(). + GetJobs(context.TODO(), "message_export", "", 0, 2). + Return(mockJobs[0:2], &model.Response{}, nil). + Times(1) + s.client. + EXPECT(). + GetJobs(context.TODO(), "message_export", "", 1, 2). + Return(mockJobs[2:4], &model.Response{}, nil). + Times(1) + s.client. + EXPECT(). + GetJobs(context.TODO(), "message_export", "", 2, 2). + Return(mockJobs[4:5], &model.Response{}, nil). + Times(1) + s.client. + EXPECT(). + GetJobs(context.TODO(), "message_export", "", 3, 2). + Return(mockJobs[5:], &model.Response{}, nil). + Times(1) + + err := complianceExportListCmdF(s.client, cmd, nil) + s.Require().Nil(err) + s.Len(printer.GetLines(), 5) + s.Len(printer.GetErrorLines(), 0) + + // Verify jobs are printed in correct order + for i := range 5 { + s.Equal(mockJobs[i].Id, printer.GetLines()[i].(*model.Job).Id) + } + }) +} + +func (s *MmctlUnitTestSuite) TestComplianceExportShowCmdF() { + s.Run("show job successfully", func() { + printer.Clean() + mockJob := &model.Job{ + Id: model.NewId(), + CreateAt: model.GetMillis(), + Type: model.JobTypeMessageExport, + } + + s.client. + EXPECT(). + GetJob(context.TODO(), mockJob.Id). + Return(mockJob, &model.Response{}, nil). + Times(1) + + cmd := makeCmd() + err := complianceExportShowCmdF(s.client, cmd, []string{mockJob.Id}) + s.Require().Nil(err) + s.Len(printer.GetLines(), 1) + s.Len(printer.GetErrorLines(), 0) + s.Equal(mockJob, printer.GetLines()[0].(*model.Job)) + }) + + s.Run("show job with error", func() { + printer.Clean() + mockError := &model.AppError{ + Message: "failed to get job", + } + + s.client. + EXPECT(). + GetJob(context.TODO(), "invalid-job-id"). + Return(nil, &model.Response{}, mockError). + Times(1) + + cmd := makeCmd() + err := complianceExportShowCmdF(s.client, cmd, []string{"invalid-job-id"}) + s.Require().NotNil(err) + s.EqualError(err, "failed to get compliance export job: failed to get job") + s.Len(printer.GetLines(), 0) + s.Len(printer.GetErrorLines(), 0) + }) +} + +func makeCmd() *cobra.Command { + cmd := &cobra.Command{} + cmd.Flags().Int("page", 0, "") + cmd.Flags().Int("per-page", DefaultPageSize, "") + cmd.Flags().Bool("all", false, "") + return cmd +} diff --git a/server/cmd/mmctl/commands/mmctl_test.go b/server/cmd/mmctl/commands/mmctl_test.go index e44dd8f4b9..c5bc832d22 100644 --- a/server/cmd/mmctl/commands/mmctl_test.go +++ b/server/cmd/mmctl/commands/mmctl_test.go @@ -4,14 +4,16 @@ package commands import ( + "github.com/golang/mock/gomock" + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/v8/channels/api4" + "github.com/mattermost/mattermost/server/v8/channels/jobs" "github.com/mattermost/mattermost/server/v8/cmd/mmctl/client" "github.com/mattermost/mattermost/server/v8/cmd/mmctl/mocks" "github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer" - - "github.com/golang/mock/gomock" + "github.com/mattermost/mattermost/server/v8/enterprise/message_export" + "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" - - "github.com/mattermost/mattermost/server/v8/channels/api4" ) var EnableEnterpriseTests string @@ -67,6 +69,26 @@ func (s *MmctlE2ETestSuite) SetupEnterpriseTestHelper() *api4.TestHelper { return s.th } +func (s *MmctlE2ETestSuite) SetupMessageExportTestHelper() *api4.TestHelper { + if EnableEnterpriseTests != "true" { + s.T().SkipNow() + } + + jobs.DefaultWatcherPollingInterval = 100 + s.th = api4.SetupEnterprise(s.T()).InitBasic() + s.th.App.Srv().SetLicense(model.NewTestLicense("message_export")) + messageExportImpl := message_export.MessageExportJobInterfaceImpl{Server: s.th.App.Srv()} + s.th.App.Srv().Jobs.RegisterJobType(model.JobTypeMessageExport, messageExportImpl.MakeWorker(), messageExportImpl.MakeScheduler()) + + err := s.th.App.Srv().Jobs.StartWorkers() + require.NoError(s.T(), err) + + err = s.th.App.Srv().Jobs.StartSchedulers() + require.NoError(s.T(), err) + + return s.th +} + // RunForSystemAdminAndLocal runs a test function for both SystemAdmin // and Local clients. Several commands work in the same way when used // by a fully privileged user and through the local mode, so this diff --git a/server/cmd/mmctl/docs/mmctl.rst b/server/cmd/mmctl/docs/mmctl.rst index c2915fd5fa..afb9eb645c 100644 --- a/server/cmd/mmctl/docs/mmctl.rst +++ b/server/cmd/mmctl/docs/mmctl.rst @@ -35,6 +35,7 @@ SEE ALSO * `mmctl channel `_ - Management of channels * `mmctl command `_ - Management of slash commands * `mmctl completion `_ - Generates autocompletion scripts for bash and zsh +* `mmctl compliance_export `_ - Management of compliance exports * `mmctl config `_ - Configuration * `mmctl docs `_ - Generates mmctl documentation * `mmctl export `_ - Management of exports diff --git a/server/cmd/mmctl/docs/mmctl_compliance_export.rst b/server/cmd/mmctl/docs/mmctl_compliance_export.rst new file mode 100644 index 0000000000..abe351204b --- /dev/null +++ b/server/cmd/mmctl/docs/mmctl_compliance_export.rst @@ -0,0 +1,42 @@ +.. _mmctl_compliance_export: + +mmctl compliance_export +----------------------- + +Management of compliance exports + +Synopsis +~~~~~~~~ + + +Management of compliance exports + +Options +~~~~~~~ + +:: + + -h, --help help for compliance_export + +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 `_ - Remote client for the Open Source, self-hosted Slack-alternative +* `mmctl compliance_export list `_ - List compliance export jobs, sorted by creation date descending (newest first) +* `mmctl compliance_export show `_ - Show compliance export job + diff --git a/server/cmd/mmctl/docs/mmctl_compliance_export_list.rst b/server/cmd/mmctl/docs/mmctl_compliance_export_list.rst new file mode 100644 index 0000000000..d81ca8e8f3 --- /dev/null +++ b/server/cmd/mmctl/docs/mmctl_compliance_export_list.rst @@ -0,0 +1,47 @@ +.. _mmctl_compliance_export_list: + +mmctl compliance_export list +---------------------------- + +List compliance export jobs, sorted by creation date descending (newest first) + +Synopsis +~~~~~~~~ + + +List compliance export jobs, sorted by creation date descending (newest first) + +:: + + mmctl compliance_export list [flags] + +Options +~~~~~~~ + +:: + + --all Fetch all compliance export jobs. --page flag will be ignored if provided + -h, --help help for list + --page int Page number to fetch for the list of compliance export jobs + --per-page int Number of compliance export jobs to be fetched (default 200) + +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 compliance_export `_ - Management of compliance exports + diff --git a/server/cmd/mmctl/docs/mmctl_compliance_export_show.rst b/server/cmd/mmctl/docs/mmctl_compliance_export_show.rst new file mode 100644 index 0000000000..2c09d3e469 --- /dev/null +++ b/server/cmd/mmctl/docs/mmctl_compliance_export_show.rst @@ -0,0 +1,51 @@ +.. _mmctl_compliance_export_show: + +mmctl compliance_export show +---------------------------- + +Show compliance export job + +Synopsis +~~~~~~~~ + + +Show compliance export job + +:: + + mmctl compliance_export show [complianceExportJobID] [flags] + +Examples +~~~~~~~~ + +:: + + compliance_export show o98rj3ur83dp5dppfyk5yk6osy + +Options +~~~~~~~ + +:: + + -h, --help help for show + +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 compliance_export `_ - Management of compliance exports +