k8s.io/client-go@v0.31.1/tools/clientcmd/loader.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 "fmt" 21 "os" 22 "path/filepath" 23 "reflect" 24 goruntime "runtime" 25 "strings" 26 27 "github.com/imdario/mergo" 28 "k8s.io/klog/v2" 29 30 "k8s.io/apimachinery/pkg/runtime" 31 "k8s.io/apimachinery/pkg/runtime/schema" 32 utilerrors "k8s.io/apimachinery/pkg/util/errors" 33 restclient "k8s.io/client-go/rest" 34 clientcmdapi "k8s.io/client-go/tools/clientcmd/api" 35 clientcmdlatest "k8s.io/client-go/tools/clientcmd/api/latest" 36 "k8s.io/client-go/util/homedir" 37 ) 38 39 const ( 40 RecommendedConfigPathFlag = "kubeconfig" 41 RecommendedConfigPathEnvVar = "KUBECONFIG" 42 RecommendedHomeDir = ".kube" 43 RecommendedFileName = "config" 44 RecommendedSchemaName = "schema" 45 ) 46 47 var ( 48 RecommendedConfigDir = filepath.Join(homedir.HomeDir(), RecommendedHomeDir) 49 RecommendedHomeFile = filepath.Join(RecommendedConfigDir, RecommendedFileName) 50 RecommendedSchemaFile = filepath.Join(RecommendedConfigDir, RecommendedSchemaName) 51 ) 52 53 // currentMigrationRules returns a map that holds the history of recommended home directories used in previous versions. 54 // Any future changes to RecommendedHomeFile and related are expected to add a migration rule here, in order to make 55 // sure existing config files are migrated to their new locations properly. 56 func currentMigrationRules() map[string]string { 57 var oldRecommendedHomeFileName string 58 if goruntime.GOOS == "windows" { 59 oldRecommendedHomeFileName = RecommendedFileName 60 } else { 61 oldRecommendedHomeFileName = ".kubeconfig" 62 } 63 return map[string]string{ 64 RecommendedHomeFile: filepath.Join(os.Getenv("HOME"), RecommendedHomeDir, oldRecommendedHomeFileName), 65 } 66 } 67 68 type ClientConfigLoader interface { 69 ConfigAccess 70 // IsDefaultConfig returns true if the returned config matches the defaults. 71 IsDefaultConfig(*restclient.Config) bool 72 // Load returns the latest config 73 Load() (*clientcmdapi.Config, error) 74 } 75 76 type KubeconfigGetter func() (*clientcmdapi.Config, error) 77 78 type ClientConfigGetter struct { 79 kubeconfigGetter KubeconfigGetter 80 } 81 82 // ClientConfigGetter implements the ClientConfigLoader interface. 83 var _ ClientConfigLoader = &ClientConfigGetter{} 84 85 func (g *ClientConfigGetter) Load() (*clientcmdapi.Config, error) { 86 return g.kubeconfigGetter() 87 } 88 89 func (g *ClientConfigGetter) GetLoadingPrecedence() []string { 90 return nil 91 } 92 func (g *ClientConfigGetter) GetStartingConfig() (*clientcmdapi.Config, error) { 93 return g.kubeconfigGetter() 94 } 95 func (g *ClientConfigGetter) GetDefaultFilename() string { 96 return "" 97 } 98 func (g *ClientConfigGetter) IsExplicitFile() bool { 99 return false 100 } 101 func (g *ClientConfigGetter) GetExplicitFile() string { 102 return "" 103 } 104 func (g *ClientConfigGetter) IsDefaultConfig(config *restclient.Config) bool { 105 return false 106 } 107 108 // ClientConfigLoadingRules is an ExplicitPath and string slice of specific locations that are used for merging together a Config 109 // Callers can put the chain together however they want, but we'd recommend: 110 // EnvVarPathFiles if set (a list of files if set) OR the HomeDirectoryPath 111 // ExplicitPath is special, because if a user specifically requests a certain file be used and error is reported if this file is not present 112 type ClientConfigLoadingRules struct { 113 ExplicitPath string 114 Precedence []string 115 116 // MigrationRules is a map of destination files to source files. If a destination file is not present, then the source file is checked. 117 // If the source file is present, then it is copied to the destination file BEFORE any further loading happens. 118 MigrationRules map[string]string 119 120 // DoNotResolvePaths indicates whether or not to resolve paths with respect to the originating files. This is phrased as a negative so 121 // that a default object that doesn't set this will usually get the behavior it wants. 122 DoNotResolvePaths bool 123 124 // DefaultClientConfig is an optional field indicating what rules to use to calculate a default configuration. 125 // This should match the overrides passed in to ClientConfig loader. 126 DefaultClientConfig ClientConfig 127 128 // WarnIfAllMissing indicates whether the configuration files pointed by KUBECONFIG environment variable are present or not. 129 // In case of missing files, it warns the user about the missing files. 130 WarnIfAllMissing bool 131 132 // Warner is the warning log callback to use in case of missing files. 133 Warner WarningHandler 134 } 135 136 // WarningHandler allows to set the logging function to use 137 type WarningHandler func(error) 138 139 func (handler WarningHandler) Warn(err error) { 140 if handler == nil { 141 klog.V(1).Info(err) 142 } else { 143 handler(err) 144 } 145 } 146 147 type MissingConfigError struct { 148 Missing []string 149 } 150 151 func (c MissingConfigError) Error() string { 152 return fmt.Sprintf("Config not found: %s", strings.Join(c.Missing, ", ")) 153 } 154 155 // ClientConfigLoadingRules implements the ClientConfigLoader interface. 156 var _ ClientConfigLoader = &ClientConfigLoadingRules{} 157 158 // NewDefaultClientConfigLoadingRules returns a ClientConfigLoadingRules object with default fields filled in. You are not required to 159 // use this constructor 160 func NewDefaultClientConfigLoadingRules() *ClientConfigLoadingRules { 161 chain := []string{} 162 warnIfAllMissing := false 163 164 envVarFiles := os.Getenv(RecommendedConfigPathEnvVar) 165 if len(envVarFiles) != 0 { 166 fileList := filepath.SplitList(envVarFiles) 167 // prevent the same path load multiple times 168 chain = append(chain, deduplicate(fileList)...) 169 warnIfAllMissing = true 170 171 } else { 172 chain = append(chain, RecommendedHomeFile) 173 } 174 175 return &ClientConfigLoadingRules{ 176 Precedence: chain, 177 MigrationRules: currentMigrationRules(), 178 WarnIfAllMissing: warnIfAllMissing, 179 } 180 } 181 182 // Load starts by running the MigrationRules and then 183 // takes the loading rules and returns a Config object based on following rules. 184 // 185 // if the ExplicitPath, return the unmerged explicit file 186 // Otherwise, return a merged config based on the Precedence slice 187 // 188 // A missing ExplicitPath file produces an error. Empty filenames or other missing files are ignored. 189 // Read errors or files with non-deserializable content produce errors. 190 // The first file to set a particular map key wins and map key's value is never changed. 191 // BUT, if you set a struct value that is NOT contained inside of map, the value WILL be changed. 192 // This results in some odd looking logic to merge in one direction, merge in the other, and then merge the two. 193 // It also means that if two files specify a "red-user", only values from the first file's red-user are used. Even 194 // non-conflicting entries from the second file's "red-user" are discarded. 195 // Relative paths inside of the .kubeconfig files are resolved against the .kubeconfig file's parent folder 196 // and only absolute file paths are returned. 197 func (rules *ClientConfigLoadingRules) Load() (*clientcmdapi.Config, error) { 198 if err := rules.Migrate(); err != nil { 199 return nil, err 200 } 201 202 errlist := []error{} 203 missingList := []string{} 204 205 kubeConfigFiles := []string{} 206 207 // Make sure a file we were explicitly told to use exists 208 if len(rules.ExplicitPath) > 0 { 209 if _, err := os.Stat(rules.ExplicitPath); os.IsNotExist(err) { 210 return nil, err 211 } 212 kubeConfigFiles = append(kubeConfigFiles, rules.ExplicitPath) 213 214 } else { 215 kubeConfigFiles = append(kubeConfigFiles, rules.Precedence...) 216 } 217 218 kubeconfigs := []*clientcmdapi.Config{} 219 // read and cache the config files so that we only look at them once 220 for _, filename := range kubeConfigFiles { 221 if len(filename) == 0 { 222 // no work to do 223 continue 224 } 225 226 config, err := LoadFromFile(filename) 227 228 if os.IsNotExist(err) { 229 // skip missing files 230 // Add to the missing list to produce a warning 231 missingList = append(missingList, filename) 232 continue 233 } 234 235 if err != nil { 236 errlist = append(errlist, fmt.Errorf("error loading config file \"%s\": %v", filename, err)) 237 continue 238 } 239 240 kubeconfigs = append(kubeconfigs, config) 241 } 242 243 if rules.WarnIfAllMissing && len(missingList) > 0 && len(kubeconfigs) == 0 { 244 rules.Warner.Warn(MissingConfigError{Missing: missingList}) 245 } 246 247 // first merge all of our maps 248 mapConfig := clientcmdapi.NewConfig() 249 250 for _, kubeconfig := range kubeconfigs { 251 mergo.Merge(mapConfig, kubeconfig, mergo.WithOverride) 252 } 253 254 // merge all of the struct values in the reverse order so that priority is given correctly 255 // errors are not added to the list the second time 256 nonMapConfig := clientcmdapi.NewConfig() 257 for i := len(kubeconfigs) - 1; i >= 0; i-- { 258 kubeconfig := kubeconfigs[i] 259 mergo.Merge(nonMapConfig, kubeconfig, mergo.WithOverride) 260 } 261 262 // since values are overwritten, but maps values are not, we can merge the non-map config on top of the map config and 263 // get the values we expect. 264 config := clientcmdapi.NewConfig() 265 mergo.Merge(config, mapConfig, mergo.WithOverride) 266 mergo.Merge(config, nonMapConfig, mergo.WithOverride) 267 268 if rules.ResolvePaths() { 269 if err := ResolveLocalPaths(config); err != nil { 270 errlist = append(errlist, err) 271 } 272 } 273 return config, utilerrors.NewAggregate(errlist) 274 } 275 276 // Migrate uses the MigrationRules map. If a destination file is not present, then the source file is checked. 277 // If the source file is present, then it is copied to the destination file BEFORE any further loading happens. 278 func (rules *ClientConfigLoadingRules) Migrate() error { 279 if rules.MigrationRules == nil { 280 return nil 281 } 282 283 for destination, source := range rules.MigrationRules { 284 if _, err := os.Stat(destination); err == nil { 285 // if the destination already exists, do nothing 286 continue 287 } else if os.IsPermission(err) { 288 // if we can't access the file, skip it 289 continue 290 } else if !os.IsNotExist(err) { 291 // if we had an error other than non-existence, fail 292 return err 293 } 294 295 if sourceInfo, err := os.Stat(source); err != nil { 296 if os.IsNotExist(err) || os.IsPermission(err) { 297 // if the source file doesn't exist or we can't access it, there's no work to do. 298 continue 299 } 300 301 // if we had an error other than non-existence, fail 302 return err 303 } else if sourceInfo.IsDir() { 304 return fmt.Errorf("cannot migrate %v to %v because it is a directory", source, destination) 305 } 306 307 data, err := os.ReadFile(source) 308 if err != nil { 309 return err 310 } 311 // destination is created with mode 0666 before umask 312 err = os.WriteFile(destination, data, 0666) 313 if err != nil { 314 return err 315 } 316 } 317 318 return nil 319 } 320 321 // GetLoadingPrecedence implements ConfigAccess 322 func (rules *ClientConfigLoadingRules) GetLoadingPrecedence() []string { 323 if len(rules.ExplicitPath) > 0 { 324 return []string{rules.ExplicitPath} 325 } 326 327 return rules.Precedence 328 } 329 330 // GetStartingConfig implements ConfigAccess 331 func (rules *ClientConfigLoadingRules) GetStartingConfig() (*clientcmdapi.Config, error) { 332 clientConfig := NewNonInteractiveDeferredLoadingClientConfig(rules, &ConfigOverrides{}) 333 rawConfig, err := clientConfig.RawConfig() 334 if os.IsNotExist(err) { 335 return clientcmdapi.NewConfig(), nil 336 } 337 if err != nil { 338 return nil, err 339 } 340 341 return &rawConfig, nil 342 } 343 344 // GetDefaultFilename implements ConfigAccess 345 func (rules *ClientConfigLoadingRules) GetDefaultFilename() string { 346 // Explicit file if we have one. 347 if rules.IsExplicitFile() { 348 return rules.GetExplicitFile() 349 } 350 // Otherwise, first existing file from precedence. 351 for _, filename := range rules.GetLoadingPrecedence() { 352 if _, err := os.Stat(filename); err == nil { 353 return filename 354 } 355 } 356 // If none exists, use the first from precedence. 357 if len(rules.Precedence) > 0 { 358 return rules.Precedence[0] 359 } 360 return "" 361 } 362 363 // IsExplicitFile implements ConfigAccess 364 func (rules *ClientConfigLoadingRules) IsExplicitFile() bool { 365 return len(rules.ExplicitPath) > 0 366 } 367 368 // GetExplicitFile implements ConfigAccess 369 func (rules *ClientConfigLoadingRules) GetExplicitFile() string { 370 return rules.ExplicitPath 371 } 372 373 // IsDefaultConfig returns true if the provided configuration matches the default 374 func (rules *ClientConfigLoadingRules) IsDefaultConfig(config *restclient.Config) bool { 375 if rules.DefaultClientConfig == nil { 376 return false 377 } 378 defaultConfig, err := rules.DefaultClientConfig.ClientConfig() 379 if err != nil { 380 return false 381 } 382 return reflect.DeepEqual(config, defaultConfig) 383 } 384 385 // LoadFromFile takes a filename and deserializes the contents into Config object 386 func LoadFromFile(filename string) (*clientcmdapi.Config, error) { 387 kubeconfigBytes, err := os.ReadFile(filename) 388 if err != nil { 389 return nil, err 390 } 391 config, err := Load(kubeconfigBytes) 392 if err != nil { 393 return nil, err 394 } 395 klog.V(6).Infoln("Config loaded from file: ", filename) 396 397 // set LocationOfOrigin on every Cluster, User, and Context 398 for key, obj := range config.AuthInfos { 399 obj.LocationOfOrigin = filename 400 config.AuthInfos[key] = obj 401 } 402 for key, obj := range config.Clusters { 403 obj.LocationOfOrigin = filename 404 config.Clusters[key] = obj 405 } 406 for key, obj := range config.Contexts { 407 obj.LocationOfOrigin = filename 408 config.Contexts[key] = obj 409 } 410 411 if config.AuthInfos == nil { 412 config.AuthInfos = map[string]*clientcmdapi.AuthInfo{} 413 } 414 if config.Clusters == nil { 415 config.Clusters = map[string]*clientcmdapi.Cluster{} 416 } 417 if config.Contexts == nil { 418 config.Contexts = map[string]*clientcmdapi.Context{} 419 } 420 421 return config, nil 422 } 423 424 // Load takes a byte slice and deserializes the contents into Config object. 425 // Encapsulates deserialization without assuming the source is a file. 426 func Load(data []byte) (*clientcmdapi.Config, error) { 427 config := clientcmdapi.NewConfig() 428 // if there's no data in a file, return the default object instead of failing (DecodeInto reject empty input) 429 if len(data) == 0 { 430 return config, nil 431 } 432 decoded, _, err := clientcmdlatest.Codec.Decode(data, &schema.GroupVersionKind{Version: clientcmdlatest.Version, Kind: "Config"}, config) 433 if err != nil { 434 return nil, err 435 } 436 return decoded.(*clientcmdapi.Config), nil 437 } 438 439 // WriteToFile serializes the config to yaml and writes it out to a file. If not present, it creates the file with the mode 0600. If it is present 440 // it stomps the contents 441 func WriteToFile(config clientcmdapi.Config, filename string) error { 442 content, err := Write(config) 443 if err != nil { 444 return err 445 } 446 dir := filepath.Dir(filename) 447 if _, err := os.Stat(dir); os.IsNotExist(err) { 448 if err = os.MkdirAll(dir, 0755); err != nil { 449 return err 450 } 451 } 452 453 if err := os.WriteFile(filename, content, 0600); err != nil { 454 return err 455 } 456 return nil 457 } 458 459 func lockFile(filename string) error { 460 // TODO: find a way to do this with actual file locks. Will 461 // probably need separate solution for windows and Linux. 462 463 // Make sure the dir exists before we try to create a lock file. 464 dir := filepath.Dir(filename) 465 if _, err := os.Stat(dir); os.IsNotExist(err) { 466 if err = os.MkdirAll(dir, 0755); err != nil { 467 return err 468 } 469 } 470 f, err := os.OpenFile(lockName(filename), os.O_CREATE|os.O_EXCL, 0) 471 if err != nil { 472 return err 473 } 474 f.Close() 475 return nil 476 } 477 478 func unlockFile(filename string) error { 479 return os.Remove(lockName(filename)) 480 } 481 482 func lockName(filename string) string { 483 return filename + ".lock" 484 } 485 486 // Write serializes the config to yaml. 487 // Encapsulates serialization without assuming the destination is a file. 488 func Write(config clientcmdapi.Config) ([]byte, error) { 489 return runtime.Encode(clientcmdlatest.Codec, &config) 490 } 491 492 func (rules ClientConfigLoadingRules) ResolvePaths() bool { 493 return !rules.DoNotResolvePaths 494 } 495 496 // ResolveLocalPaths resolves all relative paths in the config object with respect to the stanza's LocationOfOrigin 497 // this cannot be done directly inside of LoadFromFile because doing so there would make it impossible to load a file without 498 // modification of its contents. 499 func ResolveLocalPaths(config *clientcmdapi.Config) error { 500 for _, cluster := range config.Clusters { 501 if len(cluster.LocationOfOrigin) == 0 { 502 continue 503 } 504 base, err := filepath.Abs(filepath.Dir(cluster.LocationOfOrigin)) 505 if err != nil { 506 return fmt.Errorf("could not determine the absolute path of config file %s: %v", cluster.LocationOfOrigin, err) 507 } 508 509 if err := ResolvePaths(GetClusterFileReferences(cluster), base); err != nil { 510 return err 511 } 512 } 513 for _, authInfo := range config.AuthInfos { 514 if len(authInfo.LocationOfOrigin) == 0 { 515 continue 516 } 517 base, err := filepath.Abs(filepath.Dir(authInfo.LocationOfOrigin)) 518 if err != nil { 519 return fmt.Errorf("could not determine the absolute path of config file %s: %v", authInfo.LocationOfOrigin, err) 520 } 521 522 if err := ResolvePaths(GetAuthInfoFileReferences(authInfo), base); err != nil { 523 return err 524 } 525 } 526 527 return nil 528 } 529 530 // RelativizeClusterLocalPaths first absolutizes the paths by calling ResolveLocalPaths. This assumes that any NEW path is already 531 // absolute, but any existing path will be resolved relative to LocationOfOrigin 532 func RelativizeClusterLocalPaths(cluster *clientcmdapi.Cluster) error { 533 if len(cluster.LocationOfOrigin) == 0 { 534 return fmt.Errorf("no location of origin for %s", cluster.Server) 535 } 536 base, err := filepath.Abs(filepath.Dir(cluster.LocationOfOrigin)) 537 if err != nil { 538 return fmt.Errorf("could not determine the absolute path of config file %s: %v", cluster.LocationOfOrigin, err) 539 } 540 541 if err := ResolvePaths(GetClusterFileReferences(cluster), base); err != nil { 542 return err 543 } 544 if err := RelativizePathWithNoBacksteps(GetClusterFileReferences(cluster), base); err != nil { 545 return err 546 } 547 548 return nil 549 } 550 551 // RelativizeAuthInfoLocalPaths first absolutizes the paths by calling ResolveLocalPaths. This assumes that any NEW path is already 552 // absolute, but any existing path will be resolved relative to LocationOfOrigin 553 func RelativizeAuthInfoLocalPaths(authInfo *clientcmdapi.AuthInfo) error { 554 if len(authInfo.LocationOfOrigin) == 0 { 555 return fmt.Errorf("no location of origin for %v", authInfo) 556 } 557 base, err := filepath.Abs(filepath.Dir(authInfo.LocationOfOrigin)) 558 if err != nil { 559 return fmt.Errorf("could not determine the absolute path of config file %s: %v", authInfo.LocationOfOrigin, err) 560 } 561 562 if err := ResolvePaths(GetAuthInfoFileReferences(authInfo), base); err != nil { 563 return err 564 } 565 if err := RelativizePathWithNoBacksteps(GetAuthInfoFileReferences(authInfo), base); err != nil { 566 return err 567 } 568 569 return nil 570 } 571 572 func RelativizeConfigPaths(config *clientcmdapi.Config, base string) error { 573 return RelativizePathWithNoBacksteps(GetConfigFileReferences(config), base) 574 } 575 576 func ResolveConfigPaths(config *clientcmdapi.Config, base string) error { 577 return ResolvePaths(GetConfigFileReferences(config), base) 578 } 579 580 func GetConfigFileReferences(config *clientcmdapi.Config) []*string { 581 refs := []*string{} 582 583 for _, cluster := range config.Clusters { 584 refs = append(refs, GetClusterFileReferences(cluster)...) 585 } 586 for _, authInfo := range config.AuthInfos { 587 refs = append(refs, GetAuthInfoFileReferences(authInfo)...) 588 } 589 590 return refs 591 } 592 593 func GetClusterFileReferences(cluster *clientcmdapi.Cluster) []*string { 594 return []*string{&cluster.CertificateAuthority} 595 } 596 597 func GetAuthInfoFileReferences(authInfo *clientcmdapi.AuthInfo) []*string { 598 s := []*string{&authInfo.ClientCertificate, &authInfo.ClientKey, &authInfo.TokenFile} 599 // Only resolve exec command if it isn't PATH based. 600 if authInfo.Exec != nil && strings.ContainsRune(authInfo.Exec.Command, filepath.Separator) { 601 s = append(s, &authInfo.Exec.Command) 602 } 603 return s 604 } 605 606 // ResolvePaths updates the given refs to be absolute paths, relative to the given base directory 607 func ResolvePaths(refs []*string, base string) error { 608 for _, ref := range refs { 609 // Don't resolve empty paths 610 if len(*ref) > 0 { 611 // Don't resolve absolute paths 612 if !filepath.IsAbs(*ref) { 613 *ref = filepath.Join(base, *ref) 614 } 615 } 616 } 617 return nil 618 } 619 620 // RelativizePathWithNoBacksteps updates the given refs to be relative paths, relative to the given base directory as long as they do not require backsteps. 621 // Any path requiring a backstep is left as-is as long it is absolute. Any non-absolute path that can't be relativized produces an error 622 func RelativizePathWithNoBacksteps(refs []*string, base string) error { 623 for _, ref := range refs { 624 // Don't relativize empty paths 625 if len(*ref) > 0 { 626 rel, err := MakeRelative(*ref, base) 627 if err != nil { 628 return err 629 } 630 631 // if we have a backstep, don't mess with the path 632 if strings.HasPrefix(rel, "../") { 633 if filepath.IsAbs(*ref) { 634 continue 635 } 636 637 return fmt.Errorf("%v requires backsteps and is not absolute", *ref) 638 } 639 640 *ref = rel 641 } 642 } 643 return nil 644 } 645 646 func MakeRelative(path, base string) (string, error) { 647 if len(path) > 0 { 648 rel, err := filepath.Rel(base, path) 649 if err != nil { 650 return path, err 651 } 652 return rel, nil 653 } 654 return path, nil 655 } 656 657 // deduplicate removes any duplicated values and returns a new slice, keeping the order unchanged 658 func deduplicate(s []string) []string { 659 encountered := map[string]bool{} 660 ret := make([]string, 0) 661 for i := range s { 662 if encountered[s[i]] { 663 continue 664 } 665 encountered[s[i]] = true 666 ret = append(ret, s[i]) 667 } 668 return ret 669 }