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