1/*
2Copyright 2014 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 clientcmd
18
19import (
20	"errors"
21	"os"
22	"path"
23	"path/filepath"
24	"reflect"
25	"sort"
26
27	"k8s.io/klog"
28
29	restclient "k8s.io/client-go/rest"
30	clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
31)
32
33// ConfigAccess is used by subcommands and methods in this package to load and modify the appropriate config files
34type ConfigAccess interface {
35	// GetLoadingPrecedence returns the slice of files that should be used for loading and inspecting the config
36	GetLoadingPrecedence() []string
37	// GetStartingConfig returns the config that subcommands should being operating against.  It may or may not be merged depending on loading rules
38	GetStartingConfig() (*clientcmdapi.Config, error)
39	// GetDefaultFilename returns the name of the file you should write into (create if necessary), if you're trying to create a new stanza as opposed to updating an existing one.
40	GetDefaultFilename() string
41	// IsExplicitFile indicates whether or not this command is interested in exactly one file.  This implementation only ever does that  via a flag, but implementations that handle local, global, and flags may have more
42	IsExplicitFile() bool
43	// GetExplicitFile returns the particular file this command is operating against.  This implementation only ever has one, but implementations that handle local, global, and flags may have more
44	GetExplicitFile() string
45}
46
47type PathOptions struct {
48	// GlobalFile is the full path to the file to load as the global (final) option
49	GlobalFile string
50	// EnvVar is the env var name that points to the list of kubeconfig files to load
51	EnvVar string
52	// ExplicitFileFlag is the name of the flag to use for prompting for the kubeconfig file
53	ExplicitFileFlag string
54
55	// GlobalFileSubpath is an optional value used for displaying help
56	GlobalFileSubpath string
57
58	LoadingRules *ClientConfigLoadingRules
59}
60
61func (o *PathOptions) GetEnvVarFiles() []string {
62	if len(o.EnvVar) == 0 {
63		return []string{}
64	}
65
66	envVarValue := os.Getenv(o.EnvVar)
67	if len(envVarValue) == 0 {
68		return []string{}
69	}
70
71	fileList := filepath.SplitList(envVarValue)
72	// prevent the same path load multiple times
73	return deduplicate(fileList)
74}
75
76func (o *PathOptions) GetLoadingPrecedence() []string {
77	if envVarFiles := o.GetEnvVarFiles(); len(envVarFiles) > 0 {
78		return envVarFiles
79	}
80
81	return []string{o.GlobalFile}
82}
83
84func (o *PathOptions) GetStartingConfig() (*clientcmdapi.Config, error) {
85	// don't mutate the original
86	loadingRules := *o.LoadingRules
87	loadingRules.Precedence = o.GetLoadingPrecedence()
88
89	clientConfig := NewNonInteractiveDeferredLoadingClientConfig(&loadingRules, &ConfigOverrides{})
90	rawConfig, err := clientConfig.RawConfig()
91	if os.IsNotExist(err) {
92		return clientcmdapi.NewConfig(), nil
93	}
94	if err != nil {
95		return nil, err
96	}
97
98	return &rawConfig, nil
99}
100
101func (o *PathOptions) GetDefaultFilename() string {
102	if o.IsExplicitFile() {
103		return o.GetExplicitFile()
104	}
105
106	if envVarFiles := o.GetEnvVarFiles(); len(envVarFiles) > 0 {
107		if len(envVarFiles) == 1 {
108			return envVarFiles[0]
109		}
110
111		// if any of the envvar files already exists, return it
112		for _, envVarFile := range envVarFiles {
113			if _, err := os.Stat(envVarFile); err == nil {
114				return envVarFile
115			}
116		}
117
118		// otherwise, return the last one in the list
119		return envVarFiles[len(envVarFiles)-1]
120	}
121
122	return o.GlobalFile
123}
124
125func (o *PathOptions) IsExplicitFile() bool {
126	if len(o.LoadingRules.ExplicitPath) > 0 {
127		return true
128	}
129
130	return false
131}
132
133func (o *PathOptions) GetExplicitFile() string {
134	return o.LoadingRules.ExplicitPath
135}
136
137func NewDefaultPathOptions() *PathOptions {
138	ret := &PathOptions{
139		GlobalFile:       RecommendedHomeFile,
140		EnvVar:           RecommendedConfigPathEnvVar,
141		ExplicitFileFlag: RecommendedConfigPathFlag,
142
143		GlobalFileSubpath: path.Join(RecommendedHomeDir, RecommendedFileName),
144
145		LoadingRules: NewDefaultClientConfigLoadingRules(),
146	}
147	ret.LoadingRules.DoNotResolvePaths = true
148
149	return ret
150}
151
152// ModifyConfig takes a Config object, iterates through Clusters, AuthInfos, and Contexts, uses the LocationOfOrigin if specified or
153// uses the default destination file to write the results into.  This results in multiple file reads, but it's very easy to follow.
154// Preferences and CurrentContext should always be set in the default destination file.  Since we can't distinguish between empty and missing values
155// (no nil strings), we're forced have separate handling for them.  In the kubeconfig cases, newConfig should have at most one difference,
156// that means that this code will only write into a single file.  If you want to relativizePaths, you must provide a fully qualified path in any
157// modified element.
158func ModifyConfig(configAccess ConfigAccess, newConfig clientcmdapi.Config, relativizePaths bool) error {
159	possibleSources := configAccess.GetLoadingPrecedence()
160	// sort the possible kubeconfig files so we always "lock" in the same order
161	// to avoid deadlock (note: this can fail w/ symlinks, but... come on).
162	sort.Strings(possibleSources)
163	for _, filename := range possibleSources {
164		if err := lockFile(filename); err != nil {
165			return err
166		}
167		defer unlockFile(filename)
168	}
169
170	startingConfig, err := configAccess.GetStartingConfig()
171	if err != nil {
172		return err
173	}
174
175	// We need to find all differences, locate their original files, read a partial config to modify only that stanza and write out the file.
176	// Special case the test for current context and preferences since those always write to the default file.
177	if reflect.DeepEqual(*startingConfig, newConfig) {
178		// nothing to do
179		return nil
180	}
181
182	if startingConfig.CurrentContext != newConfig.CurrentContext {
183		if err := writeCurrentContext(configAccess, newConfig.CurrentContext); err != nil {
184			return err
185		}
186	}
187
188	if !reflect.DeepEqual(startingConfig.Preferences, newConfig.Preferences) {
189		if err := writePreferences(configAccess, newConfig.Preferences); err != nil {
190			return err
191		}
192	}
193
194	// Search every cluster, authInfo, and context.  First from new to old for differences, then from old to new for deletions
195	for key, cluster := range newConfig.Clusters {
196		startingCluster, exists := startingConfig.Clusters[key]
197		if !reflect.DeepEqual(cluster, startingCluster) || !exists {
198			destinationFile := cluster.LocationOfOrigin
199			if len(destinationFile) == 0 {
200				destinationFile = configAccess.GetDefaultFilename()
201			}
202
203			configToWrite, err := getConfigFromFile(destinationFile)
204			if err != nil {
205				return err
206			}
207			t := *cluster
208
209			configToWrite.Clusters[key] = &t
210			configToWrite.Clusters[key].LocationOfOrigin = destinationFile
211			if relativizePaths {
212				if err := RelativizeClusterLocalPaths(configToWrite.Clusters[key]); err != nil {
213					return err
214				}
215			}
216
217			if err := WriteToFile(*configToWrite, destinationFile); err != nil {
218				return err
219			}
220		}
221	}
222
223	// seenConfigs stores a map of config source filenames to computed config objects
224	seenConfigs := map[string]*clientcmdapi.Config{}
225
226	for key, context := range newConfig.Contexts {
227		startingContext, exists := startingConfig.Contexts[key]
228		if !reflect.DeepEqual(context, startingContext) || !exists {
229			destinationFile := context.LocationOfOrigin
230			if len(destinationFile) == 0 {
231				destinationFile = configAccess.GetDefaultFilename()
232			}
233
234			// we only obtain a fresh config object from its source file
235			// if we have not seen it already - this prevents us from
236			// reading and writing to the same number of files repeatedly
237			// when multiple / all contexts share the same destination file.
238			configToWrite, seen := seenConfigs[destinationFile]
239			if !seen {
240				var err error
241				configToWrite, err = getConfigFromFile(destinationFile)
242				if err != nil {
243					return err
244				}
245				seenConfigs[destinationFile] = configToWrite
246			}
247
248			configToWrite.Contexts[key] = context
249		}
250	}
251
252	// actually persist config object changes
253	for destinationFile, configToWrite := range seenConfigs {
254		if err := WriteToFile(*configToWrite, destinationFile); err != nil {
255			return err
256		}
257	}
258
259	for key, authInfo := range newConfig.AuthInfos {
260		startingAuthInfo, exists := startingConfig.AuthInfos[key]
261		if !reflect.DeepEqual(authInfo, startingAuthInfo) || !exists {
262			destinationFile := authInfo.LocationOfOrigin
263			if len(destinationFile) == 0 {
264				destinationFile = configAccess.GetDefaultFilename()
265			}
266
267			configToWrite, err := getConfigFromFile(destinationFile)
268			if err != nil {
269				return err
270			}
271			t := *authInfo
272			configToWrite.AuthInfos[key] = &t
273			configToWrite.AuthInfos[key].LocationOfOrigin = destinationFile
274			if relativizePaths {
275				if err := RelativizeAuthInfoLocalPaths(configToWrite.AuthInfos[key]); err != nil {
276					return err
277				}
278			}
279
280			if err := WriteToFile(*configToWrite, destinationFile); err != nil {
281				return err
282			}
283		}
284	}
285
286	for key, cluster := range startingConfig.Clusters {
287		if _, exists := newConfig.Clusters[key]; !exists {
288			destinationFile := cluster.LocationOfOrigin
289			if len(destinationFile) == 0 {
290				destinationFile = configAccess.GetDefaultFilename()
291			}
292
293			configToWrite, err := getConfigFromFile(destinationFile)
294			if err != nil {
295				return err
296			}
297			delete(configToWrite.Clusters, key)
298
299			if err := WriteToFile(*configToWrite, destinationFile); err != nil {
300				return err
301			}
302		}
303	}
304
305	for key, context := range startingConfig.Contexts {
306		if _, exists := newConfig.Contexts[key]; !exists {
307			destinationFile := context.LocationOfOrigin
308			if len(destinationFile) == 0 {
309				destinationFile = configAccess.GetDefaultFilename()
310			}
311
312			configToWrite, err := getConfigFromFile(destinationFile)
313			if err != nil {
314				return err
315			}
316			delete(configToWrite.Contexts, key)
317
318			if err := WriteToFile(*configToWrite, destinationFile); err != nil {
319				return err
320			}
321		}
322	}
323
324	for key, authInfo := range startingConfig.AuthInfos {
325		if _, exists := newConfig.AuthInfos[key]; !exists {
326			destinationFile := authInfo.LocationOfOrigin
327			if len(destinationFile) == 0 {
328				destinationFile = configAccess.GetDefaultFilename()
329			}
330
331			configToWrite, err := getConfigFromFile(destinationFile)
332			if err != nil {
333				return err
334			}
335			delete(configToWrite.AuthInfos, key)
336
337			if err := WriteToFile(*configToWrite, destinationFile); err != nil {
338				return err
339			}
340		}
341	}
342
343	return nil
344}
345
346func PersisterForUser(configAccess ConfigAccess, user string) restclient.AuthProviderConfigPersister {
347	return &persister{configAccess, user}
348}
349
350type persister struct {
351	configAccess ConfigAccess
352	user         string
353}
354
355func (p *persister) Persist(config map[string]string) error {
356	newConfig, err := p.configAccess.GetStartingConfig()
357	if err != nil {
358		return err
359	}
360	authInfo, ok := newConfig.AuthInfos[p.user]
361	if ok && authInfo.AuthProvider != nil {
362		authInfo.AuthProvider.Config = config
363		ModifyConfig(p.configAccess, *newConfig, false)
364	}
365	return nil
366}
367
368// writeCurrentContext takes three possible paths.
369// If newCurrentContext is the same as the startingConfig's current context, then we exit.
370// If newCurrentContext has a value, then that value is written into the default destination file.
371// If newCurrentContext is empty, then we find the config file that is setting the CurrentContext and clear the value from that file
372func writeCurrentContext(configAccess ConfigAccess, newCurrentContext string) error {
373	if startingConfig, err := configAccess.GetStartingConfig(); err != nil {
374		return err
375	} else if startingConfig.CurrentContext == newCurrentContext {
376		return nil
377	}
378
379	if configAccess.IsExplicitFile() {
380		file := configAccess.GetExplicitFile()
381		currConfig, err := getConfigFromFile(file)
382		if err != nil {
383			return err
384		}
385		currConfig.CurrentContext = newCurrentContext
386		if err := WriteToFile(*currConfig, file); err != nil {
387			return err
388		}
389
390		return nil
391	}
392
393	if len(newCurrentContext) > 0 {
394		destinationFile := configAccess.GetDefaultFilename()
395		config, err := getConfigFromFile(destinationFile)
396		if err != nil {
397			return err
398		}
399		config.CurrentContext = newCurrentContext
400
401		if err := WriteToFile(*config, destinationFile); err != nil {
402			return err
403		}
404
405		return nil
406	}
407
408	// we're supposed to be clearing the current context.  We need to find the first spot in the chain that is setting it and clear it
409	for _, file := range configAccess.GetLoadingPrecedence() {
410		if _, err := os.Stat(file); err == nil {
411			currConfig, err := getConfigFromFile(file)
412			if err != nil {
413				return err
414			}
415
416			if len(currConfig.CurrentContext) > 0 {
417				currConfig.CurrentContext = newCurrentContext
418				if err := WriteToFile(*currConfig, file); err != nil {
419					return err
420				}
421
422				return nil
423			}
424		}
425	}
426
427	return errors.New("no config found to write context")
428}
429
430func writePreferences(configAccess ConfigAccess, newPrefs clientcmdapi.Preferences) error {
431	if startingConfig, err := configAccess.GetStartingConfig(); err != nil {
432		return err
433	} else if reflect.DeepEqual(startingConfig.Preferences, newPrefs) {
434		return nil
435	}
436
437	if configAccess.IsExplicitFile() {
438		file := configAccess.GetExplicitFile()
439		currConfig, err := getConfigFromFile(file)
440		if err != nil {
441			return err
442		}
443		currConfig.Preferences = newPrefs
444		if err := WriteToFile(*currConfig, file); err != nil {
445			return err
446		}
447
448		return nil
449	}
450
451	for _, file := range configAccess.GetLoadingPrecedence() {
452		currConfig, err := getConfigFromFile(file)
453		if err != nil {
454			return err
455		}
456
457		if !reflect.DeepEqual(currConfig.Preferences, newPrefs) {
458			currConfig.Preferences = newPrefs
459			if err := WriteToFile(*currConfig, file); err != nil {
460				return err
461			}
462
463			return nil
464		}
465	}
466
467	return errors.New("no config found to write preferences")
468}
469
470// getConfigFromFile tries to read a kubeconfig file and if it can't, returns an error.  One exception, missing files result in empty configs, not an error.
471func getConfigFromFile(filename string) (*clientcmdapi.Config, error) {
472	config, err := LoadFromFile(filename)
473	if err != nil && !os.IsNotExist(err) {
474		return nil, err
475	}
476	if config == nil {
477		config = clientcmdapi.NewConfig()
478	}
479	return config, nil
480}
481
482// GetConfigFromFileOrDie tries to read a kubeconfig file and if it can't, it calls exit.  One exception, missing files result in empty configs, not an exit
483func GetConfigFromFileOrDie(filename string) *clientcmdapi.Config {
484	config, err := getConfigFromFile(filename)
485	if err != nil {
486		klog.FatalDepth(1, err)
487	}
488
489	return config
490}
491