blob: 7092c5b10eb0f4217e1f1804a850f50ab414d6a3 [file] [log] [blame]
Matthias Andreas Benkard832a54e2019-01-29 09:27:38 +01001/*
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 "github.com/golang/glog"
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 for key, context := range newConfig.Contexts {
224 startingContext, exists := startingConfig.Contexts[key]
225 if !reflect.DeepEqual(context, startingContext) || !exists {
226 destinationFile := context.LocationOfOrigin
227 if len(destinationFile) == 0 {
228 destinationFile = configAccess.GetDefaultFilename()
229 }
230
231 configToWrite, err := getConfigFromFile(destinationFile)
232 if err != nil {
233 return err
234 }
235 configToWrite.Contexts[key] = context
236
237 if err := WriteToFile(*configToWrite, destinationFile); err != nil {
238 return err
239 }
240 }
241 }
242
243 for key, authInfo := range newConfig.AuthInfos {
244 startingAuthInfo, exists := startingConfig.AuthInfos[key]
245 if !reflect.DeepEqual(authInfo, startingAuthInfo) || !exists {
246 destinationFile := authInfo.LocationOfOrigin
247 if len(destinationFile) == 0 {
248 destinationFile = configAccess.GetDefaultFilename()
249 }
250
251 configToWrite, err := getConfigFromFile(destinationFile)
252 if err != nil {
253 return err
254 }
255 t := *authInfo
256 configToWrite.AuthInfos[key] = &t
257 configToWrite.AuthInfos[key].LocationOfOrigin = destinationFile
258 if relativizePaths {
259 if err := RelativizeAuthInfoLocalPaths(configToWrite.AuthInfos[key]); err != nil {
260 return err
261 }
262 }
263
264 if err := WriteToFile(*configToWrite, destinationFile); err != nil {
265 return err
266 }
267 }
268 }
269
270 for key, cluster := range startingConfig.Clusters {
271 if _, exists := newConfig.Clusters[key]; !exists {
272 destinationFile := cluster.LocationOfOrigin
273 if len(destinationFile) == 0 {
274 destinationFile = configAccess.GetDefaultFilename()
275 }
276
277 configToWrite, err := getConfigFromFile(destinationFile)
278 if err != nil {
279 return err
280 }
281 delete(configToWrite.Clusters, key)
282
283 if err := WriteToFile(*configToWrite, destinationFile); err != nil {
284 return err
285 }
286 }
287 }
288
289 for key, context := range startingConfig.Contexts {
290 if _, exists := newConfig.Contexts[key]; !exists {
291 destinationFile := context.LocationOfOrigin
292 if len(destinationFile) == 0 {
293 destinationFile = configAccess.GetDefaultFilename()
294 }
295
296 configToWrite, err := getConfigFromFile(destinationFile)
297 if err != nil {
298 return err
299 }
300 delete(configToWrite.Contexts, key)
301
302 if err := WriteToFile(*configToWrite, destinationFile); err != nil {
303 return err
304 }
305 }
306 }
307
308 for key, authInfo := range startingConfig.AuthInfos {
309 if _, exists := newConfig.AuthInfos[key]; !exists {
310 destinationFile := authInfo.LocationOfOrigin
311 if len(destinationFile) == 0 {
312 destinationFile = configAccess.GetDefaultFilename()
313 }
314
315 configToWrite, err := getConfigFromFile(destinationFile)
316 if err != nil {
317 return err
318 }
319 delete(configToWrite.AuthInfos, key)
320
321 if err := WriteToFile(*configToWrite, destinationFile); err != nil {
322 return err
323 }
324 }
325 }
326
327 return nil
328}
329
330func PersisterForUser(configAccess ConfigAccess, user string) restclient.AuthProviderConfigPersister {
331 return &persister{configAccess, user}
332}
333
334type persister struct {
335 configAccess ConfigAccess
336 user string
337}
338
339func (p *persister) Persist(config map[string]string) error {
340 newConfig, err := p.configAccess.GetStartingConfig()
341 if err != nil {
342 return err
343 }
344 authInfo, ok := newConfig.AuthInfos[p.user]
345 if ok && authInfo.AuthProvider != nil {
346 authInfo.AuthProvider.Config = config
347 ModifyConfig(p.configAccess, *newConfig, false)
348 }
349 return nil
350}
351
352// writeCurrentContext takes three possible paths.
353// If newCurrentContext is the same as the startingConfig's current context, then we exit.
354// If newCurrentContext has a value, then that value is written into the default destination file.
355// If newCurrentContext is empty, then we find the config file that is setting the CurrentContext and clear the value from that file
356func writeCurrentContext(configAccess ConfigAccess, newCurrentContext string) error {
357 if startingConfig, err := configAccess.GetStartingConfig(); err != nil {
358 return err
359 } else if startingConfig.CurrentContext == newCurrentContext {
360 return nil
361 }
362
363 if configAccess.IsExplicitFile() {
364 file := configAccess.GetExplicitFile()
365 currConfig, err := getConfigFromFile(file)
366 if err != nil {
367 return err
368 }
369 currConfig.CurrentContext = newCurrentContext
370 if err := WriteToFile(*currConfig, file); err != nil {
371 return err
372 }
373
374 return nil
375 }
376
377 if len(newCurrentContext) > 0 {
378 destinationFile := configAccess.GetDefaultFilename()
379 config, err := getConfigFromFile(destinationFile)
380 if err != nil {
381 return err
382 }
383 config.CurrentContext = newCurrentContext
384
385 if err := WriteToFile(*config, destinationFile); err != nil {
386 return err
387 }
388
389 return nil
390 }
391
392 // 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
393 for _, file := range configAccess.GetLoadingPrecedence() {
394 if _, err := os.Stat(file); err == nil {
395 currConfig, err := getConfigFromFile(file)
396 if err != nil {
397 return err
398 }
399
400 if len(currConfig.CurrentContext) > 0 {
401 currConfig.CurrentContext = newCurrentContext
402 if err := WriteToFile(*currConfig, file); err != nil {
403 return err
404 }
405
406 return nil
407 }
408 }
409 }
410
411 return errors.New("no config found to write context")
412}
413
414func writePreferences(configAccess ConfigAccess, newPrefs clientcmdapi.Preferences) error {
415 if startingConfig, err := configAccess.GetStartingConfig(); err != nil {
416 return err
417 } else if reflect.DeepEqual(startingConfig.Preferences, newPrefs) {
418 return nil
419 }
420
421 if configAccess.IsExplicitFile() {
422 file := configAccess.GetExplicitFile()
423 currConfig, err := getConfigFromFile(file)
424 if err != nil {
425 return err
426 }
427 currConfig.Preferences = newPrefs
428 if err := WriteToFile(*currConfig, file); err != nil {
429 return err
430 }
431
432 return nil
433 }
434
435 for _, file := range configAccess.GetLoadingPrecedence() {
436 currConfig, err := getConfigFromFile(file)
437 if err != nil {
438 return err
439 }
440
441 if !reflect.DeepEqual(currConfig.Preferences, newPrefs) {
442 currConfig.Preferences = newPrefs
443 if err := WriteToFile(*currConfig, file); err != nil {
444 return err
445 }
446
447 return nil
448 }
449 }
450
451 return errors.New("no config found to write preferences")
452}
453
454// 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.
455func getConfigFromFile(filename string) (*clientcmdapi.Config, error) {
456 config, err := LoadFromFile(filename)
457 if err != nil && !os.IsNotExist(err) {
458 return nil, err
459 }
460 if config == nil {
461 config = clientcmdapi.NewConfig()
462 }
463 return config, nil
464}
465
466// 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
467func GetConfigFromFileOrDie(filename string) *clientcmdapi.Config {
468 config, err := getConfigFromFile(filename)
469 if err != nil {
470 glog.FatalDepth(1, err)
471 }
472
473 return config
474}