blob: b8cc39688219f60a4a19f7b55b6ddeb22255c793 [file] [log] [blame]
Zack Williamse940c7a2019-08-21 14:25:39 -07001/*
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}