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