k8s.io/client-go@v0.22.2/tools/clientcmd/config.go (about) 1 /* 2 Copyright 2014 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package clientcmd 18 19 import ( 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 34 type 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 47 type 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 61 var ( 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 70 func (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 85 func (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 96 func (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 113 func (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 137 func (o *PathOptions) IsExplicitFile() bool { 138 return len(o.LoadingRules.ExplicitPath) > 0 139 } 140 141 func (o *PathOptions) GetExplicitFile() string { 142 return o.LoadingRules.ExplicitPath 143 } 144 145 func NewDefaultPathOptions() *PathOptions { 146 ret := &PathOptions{ 147 GlobalFile: RecommendedHomeFile, 148 EnvVar: RecommendedConfigPathEnvVar, 149 ExplicitFileFlag: RecommendedConfigPathFlag, 150 151 GlobalFileSubpath: path.Join(RecommendedHomeDir, RecommendedFileName), 152 153 LoadingRules: NewDefaultClientConfigLoadingRules(), 154 } 155 ret.LoadingRules.DoNotResolvePaths = true 156 157 return ret 158 } 159 160 // ModifyConfig takes a Config object, iterates through Clusters, AuthInfos, and Contexts, uses the LocationOfOrigin if specified or 161 // uses the default destination file to write the results into. This results in multiple file reads, but it's very easy to follow. 162 // Preferences and CurrentContext should always be set in the default destination file. Since we can't distinguish between empty and missing values 163 // (no nil strings), we're forced have separate handling for them. In the kubeconfig cases, newConfig should have at most one difference, 164 // 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 165 // modified element. 166 func ModifyConfig(configAccess ConfigAccess, newConfig clientcmdapi.Config, relativizePaths bool) error { 167 if UseModifyConfigLock { 168 possibleSources := configAccess.GetLoadingPrecedence() 169 // sort the possible kubeconfig files so we always "lock" in the same order 170 // to avoid deadlock (note: this can fail w/ symlinks, but... come on). 171 sort.Strings(possibleSources) 172 for _, filename := range possibleSources { 173 if err := lockFile(filename); err != nil { 174 return err 175 } 176 defer unlockFile(filename) 177 } 178 } 179 180 startingConfig, err := configAccess.GetStartingConfig() 181 if err != nil { 182 return err 183 } 184 185 // We need to find all differences, locate their original files, read a partial config to modify only that stanza and write out the file. 186 // Special case the test for current context and preferences since those always write to the default file. 187 if reflect.DeepEqual(*startingConfig, newConfig) { 188 // nothing to do 189 return nil 190 } 191 192 if startingConfig.CurrentContext != newConfig.CurrentContext { 193 if err := writeCurrentContext(configAccess, newConfig.CurrentContext); err != nil { 194 return err 195 } 196 } 197 198 if !reflect.DeepEqual(startingConfig.Preferences, newConfig.Preferences) { 199 if err := writePreferences(configAccess, newConfig.Preferences); err != nil { 200 return err 201 } 202 } 203 204 // Search every cluster, authInfo, and context. First from new to old for differences, then from old to new for deletions 205 for key, cluster := range newConfig.Clusters { 206 startingCluster, exists := startingConfig.Clusters[key] 207 if !reflect.DeepEqual(cluster, startingCluster) || !exists { 208 destinationFile := cluster.LocationOfOrigin 209 if len(destinationFile) == 0 { 210 destinationFile = configAccess.GetDefaultFilename() 211 } 212 213 configToWrite, err := getConfigFromFile(destinationFile) 214 if err != nil { 215 return err 216 } 217 t := *cluster 218 219 configToWrite.Clusters[key] = &t 220 configToWrite.Clusters[key].LocationOfOrigin = destinationFile 221 if relativizePaths { 222 if err := RelativizeClusterLocalPaths(configToWrite.Clusters[key]); err != nil { 223 return err 224 } 225 } 226 227 if err := WriteToFile(*configToWrite, destinationFile); err != nil { 228 return err 229 } 230 } 231 } 232 233 // seenConfigs stores a map of config source filenames to computed config objects 234 seenConfigs := map[string]*clientcmdapi.Config{} 235 236 for key, context := range newConfig.Contexts { 237 startingContext, exists := startingConfig.Contexts[key] 238 if !reflect.DeepEqual(context, startingContext) || !exists { 239 destinationFile := context.LocationOfOrigin 240 if len(destinationFile) == 0 { 241 destinationFile = configAccess.GetDefaultFilename() 242 } 243 244 // we only obtain a fresh config object from its source file 245 // if we have not seen it already - this prevents us from 246 // reading and writing to the same number of files repeatedly 247 // when multiple / all contexts share the same destination file. 248 configToWrite, seen := seenConfigs[destinationFile] 249 if !seen { 250 var err error 251 configToWrite, err = getConfigFromFile(destinationFile) 252 if err != nil { 253 return err 254 } 255 seenConfigs[destinationFile] = configToWrite 256 } 257 258 configToWrite.Contexts[key] = context 259 } 260 } 261 262 // actually persist config object changes 263 for destinationFile, configToWrite := range seenConfigs { 264 if err := WriteToFile(*configToWrite, destinationFile); err != nil { 265 return err 266 } 267 } 268 269 for key, authInfo := range newConfig.AuthInfos { 270 startingAuthInfo, exists := startingConfig.AuthInfos[key] 271 if !reflect.DeepEqual(authInfo, startingAuthInfo) || !exists { 272 destinationFile := authInfo.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 t := *authInfo 282 configToWrite.AuthInfos[key] = &t 283 configToWrite.AuthInfos[key].LocationOfOrigin = destinationFile 284 if relativizePaths { 285 if err := RelativizeAuthInfoLocalPaths(configToWrite.AuthInfos[key]); err != nil { 286 return err 287 } 288 } 289 290 if err := WriteToFile(*configToWrite, destinationFile); err != nil { 291 return err 292 } 293 } 294 } 295 296 for key, cluster := range startingConfig.Clusters { 297 if _, exists := newConfig.Clusters[key]; !exists { 298 destinationFile := cluster.LocationOfOrigin 299 if len(destinationFile) == 0 { 300 destinationFile = configAccess.GetDefaultFilename() 301 } 302 303 configToWrite, err := getConfigFromFile(destinationFile) 304 if err != nil { 305 return err 306 } 307 delete(configToWrite.Clusters, key) 308 309 if err := WriteToFile(*configToWrite, destinationFile); err != nil { 310 return err 311 } 312 } 313 } 314 315 for key, context := range startingConfig.Contexts { 316 if _, exists := newConfig.Contexts[key]; !exists { 317 destinationFile := context.LocationOfOrigin 318 if len(destinationFile) == 0 { 319 destinationFile = configAccess.GetDefaultFilename() 320 } 321 322 configToWrite, err := getConfigFromFile(destinationFile) 323 if err != nil { 324 return err 325 } 326 delete(configToWrite.Contexts, key) 327 328 if err := WriteToFile(*configToWrite, destinationFile); err != nil { 329 return err 330 } 331 } 332 } 333 334 for key, authInfo := range startingConfig.AuthInfos { 335 if _, exists := newConfig.AuthInfos[key]; !exists { 336 destinationFile := authInfo.LocationOfOrigin 337 if len(destinationFile) == 0 { 338 destinationFile = configAccess.GetDefaultFilename() 339 } 340 341 configToWrite, err := getConfigFromFile(destinationFile) 342 if err != nil { 343 return err 344 } 345 delete(configToWrite.AuthInfos, key) 346 347 if err := WriteToFile(*configToWrite, destinationFile); err != nil { 348 return err 349 } 350 } 351 } 352 353 return nil 354 } 355 356 func PersisterForUser(configAccess ConfigAccess, user string) restclient.AuthProviderConfigPersister { 357 return &persister{configAccess, user} 358 } 359 360 type persister struct { 361 configAccess ConfigAccess 362 user string 363 } 364 365 func (p *persister) Persist(config map[string]string) error { 366 newConfig, err := p.configAccess.GetStartingConfig() 367 if err != nil { 368 return err 369 } 370 authInfo, ok := newConfig.AuthInfos[p.user] 371 if ok && authInfo.AuthProvider != nil { 372 authInfo.AuthProvider.Config = config 373 return ModifyConfig(p.configAccess, *newConfig, false) 374 } 375 return nil 376 } 377 378 // writeCurrentContext takes three possible paths. 379 // If newCurrentContext is the same as the startingConfig's current context, then we exit. 380 // If newCurrentContext has a value, then that value is written into the default destination file. 381 // If newCurrentContext is empty, then we find the config file that is setting the CurrentContext and clear the value from that file 382 func writeCurrentContext(configAccess ConfigAccess, newCurrentContext string) error { 383 if startingConfig, err := configAccess.GetStartingConfig(); err != nil { 384 return err 385 } else if startingConfig.CurrentContext == newCurrentContext { 386 return nil 387 } 388 389 if configAccess.IsExplicitFile() { 390 file := configAccess.GetExplicitFile() 391 currConfig, err := getConfigFromFile(file) 392 if err != nil { 393 return err 394 } 395 currConfig.CurrentContext = newCurrentContext 396 if err := WriteToFile(*currConfig, file); err != nil { 397 return err 398 } 399 400 return nil 401 } 402 403 if len(newCurrentContext) > 0 { 404 destinationFile := configAccess.GetDefaultFilename() 405 config, err := getConfigFromFile(destinationFile) 406 if err != nil { 407 return err 408 } 409 config.CurrentContext = newCurrentContext 410 411 if err := WriteToFile(*config, destinationFile); err != nil { 412 return err 413 } 414 415 return nil 416 } 417 418 // 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 419 for _, file := range configAccess.GetLoadingPrecedence() { 420 if _, err := os.Stat(file); err == nil { 421 currConfig, err := getConfigFromFile(file) 422 if err != nil { 423 return err 424 } 425 426 if len(currConfig.CurrentContext) > 0 { 427 currConfig.CurrentContext = newCurrentContext 428 if err := WriteToFile(*currConfig, file); err != nil { 429 return err 430 } 431 432 return nil 433 } 434 } 435 } 436 437 return errors.New("no config found to write context") 438 } 439 440 func writePreferences(configAccess ConfigAccess, newPrefs clientcmdapi.Preferences) error { 441 if startingConfig, err := configAccess.GetStartingConfig(); err != nil { 442 return err 443 } else if reflect.DeepEqual(startingConfig.Preferences, newPrefs) { 444 return nil 445 } 446 447 if configAccess.IsExplicitFile() { 448 file := configAccess.GetExplicitFile() 449 currConfig, err := getConfigFromFile(file) 450 if err != nil { 451 return err 452 } 453 currConfig.Preferences = newPrefs 454 if err := WriteToFile(*currConfig, file); err != nil { 455 return err 456 } 457 458 return nil 459 } 460 461 for _, file := range configAccess.GetLoadingPrecedence() { 462 currConfig, err := getConfigFromFile(file) 463 if err != nil { 464 return err 465 } 466 467 if !reflect.DeepEqual(currConfig.Preferences, newPrefs) { 468 currConfig.Preferences = newPrefs 469 if err := WriteToFile(*currConfig, file); err != nil { 470 return err 471 } 472 473 return nil 474 } 475 } 476 477 return errors.New("no config found to write preferences") 478 } 479 480 // 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. 481 func getConfigFromFile(filename string) (*clientcmdapi.Config, error) { 482 config, err := LoadFromFile(filename) 483 if err != nil && !os.IsNotExist(err) { 484 return nil, err 485 } 486 if config == nil { 487 config = clientcmdapi.NewConfig() 488 } 489 return config, nil 490 } 491 492 // 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 493 func GetConfigFromFileOrDie(filename string) *clientcmdapi.Config { 494 config, err := getConfigFromFile(filename) 495 if err != nil { 496 klog.FatalDepth(1, err) 497 } 498 499 return config 500 }