blob: 7e928a918563edce2cbd867cd3c145da33db5d80 [file] [log] [blame]
Zack Williamse940c7a2019-08-21 14:25:39 -07001/*
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"
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
132// ClientConfigLoadingRules implements the ClientConfigLoader interface.
133var _ ClientConfigLoader = &ClientConfigLoadingRules{}
134
135// NewDefaultClientConfigLoadingRules returns a ClientConfigLoadingRules object with default fields filled in. You are not required to
136// use this constructor
137func NewDefaultClientConfigLoadingRules() *ClientConfigLoadingRules {
138 chain := []string{}
139
140 envVarFiles := os.Getenv(RecommendedConfigPathEnvVar)
141 if len(envVarFiles) != 0 {
142 fileList := filepath.SplitList(envVarFiles)
143 // prevent the same path load multiple times
144 chain = append(chain, deduplicate(fileList)...)
145
146 } else {
147 chain = append(chain, RecommendedHomeFile)
148 }
149
150 return &ClientConfigLoadingRules{
151 Precedence: chain,
152 MigrationRules: currentMigrationRules(),
153 }
154}
155
156// Load starts by running the MigrationRules and then
157// takes the loading rules and returns a Config object based on following rules.
158// if the ExplicitPath, return the unmerged explicit file
159// Otherwise, return a merged config based on the Precedence slice
160// A missing ExplicitPath file produces an error. Empty filenames or other missing files are ignored.
161// Read errors or files with non-deserializable content produce errors.
162// The first file to set a particular map key wins and map key's value is never changed.
163// BUT, if you set a struct value that is NOT contained inside of map, the value WILL be changed.
164// This results in some odd looking logic to merge in one direction, merge in the other, and then merge the two.
165// It also means that if two files specify a "red-user", only values from the first file's red-user are used. Even
166// non-conflicting entries from the second file's "red-user" are discarded.
167// Relative paths inside of the .kubeconfig files are resolved against the .kubeconfig file's parent folder
168// and only absolute file paths are returned.
169func (rules *ClientConfigLoadingRules) Load() (*clientcmdapi.Config, error) {
170 if err := rules.Migrate(); err != nil {
171 return nil, err
172 }
173
174 errlist := []error{}
175
176 kubeConfigFiles := []string{}
177
178 // Make sure a file we were explicitly told to use exists
179 if len(rules.ExplicitPath) > 0 {
180 if _, err := os.Stat(rules.ExplicitPath); os.IsNotExist(err) {
181 return nil, err
182 }
183 kubeConfigFiles = append(kubeConfigFiles, rules.ExplicitPath)
184
185 } else {
186 kubeConfigFiles = append(kubeConfigFiles, rules.Precedence...)
187 }
188
189 kubeconfigs := []*clientcmdapi.Config{}
190 // read and cache the config files so that we only look at them once
191 for _, filename := range kubeConfigFiles {
192 if len(filename) == 0 {
193 // no work to do
194 continue
195 }
196
197 config, err := LoadFromFile(filename)
198 if os.IsNotExist(err) {
199 // skip missing files
200 continue
201 }
202 if err != nil {
203 errlist = append(errlist, fmt.Errorf("Error loading config file \"%s\": %v", filename, err))
204 continue
205 }
206
207 kubeconfigs = append(kubeconfigs, config)
208 }
209
210 // first merge all of our maps
211 mapConfig := clientcmdapi.NewConfig()
212
213 for _, kubeconfig := range kubeconfigs {
214 mergo.MergeWithOverwrite(mapConfig, kubeconfig)
215 }
216
217 // merge all of the struct values in the reverse order so that priority is given correctly
218 // errors are not added to the list the second time
219 nonMapConfig := clientcmdapi.NewConfig()
220 for i := len(kubeconfigs) - 1; i >= 0; i-- {
221 kubeconfig := kubeconfigs[i]
222 mergo.MergeWithOverwrite(nonMapConfig, kubeconfig)
223 }
224
225 // since values are overwritten, but maps values are not, we can merge the non-map config on top of the map config and
226 // get the values we expect.
227 config := clientcmdapi.NewConfig()
228 mergo.MergeWithOverwrite(config, mapConfig)
229 mergo.MergeWithOverwrite(config, nonMapConfig)
230
231 if rules.ResolvePaths() {
232 if err := ResolveLocalPaths(config); err != nil {
233 errlist = append(errlist, err)
234 }
235 }
236 return config, utilerrors.NewAggregate(errlist)
237}
238
239// Migrate uses the MigrationRules map. If a destination file is not present, then the source file is checked.
240// If the source file is present, then it is copied to the destination file BEFORE any further loading happens.
241func (rules *ClientConfigLoadingRules) Migrate() error {
242 if rules.MigrationRules == nil {
243 return nil
244 }
245
246 for destination, source := range rules.MigrationRules {
247 if _, err := os.Stat(destination); err == nil {
248 // if the destination already exists, do nothing
249 continue
250 } else if os.IsPermission(err) {
251 // if we can't access the file, skip it
252 continue
253 } else if !os.IsNotExist(err) {
254 // if we had an error other than non-existence, fail
255 return err
256 }
257
258 if sourceInfo, err := os.Stat(source); err != nil {
259 if os.IsNotExist(err) || os.IsPermission(err) {
260 // if the source file doesn't exist or we can't access it, there's no work to do.
261 continue
262 }
263
264 // if we had an error other than non-existence, fail
265 return err
266 } else if sourceInfo.IsDir() {
267 return fmt.Errorf("cannot migrate %v to %v because it is a directory", source, destination)
268 }
269
270 in, err := os.Open(source)
271 if err != nil {
272 return err
273 }
274 defer in.Close()
275 out, err := os.Create(destination)
276 if err != nil {
277 return err
278 }
279 defer out.Close()
280
281 if _, err = io.Copy(out, in); err != nil {
282 return err
283 }
284 }
285
286 return nil
287}
288
289// GetLoadingPrecedence implements ConfigAccess
290func (rules *ClientConfigLoadingRules) GetLoadingPrecedence() []string {
291 return rules.Precedence
292}
293
294// GetStartingConfig implements ConfigAccess
295func (rules *ClientConfigLoadingRules) GetStartingConfig() (*clientcmdapi.Config, error) {
296 clientConfig := NewNonInteractiveDeferredLoadingClientConfig(rules, &ConfigOverrides{})
297 rawConfig, err := clientConfig.RawConfig()
298 if os.IsNotExist(err) {
299 return clientcmdapi.NewConfig(), nil
300 }
301 if err != nil {
302 return nil, err
303 }
304
305 return &rawConfig, nil
306}
307
308// GetDefaultFilename implements ConfigAccess
309func (rules *ClientConfigLoadingRules) GetDefaultFilename() string {
310 // Explicit file if we have one.
311 if rules.IsExplicitFile() {
312 return rules.GetExplicitFile()
313 }
314 // Otherwise, first existing file from precedence.
315 for _, filename := range rules.GetLoadingPrecedence() {
316 if _, err := os.Stat(filename); err == nil {
317 return filename
318 }
319 }
320 // If none exists, use the first from precedence.
321 if len(rules.Precedence) > 0 {
322 return rules.Precedence[0]
323 }
324 return ""
325}
326
327// IsExplicitFile implements ConfigAccess
328func (rules *ClientConfigLoadingRules) IsExplicitFile() bool {
329 return len(rules.ExplicitPath) > 0
330}
331
332// GetExplicitFile implements ConfigAccess
333func (rules *ClientConfigLoadingRules) GetExplicitFile() string {
334 return rules.ExplicitPath
335}
336
337// IsDefaultConfig returns true if the provided configuration matches the default
338func (rules *ClientConfigLoadingRules) IsDefaultConfig(config *restclient.Config) bool {
339 if rules.DefaultClientConfig == nil {
340 return false
341 }
342 defaultConfig, err := rules.DefaultClientConfig.ClientConfig()
343 if err != nil {
344 return false
345 }
346 return reflect.DeepEqual(config, defaultConfig)
347}
348
349// LoadFromFile takes a filename and deserializes the contents into Config object
350func LoadFromFile(filename string) (*clientcmdapi.Config, error) {
351 kubeconfigBytes, err := ioutil.ReadFile(filename)
352 if err != nil {
353 return nil, err
354 }
355 config, err := Load(kubeconfigBytes)
356 if err != nil {
357 return nil, err
358 }
359 klog.V(6).Infoln("Config loaded from file", filename)
360
361 // set LocationOfOrigin on every Cluster, User, and Context
362 for key, obj := range config.AuthInfos {
363 obj.LocationOfOrigin = filename
364 config.AuthInfos[key] = obj
365 }
366 for key, obj := range config.Clusters {
367 obj.LocationOfOrigin = filename
368 config.Clusters[key] = obj
369 }
370 for key, obj := range config.Contexts {
371 obj.LocationOfOrigin = filename
372 config.Contexts[key] = obj
373 }
374
375 if config.AuthInfos == nil {
376 config.AuthInfos = map[string]*clientcmdapi.AuthInfo{}
377 }
378 if config.Clusters == nil {
379 config.Clusters = map[string]*clientcmdapi.Cluster{}
380 }
381 if config.Contexts == nil {
382 config.Contexts = map[string]*clientcmdapi.Context{}
383 }
384
385 return config, nil
386}
387
388// Load takes a byte slice and deserializes the contents into Config object.
389// Encapsulates deserialization without assuming the source is a file.
390func Load(data []byte) (*clientcmdapi.Config, error) {
391 config := clientcmdapi.NewConfig()
392 // if there's no data in a file, return the default object instead of failing (DecodeInto reject empty input)
393 if len(data) == 0 {
394 return config, nil
395 }
396 decoded, _, err := clientcmdlatest.Codec.Decode(data, &schema.GroupVersionKind{Version: clientcmdlatest.Version, Kind: "Config"}, config)
397 if err != nil {
398 return nil, err
399 }
400 return decoded.(*clientcmdapi.Config), nil
401}
402
403// 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
404// it stomps the contents
405func WriteToFile(config clientcmdapi.Config, filename string) error {
406 content, err := Write(config)
407 if err != nil {
408 return err
409 }
410 dir := filepath.Dir(filename)
411 if _, err := os.Stat(dir); os.IsNotExist(err) {
412 if err = os.MkdirAll(dir, 0755); err != nil {
413 return err
414 }
415 }
416
417 if err := ioutil.WriteFile(filename, content, 0600); err != nil {
418 return err
419 }
420 return nil
421}
422
423func lockFile(filename string) error {
424 // TODO: find a way to do this with actual file locks. Will
425 // probably need separate solution for windows and Linux.
426
427 // Make sure the dir exists before we try to create a lock file.
428 dir := filepath.Dir(filename)
429 if _, err := os.Stat(dir); os.IsNotExist(err) {
430 if err = os.MkdirAll(dir, 0755); err != nil {
431 return err
432 }
433 }
434 f, err := os.OpenFile(lockName(filename), os.O_CREATE|os.O_EXCL, 0)
435 if err != nil {
436 return err
437 }
438 f.Close()
439 return nil
440}
441
442func unlockFile(filename string) error {
443 return os.Remove(lockName(filename))
444}
445
446func lockName(filename string) string {
447 return filename + ".lock"
448}
449
450// Write serializes the config to yaml.
451// Encapsulates serialization without assuming the destination is a file.
452func Write(config clientcmdapi.Config) ([]byte, error) {
453 return runtime.Encode(clientcmdlatest.Codec, &config)
454}
455
456func (rules ClientConfigLoadingRules) ResolvePaths() bool {
457 return !rules.DoNotResolvePaths
458}
459
460// ResolveLocalPaths resolves all relative paths in the config object with respect to the stanza's LocationOfOrigin
461// this cannot be done directly inside of LoadFromFile because doing so there would make it impossible to load a file without
462// modification of its contents.
463func ResolveLocalPaths(config *clientcmdapi.Config) error {
464 for _, cluster := range config.Clusters {
465 if len(cluster.LocationOfOrigin) == 0 {
466 continue
467 }
468 base, err := filepath.Abs(filepath.Dir(cluster.LocationOfOrigin))
469 if err != nil {
470 return fmt.Errorf("Could not determine the absolute path of config file %s: %v", cluster.LocationOfOrigin, err)
471 }
472
473 if err := ResolvePaths(GetClusterFileReferences(cluster), base); err != nil {
474 return err
475 }
476 }
477 for _, authInfo := range config.AuthInfos {
478 if len(authInfo.LocationOfOrigin) == 0 {
479 continue
480 }
481 base, err := filepath.Abs(filepath.Dir(authInfo.LocationOfOrigin))
482 if err != nil {
483 return fmt.Errorf("Could not determine the absolute path of config file %s: %v", authInfo.LocationOfOrigin, err)
484 }
485
486 if err := ResolvePaths(GetAuthInfoFileReferences(authInfo), base); err != nil {
487 return err
488 }
489 }
490
491 return nil
492}
493
494// RelativizeClusterLocalPaths first absolutizes the paths by calling ResolveLocalPaths. This assumes that any NEW path is already
495// absolute, but any existing path will be resolved relative to LocationOfOrigin
496func RelativizeClusterLocalPaths(cluster *clientcmdapi.Cluster) error {
497 if len(cluster.LocationOfOrigin) == 0 {
498 return fmt.Errorf("no location of origin for %s", cluster.Server)
499 }
500 base, err := filepath.Abs(filepath.Dir(cluster.LocationOfOrigin))
501 if err != nil {
502 return fmt.Errorf("could not determine the absolute path of config file %s: %v", cluster.LocationOfOrigin, err)
503 }
504
505 if err := ResolvePaths(GetClusterFileReferences(cluster), base); err != nil {
506 return err
507 }
508 if err := RelativizePathWithNoBacksteps(GetClusterFileReferences(cluster), base); err != nil {
509 return err
510 }
511
512 return nil
513}
514
515// RelativizeAuthInfoLocalPaths first absolutizes the paths by calling ResolveLocalPaths. This assumes that any NEW path is already
516// absolute, but any existing path will be resolved relative to LocationOfOrigin
517func RelativizeAuthInfoLocalPaths(authInfo *clientcmdapi.AuthInfo) error {
518 if len(authInfo.LocationOfOrigin) == 0 {
519 return fmt.Errorf("no location of origin for %v", authInfo)
520 }
521 base, err := filepath.Abs(filepath.Dir(authInfo.LocationOfOrigin))
522 if err != nil {
523 return fmt.Errorf("could not determine the absolute path of config file %s: %v", authInfo.LocationOfOrigin, err)
524 }
525
526 if err := ResolvePaths(GetAuthInfoFileReferences(authInfo), base); err != nil {
527 return err
528 }
529 if err := RelativizePathWithNoBacksteps(GetAuthInfoFileReferences(authInfo), base); err != nil {
530 return err
531 }
532
533 return nil
534}
535
536func RelativizeConfigPaths(config *clientcmdapi.Config, base string) error {
537 return RelativizePathWithNoBacksteps(GetConfigFileReferences(config), base)
538}
539
540func ResolveConfigPaths(config *clientcmdapi.Config, base string) error {
541 return ResolvePaths(GetConfigFileReferences(config), base)
542}
543
544func GetConfigFileReferences(config *clientcmdapi.Config) []*string {
545 refs := []*string{}
546
547 for _, cluster := range config.Clusters {
548 refs = append(refs, GetClusterFileReferences(cluster)...)
549 }
550 for _, authInfo := range config.AuthInfos {
551 refs = append(refs, GetAuthInfoFileReferences(authInfo)...)
552 }
553
554 return refs
555}
556
557func GetClusterFileReferences(cluster *clientcmdapi.Cluster) []*string {
558 return []*string{&cluster.CertificateAuthority}
559}
560
561func GetAuthInfoFileReferences(authInfo *clientcmdapi.AuthInfo) []*string {
562 s := []*string{&authInfo.ClientCertificate, &authInfo.ClientKey, &authInfo.TokenFile}
563 // Only resolve exec command if it isn't PATH based.
564 if authInfo.Exec != nil && strings.ContainsRune(authInfo.Exec.Command, filepath.Separator) {
565 s = append(s, &authInfo.Exec.Command)
566 }
567 return s
568}
569
570// ResolvePaths updates the given refs to be absolute paths, relative to the given base directory
571func ResolvePaths(refs []*string, base string) error {
572 for _, ref := range refs {
573 // Don't resolve empty paths
574 if len(*ref) > 0 {
575 // Don't resolve absolute paths
576 if !filepath.IsAbs(*ref) {
577 *ref = filepath.Join(base, *ref)
578 }
579 }
580 }
581 return nil
582}
583
584// RelativizePathWithNoBacksteps updates the given refs to be relative paths, relative to the given base directory as long as they do not require backsteps.
585// 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
586func RelativizePathWithNoBacksteps(refs []*string, base string) error {
587 for _, ref := range refs {
588 // Don't relativize empty paths
589 if len(*ref) > 0 {
590 rel, err := MakeRelative(*ref, base)
591 if err != nil {
592 return err
593 }
594
595 // if we have a backstep, don't mess with the path
596 if strings.HasPrefix(rel, "../") {
597 if filepath.IsAbs(*ref) {
598 continue
599 }
600
601 return fmt.Errorf("%v requires backsteps and is not absolute", *ref)
602 }
603
604 *ref = rel
605 }
606 }
607 return nil
608}
609
610func MakeRelative(path, base string) (string, error) {
611 if len(path) > 0 {
612 rel, err := filepath.Rel(base, path)
613 if err != nil {
614 return path, err
615 }
616 return rel, nil
617 }
618 return path, nil
619}
620
621// deduplicate removes any duplicated values and returns a new slice, keeping the order unchanged
622func deduplicate(s []string) []string {
623 encountered := map[string]bool{}
624 ret := make([]string, 0)
625 for i := range s {
626 if encountered[s[i]] {
627 continue
628 }
629 encountered[s[i]] = true
630 ret = append(ret, s[i])
631 }
632 return ret
633}