1414
1515import atexit
1616import base64
17+ import copy
1718import datetime
1819import json
1920import logging
2021import os
22+ import platform
2123import tempfile
2224import time
2325
3840
3941EXPIRY_SKEW_PREVENTION_DELAY = datetime .timedelta (minutes = 5 )
4042KUBE_CONFIG_DEFAULT_LOCATION = os .environ .get ('KUBECONFIG' , '~/.kube/config' )
43+ ENV_KUBECONFIG_PATH_SEPARATOR = ';' if platform .system () == 'Windows' else ':'
4144_temp_files = {}
4245
4346
@@ -132,7 +135,12 @@ def __init__(self, config_dict, active_context=None,
132135 get_google_credentials = None ,
133136 config_base_path = "" ,
134137 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+
136144 self ._current_context = None
137145 self ._user = None
138146 self ._cluster = None
@@ -361,9 +369,10 @@ def _load_from_exec_plugin(self):
361369 logging .error (str (e ))
362370
363371 def _load_user_token (self ):
372+ base_path = self ._get_base_path (self ._user .path )
364373 token = FileOrData (
365374 self ._user , 'tokenFile' , 'token' ,
366- file_base_path = self . _config_base_path ,
375+ file_base_path = base_path ,
367376 base64_file_content = False ).as_data ()
368377 if token :
369378 self .token = "Bearer %s" % token
@@ -376,19 +385,27 @@ def _load_user_pass_token(self):
376385 self ._user ['password' ])).get ('authorization' )
377386 return True
378387
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+
379395 def _load_cluster_info (self ):
380396 if 'server' in self ._cluster :
381397 self .host = self ._cluster ['server' ].rstrip ('/' )
382398 if self .host .startswith ("https" ):
399+ base_path = self ._get_base_path (self ._cluster .path )
383400 self .ssl_ca_cert = FileOrData (
384401 self ._cluster , 'certificate-authority' ,
385- file_base_path = self . _config_base_path ).as_file ()
402+ file_base_path = base_path ).as_file ()
386403 self .cert_file = FileOrData (
387404 self ._user , 'client-certificate' ,
388- file_base_path = self . _config_base_path ).as_file ()
405+ file_base_path = base_path ).as_file ()
389406 self .key_file = FileOrData (
390407 self ._user , 'client-key' ,
391- file_base_path = self . _config_base_path ).as_file ()
408+ file_base_path = base_path ).as_file ()
392409 if 'insecure-skip-tls-verify' in self ._cluster :
393410 self .verify_ssl = not self ._cluster ['insecure-skip-tls-verify' ]
394411
@@ -435,9 +452,10 @@ class ConfigNode(object):
435452 message in case of missing keys. The assumption is all access keys are
436453 present in a well-formed kube-config."""
437454
438- def __init__ (self , name , value ):
455+ def __init__ (self , name , value , path = None ):
439456 self .name = name
440457 self .value = value
458+ self .path = path
441459
442460 def __contains__ (self , key ):
443461 return key in self .value
@@ -457,7 +475,7 @@ def __getitem__(self, key):
457475 'Invalid kube-config file. Expected key %s in %s'
458476 % (key , self .name ))
459477 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 )
461479 else :
462480 return v
463481
@@ -482,26 +500,100 @@ def get_with_name(self, name, safe=False):
482500 'Expected only one object with name %s in %s list'
483501 % (name , self .name ))
484502 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 )
486509 if safe :
487510 return None
488511 raise ConfigException (
489512 'Invalid kube-config file. '
490513 'Expected object with name %s in %s list' % (name , self .name ))
491514
492515
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 )
499591
500592
501593def list_kube_config_contexts (config_file = None ):
502594
503595 if config_file is None :
504- config_file = os . path . expanduser ( KUBE_CONFIG_DEFAULT_LOCATION )
596+ config_file = KUBE_CONFIG_DEFAULT_LOCATION
505597
506598 loader = _get_kube_config_loader_for_yaml_file (config_file )
507599 return loader .list_contexts (), loader .current_context
@@ -523,18 +615,12 @@ def load_kube_config(config_file=None, context=None,
523615 """
524616
525617 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
534619
535620 loader = _get_kube_config_loader_for_yaml_file (
536621 config_file , active_context = context ,
537- config_persister = config_persister )
622+ persist_config = persist_config )
623+
538624 if client_configuration is None :
539625 config = type .__call__ (Configuration )
540626 loader .load_and_set (config )
0 commit comments