[MM-62888] mmctl: Add compliance export list cmd (#30914)

* Add compliance export list cmd and tests

- Introduced `ListComplianceExports` method in the Client interface to retrieve compliance export jobs.
- Added `compliance_export` command with subcommand `list` for listing compliance export jobs, including pagination options.
- Implemented end-to-end and unit tests for the compliance export listing functionality.

* Add docs for mmctl

* fix test typo

* added paging, tested

* update docs with better desc (how it's sorted)

* simplified, reusing job call

* add show cmd, unit tests, e2e tests

* update mmctl docs
Этот коммит содержится в:
Christopher Poile
2025-06-24 11:20:40 -04:00
коммит произвёл GitHub
родитель 5d7c3b52ed
Коммит 9399398e04
8 изменённых файлов: 593 добавлений и 4 удалений

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

@@ -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
}

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

@@ -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)
})
}

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

@@ -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
}

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

@@ -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

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

@@ -35,6 +35,7 @@ SEE ALSO
* `mmctl channel <mmctl_channel.rst>`_ - Management of channels
* `mmctl command <mmctl_command.rst>`_ - Management of slash commands
* `mmctl completion <mmctl_completion.rst>`_ - Generates autocompletion scripts for bash and zsh
* `mmctl compliance_export <mmctl_compliance_export.rst>`_ - Management of compliance exports
* `mmctl config <mmctl_config.rst>`_ - Configuration
* `mmctl docs <mmctl_docs.rst>`_ - Generates mmctl documentation
* `mmctl export <mmctl_export.rst>`_ - Management of exports

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

@@ -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 <mmctl.rst>`_ - Remote client for the Open Source, self-hosted Slack-alternative
* `mmctl compliance_export list <mmctl_compliance_export_list.rst>`_ - List compliance export jobs, sorted by creation date descending (newest first)
* `mmctl compliance_export show <mmctl_compliance_export_show.rst>`_ - Show compliance export job

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

@@ -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 <mmctl_compliance_export.rst>`_ - Management of compliance exports

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

@@ -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 <mmctl_compliance_export.rst>`_ - Management of compliance exports