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