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' ]
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
@@ -419,9 +436,10 @@ class ConfigNode(object):
419
436
message in case of missing keys. The assumption is all access keys are
420
437
present in a well-formed kube-config."""
421
438
422
- def __init__ (self , name , value ):
439
+ def __init__ (self , name , value , path = None ):
423
440
self .name = name
424
441
self .value = value
442
+ self .path = path
425
443
426
444
def __contains__ (self , key ):
427
445
return key in self .value
@@ -441,7 +459,7 @@ def __getitem__(self, key):
441
459
'Invalid kube-config file. Expected key %s in %s'
442
460
% (key , self .name ))
443
461
if isinstance (v , dict ) or isinstance (v , list ):
444
- return ConfigNode ('%s/%s' % (self .name , key ), v )
462
+ return ConfigNode ('%s/%s' % (self .name , key ), v , self . path )
445
463
else :
446
464
return v
447
465
@@ -466,26 +484,100 @@ def get_with_name(self, name, safe=False):
466
484
'Expected only one object with name %s in %s list'
467
485
% (name , self .name ))
468
486
if result is not None :
469
- return ConfigNode ('%s[name=%s]' % (self .name , name ), result )
487
+ if isinstance (result , ConfigNode ):
488
+ return result
489
+ else :
490
+ return ConfigNode (
491
+ '%s[name=%s]' %
492
+ (self .name , name ), result , self .path )
470
493
if safe :
471
494
return None
472
495
raise ConfigException (
473
496
'Invalid kube-config file. '
474
497
'Expected object with name %s in %s list' % (name , self .name ))
475
498
476
499
477
- def _get_kube_config_loader_for_yaml_file (filename , ** kwargs ):
478
- with open (filename ) as f :
479
- return KubeConfigLoader (
480
- config_dict = yaml .load (f ),
481
- config_base_path = os .path .abspath (os .path .dirname (filename )),
482
- ** kwargs )
500
+ class KubeConfigMerger :
501
+
502
+ """Reads and merges configuration from one or more kube-config's.
503
+ The propery `config` can be passed to the KubeConfigLoader as config_dict.
504
+
505
+ It uses a path attribute from ConfigNode to store the path to kubeconfig.
506
+ This path is required to load certs from relative paths.
507
+
508
+ A method `save_changes` updates changed kubeconfig's (it compares current
509
+ state of dicts with).
510
+ """
511
+
512
+ def __init__ (self , paths ):
513
+ self .paths = []
514
+ self .config_files = {}
515
+ self .config_merged = None
516
+
517
+ for path in paths .split (ENV_KUBECONFIG_PATH_SEPARATOR ):
518
+ if path :
519
+ path = os .path .expanduser (path )
520
+ if os .path .exists (path ):
521
+ self .paths .append (path )
522
+ self .load_config (path )
523
+ self .config_saved = copy .deepcopy (self .config_files )
524
+
525
+ @property
526
+ def config (self ):
527
+ return self .config_merged
528
+
529
+ def load_config (self , path ):
530
+ with open (path ) as f :
531
+ config = yaml .load (f )
532
+
533
+ if self .config_merged is None :
534
+ config_merged = copy .deepcopy (config )
535
+ for item in ('clusters' , 'contexts' , 'users' ):
536
+ config_merged [item ] = []
537
+ self .config_merged = ConfigNode (path , config_merged , path )
538
+
539
+ for item in ('clusters' , 'contexts' , 'users' ):
540
+ self ._merge (item , config [item ], path )
541
+ self .config_files [path ] = config
542
+
543
+ def _merge (self , item , add_cfg , path ):
544
+ for new_item in add_cfg :
545
+ for exists in self .config_merged .value [item ]:
546
+ if exists ['name' ] == new_item ['name' ]:
547
+ break
548
+ else :
549
+ self .config_merged .value [item ].append (ConfigNode (
550
+ '{}/{}' .format (path , new_item ), new_item , path ))
551
+
552
+ def save_changes (self ):
553
+ for path in self .paths :
554
+ if self .config_saved [path ] != self .config_files [path ]:
555
+ self .save_config (path )
556
+ self .config_saved = copy .deepcopy (self .config_files )
557
+
558
+ def save_config (self , path ):
559
+ with open (path , 'w' ) as f :
560
+ yaml .safe_dump (self .config_files [path ], f ,
561
+ default_flow_style = False )
562
+
563
+
564
+ def _get_kube_config_loader_for_yaml_file (
565
+ filename , persist_config = False , ** kwargs ):
566
+
567
+ kcfg = KubeConfigMerger (filename )
568
+ if persist_config and 'config_persister' not in kwargs :
569
+ kwargs ['config_persister' ] = kcfg .save_changes ()
570
+
571
+ return KubeConfigLoader (
572
+ config_dict = kcfg .config ,
573
+ config_base_path = None ,
574
+ ** kwargs )
483
575
484
576
485
577
def list_kube_config_contexts (config_file = None ):
486
578
487
579
if config_file is None :
488
- config_file = os . path . expanduser ( KUBE_CONFIG_DEFAULT_LOCATION )
580
+ config_file = KUBE_CONFIG_DEFAULT_LOCATION
489
581
490
582
loader = _get_kube_config_loader_for_yaml_file (config_file )
491
583
return loader .list_contexts (), loader .current_context
@@ -507,18 +599,12 @@ def load_kube_config(config_file=None, context=None,
507
599
"""
508
600
509
601
if config_file is None :
510
- config_file = os .path .expanduser (KUBE_CONFIG_DEFAULT_LOCATION )
511
-
512
- config_persister = None
513
- if persist_config :
514
- def _save_kube_config (config_map ):
515
- with open (config_file , 'w' ) as f :
516
- yaml .safe_dump (config_map , f , default_flow_style = False )
517
- config_persister = _save_kube_config
602
+ config_file = KUBE_CONFIG_DEFAULT_LOCATION
518
603
519
604
loader = _get_kube_config_loader_for_yaml_file (
520
605
config_file , active_context = context ,
521
- config_persister = config_persister )
606
+ persist_config = persist_config )
607
+
522
608
if client_configuration is None :
523
609
config = type .__call__ (Configuration )
524
610
loader .load_and_set (config )
0 commit comments