...

Source file src/k8s.io/cli-runtime/pkg/genericclioptions/config_flags.go

Documentation: k8s.io/cli-runtime/pkg/genericclioptions

     1  /*
     2  Copyright 2018 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 genericclioptions
    18  
    19  import (
    20  	"os"
    21  	"path/filepath"
    22  	"regexp"
    23  	"strings"
    24  	"sync"
    25  	"time"
    26  
    27  	"github.com/spf13/pflag"
    28  
    29  	"k8s.io/apimachinery/pkg/api/meta"
    30  	"k8s.io/cli-runtime/pkg/genericiooptions"
    31  	"k8s.io/cli-runtime/pkg/printers"
    32  	"k8s.io/client-go/discovery"
    33  	diskcached "k8s.io/client-go/discovery/cached/disk"
    34  	"k8s.io/client-go/rest"
    35  	"k8s.io/client-go/restmapper"
    36  	"k8s.io/client-go/tools/clientcmd"
    37  	"k8s.io/client-go/util/homedir"
    38  	utilpointer "k8s.io/utils/pointer"
    39  )
    40  
    41  const (
    42  	flagClusterName        = "cluster"
    43  	flagAuthInfoName       = "user"
    44  	flagContext            = "context"
    45  	flagNamespace          = "namespace"
    46  	flagAPIServer          = "server"
    47  	flagTLSServerName      = "tls-server-name"
    48  	flagInsecure           = "insecure-skip-tls-verify"
    49  	flagCertFile           = "client-certificate"
    50  	flagKeyFile            = "client-key"
    51  	flagCAFile             = "certificate-authority"
    52  	flagBearerToken        = "token"
    53  	flagImpersonate        = "as"
    54  	flagImpersonateUID     = "as-uid"
    55  	flagImpersonateGroup   = "as-group"
    56  	flagUsername           = "username"
    57  	flagPassword           = "password"
    58  	flagTimeout            = "request-timeout"
    59  	flagCacheDir           = "cache-dir"
    60  	flagDisableCompression = "disable-compression"
    61  )
    62  
    63  // RESTClientGetter is an interface that the ConfigFlags describe to provide an easier way to mock for commands
    64  // and eliminate the direct coupling to a struct type.  Users may wish to duplicate this type in their own packages
    65  // as per the golang type overlapping.
    66  type RESTClientGetter interface {
    67  	// ToRESTConfig returns restconfig
    68  	ToRESTConfig() (*rest.Config, error)
    69  	// ToDiscoveryClient returns discovery client
    70  	ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error)
    71  	// ToRESTMapper returns a restmapper
    72  	ToRESTMapper() (meta.RESTMapper, error)
    73  	// ToRawKubeConfigLoader return kubeconfig loader as-is
    74  	ToRawKubeConfigLoader() clientcmd.ClientConfig
    75  }
    76  
    77  var _ RESTClientGetter = &ConfigFlags{}
    78  
    79  // ConfigFlags composes the set of values necessary
    80  // for obtaining a REST client config
    81  type ConfigFlags struct {
    82  	CacheDir   *string
    83  	KubeConfig *string
    84  
    85  	// config flags
    86  	ClusterName        *string
    87  	AuthInfoName       *string
    88  	Context            *string
    89  	Namespace          *string
    90  	APIServer          *string
    91  	TLSServerName      *string
    92  	Insecure           *bool
    93  	CertFile           *string
    94  	KeyFile            *string
    95  	CAFile             *string
    96  	BearerToken        *string
    97  	Impersonate        *string
    98  	ImpersonateUID     *string
    99  	ImpersonateGroup   *[]string
   100  	Username           *string
   101  	Password           *string
   102  	Timeout            *string
   103  	DisableCompression *bool
   104  	// If non-nil, wrap config function can transform the Config
   105  	// before it is returned in ToRESTConfig function.
   106  	WrapConfigFn func(*rest.Config) *rest.Config
   107  
   108  	clientConfig     clientcmd.ClientConfig
   109  	clientConfigLock sync.Mutex
   110  
   111  	restMapper     meta.RESTMapper
   112  	restMapperLock sync.Mutex
   113  
   114  	discoveryClient     discovery.CachedDiscoveryInterface
   115  	discoveryClientLock sync.Mutex
   116  
   117  	// If set to true, will use persistent client config, rest mapper, discovery client, and
   118  	// propagate them to the places that need them, rather than
   119  	// instantiating them multiple times.
   120  	usePersistentConfig bool
   121  	// Allows increasing burst used for discovery, this is useful
   122  	// in clusters with many registered resources
   123  	discoveryBurst int
   124  	// Allows increasing qps used for discovery, this is useful
   125  	// in clusters with many registered resources
   126  	discoveryQPS float32
   127  	// Allows all possible warnings are printed in a standardized
   128  	// format.
   129  	warningPrinter *printers.WarningPrinter
   130  }
   131  
   132  // ToRESTConfig implements RESTClientGetter.
   133  // Returns a REST client configuration based on a provided path
   134  // to a .kubeconfig file, loading rules, and config flag overrides.
   135  // Expects the AddFlags method to have been called. If WrapConfigFn
   136  // is non-nil this function can transform config before return.
   137  func (f *ConfigFlags) ToRESTConfig() (*rest.Config, error) {
   138  	c, err := f.ToRawKubeConfigLoader().ClientConfig()
   139  	if err != nil {
   140  		return nil, err
   141  	}
   142  	if f.WrapConfigFn != nil {
   143  		return f.WrapConfigFn(c), nil
   144  	}
   145  	return c, nil
   146  }
   147  
   148  // ToRawKubeConfigLoader binds config flag values to config overrides
   149  // Returns an interactive clientConfig if the password flag is enabled,
   150  // or a non-interactive clientConfig otherwise.
   151  func (f *ConfigFlags) ToRawKubeConfigLoader() clientcmd.ClientConfig {
   152  	if f.usePersistentConfig {
   153  		return f.toRawKubePersistentConfigLoader()
   154  	}
   155  	return f.toRawKubeConfigLoader()
   156  }
   157  
   158  func (f *ConfigFlags) toRawKubeConfigLoader() clientcmd.ClientConfig {
   159  	loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
   160  	// use the standard defaults for this client command
   161  	// DEPRECATED: remove and replace with something more accurate
   162  	loadingRules.DefaultClientConfig = &clientcmd.DefaultClientConfig
   163  
   164  	if f.KubeConfig != nil {
   165  		loadingRules.ExplicitPath = *f.KubeConfig
   166  	}
   167  
   168  	overrides := &clientcmd.ConfigOverrides{ClusterDefaults: clientcmd.ClusterDefaults}
   169  
   170  	// bind auth info flag values to overrides
   171  	if f.CertFile != nil {
   172  		overrides.AuthInfo.ClientCertificate = *f.CertFile
   173  	}
   174  	if f.KeyFile != nil {
   175  		overrides.AuthInfo.ClientKey = *f.KeyFile
   176  	}
   177  	if f.BearerToken != nil {
   178  		overrides.AuthInfo.Token = *f.BearerToken
   179  	}
   180  	if f.Impersonate != nil {
   181  		overrides.AuthInfo.Impersonate = *f.Impersonate
   182  	}
   183  	if f.ImpersonateUID != nil {
   184  		overrides.AuthInfo.ImpersonateUID = *f.ImpersonateUID
   185  	}
   186  	if f.ImpersonateGroup != nil {
   187  		overrides.AuthInfo.ImpersonateGroups = *f.ImpersonateGroup
   188  	}
   189  	if f.Username != nil {
   190  		overrides.AuthInfo.Username = *f.Username
   191  	}
   192  	if f.Password != nil {
   193  		overrides.AuthInfo.Password = *f.Password
   194  	}
   195  
   196  	// bind cluster flags
   197  	if f.APIServer != nil {
   198  		overrides.ClusterInfo.Server = *f.APIServer
   199  	}
   200  	if f.TLSServerName != nil {
   201  		overrides.ClusterInfo.TLSServerName = *f.TLSServerName
   202  	}
   203  	if f.CAFile != nil {
   204  		overrides.ClusterInfo.CertificateAuthority = *f.CAFile
   205  	}
   206  	if f.Insecure != nil {
   207  		overrides.ClusterInfo.InsecureSkipTLSVerify = *f.Insecure
   208  	}
   209  	if f.DisableCompression != nil {
   210  		overrides.ClusterInfo.DisableCompression = *f.DisableCompression
   211  	}
   212  
   213  	// bind context flags
   214  	if f.Context != nil {
   215  		overrides.CurrentContext = *f.Context
   216  	}
   217  	if f.ClusterName != nil {
   218  		overrides.Context.Cluster = *f.ClusterName
   219  	}
   220  	if f.AuthInfoName != nil {
   221  		overrides.Context.AuthInfo = *f.AuthInfoName
   222  	}
   223  	if f.Namespace != nil {
   224  		overrides.Context.Namespace = *f.Namespace
   225  	}
   226  
   227  	if f.Timeout != nil {
   228  		overrides.Timeout = *f.Timeout
   229  	}
   230  
   231  	// we only have an interactive prompt when a password is allowed
   232  	if f.Password == nil {
   233  		return &clientConfig{clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, overrides)}
   234  	}
   235  	return &clientConfig{clientcmd.NewInteractiveDeferredLoadingClientConfig(loadingRules, overrides, os.Stdin)}
   236  }
   237  
   238  // toRawKubePersistentConfigLoader binds config flag values to config overrides
   239  // Returns a persistent clientConfig for propagation.
   240  func (f *ConfigFlags) toRawKubePersistentConfigLoader() clientcmd.ClientConfig {
   241  	f.clientConfigLock.Lock()
   242  	defer f.clientConfigLock.Unlock()
   243  
   244  	if f.clientConfig == nil {
   245  		f.clientConfig = f.toRawKubeConfigLoader()
   246  	}
   247  
   248  	return f.clientConfig
   249  }
   250  
   251  // ToDiscoveryClient implements RESTClientGetter.
   252  // Expects the AddFlags method to have been called.
   253  // Returns a CachedDiscoveryInterface using a computed RESTConfig.
   254  func (f *ConfigFlags) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
   255  	if f.usePersistentConfig {
   256  		return f.toPersistentDiscoveryClient()
   257  	}
   258  	return f.toDiscoveryClient()
   259  }
   260  
   261  func (f *ConfigFlags) toPersistentDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
   262  	f.discoveryClientLock.Lock()
   263  	defer f.discoveryClientLock.Unlock()
   264  
   265  	if f.discoveryClient == nil {
   266  		discoveryClient, err := f.toDiscoveryClient()
   267  		if err != nil {
   268  			return nil, err
   269  		}
   270  		f.discoveryClient = discoveryClient
   271  	}
   272  	return f.discoveryClient, nil
   273  }
   274  
   275  func (f *ConfigFlags) toDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
   276  	config, err := f.ToRESTConfig()
   277  	if err != nil {
   278  		return nil, err
   279  	}
   280  
   281  	config.Burst = f.discoveryBurst
   282  	config.QPS = f.discoveryQPS
   283  
   284  	cacheDir := getDefaultCacheDir()
   285  
   286  	// retrieve a user-provided value for the "cache-dir"
   287  	// override httpCacheDir and discoveryCacheDir if user-value is given.
   288  	// user-provided value has higher precedence than default
   289  	// and KUBECACHEDIR environment variable.
   290  	if f.CacheDir != nil && *f.CacheDir != "" && *f.CacheDir != getDefaultCacheDir() {
   291  		cacheDir = *f.CacheDir
   292  	}
   293  
   294  	httpCacheDir := filepath.Join(cacheDir, "http")
   295  	discoveryCacheDir := computeDiscoverCacheDir(filepath.Join(cacheDir, "discovery"), config.Host)
   296  
   297  	return diskcached.NewCachedDiscoveryClientForConfig(config, discoveryCacheDir, httpCacheDir, time.Duration(6*time.Hour))
   298  }
   299  
   300  // getDefaultCacheDir returns default caching directory path.
   301  // it first looks at KUBECACHEDIR env var if it is set, otherwise
   302  // it returns standard kube cache dir.
   303  func getDefaultCacheDir() string {
   304  	if kcd := os.Getenv("KUBECACHEDIR"); kcd != "" {
   305  		return kcd
   306  	}
   307  
   308  	return filepath.Join(homedir.HomeDir(), ".kube", "cache")
   309  }
   310  
   311  // ToRESTMapper returns a mapper.
   312  func (f *ConfigFlags) ToRESTMapper() (meta.RESTMapper, error) {
   313  	if f.usePersistentConfig {
   314  		return f.toPersistentRESTMapper()
   315  	}
   316  	return f.toRESTMapper()
   317  }
   318  
   319  func (f *ConfigFlags) toPersistentRESTMapper() (meta.RESTMapper, error) {
   320  	f.restMapperLock.Lock()
   321  	defer f.restMapperLock.Unlock()
   322  
   323  	if f.restMapper == nil {
   324  		restMapper, err := f.toRESTMapper()
   325  		if err != nil {
   326  			return nil, err
   327  		}
   328  		f.restMapper = restMapper
   329  	}
   330  	return f.restMapper, nil
   331  }
   332  
   333  func (f *ConfigFlags) toRESTMapper() (meta.RESTMapper, error) {
   334  	discoveryClient, err := f.ToDiscoveryClient()
   335  	if err != nil {
   336  		return nil, err
   337  	}
   338  
   339  	mapper := restmapper.NewDeferredDiscoveryRESTMapper(discoveryClient)
   340  	expander := restmapper.NewShortcutExpander(mapper, discoveryClient, func(a string) {
   341  		if f.warningPrinter != nil {
   342  			f.warningPrinter.Print(a)
   343  		}
   344  	})
   345  	return expander, nil
   346  }
   347  
   348  // AddFlags binds client configuration flags to a given flagset
   349  func (f *ConfigFlags) AddFlags(flags *pflag.FlagSet) {
   350  	if f.KubeConfig != nil {
   351  		flags.StringVar(f.KubeConfig, "kubeconfig", *f.KubeConfig, "Path to the kubeconfig file to use for CLI requests.")
   352  	}
   353  	if f.CacheDir != nil {
   354  		flags.StringVar(f.CacheDir, flagCacheDir, *f.CacheDir, "Default cache directory")
   355  	}
   356  
   357  	// add config options
   358  	if f.CertFile != nil {
   359  		flags.StringVar(f.CertFile, flagCertFile, *f.CertFile, "Path to a client certificate file for TLS")
   360  	}
   361  	if f.KeyFile != nil {
   362  		flags.StringVar(f.KeyFile, flagKeyFile, *f.KeyFile, "Path to a client key file for TLS")
   363  	}
   364  	if f.BearerToken != nil {
   365  		flags.StringVar(f.BearerToken, flagBearerToken, *f.BearerToken, "Bearer token for authentication to the API server")
   366  	}
   367  	if f.Impersonate != nil {
   368  		flags.StringVar(f.Impersonate, flagImpersonate, *f.Impersonate, "Username to impersonate for the operation. User could be a regular user or a service account in a namespace.")
   369  	}
   370  	if f.ImpersonateUID != nil {
   371  		flags.StringVar(f.ImpersonateUID, flagImpersonateUID, *f.ImpersonateUID, "UID to impersonate for the operation.")
   372  	}
   373  	if f.ImpersonateGroup != nil {
   374  		flags.StringArrayVar(f.ImpersonateGroup, flagImpersonateGroup, *f.ImpersonateGroup, "Group to impersonate for the operation, this flag can be repeated to specify multiple groups.")
   375  	}
   376  	if f.Username != nil {
   377  		flags.StringVar(f.Username, flagUsername, *f.Username, "Username for basic authentication to the API server")
   378  	}
   379  	if f.Password != nil {
   380  		flags.StringVar(f.Password, flagPassword, *f.Password, "Password for basic authentication to the API server")
   381  	}
   382  	if f.ClusterName != nil {
   383  		flags.StringVar(f.ClusterName, flagClusterName, *f.ClusterName, "The name of the kubeconfig cluster to use")
   384  	}
   385  	if f.AuthInfoName != nil {
   386  		flags.StringVar(f.AuthInfoName, flagAuthInfoName, *f.AuthInfoName, "The name of the kubeconfig user to use")
   387  	}
   388  	if f.Namespace != nil {
   389  		flags.StringVarP(f.Namespace, flagNamespace, "n", *f.Namespace, "If present, the namespace scope for this CLI request")
   390  	}
   391  	if f.Context != nil {
   392  		flags.StringVar(f.Context, flagContext, *f.Context, "The name of the kubeconfig context to use")
   393  	}
   394  
   395  	if f.APIServer != nil {
   396  		flags.StringVarP(f.APIServer, flagAPIServer, "s", *f.APIServer, "The address and port of the Kubernetes API server")
   397  	}
   398  	if f.TLSServerName != nil {
   399  		flags.StringVar(f.TLSServerName, flagTLSServerName, *f.TLSServerName, "Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used")
   400  	}
   401  	if f.Insecure != nil {
   402  		flags.BoolVar(f.Insecure, flagInsecure, *f.Insecure, "If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure")
   403  	}
   404  	if f.CAFile != nil {
   405  		flags.StringVar(f.CAFile, flagCAFile, *f.CAFile, "Path to a cert file for the certificate authority")
   406  	}
   407  	if f.Timeout != nil {
   408  		flags.StringVar(f.Timeout, flagTimeout, *f.Timeout, "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.")
   409  	}
   410  	if f.DisableCompression != nil {
   411  		flags.BoolVar(f.DisableCompression, flagDisableCompression, *f.DisableCompression, "If true, opt-out of response compression for all requests to the server")
   412  	}
   413  }
   414  
   415  // WithDeprecatedPasswordFlag enables the username and password config flags
   416  func (f *ConfigFlags) WithDeprecatedPasswordFlag() *ConfigFlags {
   417  	f.Username = utilpointer.String("")
   418  	f.Password = utilpointer.String("")
   419  	return f
   420  }
   421  
   422  // WithDiscoveryBurst sets the RESTClient burst for discovery.
   423  func (f *ConfigFlags) WithDiscoveryBurst(discoveryBurst int) *ConfigFlags {
   424  	f.discoveryBurst = discoveryBurst
   425  	return f
   426  }
   427  
   428  // WithDiscoveryQPS sets the RESTClient QPS for discovery.
   429  func (f *ConfigFlags) WithDiscoveryQPS(discoveryQPS float32) *ConfigFlags {
   430  	f.discoveryQPS = discoveryQPS
   431  	return f
   432  }
   433  
   434  // WithWrapConfigFn allows providing a wrapper function for the client Config.
   435  func (f *ConfigFlags) WithWrapConfigFn(wrapConfigFn func(*rest.Config) *rest.Config) *ConfigFlags {
   436  	f.WrapConfigFn = wrapConfigFn
   437  	return f
   438  }
   439  
   440  // WithWarningPrinter initializes WarningPrinter with the given IOStreams
   441  func (f *ConfigFlags) WithWarningPrinter(ioStreams genericiooptions.IOStreams) *ConfigFlags {
   442  	f.warningPrinter = printers.NewWarningPrinter(ioStreams.ErrOut, printers.WarningPrinterOptions{Color: printers.AllowsColorOutput(ioStreams.ErrOut)})
   443  	return f
   444  }
   445  
   446  // NewConfigFlags returns ConfigFlags with default values set
   447  func NewConfigFlags(usePersistentConfig bool) *ConfigFlags {
   448  	impersonateGroup := []string{}
   449  	insecure := false
   450  	disableCompression := false
   451  
   452  	return &ConfigFlags{
   453  		Insecure:   &insecure,
   454  		Timeout:    utilpointer.String("0"),
   455  		KubeConfig: utilpointer.String(""),
   456  
   457  		CacheDir:           utilpointer.String(getDefaultCacheDir()),
   458  		ClusterName:        utilpointer.String(""),
   459  		AuthInfoName:       utilpointer.String(""),
   460  		Context:            utilpointer.String(""),
   461  		Namespace:          utilpointer.String(""),
   462  		APIServer:          utilpointer.String(""),
   463  		TLSServerName:      utilpointer.String(""),
   464  		CertFile:           utilpointer.String(""),
   465  		KeyFile:            utilpointer.String(""),
   466  		CAFile:             utilpointer.String(""),
   467  		BearerToken:        utilpointer.String(""),
   468  		Impersonate:        utilpointer.String(""),
   469  		ImpersonateUID:     utilpointer.String(""),
   470  		ImpersonateGroup:   &impersonateGroup,
   471  		DisableCompression: &disableCompression,
   472  
   473  		usePersistentConfig: usePersistentConfig,
   474  		// The more groups you have, the more discovery requests you need to make.
   475  		// with a burst of 300, we will not be rate-limiting for most clusters but
   476  		// the safeguard will still be here. This config is only used for discovery.
   477  		discoveryBurst: 300,
   478  	}
   479  }
   480  
   481  // overlyCautiousIllegalFileCharacters matches characters that *might* not be supported.  Windows is really restrictive, so this is really restrictive
   482  var overlyCautiousIllegalFileCharacters = regexp.MustCompile(`[^(\w/.)]`)
   483  
   484  // computeDiscoverCacheDir takes the parentDir and the host and comes up with a "usually non-colliding" name.
   485  func computeDiscoverCacheDir(parentDir, host string) string {
   486  	// strip the optional scheme from host if its there:
   487  	schemelessHost := strings.Replace(strings.Replace(host, "https://", "", 1), "http://", "", 1)
   488  	// now do a simple collapse of non-AZ09 characters.  Collisions are possible but unlikely.  Even if we do collide the problem is short lived
   489  	safeHost := overlyCautiousIllegalFileCharacters.ReplaceAllString(schemelessHost, "_")
   490  	return filepath.Join(parentDir, safeHost)
   491  }
   492  

View as plain text