From 1e86f732845682718a3c1854fbb44d4392efaa39 Mon Sep 17 00:00:00 2001 From: Irvin Lim Date: Sat, 11 Feb 2023 13:11:16 +0800 Subject: [PATCH] feat(cli): Implement --all-namespaces flag (#129) --- pkg/cli/cmd/cmd_get_job_test.go | 30 +++++++++++++++ pkg/cli/cmd/cmd_get_jobconfig_test.go | 22 +++++++++++ pkg/cli/cmd/cmd_list.go | 2 + pkg/cli/cmd/cmd_list_job.go | 35 ++++++++++++----- pkg/cli/cmd/cmd_list_job_test.go | 53 ++++++++++++++++++++++++++ pkg/cli/cmd/cmd_list_jobconfig.go | 31 ++++++++++++--- pkg/cli/cmd/cmd_list_jobconfig_test.go | 53 ++++++++++++++++++++++++++ pkg/cli/cmd/cmd_test.go | 1 + pkg/cli/common/common.go | 20 +++++++++- 9 files changed, 231 insertions(+), 16 deletions(-) diff --git a/pkg/cli/cmd/cmd_get_job_test.go b/pkg/cli/cmd/cmd_get_job_test.go index e54ed4a..8984697 100644 --- a/pkg/cli/cmd/cmd_get_job_test.go +++ b/pkg/cli/cmd/cmd_get_job_test.go @@ -232,6 +232,36 @@ var ( State: execution.JobStateQueued, }, } + + prodJobRunning = &execution.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "job-running", + Namespace: ProdNamespace, + UID: testutils.MakeUID("prod/job-running"), + }, + Status: execution.JobStatus{ + Phase: execution.JobRunning, + State: execution.JobStateRunning, + Condition: execution.JobCondition{ + Running: &execution.JobConditionRunning{ + LatestCreationTimestamp: testutils.Mkmtime(taskCreateTime), + LatestRunningTimestamp: testutils.Mkmtime(taskLaunchTime), + }, + }, + StartTime: testutils.Mkmtimep(startTime), + CreatedTasks: 1, + Tasks: []execution.TaskRef{ + { + Name: "job-running.1", + CreationTimestamp: testutils.Mkmtime(taskCreateTime), + RunningTimestamp: testutils.Mkmtimep(taskLaunchTime), + Status: execution.TaskStatus{ + State: execution.TaskRunning, + }, + }, + }, + }, + } ) func TestGetJobCommand(t *testing.T) { diff --git a/pkg/cli/cmd/cmd_get_jobconfig_test.go b/pkg/cli/cmd/cmd_get_jobconfig_test.go index 9014c7d..0a23817 100644 --- a/pkg/cli/cmd/cmd_get_jobconfig_test.go +++ b/pkg/cli/cmd/cmd_get_jobconfig_test.go @@ -141,6 +141,28 @@ var ( State: execution.JobConfigReadyEnabled, }, } + + prodJobConfig = &execution.JobConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "periodic-jobconfig", + Namespace: ProdNamespace, + UID: testutils.MakeUID("prod/periodic-jobconfig"), + }, + Spec: execution.JobConfigSpec{ + Concurrency: execution.ConcurrencySpec{ + Policy: execution.ConcurrencyPolicyForbid, + }, + Schedule: &execution.ScheduleSpec{ + Cron: &execution.CronSchedule{ + Expression: "H/5 * * * *", + Timezone: "Asia/Singapore", + }, + }, + }, + Status: execution.JobConfigStatus{ + State: execution.JobConfigReadyEnabled, + }, + } ) func TestGetJobConfigCommand(t *testing.T) { diff --git a/pkg/cli/cmd/cmd_list.go b/pkg/cli/cmd/cmd_list.go index d667cf9..530a1c4 100644 --- a/pkg/cli/cmd/cmd_list.go +++ b/pkg/cli/cmd/cmd_list.go @@ -70,6 +70,8 @@ func (c *ListCommand) RegisterFlags(cmd *cobra.Command) { cmd.Flags().String("field-selector", "", "Selector (field query) to filter on, supports '=', '==', and '!=' (e.g. --field-selector key1=value1,key2=value2). "+ "The server only supports a limited number of field queries per type.") + cmd.Flags().BoolP("all-namespaces", "A", false, + "If specified, list the requested objects across all namespaces. Namespace in current context is ignored even if specified with --namespace.") if err := completion.RegisterFlagCompletions(cmd, []completion.FlagCompletion{ {FlagName: "output", Completer: completion.NewSliceCompleter(printer.AllOutputFormats)}, diff --git a/pkg/cli/cmd/cmd_list_job.go b/pkg/cli/cmd/cmd_list_job.go index cb72079..be9e0b4 100644 --- a/pkg/cli/cmd/cmd_list_job.go +++ b/pkg/cli/cmd/cmd_list_job.go @@ -40,6 +40,9 @@ var ( # List all Jobs in current namespace. {{.CommandName}} list job +# List all Jobs across all namespaces. +{{.CommandName}} list job -A + # List all Jobs in current namespace belonging to JobConfig "daily-send-email". {{.CommandName}} list job --for daily-send-email @@ -50,12 +53,13 @@ var ( type ListJobCommand struct { *baseListCommand - streams *streams.Streams - jobConfig string - states sets.Set[execution.JobState] - output printer.OutputFormat - noHeaders bool - watch bool + streams *streams.Streams + jobConfig string + states sets.Set[execution.JobState] + output printer.OutputFormat + noHeaders bool + watch bool + allNamespaces bool // Cached set of job UIDs that were previously filtered in. // If it was displayed before, we don't want to filter it out afterwards. @@ -109,6 +113,7 @@ func (c *ListJobCommand) Complete(cmd *cobra.Command, args []string) error { c.output = common.GetOutputFormat(cmd) c.noHeaders = common.GetFlagBool(cmd, "no-headers") c.watch = common.GetFlagBool(cmd, "watch") + c.allNamespaces = common.GetFlagBool(cmd, "all-namespaces") // Handle --states. if v := common.GetFlagString(cmd, "states"); v != "" { @@ -242,13 +247,18 @@ func (c *ListJobCommand) PrintJob(p *printer.TablePrinter, job *execution.Job) e } func (c *ListJobCommand) makeJobHeader() []string { - return []string{ + var columns []string + if c.allNamespaces { + columns = append(columns, "NAMESPACE") + } + columns = append(columns, []string{ "NAME", "PHASE", "START TIME", "RUN TIME", "FINISH TIME", - } + }...) + return columns } func (c *ListJobCommand) prettyPrint(p *printer.TablePrinter, jobs []*execution.Job) { @@ -279,13 +289,18 @@ func (c *ListJobCommand) makeJobRow(job *execution.Job) []string { finishTime = format.TimeAgo(&condition.FinishTimestamp) } - return []string{ + var row []string + if c.allNamespaces { + row = append(row, job.Namespace) + } + row = append(row, []string{ job.Name, string(job.Status.Phase), startTime, runTime, finishTime, - } + }...) + return row } // FilterJobs returns a filtered list of jobs. diff --git a/pkg/cli/cmd/cmd_list_job_test.go b/pkg/cli/cmd/cmd_list_job_test.go index 14ad0e4..7929bf1 100644 --- a/pkg/cli/cmd/cmd_list_job_test.go +++ b/pkg/cli/cmd/cmd_list_job_test.go @@ -17,6 +17,7 @@ package cmd_test import ( + "regexp" "testing" "time" @@ -55,6 +56,58 @@ func TestListJobCommand(t *testing.T) { Contains: "job.execution.furiko.io/job-running", }, }, + { + Name: "only show jobs in the default namespace", + Args: []string{"list", "job", "-o", "yaml"}, + Fixtures: []runtime.Object{ + jobRunning, + prodJobRunning, + }, + Stdout: runtimetesting.Output{ + Contains: string(jobRunning.UID), + Excludes: string(prodJobRunning.UID), + }, + }, + { + Name: "use explicit namespace", + Args: []string{"list", "job", "-o", "yaml", "-n", ProdNamespace}, + Fixtures: []runtime.Object{ + jobRunning, + prodJobRunning, + }, + Stdout: runtimetesting.Output{ + Contains: string(prodJobRunning.UID), + Excludes: string(jobRunning.UID), + }, + }, + { + Name: "use all namespaces", + Args: []string{"list", "job", "-o", "yaml", "-A"}, + Fixtures: []runtime.Object{ + jobRunning, + prodJobRunning, + }, + Stdout: runtimetesting.Output{ + ContainsAll: []string{ + string(prodJobRunning.UID), + string(jobRunning.UID), + }, + }, + }, + { + Name: "use all namespaces, pretty print", + Args: []string{"list", "job", "-A"}, + Fixtures: []runtime.Object{ + jobRunning, + prodJobRunning, + }, + Stdout: runtimetesting.Output{ + MatchesAll: []*regexp.Regexp{ + regexp.MustCompile(`default\s+job-running`), + regexp.MustCompile(`prod\s+job-running`), + }, + }, + }, { Name: "can use alias", Args: []string{"list", "jobs", "-o", "name"}, diff --git a/pkg/cli/cmd/cmd_list_jobconfig.go b/pkg/cli/cmd/cmd_list_jobconfig.go index 82476b6..1232e98 100644 --- a/pkg/cli/cmd/cmd_list_jobconfig.go +++ b/pkg/cli/cmd/cmd_list_jobconfig.go @@ -39,7 +39,16 @@ var ( # List all JobConfigs in current namespace. {{.CommandName}} list jobconfig -# List all JobConfigs in JSON format. +# List all JobConfigs across all namespaces. +{{.CommandName}} list jobconfig -A + +# List only JobConfigs with a schedule. +{{.CommandName}} list jobconfig --scheduled + +# List adhoc-only JobConfigs (i.e. does not have a schedule). +{{.CommandName}} list jobconfig --adhoc-only + +# List JobConfigs in JSON format. {{.CommandName}} list jobconfig -o json`) ) @@ -53,6 +62,7 @@ type ListJobConfigCommand struct { adhocOnly bool scheduleEnabled bool watch bool + allNamespaces bool // Cached cron parser. cronParser *cron.Parser @@ -99,6 +109,7 @@ func (c *ListJobConfigCommand) Complete(cmd *cobra.Command, args []string) error c.output = common.GetOutputFormat(cmd) c.noHeaders = common.GetFlagBool(cmd, "no-headers") c.watch = common.GetFlagBool(cmd, "watch") + c.allNamespaces = common.GetFlagBool(cmd, "all-namespaces") // Prepare parser. c.cronParser = cron.NewParserFromConfig(common.GetCronDynamicConfig(cmd)) @@ -206,7 +217,11 @@ func (c *ListJobConfigCommand) prettyPrint(p *printer.TablePrinter, jobConfigs [ } func (c *ListJobConfigCommand) makeJobHeader() []string { - return []string{ + var columns []string + if c.allNamespaces { + columns = append(columns, "NAMESPACE") + } + columns = append(columns, []string{ "NAME", "STATE", "ACTIVE", @@ -215,7 +230,8 @@ func (c *ListJobConfigCommand) makeJobHeader() []string { "LAST SCHEDULED", "CRON SCHEDULE", "NEXT SCHEDULE", - } + }...) + return columns } func (c *ListJobConfigCommand) makeJobRows(jobConfigs []*execution.JobConfig) [][]string { @@ -235,7 +251,11 @@ func (c *ListJobConfigCommand) makeJobRow(jobConfig *execution.JobConfig) []stri } } - return []string{ + var row []string + if c.allNamespaces { + row = append(row, jobConfig.Namespace) + } + row = append(row, []string{ jobConfig.Name, string(jobConfig.Status.State), strconv.Itoa(int(jobConfig.Status.Active)), @@ -244,7 +264,8 @@ func (c *ListJobConfigCommand) makeJobRow(jobConfig *execution.JobConfig) []stri format.TimeAgo(jobConfig.Status.LastScheduled), cronSchedule, nextSchedule, - } + }...) + return row } // FilterJobConfigs returns a filtered list of job configs. diff --git a/pkg/cli/cmd/cmd_list_jobconfig_test.go b/pkg/cli/cmd/cmd_list_jobconfig_test.go index 478bb67..d9b1af4 100644 --- a/pkg/cli/cmd/cmd_list_jobconfig_test.go +++ b/pkg/cli/cmd/cmd_list_jobconfig_test.go @@ -17,6 +17,7 @@ package cmd_test import ( + "regexp" "testing" "time" @@ -54,6 +55,58 @@ func TestListJobConfigCommand(t *testing.T) { Contains: "jobconfig.execution.furiko.io/periodic-jobconfig", }, }, + { + Name: "only show job configs in the default namespace", + Args: []string{"list", "jobconfig", "-o", "yaml"}, + Fixtures: []runtime.Object{ + periodicJobConfig, + prodJobConfig, + }, + Stdout: runtimetesting.Output{ + Contains: string(periodicJobConfig.UID), + Excludes: string(prodJobConfig.UID), + }, + }, + { + Name: "use explicit namespace", + Args: []string{"list", "jobconfig", "-o", "yaml", "-n", ProdNamespace}, + Fixtures: []runtime.Object{ + periodicJobConfig, + prodJobConfig, + }, + Stdout: runtimetesting.Output{ + Contains: string(prodJobConfig.UID), + Excludes: string(periodicJobConfig.UID), + }, + }, + { + Name: "use all namespaces", + Args: []string{"list", "jobconfig", "-o", "yaml", "-A"}, + Fixtures: []runtime.Object{ + periodicJobConfig, + prodJobConfig, + }, + Stdout: runtimetesting.Output{ + ContainsAll: []string{ + string(prodJobConfig.UID), + string(periodicJobConfig.UID), + }, + }, + }, + { + Name: "use all namespaces, pretty print", + Args: []string{"list", "jobconfig", "-A"}, + Fixtures: []runtime.Object{ + periodicJobConfig, + prodJobConfig, + }, + Stdout: runtimetesting.Output{ + MatchesAll: []*regexp.Regexp{ + regexp.MustCompile(`default\s+periodic-jobconfig`), + regexp.MustCompile(`prod\s+periodic-jobconfig`), + }, + }, + }, { Name: "can use alias", Args: []string{"list", "jobconfigs", "-o", "name"}, diff --git a/pkg/cli/cmd/cmd_test.go b/pkg/cli/cmd/cmd_test.go index e5a549f..6501918 100644 --- a/pkg/cli/cmd/cmd_test.go +++ b/pkg/cli/cmd/cmd_test.go @@ -18,4 +18,5 @@ package cmd_test const ( DefaultNamespace = "default" + ProdNamespace = "prod" ) diff --git a/pkg/cli/common/common.go b/pkg/cli/common/common.go index 0fc4840..278518c 100644 --- a/pkg/cli/common/common.go +++ b/pkg/cli/common/common.go @@ -100,13 +100,22 @@ func SetCtrlContext(cc controllercontext.Context) { // GetNamespace returns the namespace to use depending on what was defined in the flags. func GetNamespace(cmd *cobra.Command) (string, error) { + // If --all-namespaces is defined on the command, use it first. + if allNamespaces, ok := GetFlagBoolIfExists(cmd, "all-namespaces"); ok && allNamespaces { + return metav1.NamespaceAll, nil + } + + // Read the --namespace flag if specified. namespace, err := cmd.Flags().GetString("namespace") if err != nil { - return "", err + return "", errors.Wrapf(err, "cannot get value of --namespace") } + + // Otherwise, fall back to the default namespace. if namespace == "" { namespace = metav1.NamespaceDefault } + return namespace, nil } @@ -154,6 +163,15 @@ func GetFlagBool(cmd *cobra.Command, flag string) bool { return b } +// GetFlagBoolIfExists gets the boolean value of a flag if it exists. +func GetFlagBoolIfExists(cmd *cobra.Command, flag string) (val bool, ok bool) { + if cmd.Flags().Lookup(flag) != nil { + val := GetFlagBool(cmd, flag) + return val, true + } + return false, false +} + // GetFlagString gets the string value of a flag. func GetFlagString(cmd *cobra.Command, flag string) string { v, err := cmd.Flags().GetString(flag)