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