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 <marc.khouzam@montreal.ca>
This commit is contained in:
Marc Khouzam 2021-10-15 15:36:09 -04:00
parent 55e1d2f9a7
commit 7aa5cb4031
6 changed files with 127 additions and 65 deletions

View File

@ -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
},

View File

@ -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())

View File

@ -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
}

View File

@ -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)
}
}
}

View File

@ -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
}

View File

@ -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)