mirror of
https://github.com/hanzoai/team.git
synced 2026-09-30 01:57:35 +00:00
* Add create, show, and cancel subcommands to mmctl job The `mmctl job` command previously only supported `list` and `update`. This adds three subcommands backed by existing client methods: - `job create [type] [--data k=v]` triggers a new job (CreateJob) - `job show [job]` displays a single job (GetJob) - `job cancel [job]` requests cancellation via the dedicated cancel endpoint (CancelJob), which uses the create-job permission and emits its own audit event, unlike `update [job] cancel_requested` Also fixes the leftover "import jobs" wording in the `list` flag help and regenerates the mmctl docs. * Exercise --data flag parsing in mmctl job create test The create-job test passed the Data map as the flag's default value, so GetStringToString returned the default without ever exercising pflag's stringToString parser. Declare the flag empty and Set the value explicitly so the test validates the real parse path. * Don't gate mmctl job create on model.AllJobTypes model.AllJobTypes (which IsValidJobType checks against) is a curated subset that omits creatable job types such as access_control_sync, so validating the type client-side wrongly rejected jobs the server can schedule. Drop the client-side check and let the server validate the type per its create-job permissions; it returns a clear error for anything unschedulable. Tab-completion still suggests AllJobTypes as an advisory hint. * Remove explanatory AllJobTypes comments from mmctl job * Add e2e tests for mmctl job create, show, and cancel Cover the no-permission, not-found, and success paths for the new generic job subcommands against a running server, mirroring the existing export job e2e tests. --------- Co-authored-by: Hanzo Dev <[email protected]>
304 lines
7.9 KiB
Go
304 lines
7.9 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package commands
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/hashicorp/go-multierror"
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
|
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
|
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var JobCmd = &cobra.Command{
|
|
Use: "job",
|
|
Short: "Management of jobs",
|
|
}
|
|
|
|
var listJobsCmd = &cobra.Command{
|
|
Use: "list",
|
|
Short: "List the latest jobs",
|
|
Example: ` job list
|
|
job list --ids jobID1,jobID2
|
|
job list --type ldap_sync --status success
|
|
job list --type ldap_sync --status success --page 0 --per-page 10`,
|
|
Args: cobra.NoArgs,
|
|
RunE: withClient(listJobsCmdF),
|
|
}
|
|
|
|
var createJobCmd = &cobra.Command{
|
|
Use: "create [type]",
|
|
Short: "Create a new job",
|
|
Long: "Create a new job of the given type. Use --data to pass type-specific options as key=value pairs.",
|
|
Example: ` job create ldap_sync
|
|
job create data_retention
|
|
job create message_export --data batch_size=1000`,
|
|
Args: cobra.ExactArgs(1),
|
|
ValidArgsFunction: jobTypeCompletionF,
|
|
RunE: withClient(createJobCmdF),
|
|
}
|
|
|
|
var showJobCmd = &cobra.Command{
|
|
Use: "show [job]",
|
|
Short: "Show a job",
|
|
Example: " job show jobID",
|
|
Args: cobra.ExactArgs(1),
|
|
ValidArgsFunction: validateArgsWithClient(jobCompletionF),
|
|
RunE: withClient(showJobCmdF),
|
|
}
|
|
|
|
var cancelJobCmd = &cobra.Command{
|
|
Use: "cancel [job]",
|
|
Short: "Cancel a job",
|
|
Example: " job cancel jobID",
|
|
Args: cobra.ExactArgs(1),
|
|
ValidArgsFunction: validateArgsWithClient(jobCompletionF),
|
|
RunE: withClient(cancelJobCmdF),
|
|
}
|
|
|
|
var updateJobCmd = &cobra.Command{
|
|
Use: "update [job] [status]",
|
|
Short: "Update the status of a job",
|
|
Long: `Update the status of a job. The following restrictions are in place:
|
|
- in_progress -> pending
|
|
- in_progress | pending -> cancel_requested
|
|
- cancel_requested -> canceled
|
|
|
|
Those restriction can be bypassed with --force=true but the only statuses you can go to are: pending, cancel_requested and canceled. This can have unexpected consequences and should be used with caution.`,
|
|
Example: ` job update myJobID pending
|
|
job update myJobID pending --force true
|
|
job update myJobID canceled --force true`,
|
|
Args: cobra.MinimumNArgs(2),
|
|
RunE: withClient(updateJobCmdF),
|
|
}
|
|
|
|
func init() {
|
|
listJobsCmd.Flags().Int("page", 0, "Page number to fetch for the list of jobs")
|
|
listJobsCmd.Flags().Int("per-page", 5, "Number of jobs to be fetched")
|
|
listJobsCmd.Flags().Bool("all", false, "Fetch all jobs. --page flag will be ignored if provided")
|
|
listJobsCmd.Flags().StringSlice("ids", nil, "Comma-separated list of job IDs to which the operation will be applied. All other flags are ignored")
|
|
listJobsCmd.Flags().String("status", "", "Filter by job status")
|
|
listJobsCmd.Flags().String("type", "", "Filter by job type")
|
|
|
|
createJobCmd.Flags().StringToString("data", nil, "Comma-separated list of key=value pairs passed to the job as type-specific options")
|
|
|
|
updateJobCmd.Flags().Bool("force", false, "Setting a job status is restricted to certain statuses. You can overwrite these restrictions by using --force. This might cause unexpected behaviour on your Mattermost Server. Use this option with caution.")
|
|
|
|
JobCmd.AddCommand(
|
|
listJobsCmd,
|
|
createJobCmd,
|
|
showJobCmd,
|
|
cancelJobCmd,
|
|
updateJobCmd,
|
|
)
|
|
|
|
RootCmd.AddCommand(JobCmd)
|
|
}
|
|
|
|
func listJobsCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
|
ids, err := cmd.Flags().GetStringSlice("ids")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
jobType, err := cmd.Flags().GetString("type")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
status, err := cmd.Flags().GetString("status")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(ids) > 0 {
|
|
jobs := make([]*model.Job, 0, len(ids))
|
|
var result *multierror.Error
|
|
for _, id := range ids {
|
|
isValidId := model.IsValidId(id)
|
|
if !isValidId {
|
|
result = multierror.Append(result, fmt.Errorf("invalid job ID: %s", id))
|
|
continue
|
|
}
|
|
|
|
job, _, err := c.GetJob(context.TODO(), id)
|
|
if err != nil {
|
|
result = multierror.Append(result, err)
|
|
continue
|
|
}
|
|
jobs = append(jobs, job)
|
|
}
|
|
for _, job := range jobs {
|
|
printJob(job)
|
|
}
|
|
return result.ErrorOrNil()
|
|
}
|
|
|
|
return jobListCmdF(c, cmd, jobType, status)
|
|
}
|
|
|
|
func updateJobCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
|
force, err := cmd.Flags().GetBool("force")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
jobId := args[0]
|
|
if !model.IsValidId(jobId) {
|
|
return fmt.Errorf("invalid job ID: %s", jobId)
|
|
}
|
|
status := args[1]
|
|
if !model.IsValidJobStatus(status) {
|
|
return fmt.Errorf("invalid job status: %s", status)
|
|
}
|
|
|
|
_, err = c.UpdateJobStatus(context.TODO(), jobId, status, force)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func createJobCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
|
jobType := args[0]
|
|
|
|
data, err := cmd.Flags().GetStringToString("data")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
job, _, err := c.CreateJob(context.TODO(), &model.Job{
|
|
Type: jobType,
|
|
Data: data,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create job: %w", err)
|
|
}
|
|
|
|
printer.PrintT("Job successfully created, ID: {{.Id}}", job)
|
|
|
|
return nil
|
|
}
|
|
|
|
func showJobCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
|
jobId := args[0]
|
|
if !model.IsValidId(jobId) {
|
|
return fmt.Errorf("invalid job ID: %s", jobId)
|
|
}
|
|
|
|
job, _, err := c.GetJob(context.TODO(), jobId)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get job: %w", err)
|
|
}
|
|
|
|
printJob(job)
|
|
|
|
return nil
|
|
}
|
|
|
|
func cancelJobCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
|
jobId := args[0]
|
|
if !model.IsValidId(jobId) {
|
|
return fmt.Errorf("invalid job ID: %s", jobId)
|
|
}
|
|
|
|
if _, err := c.CancelJob(context.TODO(), jobId); err != nil {
|
|
return fmt.Errorf("failed to cancel job: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func jobTypeCompletionF(_ *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) {
|
|
if len(args) > 0 {
|
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
|
}
|
|
return model.AllJobTypes[:], cobra.ShellCompDirectiveNoFileComp
|
|
}
|
|
|
|
func jobCompletionF(ctx context.Context, c client.Client, cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
|
return fetchAndComplete(
|
|
func(ctx context.Context, c client.Client, page, perPage int) ([]*model.Job, *model.Response, error) {
|
|
return c.GetJobs(ctx, "", "", page, perPage)
|
|
},
|
|
func(j *model.Job) []string { return []string{j.Id} },
|
|
)(ctx, c, cmd, args, toComplete)
|
|
}
|
|
|
|
func jobListCmdF(c client.Client, command *cobra.Command, jobType string, status string) error {
|
|
page, err := command.Flags().GetInt("page")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
perPage, err := command.Flags().GetInt("per-page")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
showAll, err := command.Flags().GetBool("all")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if showAll {
|
|
page = 0
|
|
}
|
|
|
|
if jobType != "" && !model.IsValidJobType(jobType) {
|
|
return fmt.Errorf("invalid job type: %s", jobType)
|
|
}
|
|
|
|
if status != "" && !model.IsValidJobStatus(status) {
|
|
return fmt.Errorf("invalid job status: %s", status)
|
|
}
|
|
|
|
for {
|
|
jobs, _, err := c.GetJobs(context.TODO(), jobType, status, page, perPage)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get jobs: %w", err)
|
|
}
|
|
|
|
if len(jobs) == 0 {
|
|
if !showAll || page == 0 {
|
|
printer.Print("No jobs found")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
for _, job := range jobs {
|
|
printJob(job)
|
|
}
|
|
|
|
if !showAll {
|
|
break
|
|
}
|
|
|
|
page++
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func printJob(job *model.Job) {
|
|
if job.StartAt > 0 {
|
|
printer.PrintT(fmt.Sprintf(` ID: {{.Id}}
|
|
Type: {{.Type}}
|
|
Status: {{.Status}}
|
|
Created: %s
|
|
Started: %s
|
|
Data: {{.Data}}
|
|
`,
|
|
time.Unix(job.CreateAt/1000, 0), time.Unix(job.StartAt/1000, 0)), job)
|
|
} else {
|
|
printer.PrintT(fmt.Sprintf(` ID: {{.Id}}
|
|
Status: {{.Status}}
|
|
Created: %s
|
|
`,
|
|
time.Unix(job.CreateAt/1000, 0)), job)
|
|
}
|
|
}
|