...

Source file src/github.com/datawire/ambassador/v2/pkg/k8s/client.go

Documentation: github.com/datawire/ambassador/v2/pkg/k8s

     1  // Package k8s is a facade over (super-terrible, very difficult to understand)
     2  // client-go to provide a higher-level interface to Kubernetes, with support
     3  // for simple, high-level APIs for watching resources (including from stable,
     4  // long-running processes) and implementing basic controllers.
     5  //
     6  // It is intended to offer the same API for (nearly) every Kubernetes resource,
     7  // including easy CRD access without codegen.
     8  package k8s
     9  
    10  import (
    11  	"context"
    12  	"fmt"
    13  	"strings"
    14  
    15  	"github.com/datawire/ambassador/v2/pkg/kates"
    16  
    17  	"k8s.io/apimachinery/pkg/api/meta"
    18  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    19  	"k8s.io/apimachinery/pkg/runtime/schema"
    20  
    21  	"k8s.io/cli-runtime/pkg/genericclioptions"
    22  
    23  	"k8s.io/client-go/dynamic"
    24  	"k8s.io/client-go/rest"
    25  
    26  	"github.com/kballard/go-shellquote"
    27  	"github.com/pkg/errors"
    28  	"github.com/spf13/pflag"
    29  
    30  	_ "k8s.io/client-go/plugin/pkg/client/auth"
    31  )
    32  
    33  const (
    34  	// NamespaceAll is the argument to specify on a context when you want to list or filter
    35  	// resources across all namespaces.
    36  	NamespaceAll = metav1.NamespaceAll
    37  	// NamespaceNone is the argument for a context when there is no namespace.
    38  	NamespaceNone = metav1.NamespaceNone
    39  )
    40  
    41  // KubeInfo holds the data required to talk to a cluster
    42  type KubeInfo struct {
    43  	flags       *pflag.FlagSet
    44  	configFlags *genericclioptions.ConfigFlags
    45  	config      *rest.Config
    46  	namespace   string
    47  }
    48  
    49  // NewKubeInfo returns a useable KubeInfo, handling optional
    50  // kubeconfig, context, and namespace.
    51  func NewKubeInfo(configfile, context, namespace string) *KubeInfo {
    52  	flags := pflag.NewFlagSet("KubeInfo", pflag.ContinueOnError)
    53  	result := NewKubeInfoFromFlags(flags)
    54  
    55  	var args []string
    56  	if configfile != "" {
    57  		args = append(args, "--kubeconfig", configfile)
    58  	}
    59  	if context != "" {
    60  		args = append(args, "--context", context)
    61  	}
    62  	if namespace != "" {
    63  		args = append(args, "--namespace", namespace)
    64  	}
    65  
    66  	if err := flags.Parse(args); err != nil {
    67  		// Args is constructed by us, we should never get an
    68  		// error, so it's ok to panic.
    69  		panic(err)
    70  	}
    71  	return result
    72  }
    73  
    74  // NewKubeInfoFromFlags adds the generic kubeconfig flags to the
    75  // provided FlagSet, and returns a *KubeInfo that configures itself
    76  // based on those flags.
    77  func NewKubeInfoFromFlags(flags *pflag.FlagSet) *KubeInfo {
    78  	configFlags := genericclioptions.NewConfigFlags(false)
    79  
    80  	// We can disable or enable flags by setting them to
    81  	// nil/non-nil prior to calling .AddFlags().
    82  	//
    83  	// .Username and .Password are already disabled by default in
    84  	// genericclioptions.NewConfigFlags().
    85  
    86  	configFlags.AddFlags(flags)
    87  	return &KubeInfo{flags, configFlags, nil, ""}
    88  }
    89  
    90  func (info *KubeInfo) load() error {
    91  	if info.config == nil {
    92  		configLoader := info.configFlags.ToRawKubeConfigLoader()
    93  
    94  		config, err := configLoader.ClientConfig()
    95  		if err != nil {
    96  			return errors.Errorf("Failed to get REST config: %v", err)
    97  		}
    98  
    99  		namespace, _, err := configLoader.Namespace()
   100  		if err != nil {
   101  			return errors.Errorf("Failed to get namespace: %v", err)
   102  		}
   103  
   104  		info.config = config
   105  		info.namespace = namespace
   106  	}
   107  
   108  	return nil
   109  }
   110  
   111  // GetConfigFlags returns the genericclioptions.ConfigFlags from inside the KubeInfo
   112  func (info *KubeInfo) GetConfigFlags() *genericclioptions.ConfigFlags {
   113  	return info.configFlags
   114  }
   115  
   116  // Namespace returns the namespace for a KubeInfo.
   117  func (info *KubeInfo) Namespace() (string, error) {
   118  	err := info.load()
   119  	if err != nil {
   120  		return "", err
   121  	}
   122  	return info.namespace, nil
   123  }
   124  
   125  // GetRestConfig returns a REST config
   126  func (info *KubeInfo) GetRestConfig() (*rest.Config, error) {
   127  	err := info.load()
   128  	if err != nil {
   129  		return nil, err
   130  	}
   131  	return info.config, nil
   132  }
   133  
   134  // GetKubectl returns the arguments for a runnable kubectl command that talks to
   135  // the same cluster as the associated ClientConfig.
   136  func (info *KubeInfo) GetKubectl(args string) (string, error) {
   137  	parts, err := shellquote.Split(args)
   138  	if err != nil {
   139  		return "", err
   140  	}
   141  	kargs, err := info.GetKubectlArray(parts...)
   142  	if err != nil {
   143  		return "", err
   144  	}
   145  	return strings.Join(kargs, " "), nil
   146  }
   147  
   148  // GetKubectlArray does what GetKubectl does but returns the result as a []string.
   149  func (info *KubeInfo) GetKubectlArray(args ...string) ([]string, error) {
   150  	res := []string{} // No leading "kubectl" because reasons...
   151  
   152  	info.flags.Visit(func(f *pflag.Flag) {
   153  		res = append(res, fmt.Sprintf("--%s", f.Name), f.Value.String())
   154  	})
   155  
   156  	res = append(res, args...)
   157  
   158  	return res, nil
   159  }
   160  
   161  // Client is the top-level handle to the Kubernetes cluster.
   162  type Client struct {
   163  	config     *rest.Config
   164  	Namespace  string
   165  	restMapper meta.RESTMapper
   166  }
   167  
   168  // NewClient constructs a k8s.Client, optionally using a previously-constructed
   169  // KubeInfo.
   170  func NewClient(info *KubeInfo) (*Client, error) {
   171  	if info == nil {
   172  		info = NewKubeInfo("", "", "") // Empty file/ctx/ns for defaults
   173  	}
   174  
   175  	config, err := info.GetRestConfig()
   176  	if err != nil {
   177  		return nil, err
   178  	}
   179  	namespace, err := info.Namespace()
   180  	if err != nil {
   181  		return nil, err
   182  	}
   183  
   184  	mapper, _, err := kates.NewRESTMapper(info.configFlags)
   185  	if err != nil {
   186  		return nil, err
   187  	}
   188  
   189  	return &Client{
   190  		config:     config,
   191  		Namespace:  namespace,
   192  		restMapper: mapper,
   193  	}, nil
   194  }
   195  
   196  // ResourceType describes a Kubernetes resource type in a particular cluster.
   197  // See ResolveResourceType() for more information.
   198  //
   199  // It is equivalent to a k8s.io/apimachinery/pkg/api/meta.RESTMapping
   200  type ResourceType struct {
   201  	Group      string
   202  	Version    string
   203  	Name       string // lowercase plural, called Resource in Kubernetes code
   204  	Kind       string // uppercase singular
   205  	Namespaced bool
   206  }
   207  
   208  func (r ResourceType) String() string {
   209  	return r.Name + "." + r.Version + "." + r.Group
   210  }
   211  
   212  // ResolveResourceType takes the name of a resource type
   213  // (TYPE[[.VERSION].GROUP], where TYPE may be singular, plural, or an
   214  // abbreviation; like you might pass to `kubectl get`) and returns
   215  // cluster-specific canonical information about that resource type.
   216  //
   217  // For example, with Kubernetes v1.10.5:
   218  //
   219  //	"pod"        -> {Group: "",           Version: "v1",      Name: "pods",        Kind: "Pod",        Namespaced: true}
   220  //	"deployment" -> {Group: "extensions", Version: "v1beta1", Name: "deployments", Kind: "Deployment", Namespaced: true}
   221  //	"svc.v1."    -> {Group: "",           Version: "v1",      Name: "services",    Kind: "Service",    Namespaced: true}
   222  //
   223  // Newer versions of Kubernetes might instead put "pod" in the "core"
   224  // group, or put "deployment" in apps/v1 instead of
   225  // extensions/v1beta1.
   226  func (c *Client) ResolveResourceType(resource string) (ResourceType, error) {
   227  	restmapping, err := mappingFor(resource, c.restMapper)
   228  	if err != nil {
   229  		return ResourceType{}, err
   230  	}
   231  	return ResourceType{
   232  		Group:      restmapping.GroupVersionKind.Group,
   233  		Version:    restmapping.GroupVersionKind.Version,
   234  		Name:       restmapping.Resource.Resource,
   235  		Kind:       restmapping.GroupVersionKind.Kind,
   236  		Namespaced: restmapping.Scope.Name() == meta.RESTScopeNameNamespace,
   237  	}, nil
   238  }
   239  
   240  // mappingFor returns the RESTMapping for the Kind given, or the Kind referenced by the resource.
   241  // Prefers a fully specified GroupVersionResource match. If one is not found, we match on a fully
   242  // specified GroupVersionKind, or fallback to a match on GroupKind.
   243  //
   244  // This is copy/pasted from k8s.io/cli-runtime/pkg/resource.Builder.mappingFor() (which is
   245  // unfortunately private), with modified lines marked with "// MODIFIED".
   246  func mappingFor(resourceOrKindArg string, restMapper meta.RESTMapper) (*meta.RESTMapping, error) { // MODIFIED: args
   247  	fullySpecifiedGVR, groupResource := schema.ParseResourceArg(resourceOrKindArg)
   248  	gvk := schema.GroupVersionKind{}
   249  	// MODIFIED: Don't call b.restMapperFn(), use the mapper given as an argument.
   250  
   251  	if fullySpecifiedGVR != nil {
   252  		gvk, _ = restMapper.KindFor(*fullySpecifiedGVR)
   253  	}
   254  	if gvk.Empty() {
   255  		gvk, _ = restMapper.KindFor(groupResource.WithVersion(""))
   256  	}
   257  	if !gvk.Empty() {
   258  		return restMapper.RESTMapping(gvk.GroupKind(), gvk.Version)
   259  	}
   260  
   261  	fullySpecifiedGVK, groupKind := schema.ParseKindArg(resourceOrKindArg)
   262  	if fullySpecifiedGVK == nil {
   263  		gvk := groupKind.WithVersion("")
   264  		fullySpecifiedGVK = &gvk
   265  	}
   266  
   267  	if !fullySpecifiedGVK.Empty() {
   268  		if mapping, err := restMapper.RESTMapping(fullySpecifiedGVK.GroupKind(), fullySpecifiedGVK.Version); err == nil {
   269  			return mapping, nil
   270  		}
   271  	}
   272  
   273  	mapping, err := restMapper.RESTMapping(groupKind, gvk.Version)
   274  	if err != nil {
   275  		// if we error out here, it is because we could not match a resource or a kind
   276  		// for the given argument. To maintain consistency with previous behavior,
   277  		// announce that a resource type could not be found.
   278  		// if the error is _not_ a *meta.NoKindMatchError, then we had trouble doing discovery,
   279  		// so we should return the original error since it may help a user diagnose what is actually wrong
   280  		if meta.IsNoMatchError(err) {
   281  			return nil, fmt.Errorf("the server doesn't have a resource type %q", groupResource.Resource)
   282  		}
   283  		return nil, err
   284  	}
   285  
   286  	return mapping, nil
   287  }
   288  
   289  // List calls ListNamespace(...) with NamespaceAll.
   290  func (c *Client) List(ctx context.Context, resource string) ([]Resource, error) {
   291  	return c.ListNamespace(ctx, NamespaceAll, resource)
   292  }
   293  
   294  // ListNamespace returns a slice of Resources.
   295  // If the resource is not namespaced, the namespace must be NamespaceNone.
   296  // If the resource is namespaced, NamespaceAll lists across all namespaces.
   297  func (c *Client) ListNamespace(ctx context.Context, namespace, resource string) ([]Resource, error) {
   298  	return c.SelectiveList(ctx, namespace, resource, "", "")
   299  }
   300  
   301  func (c *Client) SelectiveList(ctx context.Context, namespace, resource, fieldSelector, labelSelector string) ([]Resource, error) {
   302  	return c.ListQuery(ctx, Query{
   303  		Kind:          resource,
   304  		Namespace:     namespace,
   305  		FieldSelector: fieldSelector,
   306  		LabelSelector: labelSelector,
   307  	})
   308  }
   309  
   310  // Query describes a query for a set of Kubernetes resources.
   311  type Query struct {
   312  	// The Kind of a query may use any of the short names or abbreviations permitted by kubectl.
   313  	Kind string
   314  
   315  	// The Namespace field specifies the namespace to query.  Use NamespaceAll to query all
   316  	// namespaces.  If the resource type is not namespaced, this field must be NamespaceNone.
   317  	Namespace string
   318  
   319  	// The FieldSelector and LabelSelector fields contain field and label selectors as
   320  	// documented by kubectl.
   321  	FieldSelector string
   322  	LabelSelector string
   323  
   324  	resourceType ResourceType
   325  }
   326  
   327  func (q *Query) resolve(c *Client) error {
   328  	if q.resourceType != (ResourceType{}) {
   329  		return nil
   330  	}
   331  
   332  	rt, err := c.ResolveResourceType(q.Kind)
   333  	if err != nil {
   334  		return err
   335  	}
   336  	q.resourceType = rt
   337  	return nil
   338  }
   339  
   340  // ListQuery returns all the Kubernetes resources that satisfy the
   341  // supplied query.
   342  func (c *Client) ListQuery(ctx context.Context, query Query) ([]Resource, error) {
   343  	err := query.resolve(c)
   344  	if err != nil {
   345  		return nil, err
   346  	}
   347  
   348  	ri := query.resourceType
   349  
   350  	dyn, err := dynamic.NewForConfig(c.config)
   351  	if err != nil {
   352  		return nil, errors.Wrap(err, "failed to create dynamic context")
   353  	}
   354  
   355  	cli := dyn.Resource(schema.GroupVersionResource{
   356  		Group:    ri.Group,
   357  		Version:  ri.Version,
   358  		Resource: ri.Name,
   359  	})
   360  
   361  	var filtered dynamic.ResourceInterface
   362  	if ri.Namespaced && query.Namespace != "" {
   363  		filtered = cli.Namespace(query.Namespace)
   364  	} else {
   365  		filtered = cli
   366  	}
   367  
   368  	uns, err := filtered.List(ctx, metav1.ListOptions{
   369  		FieldSelector: query.FieldSelector,
   370  		LabelSelector: query.LabelSelector,
   371  	})
   372  	if err != nil {
   373  		return nil, err
   374  	}
   375  
   376  	result := make([]Resource, len(uns.Items))
   377  	for idx, un := range uns.Items {
   378  		result[idx] = un.UnstructuredContent()
   379  	}
   380  	return result, nil
   381  }
   382  

View as plain text