...

Source file src/k8s.io/kubectl/pkg/util/completion/completion.go

Documentation: k8s.io/kubectl/pkg/util/completion

     1  /*
     2  Copyright 2021 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package completion
    18  
    19  import (
    20  	"bytes"
    21  	"fmt"
    22  	"io"
    23  	"os"
    24  	"strings"
    25  	"time"
    26  
    27  	"github.com/spf13/cobra"
    28  
    29  	"k8s.io/apimachinery/pkg/api/meta"
    30  	"k8s.io/cli-runtime/pkg/genericclioptions"
    31  	"k8s.io/cli-runtime/pkg/genericiooptions"
    32  	"k8s.io/cli-runtime/pkg/printers"
    33  	"k8s.io/kubectl/pkg/cmd/apiresources"
    34  	"k8s.io/kubectl/pkg/cmd/get"
    35  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    36  	"k8s.io/kubectl/pkg/polymorphichelpers"
    37  	"k8s.io/kubectl/pkg/scheme"
    38  )
    39  
    40  var factory cmdutil.Factory
    41  
    42  // SetFactoryForCompletion Store the factory which is needed by the completion functions.
    43  // Not all commands have access to the factory, so cannot pass it to the completion functions.
    44  func SetFactoryForCompletion(f cmdutil.Factory) {
    45  	factory = f
    46  }
    47  
    48  // ResourceTypeAndNameCompletionFunc Returns a completion function that completes resource types
    49  // and resource names that match the toComplete prefix.  It supports the <type>/<name> form.
    50  func ResourceTypeAndNameCompletionFunc(f cmdutil.Factory) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
    51  	return resourceTypeAndNameCompletionFunc(f, nil, true)
    52  }
    53  
    54  // SpecifiedResourceTypeAndNameCompletionFunc Returns a completion function that completes resource
    55  // types limited to the specified allowedTypes, and resource names that match the toComplete prefix.
    56  // It allows for multiple resources. It supports the <type>/<name> form.
    57  func SpecifiedResourceTypeAndNameCompletionFunc(f cmdutil.Factory, allowedTypes []string) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
    58  	return resourceTypeAndNameCompletionFunc(f, allowedTypes, true)
    59  }
    60  
    61  // SpecifiedResourceTypeAndNameNoRepeatCompletionFunc Returns a completion function that completes resource
    62  // types limited to the specified allowedTypes, and resource names that match the toComplete prefix.
    63  // It only allows for one resource. It supports the <type>/<name> form.
    64  func SpecifiedResourceTypeAndNameNoRepeatCompletionFunc(f cmdutil.Factory, allowedTypes []string) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
    65  	return resourceTypeAndNameCompletionFunc(f, allowedTypes, false)
    66  }
    67  
    68  // ResourceNameCompletionFunc Returns a completion function that completes as a first argument
    69  // the resource names specified by the resourceType parameter, and which match the toComplete prefix.
    70  // This function does NOT support the <type>/<name> form: it is meant to be used by commands
    71  // that don't support that form.  For commands that apply to pods and that support the <type>/<name>
    72  // form, please use PodResourceNameCompletionFunc()
    73  func ResourceNameCompletionFunc(f cmdutil.Factory, resourceType string) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
    74  	return func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
    75  		var comps []string
    76  		if len(args) == 0 {
    77  			comps = CompGetResource(f, resourceType, toComplete)
    78  		}
    79  		return comps, cobra.ShellCompDirectiveNoFileComp
    80  	}
    81  }
    82  
    83  // PodResourceNameCompletionFunc Returns a completion function that completes:
    84  // 1- pod names that match the toComplete prefix
    85  // 2- resource types containing pods which match the toComplete prefix
    86  func PodResourceNameCompletionFunc(f cmdutil.Factory) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
    87  	return func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
    88  		var comps []string
    89  		directive := cobra.ShellCompDirectiveNoFileComp
    90  		if len(args) == 0 {
    91  			comps, directive = doPodResourceCompletion(f, toComplete)
    92  		}
    93  		return comps, directive
    94  	}
    95  }
    96  
    97  // PodResourceNameAndContainerCompletionFunc Returns a completion function that completes, as a first argument:
    98  // 1- pod names that match the toComplete prefix
    99  // 2- resource types containing pods which match the toComplete prefix
   100  // and as a second argument the containers within the specified pod.
   101  func PodResourceNameAndContainerCompletionFunc(f cmdutil.Factory) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
   102  	return func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
   103  		var comps []string
   104  		directive := cobra.ShellCompDirectiveNoFileComp
   105  		if len(args) == 0 {
   106  			comps, directive = doPodResourceCompletion(f, toComplete)
   107  		} else if len(args) == 1 {
   108  			podName := convertResourceNameToPodName(f, args[0])
   109  			comps = CompGetContainers(f, podName, toComplete)
   110  		}
   111  		return comps, directive
   112  	}
   113  }
   114  
   115  // ContainerCompletionFunc Returns a completion function that completes the containers within the
   116  // pod specified by the first argument.  The resource containing the pod can be specified in
   117  // the <type>/<name> form.
   118  func ContainerCompletionFunc(f cmdutil.Factory) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
   119  	return func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
   120  		var comps []string
   121  		// We need the pod name to be able to complete the container names, it must be in args[0].
   122  		// That first argument can also be of the form <type>/<name> so we need to convert it.
   123  		if len(args) > 0 {
   124  			podName := convertResourceNameToPodName(f, args[0])
   125  			comps = CompGetContainers(f, podName, toComplete)
   126  		}
   127  		return comps, cobra.ShellCompDirectiveNoFileComp
   128  	}
   129  }
   130  
   131  // ContextCompletionFunc is a completion function that completes as a first argument the
   132  // context names that match the toComplete prefix
   133  func ContextCompletionFunc(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
   134  	if len(args) == 0 {
   135  		return ListContextsInConfig(toComplete), cobra.ShellCompDirectiveNoFileComp
   136  	}
   137  	return nil, cobra.ShellCompDirectiveNoFileComp
   138  }
   139  
   140  // ClusterCompletionFunc is a completion function that completes as a first argument the
   141  // cluster names that match the toComplete prefix
   142  func ClusterCompletionFunc(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
   143  	if len(args) == 0 {
   144  		return ListClustersInConfig(toComplete), cobra.ShellCompDirectiveNoFileComp
   145  	}
   146  	return nil, cobra.ShellCompDirectiveNoFileComp
   147  }
   148  
   149  // UserCompletionFunc is a completion function that completes as a first argument the
   150  // user names that match the toComplete prefix
   151  func UserCompletionFunc(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
   152  	if len(args) == 0 {
   153  		return ListUsersInConfig(toComplete), cobra.ShellCompDirectiveNoFileComp
   154  	}
   155  	return nil, cobra.ShellCompDirectiveNoFileComp
   156  }
   157  
   158  // CompGetResource gets the list of the resource specified which begin with `toComplete`.
   159  func CompGetResource(f cmdutil.Factory, resourceName string, toComplete string) []string {
   160  	template := "{{ range .items  }}{{ .metadata.name }} {{ end }}"
   161  	return CompGetFromTemplate(&template, f, "", []string{resourceName}, toComplete)
   162  }
   163  
   164  // CompGetContainers gets the list of containers of the specified pod which begin with `toComplete`.
   165  func CompGetContainers(f cmdutil.Factory, podName string, toComplete string) []string {
   166  	template := "{{ range .spec.initContainers }}{{ .name }} {{end}}{{ range .spec.containers  }}{{ .name }} {{ end }}"
   167  	return CompGetFromTemplate(&template, f, "", []string{"pod", podName}, toComplete)
   168  }
   169  
   170  // CompGetFromTemplate executes a Get operation using the specified template and args and returns the results
   171  // which begin with `toComplete`.
   172  func CompGetFromTemplate(template *string, f cmdutil.Factory, namespace string, args []string, toComplete string) []string {
   173  	buf := new(bytes.Buffer)
   174  	streams := genericiooptions.IOStreams{In: os.Stdin, Out: buf, ErrOut: io.Discard}
   175  	o := get.NewGetOptions("kubectl", streams)
   176  
   177  	// Get the list of names of the specified resource
   178  	o.PrintFlags.TemplateFlags.GoTemplatePrintFlags.TemplateArgument = template
   179  	format := "go-template"
   180  	o.PrintFlags.OutputFormat = &format
   181  
   182  	// Do the steps Complete() would have done.
   183  	// We cannot actually call Complete() or Validate() as these function check for
   184  	// the presence of flags, which, in our case won't be there
   185  	if namespace != "" {
   186  		o.Namespace = namespace
   187  		o.ExplicitNamespace = true
   188  	} else {
   189  		var err error
   190  		o.Namespace, o.ExplicitNamespace, err = f.ToRawKubeConfigLoader().Namespace()
   191  		if err != nil {
   192  			return nil
   193  		}
   194  	}
   195  
   196  	o.ToPrinter = func(mapping *meta.RESTMapping, outputObjects *bool, withNamespace bool, withKind bool) (printers.ResourcePrinterFunc, error) {
   197  		printer, err := o.PrintFlags.ToPrinter()
   198  		if err != nil {
   199  			return nil, err
   200  		}
   201  		return printer.PrintObj, nil
   202  	}
   203  
   204  	o.Run(f, args)
   205  
   206  	var comps []string
   207  	resources := strings.Split(buf.String(), " ")
   208  	for _, res := range resources {
   209  		if res != "" && strings.HasPrefix(res, toComplete) {
   210  			comps = append(comps, res)
   211  		}
   212  	}
   213  	return comps
   214  }
   215  
   216  // ListContextsInConfig returns a list of context names which begin with `toComplete`
   217  func ListContextsInConfig(toComplete string) []string {
   218  	config, err := factory.ToRawKubeConfigLoader().RawConfig()
   219  	if err != nil {
   220  		return nil
   221  	}
   222  	var ret []string
   223  	for name := range config.Contexts {
   224  		if strings.HasPrefix(name, toComplete) {
   225  			ret = append(ret, name)
   226  		}
   227  	}
   228  	return ret
   229  }
   230  
   231  // ListClustersInConfig returns a list of cluster names which begin with `toComplete`
   232  func ListClustersInConfig(toComplete string) []string {
   233  	config, err := factory.ToRawKubeConfigLoader().RawConfig()
   234  	if err != nil {
   235  		return nil
   236  	}
   237  	var ret []string
   238  	for name := range config.Clusters {
   239  		if strings.HasPrefix(name, toComplete) {
   240  			ret = append(ret, name)
   241  		}
   242  	}
   243  	return ret
   244  }
   245  
   246  // ListUsersInConfig returns a list of user names which begin with `toComplete`
   247  func ListUsersInConfig(toComplete string) []string {
   248  	config, err := factory.ToRawKubeConfigLoader().RawConfig()
   249  	if err != nil {
   250  		return nil
   251  	}
   252  	var ret []string
   253  	for name := range config.AuthInfos {
   254  		if strings.HasPrefix(name, toComplete) {
   255  			ret = append(ret, name)
   256  		}
   257  	}
   258  	return ret
   259  }
   260  
   261  // compGetResourceList returns the list of api resources which begin with `toComplete`.
   262  func compGetResourceList(restClientGetter genericclioptions.RESTClientGetter, cmd *cobra.Command, toComplete string) []string {
   263  	buf := new(bytes.Buffer)
   264  	streams := genericiooptions.IOStreams{In: os.Stdin, Out: buf, ErrOut: io.Discard}
   265  	o := apiresources.NewAPIResourceOptions(streams)
   266  
   267  	o.Complete(restClientGetter, cmd, nil)
   268  
   269  	// Get the list of resources
   270  	o.Output = "name"
   271  	o.Cached = true
   272  	o.Verbs = []string{"get"}
   273  	// TODO:Should set --request-timeout=5s
   274  
   275  	// Ignore errors as the output may still be valid
   276  	o.RunAPIResources()
   277  
   278  	// Resources can be a comma-separated list.  The last element is then
   279  	// the one we should complete.  For example if toComplete=="pods,secre"
   280  	// we should return "pods,secrets"
   281  	prefix := ""
   282  	suffix := toComplete
   283  	lastIdx := strings.LastIndex(toComplete, ",")
   284  	if lastIdx != -1 {
   285  		prefix = toComplete[0 : lastIdx+1]
   286  		suffix = toComplete[lastIdx+1:]
   287  	}
   288  	var comps []string
   289  	resources := strings.Split(buf.String(), "\n")
   290  	for _, res := range resources {
   291  		if res != "" && strings.HasPrefix(res, suffix) {
   292  			comps = append(comps, fmt.Sprintf("%s%s", prefix, res))
   293  		}
   294  	}
   295  	return comps
   296  }
   297  
   298  // resourceTypeAndNameCompletionFunc Returns a completion function that completes resource types
   299  // and resource names that match the toComplete prefix.  It supports the <type>/<name> form.
   300  func resourceTypeAndNameCompletionFunc(f cmdutil.Factory, allowedTypes []string, allowRepeat bool) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
   301  	return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
   302  		var comps []string
   303  		directive := cobra.ShellCompDirectiveNoFileComp
   304  
   305  		if len(args) > 0 && !strings.Contains(args[0], "/") {
   306  			// The first argument is of the form <type> (e.g., pods)
   307  			// All following arguments should be a resource name.
   308  			if allowRepeat || len(args) == 1 {
   309  				comps = CompGetResource(f, args[0], toComplete)
   310  
   311  				// Remove choices already on the command-line
   312  				if len(args) > 1 {
   313  					comps = cmdutil.Difference(comps, args[1:])
   314  				}
   315  			}
   316  		} else {
   317  			slashIdx := strings.Index(toComplete, "/")
   318  			if slashIdx == -1 {
   319  				if len(args) == 0 {
   320  					// We are completing the first argument.  We default to the normal
   321  					// <type> form (not the form <type>/<name>).
   322  					// So we suggest resource types and let the shell add a space after
   323  					// the completion.
   324  					if len(allowedTypes) == 0 {
   325  						comps = compGetResourceList(f, cmd, toComplete)
   326  					} else {
   327  						for _, c := range allowedTypes {
   328  							if strings.HasPrefix(c, toComplete) {
   329  								comps = append(comps, c)
   330  							}
   331  						}
   332  					}
   333  				} else {
   334  					// Here we know the first argument contains a / (<type>/<name>).
   335  					// All other arguments must also use that form.
   336  					if allowRepeat {
   337  						// Since toComplete does not already contain a / we know we are completing a
   338  						// resource type. Disable adding a space after the completion, and add the /
   339  						directive |= cobra.ShellCompDirectiveNoSpace
   340  
   341  						if len(allowedTypes) == 0 {
   342  							typeComps := compGetResourceList(f, cmd, toComplete)
   343  							for _, c := range typeComps {
   344  								comps = append(comps, fmt.Sprintf("%s/", c))
   345  							}
   346  						} else {
   347  							for _, c := range allowedTypes {
   348  								if strings.HasPrefix(c, toComplete) {
   349  									comps = append(comps, fmt.Sprintf("%s/", c))
   350  								}
   351  							}
   352  						}
   353  					}
   354  				}
   355  			} else {
   356  				// We are completing an argument of the form <type>/<name>
   357  				// and since the / is already present, we are completing the resource name.
   358  				if allowRepeat || len(args) == 0 {
   359  					resourceType := toComplete[:slashIdx]
   360  					toComplete = toComplete[slashIdx+1:]
   361  					nameComps := CompGetResource(f, resourceType, toComplete)
   362  					for _, c := range nameComps {
   363  						comps = append(comps, fmt.Sprintf("%s/%s", resourceType, c))
   364  					}
   365  
   366  					// Remove choices already on the command-line.
   367  					if len(args) > 0 {
   368  						comps = cmdutil.Difference(comps, args[0:])
   369  					}
   370  				}
   371  			}
   372  		}
   373  		return comps, directive
   374  	}
   375  }
   376  
   377  // doPodResourceCompletion Returns completions of:
   378  // 1- pod names that match the toComplete prefix
   379  // 2- resource types containing pods which match the toComplete prefix
   380  func doPodResourceCompletion(f cmdutil.Factory, toComplete string) ([]string, cobra.ShellCompDirective) {
   381  	var comps []string
   382  	directive := cobra.ShellCompDirectiveNoFileComp
   383  	slashIdx := strings.Index(toComplete, "/")
   384  	if slashIdx == -1 {
   385  		// Standard case, complete pod names
   386  		comps = CompGetResource(f, "pod", toComplete)
   387  
   388  		// Also include resource choices for the <type>/<name> form,
   389  		// but only for resources that contain pods
   390  		resourcesWithPods := []string{
   391  			"daemonsets",
   392  			"deployments",
   393  			"pods",
   394  			"jobs",
   395  			"replicasets",
   396  			"replicationcontrollers",
   397  			"services",
   398  			"statefulsets"}
   399  
   400  		if len(comps) == 0 {
   401  			// If there are no pods to complete, we will only be completing
   402  			// <type>/.  We should disable adding a space after the /.
   403  			directive |= cobra.ShellCompDirectiveNoSpace
   404  		}
   405  
   406  		for _, resource := range resourcesWithPods {
   407  			if strings.HasPrefix(resource, toComplete) {
   408  				comps = append(comps, fmt.Sprintf("%s/", resource))
   409  			}
   410  		}
   411  	} else {
   412  		// Dealing with the <type>/<name> form, use the specified resource type
   413  		resourceType := toComplete[:slashIdx]
   414  		toComplete = toComplete[slashIdx+1:]
   415  		nameComps := CompGetResource(f, resourceType, toComplete)
   416  		for _, c := range nameComps {
   417  			comps = append(comps, fmt.Sprintf("%s/%s", resourceType, c))
   418  		}
   419  	}
   420  	return comps, directive
   421  }
   422  
   423  // convertResourceNameToPodName Converts a resource name to a pod name.
   424  // If the resource name is of the form <type>/<name>, we use
   425  // polymorphichelpers.AttachablePodForObjectFn(), if not, the resource name
   426  // is already a pod name.
   427  func convertResourceNameToPodName(f cmdutil.Factory, resourceName string) string {
   428  	var podName string
   429  	if !strings.Contains(resourceName, "/") {
   430  		// When we don't have the <type>/<name> form, the resource name is the pod name
   431  		podName = resourceName
   432  	} else {
   433  		// if the resource name is of the form <type>/<name>, we need to convert it to a pod name
   434  		ns, _, err := f.ToRawKubeConfigLoader().Namespace()
   435  		if err != nil {
   436  			return ""
   437  		}
   438  
   439  		resourceWithPod, err := f.NewBuilder().
   440  			WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...).
   441  			ContinueOnError().
   442  			NamespaceParam(ns).DefaultNamespace().
   443  			ResourceNames("pods", resourceName).
   444  			Do().Object()
   445  		if err != nil {
   446  			return ""
   447  		}
   448  
   449  		// For shell completion, use a short timeout
   450  		forwardablePod, err := polymorphichelpers.AttachablePodForObjectFn(f, resourceWithPod, 100*time.Millisecond)
   451  		if err != nil {
   452  			return ""
   453  		}
   454  		podName = forwardablePod.Name
   455  	}
   456  	return podName
   457  }
   458  

View as plain text