Skip to content

Commit

Permalink
feat(cli): Implement completion for flags and arguments (#106)
Browse files Browse the repository at this point in the history
  • Loading branch information
irvinlim authored Jan 21, 2023
1 parent fb02988 commit 7cbcd4f
Show file tree
Hide file tree
Showing 16 changed files with 345 additions and 36 deletions.
7 changes: 7 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,13 @@ build-execution-controller: ## Build execution-controller.
build-execution-webhook: ## Build execution-webhook.
go build -o build/execution-webhook ./cmd/execution-webhook

##@ Command-line Tools

.PHONY: install-furiko-cli
install-furiko-cli: ## Install furiko-cli to PATH.
go install ./cmd/furiko-cli
mv $(GOBIN)/furiko-cli $(GOBIN)/furiko

##@ YAML Configuration

## Location to write YAMLs to
Expand Down
6 changes: 6 additions & 0 deletions apis/execution/v1alpha1/jobconfig_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,12 @@ const (
ConcurrencyPolicyEnqueue ConcurrencyPolicy = "Enqueue"
)

var ConcurrencyPoliciesAll = []ConcurrencyPolicy{
ConcurrencyPolicyAllow,
ConcurrencyPolicyForbid,
ConcurrencyPolicyEnqueue,
}

// OptionSpec defines how a JobConfig is parameterized using Job Options.
type OptionSpec struct {
// Options is a list of job options.
Expand Down
6 changes: 6 additions & 0 deletions pkg/cli/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ func NewRootCommand(streams *streams.Streams) *cobra.Command {
"Overrides the namespace of the dynamic cluster config.")
flags.IntVarP(&c.verbosity, "v", "v", 0, "Sets the log level verbosity.")

if err := RegisterFlagCompletions(cmd, []FlagCompletion{
{FlagName: "namespace", CompletionFunc: (&CompletionHelper{}).ListNamespaces()},
}); err != nil {
Fatal(err, DefaultErrorExitCode)
}

cmd.AddCommand(NewGetCommand(streams))
cmd.AddCommand(NewListCommand(streams))
cmd.AddCommand(NewRunCommand(streams))
Expand Down
11 changes: 6 additions & 5 deletions pkg/cli/cmd/cmd_disable.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,11 @@ func NewDisableCommand(streams *streams.Streams) *cobra.Command {
If the specified JobConfig does not have a schedule, then an error will be thrown.
If the specified JobConfig is already disabled, then this is a no-op.`,
Example: DisableExample,
Args: cobra.ExactArgs(1),
PreRunE: PrerunWithKubeconfig,
RunE: c.Run,
Example: DisableExample,
Args: cobra.ExactArgs(1),
PreRunE: PrerunWithKubeconfig,
ValidArgsFunction: MakeCobraCompletionFunc((&CompletionHelper{}).ListJobConfigs()),
RunE: c.Run,
}

return cmd
Expand Down Expand Up @@ -87,7 +88,7 @@ func (c *DisableCommand) Run(cmd *cobra.Command, args []string) error {
return fmt.Errorf("job config has no schedule specified")
}
if jobConfig.Spec.Schedule.Disabled {
c.streams.Printf("Job config %v is already disabled", key)
c.streams.Printf("Job config %v is already disabled\n", key)
return nil
}

Expand Down
11 changes: 6 additions & 5 deletions pkg/cli/cmd/cmd_enable.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,11 @@ func NewEnableCommand(streams *streams.Streams) *cobra.Command {
If the specified JobConfig does not have a schedule, then an error will be thrown.
If the specified JobConfig is already enabled, then this is a no-op.`,
Example: EnableExample,
Args: cobra.ExactArgs(1),
PreRunE: PrerunWithKubeconfig,
RunE: c.Run,
Example: EnableExample,
Args: cobra.ExactArgs(1),
PreRunE: PrerunWithKubeconfig,
ValidArgsFunction: MakeCobraCompletionFunc((&CompletionHelper{}).ListJobConfigs()),
RunE: c.Run,
}

return cmd
Expand Down Expand Up @@ -87,7 +88,7 @@ func (c *EnableCommand) Run(cmd *cobra.Command, args []string) error {
return fmt.Errorf("job config has no schedule specified")
}
if !jobConfig.Spec.Schedule.Disabled {
c.streams.Printf("Job config %v is already enabled", key)
c.streams.Printf("Job config %v is already enabled\n", key)
return nil
}

Expand Down
6 changes: 6 additions & 0 deletions pkg/cli/cmd/cmd_get.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ func NewGetCommand(streams *streams.Streams) *cobra.Command {
cmd.PersistentFlags().StringP("output", "o", string(printer.OutputFormatPretty),
fmt.Sprintf("Output format. One of: %v", strings.Join(printer.GetAllOutputFormatStrings(), "|")))

if err := RegisterFlagCompletions(cmd, []FlagCompletion{
{FlagName: "output", CompletionFunc: (&CompletionHelper{}).FromSlice(printer.AllOutputFormats)},
}); err != nil {
Fatal(err, DefaultErrorExitCode)
}

cmd.AddCommand(NewGetJobCommand(streams))
cmd.AddCommand(NewGetJobConfigCommand(streams))

Expand Down
15 changes: 8 additions & 7 deletions pkg/cli/cmd/cmd_get_job.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,14 @@ func NewGetJobCommand(streams *streams.Streams) *cobra.Command {
}

cmd := &cobra.Command{
Use: "job",
Aliases: []string{"jobs"},
Short: "Displays information about one or more Jobs.",
Example: GetJobExample,
PreRunE: PrerunWithKubeconfig,
Args: cobra.MinimumNArgs(1),
RunE: c.Run,
Use: "job",
Aliases: []string{"jobs"},
Short: "Displays information about one or more Jobs.",
Example: GetJobExample,
PreRunE: PrerunWithKubeconfig,
Args: cobra.MinimumNArgs(1),
ValidArgsFunction: MakeCobraCompletionFunc((&CompletionHelper{}).ListJobs()),
RunE: c.Run,
}

return cmd
Expand Down
15 changes: 8 additions & 7 deletions pkg/cli/cmd/cmd_get_jobconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,14 @@ func NewGetJobConfigCommand(streams *streams.Streams) *cobra.Command {
}

cmd := &cobra.Command{
Use: "jobconfig",
Aliases: []string{"jobconfigs"},
Short: "Displays information about one or more JobConfigs.",
Example: GetJobConfigExample,
PreRunE: PrerunWithKubeconfig,
Args: cobra.MinimumNArgs(1),
RunE: c.Run,
Use: "jobconfig",
Aliases: []string{"jobconfigs"},
Short: "Displays information about one or more JobConfigs.",
Example: GetJobConfigExample,
PreRunE: PrerunWithKubeconfig,
Args: cobra.MinimumNArgs(1),
ValidArgsFunction: MakeCobraCompletionFunc((&CompletionHelper{}).ListJobConfigs()),
RunE: c.Run,
}

return cmd
Expand Down
15 changes: 8 additions & 7 deletions pkg/cli/cmd/cmd_kill.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,14 @@ func NewKillCommand(streams *streams.Streams) *cobra.Command {
}

cmd := &cobra.Command{
Use: "kill",
Short: "Kill an ongoing Job.",
Long: `Kills an ongoing Job that is currently running or pending.`,
Example: KillExample,
Args: cobra.ExactArgs(1),
PreRunE: PrerunWithKubeconfig,
RunE: c.Run,
Use: "kill",
Short: "Kill an ongoing Job.",
Long: `Kills an ongoing Job that is currently running or pending.`,
Example: KillExample,
Args: cobra.ExactArgs(1),
PreRunE: PrerunWithKubeconfig,
ValidArgsFunction: MakeCobraCompletionFunc((&CompletionHelper{}).ListJobs()),
RunE: c.Run,
}

cmd.Flags().BoolVar(&c.override, "override", false,
Expand Down
6 changes: 6 additions & 0 deletions pkg/cli/cmd/cmd_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ func NewListCommand(streams *streams.Streams) *cobra.Command {
cmd.PersistentFlags().StringP("output", "o", string(printer.OutputFormatPretty),
fmt.Sprintf("Output format. One of: %v", strings.Join(printer.GetAllOutputFormatStrings(), "|")))

if err := RegisterFlagCompletions(cmd, []FlagCompletion{
{FlagName: "output", CompletionFunc: (&CompletionHelper{}).FromSlice(printer.AllOutputFormats)},
}); err != nil {
Fatal(err, DefaultErrorExitCode)
}

cmd.AddCommand(NewListJobCommand(streams))
cmd.AddCommand(NewListJobConfigCommand(streams))

Expand Down
6 changes: 6 additions & 0 deletions pkg/cli/cmd/cmd_list_job.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ func NewListJobCommand(streams *streams.Streams) *cobra.Command {

cmd.Flags().StringVar(&c.jobConfig, "for", "", "Return only jobs for the given job config.")

if err := RegisterFlagCompletions(cmd, []FlagCompletion{
{FlagName: "for", CompletionFunc: (&CompletionHelper{}).ListJobConfigs()},
}); err != nil {
Fatal(err, DefaultErrorExitCode)
}

return cmd
}

Expand Down
15 changes: 11 additions & 4 deletions pkg/cli/cmd/cmd_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,11 @@ func NewRunCommand(streams *streams.Streams) *cobra.Command {
Long: `Runs a new Job from an existing JobConfig.
If the JobConfig has some options defined, an interactive prompt will be shown.`,
Example: RunExample,
Args: cobra.ExactArgs(1),
PreRunE: PrerunWithKubeconfig,
RunE: c.Run,
Example: RunExample,
Args: cobra.ExactArgs(1),
PreRunE: PrerunWithKubeconfig,
ValidArgsFunction: MakeCobraCompletionFunc((&CompletionHelper{}).ListJobConfigs()),
RunE: c.Run,
}

cmd.Flags().StringVar(&c.name, "name", "",
Expand All @@ -92,6 +93,12 @@ If the JobConfig has some options defined, an interactive prompt will be shown.`
"Specify an explicit concurrency policy to use for the job, overriding the "+
"JobConfig's concurrency policy.")

if err := RegisterFlagCompletions(cmd, []FlagCompletion{
{FlagName: "concurrency-policy", CompletionFunc: (&CompletionHelper{}).FromSlice(execution.ConcurrencyPoliciesAll)},
}); err != nil {
Fatal(err, DefaultErrorExitCode)
}

return cmd
}

Expand Down
26 changes: 25 additions & 1 deletion pkg/cli/cmd/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ package cmd

import (
"context"
"fmt"
"os"
"strings"

"github.com/kr/text"
Expand All @@ -33,6 +35,10 @@ import (
"github.com/furiko-io/furiko/pkg/utils/jsonyaml"
)

const (
DefaultErrorExitCode = 1
)

var (
ctrlContext controllercontext.Context
)
Expand All @@ -59,9 +65,14 @@ func NewContext(_ *cobra.Command) (controllercontext.Context, error) {
}

// PrerunWithKubeconfig is a pre-run function that will set up the common context when kubeconfig is needed.
func PrerunWithKubeconfig(cmd *cobra.Command, _ []string) error {
return SetupCtrlContext(cmd)
}

// SetupCtrlContext sets up the common context.
// TODO(irvinlim): We currently reuse controllercontext, but most of it is unusable for CLI interfaces.
// We should create a new common context as needed.
func PrerunWithKubeconfig(cmd *cobra.Command, _ []string) error {
func SetupCtrlContext(cmd *cobra.Command) error {
// Already set up previously.
if ctrlContext != nil {
return nil
Expand Down Expand Up @@ -136,3 +147,16 @@ func PrepareExample(example string) string {
example = strings.ReplaceAll(example, "{{.CommandName}}", CommandName)
return text.Indent(example, " ")
}

// Fatal terminates the command immediately and writes to stderr.
func Fatal(err error, code int) {
msg := err.Error()
if len(msg) > 0 {
// add newline if needed
if !strings.HasSuffix(msg, "\n") {
msg += "\n"
}
_, _ = fmt.Fprint(os.Stderr, msg)
}
os.Exit(code)
}
61 changes: 61 additions & 0 deletions pkg/cli/cmd/completion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright 2022 The Furiko Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package cmd

import (
"context"

"github.com/pkg/errors"
"github.com/spf13/cobra"

"github.com/furiko-io/furiko/pkg/runtime/controllercontext"
)

// CompletionFunc knows how to return completions.
type CompletionFunc func(ctx context.Context, ctrlContext controllercontext.Context, cmd *cobra.Command) ([]string, error)

// FlagCompletion defines a single flag completion entry.
type FlagCompletion struct {
FlagName string
CompletionFunc CompletionFunc
}

// RegisterFlagCompletions registers all FlagCompletion entries and returns an error if any flag returned an error.
func RegisterFlagCompletions(cmd *cobra.Command, completions []FlagCompletion) error {
for _, completion := range completions {
if err := cmd.RegisterFlagCompletionFunc(completion.FlagName, MakeCobraCompletionFunc(completion.CompletionFunc)); err != nil {
return errors.Wrapf(err, `cannot register flag completion func for "%v"`, completion.FlagName)
}
}
return nil
}

// MakeCobraCompletionFunc converts a CompletionFunc into a cobra completion function.
func MakeCobraCompletionFunc(f CompletionFunc) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
ctx := cmd.Context()
if err := SetupCtrlContext(cmd); err != nil {
Fatal(errors.Wrap(err, "cannot set up context"), DefaultErrorExitCode)
}
completions, err := f(ctx, ctrlContext, cmd)
if err != nil {
Fatal(err, DefaultErrorExitCode)
return nil, cobra.ShellCompDirectiveError
}
return completions, cobra.ShellCompDirectiveNoFileComp
}
}
Loading

0 comments on commit 7cbcd4f

Please sign in to comment.