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

Commit 328b2d1

Browse files
committed
feat: merging kubeconfig files
1 parent bd9a852 commit 328b2d1

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
@@ -14,10 +14,12 @@
1414

1515
import atexit
1616
import base64
17+
import copy
1718
import datetime
1819
import json
1920
import logging
2021
import os
22+
import platform
2123
import tempfile
2224
import time
2325

@@ -38,6 +40,7 @@
3840

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

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

Comments
 (0)