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