github.com/splunk/dan1-qbec@v0.7.3/internal/remote/config.go (about)

     1  /*
     2     Copyright 2019 Splunk Inc.
     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 remote has the client implementation for interrogating and updating K8s objects and their metadata.
    18  package remote
    19  
    20  import (
    21  	"fmt"
    22  	"path/filepath"
    23  	"strings"
    24  	"sync"
    25  
    26  	"github.com/pkg/errors"
    27  	"github.com/spf13/cobra"
    28  	"github.com/splunk/qbec/internal/sio"
    29  	"k8s.io/client-go/discovery"
    30  	"k8s.io/client-go/dynamic"
    31  	"k8s.io/client-go/rest"
    32  	"k8s.io/client-go/tools/clientcmd"
    33  )
    34  
    35  // inspired by the config code in ksonnet but implemented differently.
    36  
    37  // ConnectOpts are the connection options required for the config.
    38  type ConnectOpts struct {
    39  	EnvName   string // environment name, display purposes only
    40  	ServerURL string // the server URL to connect to, must be configured in the kubeconfig
    41  	Namespace string // the default namespace to set for the context
    42  	Verbosity int    // verbosity of client interactions
    43  }
    44  
    45  // Config provides clients for specific contexts out of a kubeconfig file, with overrides for auth.
    46  type Config struct {
    47  	loadingRules *clientcmd.ClientConfigLoadingRules
    48  	overrides    *clientcmd.ConfigOverrides
    49  	l            sync.Mutex
    50  	kubeconfig   clientcmd.ClientConfig
    51  }
    52  
    53  // NewConfig returns a new configuration, adding flags to the supplied command to set k8s access overrides, prefixed by
    54  // the supplied string.
    55  func NewConfig(cmd *cobra.Command, prefix string) *Config {
    56  	loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
    57  	overrides := &clientcmd.ConfigOverrides{}
    58  	cmd.PersistentFlags().StringVar(&loadingRules.ExplicitPath, prefix+"kubeconfig", "", "Path to a kubeconfig file. Alternative to env var $KUBECONFIG.")
    59  	clientcmd.BindOverrideFlags(overrides, cmd.PersistentFlags(), clientcmd.ConfigOverrideFlags{
    60  		AuthOverrideFlags: clientcmd.RecommendedAuthOverrideFlags(prefix),
    61  		Timeout: clientcmd.FlagInfo{
    62  			LongName:    prefix + clientcmd.FlagTimeout,
    63  			Default:     "0",
    64  			Description: "The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests."},
    65  	})
    66  	return &Config{
    67  		loadingRules: loadingRules,
    68  		overrides:    overrides,
    69  	}
    70  }
    71  
    72  func (c *Config) getRESTConfig(opts ConnectOpts) (*rest.Config, error) {
    73  	if c.kubeconfig == nil {
    74  		c.kubeconfig = clientcmd.NewNonInteractiveDeferredLoadingClientConfig(c.loadingRules, c.overrides)
    75  	}
    76  	if err := c.overrideCluster(c.kubeconfig, opts); err != nil {
    77  		return nil, err
    78  	}
    79  	restConfig, err := c.kubeconfig.ClientConfig()
    80  	if err != nil {
    81  		return nil, err
    82  	}
    83  	return restConfig, nil
    84  }
    85  
    86  func (c *Config) overrideCluster(kc clientcmd.ClientConfig, opts ConnectOpts) error {
    87  	rc, err := kc.RawConfig()
    88  	if err != nil {
    89  		return errors.Wrap(err, "raw Config from kubeconfig")
    90  	}
    91  	for name, cluster := range rc.Clusters {
    92  		if cluster.Server == opts.ServerURL {
    93  			sio.Noticeln("setting cluster to", name)
    94  			c.overrides.Context.Cluster = name
    95  			c.overrides.Context.Namespace = opts.Namespace
    96  			for contextName, ctx := range rc.Contexts {
    97  				if ctx.Cluster == name {
    98  					sio.Noticeln("setting context to", contextName)
    99  					c.overrides.CurrentContext = contextName
   100  				}
   101  			}
   102  			return nil
   103  		}
   104  	}
   105  	return fmt.Errorf("unable to find any cluster with URL %q  (for env %s) in the kube config", opts.ServerURL, opts.EnvName)
   106  }
   107  
   108  // KubeAttributes is a collection k8s attributes pertaining to an connection.
   109  type KubeAttributes struct {
   110  	ConfigFile string `json:"configFile"` // the kubeconfig file or a list of such file separated by the list path separator
   111  	Context    string `json:"context"`    // the context to use, if known
   112  	Cluster    string `json:"cluster"`    // the cluster to use, always set
   113  	Namespace  string `json:"namespace"`  // the nanespace to use
   114  }
   115  
   116  // KubeAttributes returns client attributes for the supplied connection options.
   117  func (c *Config) KubeAttributes(opts ConnectOpts) (*KubeAttributes, error) {
   118  	if c.kubeconfig == nil {
   119  		c.kubeconfig = clientcmd.NewNonInteractiveDeferredLoadingClientConfig(c.loadingRules, c.overrides)
   120  	}
   121  	if err := c.overrideCluster(c.kubeconfig, opts); err != nil {
   122  		return nil, err
   123  	}
   124  	return &KubeAttributes{
   125  		ConfigFile: strings.Join(c.loadingRules.Precedence, string(filepath.ListSeparator)),
   126  		Cluster:    c.overrides.Context.Cluster,
   127  		Context:    c.overrides.CurrentContext,
   128  		Namespace:  opts.Namespace,
   129  	}, nil
   130  }
   131  
   132  // Client returns a client that correctly points to the server as specified in the connection options.
   133  // For this to work correctly, the kubernetes config that is used *must* have a cluster that has the supplied
   134  // server URL as an endpoint, so that correct TLS certs are used for authenticating the server.
   135  func (c *Config) Client(opts ConnectOpts) (*Client, error) {
   136  	c.l.Lock()
   137  	defer c.l.Unlock()
   138  	conf, err := c.getRESTConfig(opts)
   139  	if err != nil {
   140  		return nil, err
   141  	}
   142  
   143  	disco, err := discovery.NewDiscoveryClientForConfig(conf)
   144  	if err != nil {
   145  		return nil, err
   146  	}
   147  
   148  	discoCache := newCachedDiscoveryClient(disco)
   149  	mapper := discovery.NewDeferredDiscoveryRESTMapper(discoCache, dynamic.VersionInterfaces)
   150  	pathResolver := dynamic.LegacyAPIPathResolverFunc
   151  	pool := dynamic.NewClientPool(conf, mapper, pathResolver)
   152  	return newClient(pool, disco, opts.Namespace, opts.Verbosity)
   153  }
   154  
   155  // ContextInfo has information we care about a K8s context
   156  type ContextInfo struct {
   157  	ServerURL string // the server URL defined for the cluster
   158  	Namespace string // the namespace if set for the context, else "default"
   159  }
   160  
   161  // CurrentContextInfo returns information for the current context found in kubeconfig.
   162  func CurrentContextInfo() (*ContextInfo, error) {
   163  	loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
   164  	overrides := &clientcmd.ConfigOverrides{}
   165  	cc := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, overrides)
   166  	kc, err := cc.RawConfig()
   167  	if err != nil {
   168  		return nil, err
   169  	}
   170  	if kc.CurrentContext == "" {
   171  		return nil, fmt.Errorf("no current context set")
   172  	}
   173  	var cluster string
   174  	ns := "default"
   175  	for name, ctx := range kc.Contexts {
   176  		if name == kc.CurrentContext {
   177  			if ctx.Namespace != "" {
   178  				ns = ctx.Namespace
   179  			}
   180  			cluster = ctx.Cluster
   181  		}
   182  	}
   183  	if cluster == "" {
   184  		return nil, fmt.Errorf("no cluster found for context %s", kc.CurrentContext)
   185  	}
   186  	var serverURL string
   187  	for cname, clusterInfo := range kc.Clusters {
   188  		if cluster == cname {
   189  			serverURL = clusterInfo.Server
   190  		}
   191  	}
   192  	if serverURL == "" {
   193  		return nil, fmt.Errorf("unable to find server URL for cluster %s", cluster)
   194  	}
   195  	return &ContextInfo{
   196  		ServerURL: serverURL,
   197  		Namespace: ns,
   198  	}, nil
   199  }