...

Source file src/github.com/linkerd/linkerd2/pkg/k8s/completion.go

Documentation: github.com/linkerd/linkerd2/pkg/k8s

     1  package k8s
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strings"
     7  	"time"
     8  
     9  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    10  	"k8s.io/apimachinery/pkg/runtime/schema"
    11  )
    12  
    13  // CommandCompletion generates CLI suggestions from resources in a given cluster
    14  // It uses a list of arguments and a substring from the CLI to filter suggestions.
    15  type CommandCompletion struct {
    16  	k8sAPI    *KubernetesAPI
    17  	namespace string
    18  }
    19  
    20  // NewCommandCompletion creates a command completion module
    21  func NewCommandCompletion(
    22  	k8sAPI *KubernetesAPI,
    23  	namespace string,
    24  ) *CommandCompletion {
    25  	return &CommandCompletion{
    26  		k8sAPI:    k8sAPI,
    27  		namespace: namespace,
    28  	}
    29  }
    30  
    31  // Complete accepts a list of arguments and a substring to generate CLI suggestions.
    32  // `args` represent a list of arguments a user has already entered in the CLI. These
    33  // arguments are used for determining what resource type we'd like to receive
    34  // suggestions for as well as a list of resources names that have already provided.
    35  // `toComplete` represents the string prefix of a resource name that we'd like to
    36  // use to search for suggestions
    37  //
    38  // If `args` is empty, send back a list of all resource types support by the CLI.
    39  // If `args` has at least one or more items, assume that the first item in `args`
    40  // is the resource type we are trying to get suggestions for e.g. Deployment, StatefulSets.
    41  //
    42  // Complete is generic enough so that it can find suggestions for any type of resource
    43  // in a Kubernetes cluster. It does this by first querying what GroupVersion a resource
    44  // belongs to and then does a dynamic `List` query to get resources under that GroupVersion
    45  func (c *CommandCompletion) Complete(args []string, toComplete string) ([]string, error) {
    46  	ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
    47  	defer cancelFn()
    48  
    49  	suggestions := []string{}
    50  	if len(args) == 0 && toComplete == "" {
    51  		return CompletionResourceTypes, nil
    52  	}
    53  
    54  	if len(args) == 0 && toComplete != "" {
    55  		for _, t := range CompletionResourceTypes {
    56  			if strings.HasPrefix(t, toComplete) {
    57  				suggestions = append(suggestions, t)
    58  			}
    59  		}
    60  		return suggestions, nil
    61  	}
    62  
    63  	// Similar to kubectl, we don't provide resource completion
    64  	// when the resource provided is in format <kind>/<resourceName>
    65  	if strings.Contains(args[0], "/") {
    66  		return []string{}, nil
    67  	}
    68  
    69  	resType, err := CanonicalResourceNameFromFriendlyName(args[0])
    70  	if err != nil {
    71  		return nil, fmt.Errorf("%s is not a valid resource name", args)
    72  	}
    73  
    74  	// if we are looking for namespace suggestions clear namespace selector
    75  	if resType == "namespace" {
    76  		c.namespace = ""
    77  	}
    78  
    79  	gvr, err := c.getGroupVersionKindForResource(resType)
    80  	if err != nil {
    81  		return nil, err
    82  	}
    83  
    84  	uList, err := c.k8sAPI.DynamicClient.
    85  		Resource(*gvr).
    86  		Namespace(c.namespace).
    87  		List(ctx, metav1.ListOptions{})
    88  	if err != nil {
    89  		return nil, err
    90  	}
    91  
    92  	otherResources := args[1:]
    93  	for _, u := range uList.Items {
    94  		name := u.GetName()
    95  
    96  		// Filter out the list of resource items returned from k8s API to
    97  		// only include items that have the prefix `toComplete` and items
    98  		// that aren't in the list of resources already provided in the
    99  		// list of arguments.
   100  		//
   101  		// This is useful so that we avoid duplicate suggestions if they
   102  		// are already in the list of args
   103  		if strings.HasPrefix(name, toComplete) &&
   104  			!containsResource(name, otherResources) {
   105  			suggestions = append(suggestions, name)
   106  		}
   107  	}
   108  
   109  	return suggestions, nil
   110  }
   111  
   112  func (c *CommandCompletion) getGroupVersionKindForResource(resourceName string) (*schema.GroupVersionResource, error) {
   113  	_, apiResourceList, err := c.k8sAPI.Discovery().ServerGroupsAndResources()
   114  	if err != nil {
   115  		return nil, err
   116  	}
   117  
   118  	// find the plural name to ensure the resource we are searching for is not a subresource
   119  	// i.e. deployment/scale
   120  	pluralResourceName, err := PluralResourceNameFromFriendlyName(resourceName)
   121  	if err != nil {
   122  		return nil, fmt.Errorf("%s not a valid resource name", resourceName)
   123  	}
   124  
   125  	gvr, err := findGroupVersionResource(resourceName, pluralResourceName, apiResourceList)
   126  	if err != nil {
   127  		return nil, fmt.Errorf("could not find GroupVersionResource for %s", resourceName)
   128  	}
   129  
   130  	return gvr, nil
   131  }
   132  
   133  func findGroupVersionResource(singularName string, pluralName string, apiResourceList []*metav1.APIResourceList) (*schema.GroupVersionResource, error) {
   134  	err := fmt.Errorf("could not find the requested resource")
   135  	for _, res := range apiResourceList {
   136  		for _, r := range res.APIResources {
   137  
   138  			// Make sure we get a resource type where its Kind matches the
   139  			// singularName passed into this function and its Name (which is always
   140  			// the pluralName of an api resource) matches the pluralName passed
   141  			// into this function. Skip further processing of this APIResource
   142  			// if this is not the case.
   143  			if strings.ToLower(r.Kind) != singularName || r.Name != pluralName {
   144  				continue
   145  			}
   146  
   147  			gv := strings.Split(res.GroupVersion, "/")
   148  
   149  			if len(gv) == 1 && gv[0] == "v1" {
   150  				return &schema.GroupVersionResource{
   151  					Version:  gv[0],
   152  					Resource: r.Name,
   153  				}, nil
   154  			}
   155  
   156  			if len(gv) != 2 {
   157  				return nil, err
   158  			}
   159  
   160  			return &schema.GroupVersionResource{
   161  				Group:    gv[0],
   162  				Version:  gv[1],
   163  				Resource: r.Name,
   164  			}, nil
   165  		}
   166  	}
   167  
   168  	return nil, err
   169  }
   170  
   171  func containsResource(resource string, otherResources []string) bool {
   172  	for _, r := range otherResources {
   173  		if r == resource {
   174  			return true
   175  		}
   176  	}
   177  
   178  	return false
   179  }
   180  

View as plain text