/* Copyright 2018 The Kubernetes Authors. Copy From: https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/kubectl/pkg/cmd/get/get_flags.go Change: ConfigFlags struct add CaBundle fields, toRawKubeConfigLoader method modify new loadRules and overrides. */ package karmadactl import ( "os" "path/filepath" "regexp" "strings" "sync" "time" "github.com/spf13/pflag" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/client-go/discovery" diskcached "k8s.io/client-go/discovery/cached/disk" "k8s.io/client-go/rest" "k8s.io/client-go/restmapper" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" "k8s.io/client-go/util/homedir" ) const ( flagClusterName = "cluster" flagAuthInfoName = "user" flagContext = "context" flagNamespace = "namespace" flagAPIServer = "server" flagTLSServerName = "tls-server-name" flagInsecure = "insecure-skip-tls-verify" flagCertFile = "client-certificate" flagKeyFile = "client-key" flagCAFile = "certificate-authority" flagBearerToken = "token" flagTimeout = "request-timeout" flagCacheDir = "cache-dir" ) var ( defaultCacheDir = filepath.Join(homedir.HomeDir(), ".kube", "cache") // ErrEmptyConfig is the error message to be displayed if the configuration info is missing or incomplete ErrEmptyConfig = clientcmd.NewEmptyConfigError( `Missing or incomplete configuration info. Please point to an existing, complete config file: 1. Via the command-line flag --kubeconfig 2. Via the KUBECONFIG environment variable 3. In your home directory as ~/.kube/config To view or setup config directly use the 'config' command.`) ) // RESTClientGetter is an interface that the ConfigFlags describe to provide an easier way to mock for commands // and eliminate the direct coupling to a struct type. Users may wish to duplicate this type in their own packages // as per the golang type overlapping. type RESTClientGetter interface { // ToRESTConfig returns restconfig ToRESTConfig() (*rest.Config, error) // ToDiscoveryClient returns discovery client ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) // ToRESTMapper returns a restmapper ToRESTMapper() (meta.RESTMapper, error) // ToRawKubeConfigLoader return kubeconfig loader as-is ToRawKubeConfigLoader() clientcmd.ClientConfig } type clientConfig struct { defaultClientConfig clientcmd.ClientConfig } func (c *clientConfig) RawConfig() (clientcmdapi.Config, error) { config, err := c.defaultClientConfig.RawConfig() // replace client-go's ErrEmptyConfig error with our custom, more verbose version if clientcmd.IsEmptyConfig(err) { return config, ErrEmptyConfig } return config, err } func (c *clientConfig) ClientConfig() (*rest.Config, error) { config, err := c.defaultClientConfig.ClientConfig() // replace client-go's ErrEmptyConfig error with our custom, more verbose version if clientcmd.IsEmptyConfig(err) { return config, ErrEmptyConfig } return config, err } func (c *clientConfig) Namespace() (string, bool, error) { namespace, ok, err := c.defaultClientConfig.Namespace() // replace client-go's ErrEmptyConfig error with our custom, more verbose version if clientcmd.IsEmptyConfig(err) { return namespace, ok, ErrEmptyConfig } return namespace, ok, err } func (c *clientConfig) ConfigAccess() clientcmd.ConfigAccess { return c.defaultClientConfig.ConfigAccess() } var _ RESTClientGetter = &ConfigFlags{} // ConfigFlags composes the set of values necessary // for obtaining a REST client config type ConfigFlags struct { CaBundle *string CacheDir *string KubeConfig *string // config flags ClusterName *string AuthInfoName *string Context *string Namespace *string APIServer *string TLSServerName *string Insecure *bool CertFile *string KeyFile *string CAFile *string BearerToken *string Impersonate *string ImpersonateGroup *[]string Username *string Password *string Timeout *string // If non-nil, wrap config function can transform the Config // before it is returned in ToRESTConfig function. WrapConfigFn func(*rest.Config) *rest.Config clientConfig clientcmd.ClientConfig lock sync.Mutex // If set to true, will use persistent client config and // propagate the config to the places that need it, rather than // loading the config multiple times usePersistentConfig bool // Allows increasing burst used for discovery, this is useful // in clusters with many registered resources discoveryBurst int } // ToRESTConfig implements RESTClientGetter. // Returns a REST client configuration based on a provided path // to a .kubeconfig file, loading rules, and config flag overrides. // Expects the AddFlags method to have been called. If WrapConfigFn // is non-nil this function can transform config before return. func (f *ConfigFlags) ToRESTConfig() (*rest.Config, error) { c, err := f.ToRawKubeConfigLoader().ClientConfig() if err != nil { return nil, err } if f.WrapConfigFn != nil { return f.WrapConfigFn(c), nil } return c, nil } // ToRawKubeConfigLoader binds config flag values to config overrides // Returns an interactive clientConfig if the password flag is enabled, // or a non-interactive clientConfig otherwise. func (f *ConfigFlags) ToRawKubeConfigLoader() clientcmd.ClientConfig { if f.usePersistentConfig { return f.toRawKubePersistentConfigLoader() } return f.toRawKubeConfigLoader() } func (f *ConfigFlags) toRawKubeConfigLoader() clientcmd.ClientConfig { //loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() loadingRules := &clientcmd.ClientConfigLoadingRules{} // use the standard defaults for this client command // DEPRECATED: remove and replace with something more accurate loadingRules.DefaultClientConfig = &clientcmd.DefaultClientConfig if f.KubeConfig != nil { loadingRules.ExplicitPath = *f.KubeConfig } clusterOverrides := clientcmd.ClusterDefaults clusterOverrides.CertificateAuthorityData = []byte(*f.CaBundle) overrides := &clientcmd.ConfigOverrides{ClusterDefaults: clusterOverrides} // bind auth info flag values to overrides if f.CertFile != nil { overrides.AuthInfo.ClientCertificate = *f.CertFile } if f.KeyFile != nil { overrides.AuthInfo.ClientKey = *f.KeyFile } if f.BearerToken != nil { overrides.AuthInfo.Token = *f.BearerToken } // bind cluster flags if f.APIServer != nil { overrides.ClusterInfo.Server = *f.APIServer } if f.TLSServerName != nil { overrides.ClusterInfo.TLSServerName = *f.TLSServerName } if f.CAFile != nil { overrides.ClusterInfo.CertificateAuthority = *f.CAFile } if f.Insecure != nil { overrides.ClusterInfo.InsecureSkipTLSVerify = *f.Insecure } // bind context flags if f.Context != nil { overrides.CurrentContext = *f.Context } if f.ClusterName != nil { overrides.Context.Cluster = *f.ClusterName } if f.AuthInfoName != nil { overrides.Context.AuthInfo = *f.AuthInfoName } if f.Namespace != nil { overrides.Context.Namespace = *f.Namespace } if f.Timeout != nil { overrides.Timeout = *f.Timeout } // we only have an interactive prompt when a password is allowed if f.Password == nil { return &clientConfig{clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, overrides)} } return &clientConfig{clientcmd.NewInteractiveDeferredLoadingClientConfig(loadingRules, overrides, os.Stdin)} } // toRawKubePersistentConfigLoader binds config flag values to config overrides // Returns a persistent clientConfig for propagation. func (f *ConfigFlags) toRawKubePersistentConfigLoader() clientcmd.ClientConfig { f.lock.Lock() defer f.lock.Unlock() if f.clientConfig == nil { f.clientConfig = f.toRawKubeConfigLoader() } return f.clientConfig } // ToDiscoveryClient implements RESTClientGetter. // Expects the AddFlags method to have been called. // Returns a CachedDiscoveryInterface using a computed RESTConfig. func (f *ConfigFlags) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) { config, err := f.ToRESTConfig() if err != nil { return nil, err } // The more groups you have, the more discovery requests you need to make. // given 25 groups (our groups + a few custom resources) with one-ish version each, discovery needs to make 50 requests // double it just so we don't end up here again for a while. This config is only used for discovery. config.Burst = f.discoveryBurst cacheDir := defaultCacheDir // retrieve a user-provided value for the "cache-dir" // override httpCacheDir and discoveryCacheDir if user-value is given. if f.CacheDir != nil { cacheDir = *f.CacheDir } httpCacheDir := filepath.Join(cacheDir, "http") discoveryCacheDir := computeDiscoverCacheDir(filepath.Join(cacheDir, "discovery"), config.Host) return diskcached.NewCachedDiscoveryClientForConfig(config, discoveryCacheDir, httpCacheDir, time.Duration(10*time.Minute)) } // ToRESTMapper returns a mapper. func (f *ConfigFlags) ToRESTMapper() (meta.RESTMapper, error) { discoveryClient, err := f.ToDiscoveryClient() if err != nil { return nil, err } mapper := restmapper.NewDeferredDiscoveryRESTMapper(discoveryClient) expander := restmapper.NewShortcutExpander(mapper, discoveryClient) return expander, nil } // AddFlags binds client configuration flags to a given flagset func (f *ConfigFlags) AddFlags(flags *pflag.FlagSet) { if f.KubeConfig != nil { flags.StringVar(f.KubeConfig, "kubeconfig", *f.KubeConfig, "Path to the kubeconfig file to use for CLI requests.") } if f.CacheDir != nil { flags.StringVar(f.CacheDir, flagCacheDir, *f.CacheDir, "Default cache directory") } // add config options if f.CertFile != nil { flags.StringVar(f.CertFile, flagCertFile, *f.CertFile, "Path to a client certificate file for TLS") } if f.KeyFile != nil { flags.StringVar(f.KeyFile, flagKeyFile, *f.KeyFile, "Path to a client key file for TLS") } if f.BearerToken != nil { flags.StringVar(f.BearerToken, flagBearerToken, *f.BearerToken, "Bearer token for authentication to the API server") } if f.ClusterName != nil { flags.StringVar(f.ClusterName, flagClusterName, *f.ClusterName, "The name of the kubeconfig cluster to use") } if f.AuthInfoName != nil { flags.StringVar(f.AuthInfoName, flagAuthInfoName, *f.AuthInfoName, "The name of the kubeconfig user to use") } if f.Namespace != nil { flags.StringVarP(f.Namespace, flagNamespace, "n", *f.Namespace, "If present, the namespace scope for this CLI request") } if f.Context != nil { flags.StringVar(f.Context, flagContext, *f.Context, "The name of the kubeconfig context to use") } if f.APIServer != nil { flags.StringVarP(f.APIServer, flagAPIServer, "s", *f.APIServer, "The address and port of the Kubernetes API server") } if f.TLSServerName != nil { 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") } if f.Insecure != nil { 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") } if f.CAFile != nil { flags.StringVar(f.CAFile, flagCAFile, *f.CAFile, "Path to a cert file for the certificate authority") } if f.Timeout != nil { 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.") } } // WithDeprecatedPasswordFlag enables the username and password config flags func (f *ConfigFlags) WithDeprecatedPasswordFlag() *ConfigFlags { f.Username = stringptr("") f.Password = stringptr("") return f } // WithDiscoveryBurst sets the RESTClient burst for discovery. func (f *ConfigFlags) WithDiscoveryBurst(discoveryBurst int) *ConfigFlags { f.discoveryBurst = discoveryBurst return f } // NewConfigFlags returns ConfigFlags with default values set func NewConfigFlags(usePersistentConfig bool) *ConfigFlags { impersonateGroup := []string{} insecure := false return &ConfigFlags{ Insecure: &insecure, Timeout: stringptr("0"), KubeConfig: stringptr(""), CacheDir: stringptr(defaultCacheDir), ClusterName: stringptr(""), AuthInfoName: stringptr(""), Context: stringptr(""), Namespace: stringptr(""), APIServer: stringptr(""), TLSServerName: stringptr(""), CertFile: stringptr(""), KeyFile: stringptr(""), CAFile: stringptr(""), BearerToken: stringptr(""), Impersonate: stringptr(""), ImpersonateGroup: &impersonateGroup, usePersistentConfig: usePersistentConfig, // The more groups you have, the more discovery requests you need to make. // given 25 groups (our groups + a few custom resources) with one-ish version each, discovery needs to make 50 requests // double it just so we don't end up here again for a while. This config is only used for discovery. discoveryBurst: 100, } } func stringptr(val string) *string { return &val } // overlyCautiousIllegalFileCharacters matches characters that *might* not be supported. Windows is really restrictive, so this is really restrictive var overlyCautiousIllegalFileCharacters = regexp.MustCompile(`[^(\w/\.)]`) // computeDiscoverCacheDir takes the parentDir and the host and comes up with a "usually non-colliding" name. func computeDiscoverCacheDir(parentDir, host string) string { // strip the optional scheme from host if its there: schemelessHost := strings.Replace(strings.Replace(host, "https://", "", 1), "http://", "", 1) // now do a simple collapse of non-AZ09 characters. Collisions are possible but unlikely. Even if we do collide the problem is short lived safeHost := overlyCautiousIllegalFileCharacters.ReplaceAllString(schemelessHost, "_") return filepath.Join(parentDir, safeHost) }