From 7aa5cb40316dbeb970250ee0835fbd597fd19a20 Mon Sep 17 00:00:00 2001 From: Marc Khouzam Date: Fri, 15 Oct 2021 15:36:09 -0400 Subject: [PATCH] Complete multiple resource names This commit teaches the completion function to repeat resource names when supported by the command. The logic checks if a resource name has already been specified by the user and does not include it again when repeating the completion. For example, the get command can receive multiple pods names, therefore with this commit we have: kubectl get pod pod1 [tab] will provide completion of pod names again, but not show 'pod1' since it is already part of the command-line. The improvement affects the following commands: - annotate - apply edit-last-applied - apply view-last-applied - autoscale - delete - describe - edit - expose - get - label - patch - rollout history - rollout pause - rollout restart - rollout resume - rollout undo - scale - taint Note that "rollout status" only accepts a single resource name, unlike the other "rollout ..." commands; this required the creation of a special completion function that did not repeat just for that case. Signed-off-by: Marc Khouzam --- staging/src/k8s.io/kubectl/pkg/cmd/get/get.go | 5 +- .../kubectl/pkg/cmd/rollout/rollout_status.go | 2 +- .../k8s.io/kubectl/pkg/cmd/util/helpers.go | 15 +++ .../kubectl/pkg/cmd/util/helpers_test.go | 40 +++++++ .../src/k8s.io/kubectl/pkg/util/completion.go | 29 ++++- .../kubectl/pkg/util/completion_test.go | 101 ++++++++---------- 6 files changed, 127 insertions(+), 65 deletions(-) diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/get/get.go b/staging/src/k8s.io/kubectl/pkg/cmd/get/get.go index b04489b7348..9cfd37da044 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/get/get.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/get/get.go @@ -167,8 +167,11 @@ func NewCmdGet(parent string, f cmdutil.Factory, streams genericclioptions.IOStr var comps []string if len(args) == 0 { comps = apiresources.CompGetResourceList(f, cmd, toComplete) - } else if len(args) == 1 { + } else { comps = CompGetResource(f, cmd, args[0], toComplete) + if len(args) > 1 { + comps = cmdutil.Difference(comps, args[1:]) + } } return comps, cobra.ShellCompDirectiveNoFileComp }, diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/rollout/rollout_status.go b/staging/src/k8s.io/kubectl/pkg/cmd/rollout/rollout_status.go index 11704af03a9..39e012a8d27 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/rollout/rollout_status.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/rollout/rollout_status.go @@ -102,7 +102,7 @@ func NewCmdRolloutStatus(f cmdutil.Factory, streams genericclioptions.IOStreams) Short: i18n.T("Show the status of the rollout"), Long: statusLong, Example: statusExample, - ValidArgsFunction: util.SpecifiedResourceTypeAndNameCompletionFunc(f, validArgs), + ValidArgsFunction: util.SpecifiedResourceTypeAndNameNoRepeatCompletionFunc(f, validArgs), Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, args)) cmdutil.CheckErr(o.Validate()) diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/util/helpers.go b/staging/src/k8s.io/kubectl/pkg/cmd/util/helpers.go index 7b03deb29c0..bb24f61e65f 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/util/helpers.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/util/helpers.go @@ -733,3 +733,18 @@ func Warning(cmdErr io.Writer, newGeneratorName, oldGeneratorName string) { oldGeneratorName, ) } + +// Difference removes any elements of subArray from fullArray and returns the result +func Difference(fullArray []string, subArray []string) []string { + exclude := make(map[string]bool, len(subArray)) + for _, elem := range subArray { + exclude[elem] = true + } + var result []string + for _, elem := range fullArray { + if _, found := exclude[elem]; !found { + result = append(result, elem) + } + } + return result +} diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/util/helpers_test.go b/staging/src/k8s.io/kubectl/pkg/cmd/util/helpers_test.go index 55785335e6d..f268cdfa3ab 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/util/helpers_test.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/util/helpers_test.go @@ -25,6 +25,9 @@ import ( "syscall" "testing" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + corev1 "k8s.io/api/core/v1" apiequality "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/errors" @@ -321,3 +324,40 @@ func TestDumpReaderToFile(t *testing.T) { t.Fatalf("Wrong file content %s != %s", testString, stringData) } } + +func TestDifferenceFunc(t *testing.T) { + tests := []struct { + name string + fullArray []string + subArray []string + expected []string + }{ + { + name: "remove some", + fullArray: []string{"a", "b", "c", "d"}, + subArray: []string{"c", "b"}, + expected: []string{"a", "d"}, + }, + { + name: "remove all", + fullArray: []string{"a", "b", "c", "d"}, + subArray: []string{"b", "d", "a", "c"}, + expected: nil, + }, + { + name: "remove none", + fullArray: []string{"a", "b", "c", "d"}, + subArray: nil, + expected: []string{"a", "b", "c", "d"}, + }, + } + + for _, tc := range tests { + result := Difference(tc.fullArray, tc.subArray) + if !cmp.Equal(tc.expected, result, cmpopts.SortSlices(func(x, y string) bool { + return x < y + })) { + t.Errorf("%s -> Expected: %v, but got: %v", tc.name, tc.expected, result) + } + } +} diff --git a/staging/src/k8s.io/kubectl/pkg/util/completion.go b/staging/src/k8s.io/kubectl/pkg/util/completion.go index 3253b75764c..3fe8f8cd95f 100644 --- a/staging/src/k8s.io/kubectl/pkg/util/completion.go +++ b/staging/src/k8s.io/kubectl/pkg/util/completion.go @@ -34,15 +34,18 @@ func SetFactoryForCompletion(f cmdutil.Factory) { } // ResourceTypeAndNameCompletionFunc Returns a completion function that completes as a first argument -// the resource types that match the toComplete prefix, and as a second argument the resource names that match +// the resource types that match the toComplete prefix, and all following arguments as resource names that match // the toComplete prefix. func ResourceTypeAndNameCompletionFunc(f cmdutil.Factory) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { var comps []string if len(args) == 0 { comps = apiresources.CompGetResourceList(f, cmd, toComplete) - } else if len(args) == 1 { + } else { comps = get.CompGetResource(f, cmd, args[0], toComplete) + if len(args) > 1 { + comps = cmdutil.Difference(comps, args[1:]) + } } return comps, cobra.ShellCompDirectiveNoFileComp } @@ -50,8 +53,19 @@ func ResourceTypeAndNameCompletionFunc(f cmdutil.Factory) func(*cobra.Command, [ // SpecifiedResourceTypeAndNameCompletionFunc Returns a completion function that completes as a first // argument the resource types that match the toComplete prefix and are limited to the allowedTypes, -// and as a second argument the specified resource names that match the toComplete prefix. +// and all following arguments as resource names that match the toComplete prefix. func SpecifiedResourceTypeAndNameCompletionFunc(f cmdutil.Factory, allowedTypes []string) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { + return doSpecifiedResourceTypeAndNameComp(f, allowedTypes, true) +} + +// SpecifiedResourceTypeAndNameNoRepeatCompletionFunc Returns a completion function that completes as a first +// argument the resource types that match the toComplete prefix and are limited to the allowedTypes, and as +// a second argument a resource name that match the toComplete prefix. +func SpecifiedResourceTypeAndNameNoRepeatCompletionFunc(f cmdutil.Factory, allowedTypes []string) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { + return doSpecifiedResourceTypeAndNameComp(f, allowedTypes, false) +} + +func doSpecifiedResourceTypeAndNameComp(f cmdutil.Factory, allowedTypes []string, repeat bool) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { var comps []string if len(args) == 0 { @@ -60,8 +74,13 @@ func SpecifiedResourceTypeAndNameCompletionFunc(f cmdutil.Factory, allowedTypes comps = append(comps, comp) } } - } else if len(args) == 1 { - comps = get.CompGetResource(f, cmd, args[0], toComplete) + } else { + if repeat || len(args) == 1 { + comps = get.CompGetResource(f, cmd, args[0], toComplete) + if repeat && len(args) > 1 { + comps = cmdutil.Difference(comps, args[1:]) + } + } } return comps, cobra.ShellCompDirectiveNoFileComp } diff --git a/staging/src/k8s.io/kubectl/pkg/util/completion_test.go b/staging/src/k8s.io/kubectl/pkg/util/completion_test.go index 663b306720e..da94faa99ae 100644 --- a/staging/src/k8s.io/kubectl/pkg/util/completion_test.go +++ b/staging/src/k8s.io/kubectl/pkg/util/completion_test.go @@ -35,115 +35,100 @@ import ( func TestResourceTypeAndNameCompletionFuncOneArg(t *testing.T) { tf, cmd := prepareCompletionTest() addPodsToFactory(tf) + compFunc := ResourceTypeAndNameCompletionFunc(tf) comps, directive := compFunc(cmd, []string{"pod"}, "b") checkCompletion(t, comps, []string{"bar"}, directive, cobra.ShellCompDirectiveNoFileComp) } -func TestResourceTypeAndNameCompletionFuncTooManyArgs(t *testing.T) { - tf := cmdtesting.NewTestFactory() - defer tf.Cleanup() +func TestResourceTypeAndNameCompletionFuncRepeating(t *testing.T) { + tf, cmd := prepareCompletionTest() + addPodsToFactory(tf) - streams, _, _, _ := genericclioptions.NewTestIOStreams() - cmd := get.NewCmdGet("kubectl", tf, streams) compFunc := ResourceTypeAndNameCompletionFunc(tf) - comps, directive := compFunc(cmd, []string{"pod", "pod-name"}, "") - checkCompletion(t, comps, []string{}, directive, cobra.ShellCompDirectiveNoFileComp) + comps, directive := compFunc(cmd, []string{"pod", "bar"}, "") + // The other pods should be completed, but not the already specified ones + checkCompletion(t, comps, []string{"foo"}, directive, cobra.ShellCompDirectiveNoFileComp) } func TestSpecifiedResourceTypeAndNameCompletionFuncNoArgs(t *testing.T) { - tf := cmdtesting.NewTestFactory() - defer tf.Cleanup() + tf, cmd := prepareCompletionTest() + addPodsToFactory(tf) - streams, _, _, _ := genericclioptions.NewTestIOStreams() - cmd := get.NewCmdGet("kubectl", tf, streams) compFunc := SpecifiedResourceTypeAndNameCompletionFunc(tf, []string{"pod", "service", "statefulset"}) comps, directive := compFunc(cmd, []string{}, "s") checkCompletion(t, comps, []string{"service", "statefulset"}, directive, cobra.ShellCompDirectiveNoFileComp) } func TestSpecifiedResourceTypeAndNameCompletionFuncOneArg(t *testing.T) { - tf := cmdtesting.NewTestFactory().WithNamespace("test") - defer tf.Cleanup() + tf, cmd := prepareCompletionTest() + addPodsToFactory(tf) - pods, _, _ := cmdtesting.TestData() - codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) - tf.UnstructuredClient = &fake.RESTClient{ - NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, - Resp: &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, pods)}, - } - - streams, _, _, _ := genericclioptions.NewTestIOStreams() - cmd := get.NewCmdGet("kubectl", tf, streams) compFunc := SpecifiedResourceTypeAndNameCompletionFunc(tf, []string{"pod"}) comps, directive := compFunc(cmd, []string{"pod"}, "b") checkCompletion(t, comps, []string{"bar"}, directive, cobra.ShellCompDirectiveNoFileComp) } -func TestSpecifiedResourceTypeAndNameCompletionFuncTooManyArgs(t *testing.T) { - tf := cmdtesting.NewTestFactory() - defer tf.Cleanup() +func TestSpecifiedResourceTypeAndNameCompletionFuncRepeating(t *testing.T) { + tf, cmd := prepareCompletionTest() + addPodsToFactory(tf) - streams, _, _, _ := genericclioptions.NewTestIOStreams() - cmd := get.NewCmdGet("kubectl", tf, streams) compFunc := SpecifiedResourceTypeAndNameCompletionFunc(tf, []string{"pod"}) - comps, directive := compFunc(cmd, []string{"pod", "pod-name"}, "") + comps, directive := compFunc(cmd, []string{"pod", "bar"}, "") + // The other pods should be completed, but not the already specified ones + checkCompletion(t, comps, []string{"foo"}, directive, cobra.ShellCompDirectiveNoFileComp) +} + +func TestSpecifiedResourceTypeAndNameCompletionNoRepeatFuncOneArg(t *testing.T) { + tf, cmd := prepareCompletionTest() + addPodsToFactory(tf) + + compFunc := SpecifiedResourceTypeAndNameNoRepeatCompletionFunc(tf, []string{"pod"}) + comps, directive := compFunc(cmd, []string{"pod"}, "b") + checkCompletion(t, comps, []string{"bar"}, directive, cobra.ShellCompDirectiveNoFileComp) +} + +func TestSpecifiedResourceTypeAndNameCompletionNoRepeatFuncMultiArg(t *testing.T) { + tf, cmd := prepareCompletionTest() + addPodsToFactory(tf) + + compFunc := SpecifiedResourceTypeAndNameNoRepeatCompletionFunc(tf, []string{"pod"}) + comps, directive := compFunc(cmd, []string{"pod", "bar"}, "") + // There should not be any more pods shown as this function should not repeat the completion checkCompletion(t, comps, []string{}, directive, cobra.ShellCompDirectiveNoFileComp) } func TestResourceNameCompletionFuncNoArgs(t *testing.T) { - tf := cmdtesting.NewTestFactory().WithNamespace("test") - defer tf.Cleanup() + tf, cmd := prepareCompletionTest() + addPodsToFactory(tf) - pods, _, _ := cmdtesting.TestData() - codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) - tf.UnstructuredClient = &fake.RESTClient{ - NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, - Resp: &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, pods)}, - } - - streams, _, _, _ := genericclioptions.NewTestIOStreams() - cmd := get.NewCmdGet("kubectl", tf, streams) compFunc := ResourceNameCompletionFunc(tf, "pod") comps, directive := compFunc(cmd, []string{}, "b") checkCompletion(t, comps, []string{"bar"}, directive, cobra.ShellCompDirectiveNoFileComp) } func TestResourceNameCompletionFuncTooManyArgs(t *testing.T) { - tf := cmdtesting.NewTestFactory() - defer tf.Cleanup() + tf, cmd := prepareCompletionTest() + addPodsToFactory(tf) - streams, _, _, _ := genericclioptions.NewTestIOStreams() - cmd := get.NewCmdGet("kubectl", tf, streams) compFunc := ResourceNameCompletionFunc(tf, "pod") comps, directive := compFunc(cmd, []string{"pod-name"}, "") checkCompletion(t, comps, []string{}, directive, cobra.ShellCompDirectiveNoFileComp) } func TestPodResourceNameAndContainerCompletionFuncNoArgs(t *testing.T) { - tf := cmdtesting.NewTestFactory().WithNamespace("test") - defer tf.Cleanup() + tf, cmd := prepareCompletionTest() + addPodsToFactory(tf) - pods, _, _ := cmdtesting.TestData() - codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) - tf.UnstructuredClient = &fake.RESTClient{ - NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, - Resp: &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, pods)}, - } - - streams, _, _, _ := genericclioptions.NewTestIOStreams() - cmd := get.NewCmdGet("kubectl", tf, streams) compFunc := PodResourceNameAndContainerCompletionFunc(tf) comps, directive := compFunc(cmd, []string{}, "b") checkCompletion(t, comps, []string{"bar"}, directive, cobra.ShellCompDirectiveNoFileComp) } func TestPodResourceNameAndContainerCompletionFuncTooManyArgs(t *testing.T) { - tf := cmdtesting.NewTestFactory().WithNamespace("test") - defer tf.Cleanup() + tf, cmd := prepareCompletionTest() + addPodsToFactory(tf) - streams, _, _, _ := genericclioptions.NewTestIOStreams() - cmd := get.NewCmdGet("kubectl", tf, streams) compFunc := PodResourceNameAndContainerCompletionFunc(tf) comps, directive := compFunc(cmd, []string{"pod-name", "container-name"}, "") checkCompletion(t, comps, []string{}, directive, cobra.ShellCompDirectiveNoFileComp)