Skip to content
This repository was archived by the owner on Mar 13, 2022. It is now read-only.

Commit 5c03b3b

Browse files
authored
Merge pull request #94 from tomplus/feat/merge-kubeconfigs
feat: merging kubeconfig files
2 parents c4de8bd + 328b2d1 commit 5c03b3b

File tree

2 files changed

+274
-25
lines changed

2 files changed

+274
-25
lines changed

config/kube_config.py

+110-24
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@
1616

1717
import atexit
1818
import base64
19+
import copy
1920
import datetime
2021
import json
2122
import logging
2223
import os
24+
import platform
2325
import tempfile
2426
import time
2527

@@ -44,6 +46,7 @@
4446

4547
EXPIRY_SKEW_PREVENTION_DELAY = datetime.timedelta(minutes=5)
4648
KUBE_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

510602
def 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

Comments
 (0)