16
16
17
17
import atexit
18
18
import base64
19
+ import copy
19
20
import datetime
20
21
import json
21
22
import logging
22
23
import os
24
+ import platform
23
25
import tempfile
24
26
import time
25
27
44
46
45
47
EXPIRY_SKEW_PREVENTION_DELAY = datetime .timedelta (minutes = 5 )
46
48
KUBE_CONFIG_DEFAULT_LOCATION = os .environ .get ('KUBECONFIG' , '~/.kube/config' )
49
+ ENV_KUBECONFIG_PATH_SEPARATOR = ';' if platform .system () == 'Windows' else ':'
47
50
_temp_files = {}
48
51
49
52
@@ -138,7 +141,12 @@ def __init__(self, config_dict, active_context=None,
138
141
get_google_credentials = None ,
139
142
config_base_path = "" ,
140
143
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
+
142
150
self ._current_context = None
143
151
self ._user = None
144
152
self ._cluster = None
@@ -370,9 +378,10 @@ def _load_from_exec_plugin(self):
370
378
logging .error (str (e ))
371
379
372
380
def _load_user_token (self ):
381
+ base_path = self ._get_base_path (self ._user .path )
373
382
token = FileOrData (
374
383
self ._user , 'tokenFile' , 'token' ,
375
- file_base_path = self . _config_base_path ,
384
+ file_base_path = base_path ,
376
385
base64_file_content = False ).as_data ()
377
386
if token :
378
387
self .token = "Bearer %s" % token
@@ -385,19 +394,27 @@ def _load_user_pass_token(self):
385
394
self ._user ['password' ])).get ('authorization' )
386
395
return True
387
396
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
+
388
404
def _load_cluster_info (self ):
389
405
if 'server' in self ._cluster :
390
406
self .host = self ._cluster ['server' ].rstrip ('/' )
391
407
if self .host .startswith ("https" ):
408
+ base_path = self ._get_base_path (self ._cluster .path )
392
409
self .ssl_ca_cert = FileOrData (
393
410
self ._cluster , 'certificate-authority' ,
394
- file_base_path = self . _config_base_path ).as_file ()
411
+ file_base_path = base_path ).as_file ()
395
412
self .cert_file = FileOrData (
396
413
self ._user , 'client-certificate' ,
397
- file_base_path = self . _config_base_path ).as_file ()
414
+ file_base_path = base_path ).as_file ()
398
415
self .key_file = FileOrData (
399
416
self ._user , 'client-key' ,
400
- file_base_path = self . _config_base_path ).as_file ()
417
+ file_base_path = base_path ).as_file ()
401
418
if 'insecure-skip-tls-verify' in self ._cluster :
402
419
self .verify_ssl = not self ._cluster ['insecure-skip-tls-verify' ]
403
420
@@ -444,9 +461,10 @@ class ConfigNode(object):
444
461
message in case of missing keys. The assumption is all access keys are
445
462
present in a well-formed kube-config."""
446
463
447
- def __init__ (self , name , value ):
464
+ def __init__ (self , name , value , path = None ):
448
465
self .name = name
449
466
self .value = value
467
+ self .path = path
450
468
451
469
def __contains__ (self , key ):
452
470
return key in self .value
@@ -466,7 +484,7 @@ def __getitem__(self, key):
466
484
'Invalid kube-config file. Expected key %s in %s'
467
485
% (key , self .name ))
468
486
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 )
470
488
else :
471
489
return v
472
490
@@ -491,26 +509,100 @@ def get_with_name(self, name, safe=False):
491
509
'Expected only one object with name %s in %s list'
492
510
% (name , self .name ))
493
511
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 )
495
518
if safe :
496
519
return None
497
520
raise ConfigException (
498
521
'Invalid kube-config file. '
499
522
'Expected object with name %s in %s list' % (name , self .name ))
500
523
501
524
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 )
508
600
509
601
510
602
def list_kube_config_contexts (config_file = None ):
511
603
512
604
if config_file is None :
513
- config_file = os . path . expanduser ( KUBE_CONFIG_DEFAULT_LOCATION )
605
+ config_file = KUBE_CONFIG_DEFAULT_LOCATION
514
606
515
607
loader = _get_kube_config_loader_for_yaml_file (config_file )
516
608
return loader .list_contexts (), loader .current_context
@@ -532,18 +624,12 @@ def load_kube_config(config_file=None, context=None,
532
624
"""
533
625
534
626
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
543
628
544
629
loader = _get_kube_config_loader_for_yaml_file (
545
630
config_file , active_context = context ,
546
- config_persister = config_persister )
631
+ persist_config = persist_config )
632
+
547
633
if client_configuration is None :
548
634
config = type .__call__ (Configuration )
549
635
loader .load_and_set (config )
0 commit comments