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