1616
1717import atexit
1818import base64
19+ import copy
1920import datetime
2021import json
2122import logging
2223import os
24+ import platform
2325import tempfile
2426import time
2527
4446
4547EXPIRY_SKEW_PREVENTION_DELAY = datetime .timedelta (minutes = 5 )
4648KUBE_CONFIG_DEFAULT_LOCATION = os .environ .get ('KUBECONFIG' , '~/.kube/config' )
49+ ENV_KUBECONFIG_PATH_SEPARATOR = ';' if platform .system () == 'Windows' else ':'
4750_temp_files = {}
4851
4952
@@ -138,7 +141,12 @@ def __init__(self, config_dict, active_context=None,
138141 get_google_credentials = None ,
139142 config_base_path = "" ,
140143 config_persister = None ):
141- self ._config = ConfigNode ('kube-config' , config_dict )
144+
145+ if isinstance (config_dict , ConfigNode ):
146+ self ._config = config_dict
147+ else :
148+ self ._config = ConfigNode ('kube-config' , config_dict )
149+
142150 self ._current_context = None
143151 self ._user = None
144152 self ._cluster = None
@@ -370,9 +378,10 @@ def _load_from_exec_plugin(self):
370378 logging .error (str (e ))
371379
372380 def _load_user_token (self ):
381+ base_path = self ._get_base_path (self ._user .path )
373382 token = FileOrData (
374383 self ._user , 'tokenFile' , 'token' ,
375- file_base_path = self . _config_base_path ,
384+ file_base_path = base_path ,
376385 base64_file_content = False ).as_data ()
377386 if token :
378387 self .token = "Bearer %s" % token
@@ -385,19 +394,27 @@ def _load_user_pass_token(self):
385394 self ._user ['password' ])).get ('authorization' )
386395 return True
387396
397+ def _get_base_path (self , config_path ):
398+ if self ._config_base_path is not None :
399+ return self ._config_base_path
400+ if config_path is not None :
401+ return os .path .abspath (os .path .dirname (config_path ))
402+ return ""
403+
388404 def _load_cluster_info (self ):
389405 if 'server' in self ._cluster :
390406 self .host = self ._cluster ['server' ].rstrip ('/' )
391407 if self .host .startswith ("https" ):
408+ base_path = self ._get_base_path (self ._cluster .path )
392409 self .ssl_ca_cert = FileOrData (
393410 self ._cluster , 'certificate-authority' ,
394- file_base_path = self . _config_base_path ).as_file ()
411+ file_base_path = base_path ).as_file ()
395412 self .cert_file = FileOrData (
396413 self ._user , 'client-certificate' ,
397- file_base_path = self . _config_base_path ).as_file ()
414+ file_base_path = base_path ).as_file ()
398415 self .key_file = FileOrData (
399416 self ._user , 'client-key' ,
400- file_base_path = self . _config_base_path ).as_file ()
417+ file_base_path = base_path ).as_file ()
401418 if 'insecure-skip-tls-verify' in self ._cluster :
402419 self .verify_ssl = not self ._cluster ['insecure-skip-tls-verify' ]
403420
@@ -444,9 +461,10 @@ class ConfigNode(object):
444461 message in case of missing keys. The assumption is all access keys are
445462 present in a well-formed kube-config."""
446463
447- def __init__ (self , name , value ):
464+ def __init__ (self , name , value , path = None ):
448465 self .name = name
449466 self .value = value
467+ self .path = path
450468
451469 def __contains__ (self , key ):
452470 return key in self .value
@@ -466,7 +484,7 @@ def __getitem__(self, key):
466484 'Invalid kube-config file. Expected key %s in %s'
467485 % (key , self .name ))
468486 if isinstance (v , dict ) or isinstance (v , list ):
469- return ConfigNode ('%s/%s' % (self .name , key ), v )
487+ return ConfigNode ('%s/%s' % (self .name , key ), v , self . path )
470488 else :
471489 return v
472490
@@ -491,26 +509,100 @@ def get_with_name(self, name, safe=False):
491509 'Expected only one object with name %s in %s list'
492510 % (name , self .name ))
493511 if result is not None :
494- return ConfigNode ('%s[name=%s]' % (self .name , name ), result )
512+ if isinstance (result , ConfigNode ):
513+ return result
514+ else :
515+ return ConfigNode (
516+ '%s[name=%s]' %
517+ (self .name , name ), result , self .path )
495518 if safe :
496519 return None
497520 raise ConfigException (
498521 'Invalid kube-config file. '
499522 'Expected object with name %s in %s list' % (name , self .name ))
500523
501524
502- def _get_kube_config_loader_for_yaml_file (filename , ** kwargs ):
503- with open (filename ) as f :
504- return KubeConfigLoader (
505- config_dict = yaml .safe_load (f ),
506- config_base_path = os .path .abspath (os .path .dirname (filename )),
507- ** kwargs )
525+ class KubeConfigMerger :
526+
527+ """Reads and merges configuration from one or more kube-config's.
528+ The propery `config` can be passed to the KubeConfigLoader as config_dict.
529+
530+ It uses a path attribute from ConfigNode to store the path to kubeconfig.
531+ This path is required to load certs from relative paths.
532+
533+ A method `save_changes` updates changed kubeconfig's (it compares current
534+ state of dicts with).
535+ """
536+
537+ def __init__ (self , paths ):
538+ self .paths = []
539+ self .config_files = {}
540+ self .config_merged = None
541+
542+ for path in paths .split (ENV_KUBECONFIG_PATH_SEPARATOR ):
543+ if path :
544+ path = os .path .expanduser (path )
545+ if os .path .exists (path ):
546+ self .paths .append (path )
547+ self .load_config (path )
548+ self .config_saved = copy .deepcopy (self .config_files )
549+
550+ @property
551+ def config (self ):
552+ return self .config_merged
553+
554+ def load_config (self , path ):
555+ with open (path ) as f :
556+ config = yaml .safe_load (f )
557+
558+ if self .config_merged is None :
559+ config_merged = copy .deepcopy (config )
560+ for item in ('clusters' , 'contexts' , 'users' ):
561+ config_merged [item ] = []
562+ self .config_merged = ConfigNode (path , config_merged , path )
563+
564+ for item in ('clusters' , 'contexts' , 'users' ):
565+ self ._merge (item , config [item ], path )
566+ self .config_files [path ] = config
567+
568+ def _merge (self , item , add_cfg , path ):
569+ for new_item in add_cfg :
570+ for exists in self .config_merged .value [item ]:
571+ if exists ['name' ] == new_item ['name' ]:
572+ break
573+ else :
574+ self .config_merged .value [item ].append (ConfigNode (
575+ '{}/{}' .format (path , new_item ), new_item , path ))
576+
577+ def save_changes (self ):
578+ for path in self .paths :
579+ if self .config_saved [path ] != self .config_files [path ]:
580+ self .save_config (path )
581+ self .config_saved = copy .deepcopy (self .config_files )
582+
583+ def save_config (self , path ):
584+ with open (path , 'w' ) as f :
585+ yaml .safe_dump (self .config_files [path ], f ,
586+ default_flow_style = False )
587+
588+
589+ def _get_kube_config_loader_for_yaml_file (
590+ filename , persist_config = False , ** kwargs ):
591+
592+ kcfg = KubeConfigMerger (filename )
593+ if persist_config and 'config_persister' not in kwargs :
594+ kwargs ['config_persister' ] = kcfg .save_changes ()
595+
596+ return KubeConfigLoader (
597+ config_dict = kcfg .config ,
598+ config_base_path = None ,
599+ ** kwargs )
508600
509601
510602def list_kube_config_contexts (config_file = None ):
511603
512604 if config_file is None :
513- config_file = os . path . expanduser ( KUBE_CONFIG_DEFAULT_LOCATION )
605+ config_file = KUBE_CONFIG_DEFAULT_LOCATION
514606
515607 loader = _get_kube_config_loader_for_yaml_file (config_file )
516608 return loader .list_contexts (), loader .current_context
@@ -532,18 +624,12 @@ def load_kube_config(config_file=None, context=None,
532624 """
533625
534626 if config_file is None :
535- config_file = os .path .expanduser (KUBE_CONFIG_DEFAULT_LOCATION )
536-
537- config_persister = None
538- if persist_config :
539- def _save_kube_config (config_map ):
540- with open (config_file , 'w' ) as f :
541- yaml .safe_dump (config_map , f , default_flow_style = False )
542- config_persister = _save_kube_config
627+ config_file = KUBE_CONFIG_DEFAULT_LOCATION
543628
544629 loader = _get_kube_config_loader_for_yaml_file (
545630 config_file , active_context = context ,
546- config_persister = config_persister )
631+ persist_config = persist_config )
632+
547633 if client_configuration is None :
548634 config = type .__call__ (Configuration )
549635 loader .load_and_set (config )
0 commit comments