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

Commit d7f8138

Browse files
committed
feat: merging kubeconfig files
1 parent 3682e9b commit d7f8138

File tree

2 files changed

+274
-25
lines changed

2 files changed

+274
-25
lines changed

Diff for: 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']
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

@@ -419,9 +436,10 @@ class ConfigNode(object):
419436
message in case of missing keys. The assumption is all access keys are
420437
present in a well-formed kube-config."""
421438

422-
def __init__(self, name, value):
439+
def __init__(self, name, value, path=None):
423440
self.name = name
424441
self.value = value
442+
self.path = path
425443

426444
def __contains__(self, key):
427445
return key in self.value
@@ -441,7 +459,7 @@ def __getitem__(self, key):
441459
'Invalid kube-config file. Expected key %s in %s'
442460
% (key, self.name))
443461
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)
445463
else:
446464
return v
447465

@@ -466,26 +484,100 @@ def get_with_name(self, name, safe=False):
466484
'Expected only one object with name %s in %s list'
467485
% (name, self.name))
468486
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)
470493
if safe:
471494
return None
472495
raise ConfigException(
473496
'Invalid kube-config file. '
474497
'Expected object with name %s in %s list' % (name, self.name))
475498

476499

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)
483575

484576

485577
def list_kube_config_contexts(config_file=None):
486578

487579
if config_file is None:
488-
config_file = os.path.expanduser(KUBE_CONFIG_DEFAULT_LOCATION)
580+
config_file = KUBE_CONFIG_DEFAULT_LOCATION
489581

490582
loader = _get_kube_config_loader_for_yaml_file(config_file)
491583
return loader.list_contexts(), loader.current_context
@@ -507,18 +599,12 @@ def load_kube_config(config_file=None, context=None,
507599
"""
508600

509601
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
518603

519604
loader = _get_kube_config_loader_for_yaml_file(
520605
config_file, active_context=context,
521-
config_persister=config_persister)
606+
persist_config=persist_config)
607+
522608
if client_configuration is None:
523609
config = type.__call__(Configuration)
524610
loader.load_and_set(config)

0 commit comments

Comments
 (0)