1/* 2Copyright 2018 The Kubernetes Authors. 3 4Licensed under the Apache License, Version 2.0 (the "License"); 5you may not use this file except in compliance with the License. 6You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10Unless required by applicable law or agreed to in writing, software 11distributed under the License is distributed on an "AS IS" BASIS, 12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13See the License for the specific language governing permissions and 14limitations under the License. 15*/ 16 17package genericclioptions 18 19import ( 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/client-go/discovery" 31 diskcached "k8s.io/client-go/discovery/cached/disk" 32 "k8s.io/client-go/rest" 33 "k8s.io/client-go/restmapper" 34 "k8s.io/client-go/tools/clientcmd" 35 "k8s.io/client-go/util/homedir" 36) 37 38const ( 39 flagClusterName = "cluster" 40 flagAuthInfoName = "user" 41 flagContext = "context" 42 flagNamespace = "namespace" 43 flagAPIServer = "server" 44 flagTLSServerName = "tls-server-name" 45 flagInsecure = "insecure-skip-tls-verify" 46 flagCertFile = "client-certificate" 47 flagKeyFile = "client-key" 48 flagCAFile = "certificate-authority" 49 flagBearerToken = "token" 50 flagImpersonate = "as" 51 flagImpersonateGroup = "as-group" 52 flagUsername = "username" 53 flagPassword = "password" 54 flagTimeout = "request-timeout" 55 flagCacheDir = "cache-dir" 56) 57 58var ( 59 defaultCacheDir = filepath.Join(homedir.HomeDir(), ".kube", "cache") 60) 61 62// RESTClientGetter is an interface that the ConfigFlags describe to provide an easier way to mock for commands 63// and eliminate the direct coupling to a struct type. Users may wish to duplicate this type in their own packages 64// as per the golang type overlapping. 65type RESTClientGetter interface { 66 // ToRESTConfig returns restconfig 67 ToRESTConfig() (*rest.Config, error) 68 // ToDiscoveryClient returns discovery client 69 ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) 70 // ToRESTMapper returns a restmapper 71 ToRESTMapper() (meta.RESTMapper, error) 72 // ToRawKubeConfigLoader return kubeconfig loader as-is 73 ToRawKubeConfigLoader() clientcmd.ClientConfig 74} 75 76var _ RESTClientGetter = &ConfigFlags{} 77 78// ConfigFlags composes the set of values necessary 79// for obtaining a REST client config 80type ConfigFlags struct { 81 CacheDir *string 82 KubeConfig *string 83 84 // config flags 85 ClusterName *string 86 AuthInfoName *string 87 Context *string 88 Namespace *string 89 APIServer *string 90 TLSServerName *string 91 Insecure *bool 92 CertFile *string 93 KeyFile *string 94 CAFile *string 95 BearerToken *string 96 Impersonate *string 97 ImpersonateGroup *[]string 98 Username *string 99 Password *string 100 Timeout *string 101 102 clientConfig clientcmd.ClientConfig 103 lock sync.Mutex 104 // If set to true, will use persistent client config and 105 // propagate the config to the places that need it, rather than 106 // loading the config multiple times 107 usePersistentConfig bool 108} 109 110// ToRESTConfig implements RESTClientGetter. 111// Returns a REST client configuration based on a provided path 112// to a .kubeconfig file, loading rules, and config flag overrides. 113// Expects the AddFlags method to have been called. 114func (f *ConfigFlags) ToRESTConfig() (*rest.Config, error) { 115 return f.ToRawKubeConfigLoader().ClientConfig() 116} 117 118// ToRawKubeConfigLoader binds config flag values to config overrides 119// Returns an interactive clientConfig if the password flag is enabled, 120// or a non-interactive clientConfig otherwise. 121func (f *ConfigFlags) ToRawKubeConfigLoader() clientcmd.ClientConfig { 122 if f.usePersistentConfig { 123 return f.toRawKubePersistentConfigLoader() 124 } 125 return f.toRawKubeConfigLoader() 126} 127 128func (f *ConfigFlags) toRawKubeConfigLoader() clientcmd.ClientConfig { 129 loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() 130 // use the standard defaults for this client command 131 // DEPRECATED: remove and replace with something more accurate 132 loadingRules.DefaultClientConfig = &clientcmd.DefaultClientConfig 133 134 if f.KubeConfig != nil { 135 loadingRules.ExplicitPath = *f.KubeConfig 136 } 137 138 overrides := &clientcmd.ConfigOverrides{ClusterDefaults: clientcmd.ClusterDefaults} 139 140 // bind auth info flag values to overrides 141 if f.CertFile != nil { 142 overrides.AuthInfo.ClientCertificate = *f.CertFile 143 } 144 if f.KeyFile != nil { 145 overrides.AuthInfo.ClientKey = *f.KeyFile 146 } 147 if f.BearerToken != nil { 148 overrides.AuthInfo.Token = *f.BearerToken 149 } 150 if f.Impersonate != nil { 151 overrides.AuthInfo.Impersonate = *f.Impersonate 152 } 153 if f.ImpersonateGroup != nil { 154 overrides.AuthInfo.ImpersonateGroups = *f.ImpersonateGroup 155 } 156 if f.Username != nil { 157 overrides.AuthInfo.Username = *f.Username 158 } 159 if f.Password != nil { 160 overrides.AuthInfo.Password = *f.Password 161 } 162 163 // bind cluster flags 164 if f.APIServer != nil { 165 overrides.ClusterInfo.Server = *f.APIServer 166 } 167 if f.TLSServerName != nil { 168 overrides.ClusterInfo.TLSServerName = *f.TLSServerName 169 } 170 if f.CAFile != nil { 171 overrides.ClusterInfo.CertificateAuthority = *f.CAFile 172 } 173 if f.Insecure != nil { 174 overrides.ClusterInfo.InsecureSkipTLSVerify = *f.Insecure 175 } 176 177 // bind context flags 178 if f.Context != nil { 179 overrides.CurrentContext = *f.Context 180 } 181 if f.ClusterName != nil { 182 overrides.Context.Cluster = *f.ClusterName 183 } 184 if f.AuthInfoName != nil { 185 overrides.Context.AuthInfo = *f.AuthInfoName 186 } 187 if f.Namespace != nil { 188 overrides.Context.Namespace = *f.Namespace 189 } 190 191 if f.Timeout != nil { 192 overrides.Timeout = *f.Timeout 193 } 194 195 // we only have an interactive prompt when a password is allowed 196 if f.Password == nil { 197 return &clientConfig{clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, overrides)} 198 } 199 return &clientConfig{clientcmd.NewInteractiveDeferredLoadingClientConfig(loadingRules, overrides, os.Stdin)} 200} 201 202// toRawKubePersistentConfigLoader binds config flag values to config overrides 203// Returns a persistent clientConfig for propagation. 204func (f *ConfigFlags) toRawKubePersistentConfigLoader() clientcmd.ClientConfig { 205 f.lock.Lock() 206 defer f.lock.Unlock() 207 208 if f.clientConfig == nil { 209 f.clientConfig = f.toRawKubeConfigLoader() 210 } 211 212 return f.clientConfig 213} 214 215// ToDiscoveryClient implements RESTClientGetter. 216// Expects the AddFlags method to have been called. 217// Returns a CachedDiscoveryInterface using a computed RESTConfig. 218func (f *ConfigFlags) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) { 219 config, err := f.ToRESTConfig() 220 if err != nil { 221 return nil, err 222 } 223 224 // The more groups you have, the more discovery requests you need to make. 225 // given 25 groups (our groups + a few custom resources) with one-ish version each, discovery needs to make 50 requests 226 // double it just so we don't end up here again for a while. This config is only used for discovery. 227 config.Burst = 100 228 229 cacheDir := defaultCacheDir 230 231 // retrieve a user-provided value for the "cache-dir" 232 // override httpCacheDir and discoveryCacheDir if user-value is given. 233 if f.CacheDir != nil { 234 cacheDir = *f.CacheDir 235 } 236 httpCacheDir := filepath.Join(cacheDir, "http") 237 discoveryCacheDir := computeDiscoverCacheDir(filepath.Join(cacheDir, "discovery"), config.Host) 238 239 return diskcached.NewCachedDiscoveryClientForConfig(config, discoveryCacheDir, httpCacheDir, time.Duration(10*time.Minute)) 240} 241 242// ToRESTMapper returns a mapper. 243func (f *ConfigFlags) ToRESTMapper() (meta.RESTMapper, error) { 244 discoveryClient, err := f.ToDiscoveryClient() 245 if err != nil { 246 return nil, err 247 } 248 249 mapper := restmapper.NewDeferredDiscoveryRESTMapper(discoveryClient) 250 expander := restmapper.NewShortcutExpander(mapper, discoveryClient) 251 return expander, nil 252} 253 254// AddFlags binds client configuration flags to a given flagset 255func (f *ConfigFlags) AddFlags(flags *pflag.FlagSet) { 256 if f.KubeConfig != nil { 257 flags.StringVar(f.KubeConfig, "kubeconfig", *f.KubeConfig, "Path to the kubeconfig file to use for CLI requests.") 258 } 259 if f.CacheDir != nil { 260 flags.StringVar(f.CacheDir, flagCacheDir, *f.CacheDir, "Default cache directory") 261 } 262 263 // add config options 264 if f.CertFile != nil { 265 flags.StringVar(f.CertFile, flagCertFile, *f.CertFile, "Path to a client certificate file for TLS") 266 } 267 if f.KeyFile != nil { 268 flags.StringVar(f.KeyFile, flagKeyFile, *f.KeyFile, "Path to a client key file for TLS") 269 } 270 if f.BearerToken != nil { 271 flags.StringVar(f.BearerToken, flagBearerToken, *f.BearerToken, "Bearer token for authentication to the API server") 272 } 273 if f.Impersonate != nil { 274 flags.StringVar(f.Impersonate, flagImpersonate, *f.Impersonate, "Username to impersonate for the operation") 275 } 276 if f.ImpersonateGroup != nil { 277 flags.StringArrayVar(f.ImpersonateGroup, flagImpersonateGroup, *f.ImpersonateGroup, "Group to impersonate for the operation, this flag can be repeated to specify multiple groups.") 278 } 279 if f.Username != nil { 280 flags.StringVar(f.Username, flagUsername, *f.Username, "Username for basic authentication to the API server") 281 } 282 if f.Password != nil { 283 flags.StringVar(f.Password, flagPassword, *f.Password, "Password for basic authentication to the API server") 284 } 285 if f.ClusterName != nil { 286 flags.StringVar(f.ClusterName, flagClusterName, *f.ClusterName, "The name of the kubeconfig cluster to use") 287 } 288 if f.AuthInfoName != nil { 289 flags.StringVar(f.AuthInfoName, flagAuthInfoName, *f.AuthInfoName, "The name of the kubeconfig user to use") 290 } 291 if f.Namespace != nil { 292 flags.StringVarP(f.Namespace, flagNamespace, "n", *f.Namespace, "If present, the namespace scope for this CLI request") 293 } 294 if f.Context != nil { 295 flags.StringVar(f.Context, flagContext, *f.Context, "The name of the kubeconfig context to use") 296 } 297 298 if f.APIServer != nil { 299 flags.StringVarP(f.APIServer, flagAPIServer, "s", *f.APIServer, "The address and port of the Kubernetes API server") 300 } 301 if f.TLSServerName != nil { 302 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") 303 } 304 if f.Insecure != nil { 305 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") 306 } 307 if f.CAFile != nil { 308 flags.StringVar(f.CAFile, flagCAFile, *f.CAFile, "Path to a cert file for the certificate authority") 309 } 310 if f.Timeout != nil { 311 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.") 312 } 313 314} 315 316// WithDeprecatedPasswordFlag enables the username and password config flags 317func (f *ConfigFlags) WithDeprecatedPasswordFlag() *ConfigFlags { 318 f.Username = stringptr("") 319 f.Password = stringptr("") 320 return f 321} 322 323// NewConfigFlags returns ConfigFlags with default values set 324func NewConfigFlags(usePersistentConfig bool) *ConfigFlags { 325 impersonateGroup := []string{} 326 insecure := false 327 328 return &ConfigFlags{ 329 Insecure: &insecure, 330 Timeout: stringptr("0"), 331 KubeConfig: stringptr(""), 332 333 CacheDir: stringptr(defaultCacheDir), 334 ClusterName: stringptr(""), 335 AuthInfoName: stringptr(""), 336 Context: stringptr(""), 337 Namespace: stringptr(""), 338 APIServer: stringptr(""), 339 TLSServerName: stringptr(""), 340 CertFile: stringptr(""), 341 KeyFile: stringptr(""), 342 CAFile: stringptr(""), 343 BearerToken: stringptr(""), 344 Impersonate: stringptr(""), 345 ImpersonateGroup: &impersonateGroup, 346 347 usePersistentConfig: usePersistentConfig, 348 } 349} 350 351func stringptr(val string) *string { 352 return &val 353} 354 355// overlyCautiousIllegalFileCharacters matches characters that *might* not be supported. Windows is really restrictive, so this is really restrictive 356var overlyCautiousIllegalFileCharacters = regexp.MustCompile(`[^(\w/\.)]`) 357 358// computeDiscoverCacheDir takes the parentDir and the host and comes up with a "usually non-colliding" name. 359func computeDiscoverCacheDir(parentDir, host string) string { 360 // strip the optional scheme from host if its there: 361 schemelessHost := strings.Replace(strings.Replace(host, "https://", "", 1), "http://", "", 1) 362 // now do a simple collapse of non-AZ09 characters. Collisions are possible but unlikely. Even if we do collide the problem is short lived 363 safeHost := overlyCautiousIllegalFileCharacters.ReplaceAllString(schemelessHost, "_") 364 return filepath.Join(parentDir, safeHost) 365} 366