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