1
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
54
55
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
71 IsDefaultConfig(*restclient.Config) bool
72
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
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
109
110
111
112 type ClientConfigLoadingRules struct {
113 ExplicitPath string
114 Precedence []string
115
116
117
118 MigrationRules map[string]string
119
120
121
122 DoNotResolvePaths bool
123
124
125
126 DefaultClientConfig ClientConfig
127
128
129
130 WarnIfAllMissing bool
131
132
133 Warner WarningHandler
134 }
135
136
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
156 var _ ClientConfigLoader = &ClientConfigLoadingRules{}
157
158
159
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
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
183
184
185
186
187
188
189
190
191
192
193
194
195
196
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
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
220 for _, filename := range kubeConfigFiles {
221 if len(filename) == 0 {
222
223 continue
224 }
225
226 config, err := LoadFromFile(filename)
227
228 if os.IsNotExist(err) {
229
230
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
248 mapConfig := clientcmdapi.NewConfig()
249
250 for _, kubeconfig := range kubeconfigs {
251 mergo.Merge(mapConfig, kubeconfig, mergo.WithOverride)
252 }
253
254
255
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
263
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
277
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
286 continue
287 } else if os.IsPermission(err) {
288
289 continue
290 } else if !os.IsNotExist(err) {
291
292 return err
293 }
294
295 if sourceInfo, err := os.Stat(source); err != nil {
296 if os.IsNotExist(err) || os.IsPermission(err) {
297
298 continue
299 }
300
301
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
312 err = os.WriteFile(destination, data, 0666)
313 if err != nil {
314 return err
315 }
316 }
317
318 return nil
319 }
320
321
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
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
345 func (rules *ClientConfigLoadingRules) GetDefaultFilename() string {
346
347 if rules.IsExplicitFile() {
348 return rules.GetExplicitFile()
349 }
350
351 for _, filename := range rules.GetLoadingPrecedence() {
352 if _, err := os.Stat(filename); err == nil {
353 return filename
354 }
355 }
356
357 if len(rules.Precedence) > 0 {
358 return rules.Precedence[0]
359 }
360 return ""
361 }
362
363
364 func (rules *ClientConfigLoadingRules) IsExplicitFile() bool {
365 return len(rules.ExplicitPath) > 0
366 }
367
368
369 func (rules *ClientConfigLoadingRules) GetExplicitFile() string {
370 return rules.ExplicitPath
371 }
372
373
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
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
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
425
426 func Load(data []byte) (*clientcmdapi.Config, error) {
427 config := clientcmdapi.NewConfig()
428
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
440
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
461
462
463
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
487
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
497
498
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
531
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
552
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
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
607 func ResolvePaths(refs []*string, base string) error {
608 for _, ref := range refs {
609
610 if len(*ref) > 0 {
611
612 if !filepath.IsAbs(*ref) {
613 *ref = filepath.Join(base, *ref)
614 }
615 }
616 }
617 return nil
618 }
619
620
621
622 func RelativizePathWithNoBacksteps(refs []*string, base string) error {
623 for _, ref := range refs {
624
625 if len(*ref) > 0 {
626 rel, err := MakeRelative(*ref, base)
627 if err != nil {
628 return err
629 }
630
631
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
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 }
670
View as plain text