14
14
15
15
import atexit
16
16
import base64
17
+ import copy
17
18
import datetime
18
19
import json
19
20
import logging
20
21
import os
22
+ import platform
21
23
import tempfile
22
24
import time
23
25
38
40
39
41
EXPIRY_SKEW_PREVENTION_DELAY = datetime .timedelta (minutes = 5 )
40
42
KUBE_CONFIG_DEFAULT_LOCATION = os .environ .get ('KUBECONFIG' , '~/.kube/config' )
43
+ ENV_KUBECONFIG_PATH_SEPARATOR = ';' if platform .system () == 'Windows' else ':'
41
44
_temp_files = {}
42
45
43
46
@@ -132,7 +135,12 @@ def __init__(self, config_dict, active_context=None,
132
135
get_google_credentials = None ,
133
136
config_base_path = "" ,
134
137
config_persister = None ):
135
- self ._config = ConfigNode ('kube-config' , config_dict )
138
+
139
+ if isinstance (config_dict , ConfigNode ):
140
+ self ._config = config_dict
141
+ else :
142
+ self ._config = ConfigNode ('kube-config' , config_dict )
143
+
136
144
self ._current_context = None
137
145
self ._user = None
138
146
self ._cluster = None
@@ -361,9 +369,10 @@ def _load_from_exec_plugin(self):
361
369
logging .error (str (e ))
362
370
363
371
def _load_user_token (self ):
372
+ base_path = self ._get_base_path (self ._user .path )
364
373
token = FileOrData (
365
374
self ._user , 'tokenFile' , 'token' ,
366
- file_base_path = self . _config_base_path ,
375
+ file_base_path = base_path ,
367
376
base64_file_content = False ).as_data ()
368
377
if token :
369
378
self .token = "Bearer %s" % token
@@ -376,19 +385,27 @@ def _load_user_pass_token(self):
376
385
self ._user ['password' ])).get ('authorization' )
377
386
return True
378
387
388
+ def _get_base_path (self , config_path ):
389
+ if self ._config_base_path is not None :
390
+ return self ._config_base_path
391
+ if config_path is not None :
392
+ return os .path .abspath (os .path .dirname (config_path ))
393
+ return ""
394
+
379
395
def _load_cluster_info (self ):
380
396
if 'server' in self ._cluster :
381
397
self .host = self ._cluster ['server' ].rstrip ('/' )
382
398
if self .host .startswith ("https" ):
399
+ base_path = self ._get_base_path (self ._cluster .path )
383
400
self .ssl_ca_cert = FileOrData (
384
401
self ._cluster , 'certificate-authority' ,
385
- file_base_path = self . _config_base_path ).as_file ()
402
+ file_base_path = base_path ).as_file ()
386
403
self .cert_file = FileOrData (
387
404
self ._user , 'client-certificate' ,
388
- file_base_path = self . _config_base_path ).as_file ()
405
+ file_base_path = base_path ).as_file ()
389
406
self .key_file = FileOrData (
390
407
self ._user , 'client-key' ,
391
- file_base_path = self . _config_base_path ).as_file ()
408
+ file_base_path = base_path ).as_file ()
392
409
if 'insecure-skip-tls-verify' in self ._cluster :
393
410
self .verify_ssl = not self ._cluster ['insecure-skip-tls-verify' ]
394
411
@@ -435,9 +452,10 @@ class ConfigNode(object):
435
452
message in case of missing keys. The assumption is all access keys are
436
453
present in a well-formed kube-config."""
437
454
438
- def __init__ (self , name , value ):
455
+ def __init__ (self , name , value , path = None ):
439
456
self .name = name
440
457
self .value = value
458
+ self .path = path
441
459
442
460
def __contains__ (self , key ):
443
461
return key in self .value
@@ -457,7 +475,7 @@ def __getitem__(self, key):
457
475
'Invalid kube-config file. Expected key %s in %s'
458
476
% (key , self .name ))
459
477
if isinstance (v , dict ) or isinstance (v , list ):
460
- return ConfigNode ('%s/%s' % (self .name , key ), v )
478
+ return ConfigNode ('%s/%s' % (self .name , key ), v , self . path )
461
479
else :
462
480
return v
463
481
@@ -482,26 +500,100 @@ def get_with_name(self, name, safe=False):
482
500
'Expected only one object with name %s in %s list'
483
501
% (name , self .name ))
484
502
if result is not None :
485
- return ConfigNode ('%s[name=%s]' % (self .name , name ), result )
503
+ if isinstance (result , ConfigNode ):
504
+ return result
505
+ else :
506
+ return ConfigNode (
507
+ '%s[name=%s]' %
508
+ (self .name , name ), result , self .path )
486
509
if safe :
487
510
return None
488
511
raise ConfigException (
489
512
'Invalid kube-config file. '
490
513
'Expected object with name %s in %s list' % (name , self .name ))
491
514
492
515
493
- def _get_kube_config_loader_for_yaml_file (filename , ** kwargs ):
494
- with open (filename ) as f :
495
- return KubeConfigLoader (
496
- config_dict = yaml .safe_load (f ),
497
- config_base_path = os .path .abspath (os .path .dirname (filename )),
498
- ** kwargs )
516
+ class KubeConfigMerger :
517
+
518
+ """Reads and merges configuration from one or more kube-config's.
519
+ The propery `config` can be passed to the KubeConfigLoader as config_dict.
520
+
521
+ It uses a path attribute from ConfigNode to store the path to kubeconfig.
522
+ This path is required to load certs from relative paths.
523
+
524
+ A method `save_changes` updates changed kubeconfig's (it compares current
525
+ state of dicts with).
526
+ """
527
+
528
+ def __init__ (self , paths ):
529
+ self .paths = []
530
+ self .config_files = {}
531
+ self .config_merged = None
532
+
533
+ for path in paths .split (ENV_KUBECONFIG_PATH_SEPARATOR ):
534
+ if path :
535
+ path = os .path .expanduser (path )
536
+ if os .path .exists (path ):
537
+ self .paths .append (path )
538
+ self .load_config (path )
539
+ self .config_saved = copy .deepcopy (self .config_files )
540
+
541
+ @property
542
+ def config (self ):
543
+ return self .config_merged
544
+
545
+ def load_config (self , path ):
546
+ with open (path ) as f :
547
+ config = yaml .safe_load (f )
548
+
549
+ if self .config_merged is None :
550
+ config_merged = copy .deepcopy (config )
551
+ for item in ('clusters' , 'contexts' , 'users' ):
552
+ config_merged [item ] = []
553
+ self .config_merged = ConfigNode (path , config_merged , path )
554
+
555
+ for item in ('clusters' , 'contexts' , 'users' ):
556
+ self ._merge (item , config [item ], path )
557
+ self .config_files [path ] = config
558
+
559
+ def _merge (self , item , add_cfg , path ):
560
+ for new_item in add_cfg :
561
+ for exists in self .config_merged .value [item ]:
562
+ if exists ['name' ] == new_item ['name' ]:
563
+ break
564
+ else :
565
+ self .config_merged .value [item ].append (ConfigNode (
566
+ '{}/{}' .format (path , new_item ), new_item , path ))
567
+
568
+ def save_changes (self ):
569
+ for path in self .paths :
570
+ if self .config_saved [path ] != self .config_files [path ]:
571
+ self .save_config (path )
572
+ self .config_saved = copy .deepcopy (self .config_files )
573
+
574
+ def save_config (self , path ):
575
+ with open (path , 'w' ) as f :
576
+ yaml .safe_dump (self .config_files [path ], f ,
577
+ default_flow_style = False )
578
+
579
+
580
+ def _get_kube_config_loader_for_yaml_file (
581
+ filename , persist_config = False , ** kwargs ):
582
+
583
+ kcfg = KubeConfigMerger (filename )
584
+ if persist_config and 'config_persister' not in kwargs :
585
+ kwargs ['config_persister' ] = kcfg .save_changes ()
586
+
587
+ return KubeConfigLoader (
588
+ config_dict = kcfg .config ,
589
+ config_base_path = None ,
590
+ ** kwargs )
499
591
500
592
501
593
def list_kube_config_contexts (config_file = None ):
502
594
503
595
if config_file is None :
504
- config_file = os . path . expanduser ( KUBE_CONFIG_DEFAULT_LOCATION )
596
+ config_file = KUBE_CONFIG_DEFAULT_LOCATION
505
597
506
598
loader = _get_kube_config_loader_for_yaml_file (config_file )
507
599
return loader .list_contexts (), loader .current_context
@@ -523,18 +615,12 @@ def load_kube_config(config_file=None, context=None,
523
615
"""
524
616
525
617
if config_file is None :
526
- config_file = os .path .expanduser (KUBE_CONFIG_DEFAULT_LOCATION )
527
-
528
- config_persister = None
529
- if persist_config :
530
- def _save_kube_config (config_map ):
531
- with open (config_file , 'w' ) as f :
532
- yaml .safe_dump (config_map , f , default_flow_style = False )
533
- config_persister = _save_kube_config
618
+ config_file = KUBE_CONFIG_DEFAULT_LOCATION
534
619
535
620
loader = _get_kube_config_loader_for_yaml_file (
536
621
config_file , active_context = context ,
537
- config_persister = config_persister )
622
+ persist_config = persist_config )
623
+
538
624
if client_configuration is None :
539
625
config = type .__call__ (Configuration )
540
626
loader .load_and_set (config )
0 commit comments