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