diff --git a/cli/cmd/install/install.go b/cli/cmd/install/install.go index 3329c6fc58..61742cebbe 100644 --- a/cli/cmd/install/install.go +++ b/cli/cmd/install/install.go @@ -16,6 +16,7 @@ import ( "github.com/hashicorp/consul-k8s/cli/helm" "github.com/hashicorp/consul-k8s/cli/release" "github.com/hashicorp/consul-k8s/cli/validation" + "github.com/posener/complete" "helm.sh/helm/v3/pkg/action" helmCLI "helm.sh/helm/v3/pkg/cli" "helm.sh/helm/v3/pkg/cli/values" @@ -52,6 +53,9 @@ const ( flagNameWait = "wait" defaultWait = true + + flagNameContext = "context" + flagNameKubeconfig = "kubeconfig" ) type Command struct { @@ -158,14 +162,14 @@ func (c *Command) init() { f = c.set.NewSet("Global Options") f.StringVar(&flag.StringVar{ - Name: "kubeconfig", + Name: flagNameKubeconfig, Aliases: []string{"c"}, Target: &c.flagKubeConfig, Default: "", Usage: "Set the path to kubeconfig file.", }) f.StringVar(&flag.StringVar{ - Name: "context", + Name: flagNameContext, Target: &c.flagKubeContext, Default: "", Usage: "Set the Kubernetes context to use.", @@ -265,20 +269,20 @@ func (c *Command) Run(args []string) int { return 1 } - var values helm.Values - err = yaml.Unmarshal(valuesYaml, &values) + var helmVals helm.Values + err = yaml.Unmarshal(valuesYaml, &helmVals) if err != nil { c.UI.Output(err.Error(), terminal.WithErrorStyle()) return 1 } - release := release.Release{ + rel := release.Release{ Name: "consul", Namespace: c.flagNamespace, - Configuration: values, + Configuration: helmVals, } - msg, err := c.checkForPreviousSecrets(release) + msg, err := c.checkForPreviousSecrets(rel) if err != nil { c.UI.Output(err.Error(), terminal.WithErrorStyle()) return 1 @@ -286,8 +290,8 @@ func (c *Command) Run(args []string) int { c.UI.Output(msg, terminal.WithSuccessStyle()) // If an enterprise license secret was provided, check that the secret exists and that the enterprise Consul image is set. - if values.Global.EnterpriseLicense.SecretName != "" { - if err := c.checkValidEnterprise(release.Configuration.Global.EnterpriseLicense.SecretName); err != nil { + if helmVals.Global.EnterpriseLicense.SecretName != "" { + if err := c.checkValidEnterprise(rel.Configuration.Global.EnterpriseLicense.SecretName); err != nil { c.UI.Output(err.Error(), terminal.WithErrorStyle()) return 1 } @@ -383,6 +387,34 @@ func (c *Command) Synopsis() string { return "Install Consul on Kubernetes." } +// AutocompleteFlags returns a mapping of supported flags and autocomplete +// options for this command. The map key for the Flags map should be the +// complete flag such as "-foo" or "--foo". +func (c *Command) AutocompleteFlags() complete.Flags { + return complete.Flags{ + fmt.Sprintf("-%s", flagNamePreset): complete.PredictNothing, + fmt.Sprintf("-%s", flagNameNamespace): complete.PredictNothing, + fmt.Sprintf("-%s", flagNameDryRun): complete.PredictNothing, + fmt.Sprintf("-%s", flagNameAutoApprove): complete.PredictNothing, + fmt.Sprintf("-%s", flagNameConfigFile): complete.PredictFiles("*"), + fmt.Sprintf("-%s", flagNameSetStringValues): complete.PredictNothing, + fmt.Sprintf("-%s", flagNameSetValues): complete.PredictNothing, + fmt.Sprintf("-%s", flagNameFileValues): complete.PredictFiles("*"), + fmt.Sprintf("-%s", flagNameTimeout): complete.PredictNothing, + fmt.Sprintf("-%s", flagNameVerbose): complete.PredictNothing, + fmt.Sprintf("-%s", flagNameWait): complete.PredictNothing, + fmt.Sprintf("-%s", flagNameContext): complete.PredictNothing, + fmt.Sprintf("-%s", flagNameKubeconfig): complete.PredictNothing, + } +} + +// AutocompleteArgs returns the argument predictor for this command. +// Since argument completion is not supported, this will return +// complete.PredictNothing. +func (c *Command) AutocompleteArgs() complete.Predictor { + return complete.PredictNothing +} + // checkForPreviousPVCs checks for existing Kubernetes persistent volume claims with a name containing "consul-server" // and returns an error with a list of PVCs it finds if any match. func (c *Command) checkForPreviousPVCs() error { diff --git a/cli/cmd/install/install_test.go b/cli/cmd/install/install_test.go index 4bbe518932..a66febc336 100644 --- a/cli/cmd/install/install_test.go +++ b/cli/cmd/install/install_test.go @@ -2,13 +2,18 @@ package install import ( "context" + "flag" + "fmt" "os" "testing" "github.com/hashicorp/consul-k8s/cli/common" + cmnFlag "github.com/hashicorp/consul-k8s/cli/common/flag" "github.com/hashicorp/consul-k8s/cli/helm" "github.com/hashicorp/consul-k8s/cli/release" "github.com/hashicorp/go-hclog" + "github.com/posener/complete" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -248,3 +253,33 @@ func TestCheckValidEnterprise(t *testing.T) { require.Error(t, err) require.Contains(t, err.Error(), "please make sure that the secret exists") } + +func TestTaskCreateCommand_AutocompleteFlags(t *testing.T) { + t.Parallel() + cmd := getInitializedCommand(t) + + predictor := cmd.AutocompleteFlags() + + // Test that we get the expected number of predictions + args := complete.Args{Last: "-"} + res := predictor.Predict(args) + + // Grab the list of flags from the Flag object + flags := make([]string, 0) + cmd.set.VisitSets(func(name string, set *cmnFlag.Set) { + set.VisitAll(func(flag *flag.Flag) { + flags = append(flags, fmt.Sprintf("-%s", flag.Name)) + }) + }) + + // Verify that there is a prediction for each flag associated with the command + assert.Equal(t, len(flags), len(res)) + assert.ElementsMatch(t, flags, res, "flags and predictions didn't match, make sure to add "+ + "new flags to the command AutoCompleteFlags function") +} + +func TestTaskCreateCommand_AutocompleteArgs(t *testing.T) { + cmd := getInitializedCommand(t) + c := cmd.AutocompleteArgs() + assert.Equal(t, complete.PredictNothing, c) +} diff --git a/cli/cmd/proxy/command.go b/cli/cmd/proxy/command.go index 663893de5e..bc55ee0312 100644 --- a/cli/cmd/proxy/command.go +++ b/cli/cmd/proxy/command.go @@ -13,7 +13,7 @@ type ProxyCommand struct { } // Run prints out information about the subcommands. -func (c *ProxyCommand) Run(args []string) int { +func (c *ProxyCommand) Run([]string) int { return cli.RunResultHelp } diff --git a/cli/cmd/proxy/list/command.go b/cli/cmd/proxy/list/command.go index 780a376a0f..cbecda79a1 100644 --- a/cli/cmd/proxy/list/command.go +++ b/cli/cmd/proxy/list/command.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/consul-k8s/cli/common" "github.com/hashicorp/consul-k8s/cli/common/flag" "github.com/hashicorp/consul-k8s/cli/common/terminal" + "github.com/posener/complete" helmCLI "helm.sh/helm/v3/pkg/cli" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/validation" @@ -16,6 +17,13 @@ import ( "k8s.io/client-go/kubernetes" ) +const ( + flagNameNamespace = "namespace" + flagNameAllNamespaces = "all-namespaces" + flagNameKubeConfig = "kubeconfig" + flagNameKubeContext = "context" +) + // ListCommand is the command struct for the proxy list command. type ListCommand struct { *common.BaseCommand @@ -40,13 +48,13 @@ func (c *ListCommand) init() { f := c.set.NewSet("Command Options") f.StringVar(&flag.StringVar{ - Name: "namespace", + Name: flagNameNamespace, Target: &c.flagNamespace, Usage: "The namespace to list proxies in.", Aliases: []string{"n"}, }) f.BoolVar(&flag.BoolVar{ - Name: "all-namespaces", + Name: flagNameAllNamespaces, Target: &c.flagAllNamespaces, Default: false, Usage: "List pods in all namespaces.", @@ -55,14 +63,14 @@ func (c *ListCommand) init() { f = c.set.NewSet("Global Options") f.StringVar(&flag.StringVar{ - Name: "kubeconfig", + Name: flagNameKubeConfig, Aliases: []string{"c"}, Target: &c.flagKubeConfig, Default: "", Usage: "Set the path to kubeconfig file.", }) f.StringVar(&flag.StringVar{ - Name: "context", + Name: flagNameKubeContext, Target: &c.flagKubeContext, Default: "", Usage: "Set the Kubernetes context to use.", @@ -117,6 +125,25 @@ func (c *ListCommand) Synopsis() string { return "List all Pods running proxies managed by Consul." } +// AutocompleteFlags returns a mapping of supported flags and autocomplete +// options for this command. The map key for the Flags map should be the +// complete flag such as "-foo" or "--foo". +func (c *ListCommand) AutocompleteFlags() complete.Flags { + return complete.Flags{ + fmt.Sprintf("-%s", flagNameNamespace): complete.PredictNothing, + fmt.Sprintf("-%s", flagNameAllNamespaces): complete.PredictNothing, + fmt.Sprintf("-%s", flagNameKubeConfig): complete.PredictFiles("*"), + fmt.Sprintf("-%s", flagNameKubeContext): complete.PredictNothing, + } +} + +// AutocompleteArgs returns the argument predictor for this command. +// Since argument completion is not supported, this will return +// complete.PredictNothing. +func (c *ListCommand) AutocompleteArgs() complete.Predictor { + return complete.PredictNothing +} + // validateFlags ensures that the flags passed in by the can be used. func (c *ListCommand) validateFlags() error { if len(c.set.Args()) > 0 { @@ -125,6 +152,7 @@ func (c *ListCommand) validateFlags() error { if errs := validation.ValidateNamespaceName(c.flagNamespace, false); c.flagNamespace != "" && len(errs) > 0 { return fmt.Errorf("invalid namespace name passed for -namespace/-n: %v", strings.Join(errs, "; ")) } + return nil } diff --git a/cli/cmd/proxy/list/command_test.go b/cli/cmd/proxy/list/command_test.go index a29c851391..b2c2cb6043 100644 --- a/cli/cmd/proxy/list/command_test.go +++ b/cli/cmd/proxy/list/command_test.go @@ -3,13 +3,18 @@ package list import ( "bytes" "context" + "flag" + "fmt" "io" "os" "testing" "github.com/hashicorp/consul-k8s/cli/common" + cmnFlag "github.com/hashicorp/consul-k8s/cli/common/flag" "github.com/hashicorp/consul-k8s/cli/common/terminal" "github.com/hashicorp/go-hclog" + "github.com/posener/complete" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -359,3 +364,35 @@ func setupCommand(buf io.Writer) *ListCommand { return command } + +func TestTaskCreateCommand_AutocompleteFlags(t *testing.T) { + t.Parallel() + buf := new(bytes.Buffer) + cmd := setupCommand(buf) + + predictor := cmd.AutocompleteFlags() + + // Test that we get the expected number of predictions + args := complete.Args{Last: "-"} + res := predictor.Predict(args) + + // Grab the list of flags from the Flag object + flags := make([]string, 0) + cmd.set.VisitSets(func(name string, set *cmnFlag.Set) { + set.VisitAll(func(flag *flag.Flag) { + flags = append(flags, fmt.Sprintf("-%s", flag.Name)) + }) + }) + + // Verify that there is a prediction for each flag associated with the command + assert.Equal(t, len(flags), len(res)) + assert.ElementsMatch(t, flags, res, "flags and predictions didn't match, make sure to add "+ + "new flags to the command AutoCompleteFlags function") +} + +func TestTaskCreateCommand_AutocompleteArgs(t *testing.T) { + buf := new(bytes.Buffer) + cmd := setupCommand(buf) + c := cmd.AutocompleteArgs() + assert.Equal(t, complete.PredictNothing, c) +} diff --git a/cli/cmd/proxy/read/command.go b/cli/cmd/proxy/read/command.go index 4123f70adb..ad2bb96303 100644 --- a/cli/cmd/proxy/read/command.go +++ b/cli/cmd/proxy/read/command.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/consul-k8s/cli/common" "github.com/hashicorp/consul-k8s/cli/common/flag" "github.com/hashicorp/consul-k8s/cli/common/terminal" + "github.com/posener/complete" helmCLI "helm.sh/helm/v3/pkg/cli" "k8s.io/apimachinery/pkg/api/validation" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -25,6 +26,19 @@ const ( Table = "table" JSON = "json" Raw = "raw" + + flagNameNamespace = "namespace" + flagNameOutput = "output" + flagNameClusters = "clusters" + flagNameListeners = "listeners" + flagNameRoutes = "routes" + flagNameEndpoints = "endpoints" + flagNameSecrets = "secrets" + flagNameFQDN = "fqdn" + flagNameAddress = "address" + flagNamePort = "port" + flagNameKubeConfig = "kubeconfig" + flagNameKubeContext = "context" ) type ReadCommand struct { @@ -69,13 +83,13 @@ func (c *ReadCommand) init() { c.set = flag.NewSets() f := c.set.NewSet("Command Options") f.StringVar(&flag.StringVar{ - Name: "namespace", + Name: flagNameNamespace, Target: &c.flagNamespace, Usage: "The namespace where the target Pod can be found.", Aliases: []string{"n"}, }) f.StringVar(&flag.StringVar{ - Name: "output", + Name: flagNameOutput, Target: &c.flagOutput, Usage: "Output the Envoy configuration as 'table', 'json', or 'raw'.", Default: Table, @@ -84,42 +98,42 @@ func (c *ReadCommand) init() { f = c.set.NewSet("Output Filtering Options") f.BoolVar(&flag.BoolVar{ - Name: "clusters", + Name: flagNameClusters, Target: &c.flagClusters, Usage: "Filter output to only show clusters.", }) f.BoolVar(&flag.BoolVar{ - Name: "listeners", + Name: flagNameListeners, Target: &c.flagListeners, Usage: "Filter output to only show listeners.", }) f.BoolVar(&flag.BoolVar{ - Name: "routes", + Name: flagNameRoutes, Target: &c.flagRoutes, Usage: "Filter output to only show routes.", }) f.BoolVar(&flag.BoolVar{ - Name: "endpoints", + Name: flagNameEndpoints, Target: &c.flagEndpoints, Usage: "Filter output to only show endpoints.", }) f.BoolVar(&flag.BoolVar{ - Name: "secrets", + Name: flagNameSecrets, Target: &c.flagSecrets, Usage: "Filter output to only show secrets.", }) f.StringVar(&flag.StringVar{ - Name: "fqdn", + Name: flagNameFQDN, Target: &c.flagFQDN, Usage: "Filter cluster output to clusters with a fully qualified domain name which contains the given value. May be combined with -address and -port.", }) f.StringVar(&flag.StringVar{ - Name: "address", + Name: flagNameAddress, Target: &c.flagAddress, Usage: "Filter clusters, endpoints, and listeners output to those with addresses which contain the given value. May be combined with -fqdn and -port", }) f.IntVar(&flag.IntVar{ - Name: "port", + Name: flagNamePort, Target: &c.flagPort, Usage: "Filter endpoints and listeners output to addresses with the given port number. May be combined with -fqdn and -address.", Default: -1, @@ -127,13 +141,13 @@ func (c *ReadCommand) init() { f = c.set.NewSet("GlobalOptions") f.StringVar(&flag.StringVar{ - Name: "kubeconfig", + Name: flagNameKubeConfig, Aliases: []string{"c"}, Target: &c.flagKubeConfig, Usage: "Set the path to kubeconfig file.", }) f.StringVar(&flag.StringVar{ - Name: "context", + Name: flagNameKubeContext, Target: &c.flagKubeContext, Usage: "Set the Kubernetes context to use.", }) @@ -193,6 +207,33 @@ func (c *ReadCommand) Synopsis() string { return "Inspect the Envoy configuration for a given Pod." } +// AutocompleteFlags returns a mapping of supported flags and autocomplete +// options for this command. The map key for the Flags map should be the +// complete flag such as "-foo" or "--foo". +func (c *ReadCommand) AutocompleteFlags() complete.Flags { + return complete.Flags{ + fmt.Sprintf("-%s", flagNameNamespace): complete.PredictNothing, + fmt.Sprintf("-%s", flagNameOutput): complete.PredictNothing, + fmt.Sprintf("-%s", flagNameClusters): complete.PredictNothing, + fmt.Sprintf("-%s", flagNameListeners): complete.PredictNothing, + fmt.Sprintf("-%s", flagNameRoutes): complete.PredictNothing, + fmt.Sprintf("-%s", flagNameEndpoints): complete.PredictNothing, + fmt.Sprintf("-%s", flagNameSecrets): complete.PredictNothing, + fmt.Sprintf("-%s", flagNameFQDN): complete.PredictNothing, + fmt.Sprintf("-%s", flagNameAddress): complete.PredictNothing, + fmt.Sprintf("-%s", flagNamePort): complete.PredictNothing, + fmt.Sprintf("-%s", flagNameKubeConfig): complete.PredictFiles("*"), + fmt.Sprintf("-%s", flagNameKubeContext): complete.PredictNothing, + } +} + +// AutocompleteArgs returns the argument predictor for this command. +// Since argument completion is not supported, this will return +// complete.PredictNothing. +func (c *ReadCommand) AutocompleteArgs() complete.Predictor { + return complete.PredictNothing +} + func (c *ReadCommand) parseFlags(args []string) error { // Separate positional arguments from keyed arguments. positional := []string{} diff --git a/cli/cmd/proxy/read/command_test.go b/cli/cmd/proxy/read/command_test.go index 9a6902ccae..27f19e7370 100644 --- a/cli/cmd/proxy/read/command_test.go +++ b/cli/cmd/proxy/read/command_test.go @@ -3,14 +3,18 @@ package read import ( "bytes" "context" + "flag" "fmt" "io" "os" "testing" "github.com/hashicorp/consul-k8s/cli/common" + cmnFlag "github.com/hashicorp/consul-k8s/cli/common/flag" "github.com/hashicorp/consul-k8s/cli/common/terminal" "github.com/hashicorp/go-hclog" + "github.com/posener/complete" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -259,3 +263,35 @@ func setupCommand(buf io.Writer) *ReadCommand { return command } + +func TestTaskCreateCommand_AutocompleteFlags(t *testing.T) { + t.Parallel() + buf := new(bytes.Buffer) + cmd := setupCommand(buf) + + predictor := cmd.AutocompleteFlags() + + // Test that we get the expected number of predictions + args := complete.Args{Last: "-"} + res := predictor.Predict(args) + + // Grab the list of flags from the Flag object + flags := make([]string, 0) + cmd.set.VisitSets(func(name string, set *cmnFlag.Set) { + set.VisitAll(func(flag *flag.Flag) { + flags = append(flags, fmt.Sprintf("-%s", flag.Name)) + }) + }) + + // Verify that there is a prediction for each flag associated with the command + assert.Equal(t, len(flags), len(res)) + assert.ElementsMatch(t, flags, res, "flags and predictions didn't match, make sure to add "+ + "new flags to the command AutoCompleteFlags function") +} + +func TestTaskCreateCommand_AutocompleteArgs(t *testing.T) { + buf := new(bytes.Buffer) + cmd := setupCommand(buf) + c := cmd.AutocompleteArgs() + assert.Equal(t, complete.PredictNothing, c) +} diff --git a/cli/cmd/status/status.go b/cli/cmd/status/status.go index 926d8d790b..19f5a52398 100644 --- a/cli/cmd/status/status.go +++ b/cli/cmd/status/status.go @@ -6,6 +6,7 @@ import ( "strconv" "sync" + "github.com/posener/complete" "helm.sh/helm/v3/pkg/release" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -19,6 +20,11 @@ import ( "sigs.k8s.io/yaml" ) +const ( + flagNameKubeConfig = "kubeconfig" + flagNameKubeContext = "context" +) + type Command struct { *common.BaseCommand @@ -68,7 +74,7 @@ func (c *Command) Run(args []string) int { return 1 } - if err := c.validateFlags(args); err != nil { + if err := c.validateFlags(); err != nil { c.UI.Output(err.Error()) return 1 } @@ -124,13 +130,30 @@ func (c *Command) Run(args []string) int { } // validateFlags checks the command line flags and values for errors. -func (c *Command) validateFlags(args []string) error { +func (c *Command) validateFlags() error { if len(c.set.Args()) > 0 { return errors.New("should have no non-flag arguments") } return nil } +// AutocompleteFlags returns a mapping of supported flags and autocomplete +// options for this command. The map key for the Flags map should be the +// complete flag such as "-foo" or "--foo". +func (c *Command) AutocompleteFlags() complete.Flags { + return complete.Flags{ + fmt.Sprintf("-%s", flagNameKubeConfig): complete.PredictFiles("*"), + fmt.Sprintf("-%s", flagNameKubeContext): complete.PredictNothing, + } +} + +// AutocompleteArgs returns the argument predictor for this command. +// Since argument completion is not supported, this will return +// complete.PredictNothing. +func (c *Command) AutocompleteArgs() complete.Predictor { + return complete.PredictNothing +} + // checkHelmInstallation uses the helm Go SDK to depict the status of a named release. This function then prints // the version of the release, it's status (unknown, deployed, uninstalled, ...), and the overwritten values. func (c *Command) checkHelmInstallation(settings *helmCLI.EnvSettings, uiLogger action.DebugLog, releaseName, namespace string) error { diff --git a/cli/cmd/status/status_test.go b/cli/cmd/status/status_test.go index 79f908654f..b45ffef556 100644 --- a/cli/cmd/status/status_test.go +++ b/cli/cmd/status/status_test.go @@ -2,12 +2,16 @@ package status import ( "context" + "flag" "fmt" "os" "testing" "github.com/hashicorp/consul-k8s/cli/common" + cmnFlag "github.com/hashicorp/consul-k8s/cli/common/flag" "github.com/hashicorp/go-hclog" + "github.com/posener/complete" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" appsv1 "k8s.io/api/apps/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -184,3 +188,33 @@ func getInitializedCommand(t *testing.T) *Command { c.init() return c } + +func TestTaskCreateCommand_AutocompleteFlags(t *testing.T) { + t.Parallel() + cmd := getInitializedCommand(t) + + predictor := cmd.AutocompleteFlags() + + // Test that we get the expected number of predictions + args := complete.Args{Last: "-"} + res := predictor.Predict(args) + + // Grab the list of flags from the Flag object + flags := make([]string, 0) + cmd.set.VisitSets(func(name string, set *cmnFlag.Set) { + set.VisitAll(func(flag *flag.Flag) { + flags = append(flags, fmt.Sprintf("-%s", flag.Name)) + }) + }) + + // Verify that there is a prediction for each flag associated with the command + assert.Equal(t, len(flags), len(res)) + assert.ElementsMatch(t, flags, res, "flags and predictions didn't match, make sure to add "+ + "new flags to the command AutoCompleteFlags function") +} + +func TestTaskCreateCommand_AutocompleteArgs(t *testing.T) { + cmd := getInitializedCommand(t) + c := cmd.AutocompleteArgs() + assert.Equal(t, complete.PredictNothing, c) +} diff --git a/cli/cmd/uninstall/uninstall.go b/cli/cmd/uninstall/uninstall.go index 01a442226d..07b945bf79 100644 --- a/cli/cmd/uninstall/uninstall.go +++ b/cli/cmd/uninstall/uninstall.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/consul-k8s/cli/common/flag" "github.com/hashicorp/consul-k8s/cli/common/terminal" "github.com/hashicorp/consul-k8s/cli/helm" + "github.com/posener/complete" "helm.sh/helm/v3/pkg/action" helmCLI "helm.sh/helm/v3/pkg/cli" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -32,6 +33,9 @@ const ( flagTimeout = "timeout" defaultTimeout = "10m" + + flagContext = "context" + flagKubeconfig = "kubeconfig" ) type Command struct { @@ -91,14 +95,14 @@ func (c *Command) init() { f = c.set.NewSet("Global Options") f.StringVar(&flag.StringVar{ - Name: "kubeconfig", + Name: flagKubeconfig, Aliases: []string{"c"}, Target: &c.flagKubeConfig, Default: "", Usage: "Path to kubeconfig file.", }) f.StringVar(&flag.StringVar{ - Name: "context", + Name: flagContext, Target: &c.flagKubeContext, Default: "", Usage: "Kubernetes context to use.", @@ -277,7 +281,7 @@ func (c *Command) Run(args []string) int { return 1 } - if err := c.deleteSecrets(foundReleaseName, foundReleaseNamespace); err != nil { + if err := c.deleteSecrets(foundReleaseNamespace); err != nil { c.UI.Output(err.Error(), terminal.WithErrorStyle()) return 1 } @@ -325,6 +329,28 @@ func (c *Command) Synopsis() string { return "Uninstall Consul deployment." } +// AutocompleteFlags returns a mapping of supported flags and autocomplete +// options for this command. The map key for the Flags map should be the +// complete flag such as "-foo" or "--foo". +func (c *Command) AutocompleteFlags() complete.Flags { + return complete.Flags{ + fmt.Sprintf("-%s", flagAutoApprove): complete.PredictNothing, + fmt.Sprintf("-%s", flagNamespace): complete.PredictNothing, + fmt.Sprintf("-%s", flagReleaseName): complete.PredictNothing, + fmt.Sprintf("-%s", flagWipeData): complete.PredictNothing, + fmt.Sprintf("-%s", flagTimeout): complete.PredictNothing, + fmt.Sprintf("-%s", flagContext): complete.PredictNothing, + fmt.Sprintf("-%s", flagKubeconfig): complete.PredictFiles("*"), + } +} + +// AutocompleteArgs returns the argument predictor for this command. +// Since argument completion is not supported, this will return +// complete.PredictNothing. +func (c *Command) AutocompleteArgs() complete.Predictor { + return complete.PredictNothing +} + func (c *Command) findExistingInstallation(settings *helmCLI.EnvSettings, uiLogger action.DebugLog) (bool, string, string, error) { releaseName, namespace, err := common.CheckForInstallations(settings, uiLogger) if err != nil { @@ -378,7 +404,7 @@ func (c *Command) deletePVCs(foundReleaseName, foundReleaseNamespace string) err } // deleteSecrets deletes any secrets that have the label "managed-by" set to "consul-k8s". -func (c *Command) deleteSecrets(foundReleaseName, foundReleaseNamespace string) error { +func (c *Command) deleteSecrets(foundReleaseNamespace string) error { secrets, err := c.kubernetes.CoreV1().Secrets(foundReleaseNamespace).List(c.Ctx, metav1.ListOptions{ LabelSelector: common.CLILabelKey + "=" + common.CLILabelValue, }) diff --git a/cli/cmd/uninstall/uninstall_test.go b/cli/cmd/uninstall/uninstall_test.go index 7d36ce5d3d..8fa92e92b7 100644 --- a/cli/cmd/uninstall/uninstall_test.go +++ b/cli/cmd/uninstall/uninstall_test.go @@ -2,12 +2,17 @@ package uninstall import ( "context" + "flag" + "fmt" "os" "testing" "github.com/hashicorp/consul-k8s/cli/common" + cmnFlag "github.com/hashicorp/consul-k8s/cli/common/flag" "github.com/hashicorp/consul-k8s/cli/common/terminal" "github.com/hashicorp/go-hclog" + "github.com/posener/complete" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" batchv1 "k8s.io/api/batch/v1" v1 "k8s.io/api/core/v1" @@ -91,7 +96,7 @@ func TestDeleteSecrets(t *testing.T) { require.NoError(t, err) _, err = c.kubernetes.CoreV1().Secrets("default").Create(context.Background(), secret3, metav1.CreateOptions{}) require.NoError(t, err) - err = c.deleteSecrets("consul", "default") + err = c.deleteSecrets("default") require.NoError(t, err) secrets, err := c.kubernetes.CoreV1().Secrets("default").List(context.Background(), metav1.ListOptions{}) require.NoError(t, err) @@ -366,3 +371,33 @@ func getInitializedCommand(t *testing.T) *Command { c.init() return c } + +func TestTaskCreateCommand_AutocompleteFlags(t *testing.T) { + t.Parallel() + cmd := getInitializedCommand(t) + + predictor := cmd.AutocompleteFlags() + + // Test that we get the expected number of predictions + args := complete.Args{Last: "-"} + res := predictor.Predict(args) + + // Grab the list of flags from the Flag object + flags := make([]string, 0) + cmd.set.VisitSets(func(name string, set *cmnFlag.Set) { + set.VisitAll(func(flag *flag.Flag) { + flags = append(flags, fmt.Sprintf("-%s", flag.Name)) + }) + }) + + // Verify that there is a prediction for each flag associated with the command + assert.Equal(t, len(flags), len(res)) + assert.ElementsMatch(t, flags, res, "flags and predictions didn't match, make sure to add "+ + "new flags to the command AutoCompleteFlags function") +} + +func TestTaskCreateCommand_AutocompleteArgs(t *testing.T) { + cmd := getInitializedCommand(t) + c := cmd.AutocompleteArgs() + assert.Equal(t, complete.PredictNothing, c) +} diff --git a/cli/cmd/upgrade/upgrade.go b/cli/cmd/upgrade/upgrade.go index e145e238e1..e1bb744ce1 100644 --- a/cli/cmd/upgrade/upgrade.go +++ b/cli/cmd/upgrade/upgrade.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/consul-k8s/cli/common/terminal" "github.com/hashicorp/consul-k8s/cli/config" "github.com/hashicorp/consul-k8s/cli/helm" + "github.com/posener/complete" "helm.sh/helm/v3/pkg/action" helmCLI "helm.sh/helm/v3/pkg/cli" "helm.sh/helm/v3/pkg/cli/values" @@ -44,6 +45,9 @@ const ( flagNameWait = "wait" defaultWait = true + + flagNameContext = "context" + flagNameKubeconfig = "kubeconfig" ) type Command struct { @@ -143,14 +147,14 @@ func (c *Command) init() { f = c.set.NewSet("Global Options") f.StringVar(&flag.StringVar{ - Name: "kubeconfig", + Name: flagNameKubeconfig, Aliases: []string{"c"}, Target: &c.flagKubeConfig, Default: "", Usage: "Set the path to kubeconfig file.", }) f.StringVar(&flag.StringVar{ - Name: "context", + Name: flagNameContext, Target: &c.flagKubeContext, Default: "", Usage: "Set the Kubernetes context to use.", @@ -308,6 +312,33 @@ func (c *Command) Run(args []string) int { return 0 } +// AutocompleteFlags returns a mapping of supported flags and autocomplete +// options for this command. The map key for the Flags map should be the +// complete flag such as "-foo" or "--foo". +func (c *Command) AutocompleteFlags() complete.Flags { + return complete.Flags{ + fmt.Sprintf("-%s", flagNamePreset): complete.PredictNothing, + fmt.Sprintf("-%s", flagNameConfigFile): complete.PredictFiles("*"), + fmt.Sprintf("-%s", flagNameSetStringValues): complete.PredictNothing, + fmt.Sprintf("-%s", flagNameSetValues): complete.PredictNothing, + fmt.Sprintf("-%s", flagNameFileValues): complete.PredictFiles("*"), + fmt.Sprintf("-%s", flagNameDryRun): complete.PredictNothing, + fmt.Sprintf("-%s", flagNameAutoApprove): complete.PredictNothing, + fmt.Sprintf("-%s", flagNameTimeout): complete.PredictNothing, + fmt.Sprintf("-%s", flagNameVerbose): complete.PredictNothing, + fmt.Sprintf("-%s", flagNameWait): complete.PredictNothing, + fmt.Sprintf("-%s", flagNameContext): complete.PredictNothing, + fmt.Sprintf("-%s", flagNameKubeconfig): complete.PredictFiles("*"), + } +} + +// AutocompleteArgs returns the argument predictor for this command. +// Since argument completion is not supported, this will return +// complete.PredictNothing. +func (c *Command) AutocompleteArgs() complete.Predictor { + return complete.PredictNothing +} + // validateFlags checks that the user's provided flags are valid. func (c *Command) validateFlags(args []string) error { if err := c.set.Parse(args); err != nil { diff --git a/cli/cmd/upgrade/upgrade_test.go b/cli/cmd/upgrade/upgrade_test.go index f70496bda6..9b4636eb57 100644 --- a/cli/cmd/upgrade/upgrade_test.go +++ b/cli/cmd/upgrade/upgrade_test.go @@ -1,11 +1,16 @@ package upgrade import ( + "flag" + "fmt" "os" "testing" "github.com/hashicorp/consul-k8s/cli/common" + cmnFlag "github.com/hashicorp/consul-k8s/cli/common/flag" "github.com/hashicorp/go-hclog" + "github.com/posener/complete" + "github.com/stretchr/testify/assert" ) // TestValidateFlags tests the validate flags function. @@ -66,3 +71,33 @@ func getInitializedCommand(t *testing.T) *Command { c.init() return c } + +func TestTaskCreateCommand_AutocompleteFlags(t *testing.T) { + t.Parallel() + cmd := getInitializedCommand(t) + + predictor := cmd.AutocompleteFlags() + + // Test that we get the expected number of predictions + args := complete.Args{Last: "-"} + res := predictor.Predict(args) + + // Grab the list of flags from the Flag object + flags := make([]string, 0) + cmd.set.VisitSets(func(name string, set *cmnFlag.Set) { + set.VisitAll(func(flag *flag.Flag) { + flags = append(flags, fmt.Sprintf("-%s", flag.Name)) + }) + }) + + // Verify that there is a prediction for each flag associated with the command + assert.Equal(t, len(flags), len(res)) + assert.ElementsMatch(t, flags, res, "flags and predictions didn't match, make sure to add "+ + "new flags to the command AutoCompleteFlags function") +} + +func TestTaskCreateCommand_AutocompleteArgs(t *testing.T) { + cmd := getInitializedCommand(t) + c := cmd.AutocompleteArgs() + assert.Equal(t, complete.PredictNothing, c) +} diff --git a/cli/common/flag/set.go b/cli/common/flag/set.go index 6e5e615555..03ee353f80 100644 --- a/cli/common/flag/set.go +++ b/cli/common/flag/set.go @@ -43,23 +43,23 @@ func NewSets() *Sets { // NewSet creates a new single flag set. A set should be created for // any grouping of flags, for example "Common Options", "Auth Options", etc. -func (f *Sets) NewSet(name string) *Set { +func (s *Sets) NewSet(name string) *Set { flagSet := NewSet(name) // The union and completions are pointers to our own values - flagSet.unionSet = f.unionSet - flagSet.completions = f.completions + flagSet.unionSet = s.unionSet + flagSet.completions = s.completions // Keep track of it for help generation - f.flagSets = append(f.flagSets, flagSet) + s.flagSets = append(s.flagSets, flagSet) return flagSet } // GetSetFlags returns a slice of flags for a given set. // If the requested set does not exist, this will return an empty slice. -func (f *Sets) GetSetFlags(setName string) []string { +func (s *Sets) GetSetFlags(setName string) []string { var setFlags []string - for _, set := range f.flagSets { + for _, set := range s.flagSets { if set.name == setName { set.flagSet.VisitAll(func(f *flag.Flag) { setFlags = append(setFlags, fmt.Sprintf("-%s", f.Name)) @@ -72,36 +72,36 @@ func (f *Sets) GetSetFlags(setName string) []string { } // Completions returns the completions for this flag set. -func (f *Sets) Completions() complete.Flags { - return f.completions +func (s *Sets) Completions() complete.Flags { + return s.completions } // Parse parses the given flags, returning any errors. -func (f *Sets) Parse(args []string) error { - return f.unionSet.Parse(args) +func (s *Sets) Parse(args []string) error { + return s.unionSet.Parse(args) } // Parsed reports whether the command-line flags have been parsed. -func (f *Sets) Parsed() bool { - return f.unionSet.Parsed() +func (s *Sets) Parsed() bool { + return s.unionSet.Parsed() } // Args returns the remaining args after parsing. -func (f *Sets) Args() []string { - return f.unionSet.Args() +func (s *Sets) Args() []string { + return s.unionSet.Args() } // Visit visits the flags in lexicographical order, calling fn for each. It // visits only those flags that have been set. -func (f *Sets) Visit(fn func(*flag.Flag)) { - f.unionSet.Visit(fn) +func (s *Sets) Visit(fn func(*flag.Flag)) { + s.unionSet.Visit(fn) } // Help builds custom help for this command, grouping by flag set. -func (fs *Sets) Help() string { +func (s *Sets) Help() string { var out bytes.Buffer - for _, set := range fs.flagSets { + for _, set := range s.flagSets { printFlagTitle(&out, set.name+":") set.VisitAll(func(f *flag.Flag) { // Skip any hidden flags @@ -115,9 +115,10 @@ func (fs *Sets) Help() string { return strings.TrimRight(out.String(), "\n") } -// Help builds custom help for this command, grouping by flag set. -func (fs *Sets) VisitSets(fn func(name string, set *Set)) { - for _, set := range fs.flagSets { +// VisitSets visits each set and performs action based on the passed in +// function. +func (s *Sets) VisitSets(fn func(name string, set *Set)) { + for _, set := range s.flagSets { fn(set.name, set) } } diff --git a/cli/main.go b/cli/main.go index c979ec625a..0ffd03328c 100644 --- a/cli/main.go +++ b/cli/main.go @@ -15,6 +15,9 @@ func main() { c := cli.NewCLI("consul-k8s", version.GetHumanVersion()) c.Args = os.Args[1:] + // Enable CLI autocomplete + c.Autocomplete = true + log := hclog.New(&hclog.LoggerOptions{ Name: "cli", Level: hclog.Info,