Skip to content

Commit

Permalink
feat: merging kubeconfig files (#69)
Browse files Browse the repository at this point in the history
* feat: merging kubeconfig files
  • Loading branch information
tomplus authored Apr 12, 2019
1 parent 959ca9f commit 8a65d31
Show file tree
Hide file tree
Showing 2 changed files with 273 additions and 26 deletions.
129 changes: 106 additions & 23 deletions kubernetes_asyncio/config/kube_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@
import asyncio
import atexit
import base64
import copy
import datetime
import json
import logging
import os
import platform
import tempfile

import yaml
Expand All @@ -33,6 +35,7 @@

EXPIRY_SKEW_PREVENTION_DELAY = datetime.timedelta(minutes=5)
KUBE_CONFIG_DEFAULT_LOCATION = os.environ.get('KUBECONFIG', '~/.kube/config')
ENV_KUBECONFIG_PATH_SEPARATOR = ';' if platform.system() == 'Windows' else ':'
PROVIDER_TYPE_OIDC = 'oidc'
_temp_files = {}

Expand Down Expand Up @@ -128,7 +131,12 @@ def __init__(self, config_dict, active_context=None,
get_google_credentials=None,
config_base_path="",
config_persister=None):
self._config = ConfigNode('kube-config', config_dict)

if isinstance(config_dict, ConfigNode):
self._config = config_dict
else:
self._config = ConfigNode('kube-config', config_dict)

self._current_context = None
self._user = None
self._cluster = None
Expand Down Expand Up @@ -308,9 +316,10 @@ async def _load_from_exec_plugin(self):
logging.error(str(e))

def _load_user_token(self):
base_path = self._get_base_path(self._user.path)
token = FileOrData(
self._user, 'tokenFile', 'token',
file_base_path=self._config_base_path,
file_base_path=base_path,
base64_file_content=False).as_data()
if token:
self.token = "Bearer %s" % token
Expand All @@ -323,19 +332,27 @@ def _load_user_pass_token(self):
basic_auth.encode()).decode('utf-8')
return True

def _get_base_path(self, config_path):
if self._config_base_path is not None:
return self._config_base_path
if config_path is not None:
return os.path.abspath(os.path.dirname(config_path))
return ""

def _load_cluster_info(self):
if 'server' in self._cluster:
self.host = self._cluster['server'].rstrip('/')
if self.host.startswith("https"):
base_path = self._get_base_path(self._cluster.path)
self.ssl_ca_cert = FileOrData(
self._cluster, 'certificate-authority',
file_base_path=self._config_base_path).as_file()
file_base_path=base_path).as_file()
self.cert_file = FileOrData(
self._user, 'client-certificate',
file_base_path=self._config_base_path).as_file()
file_base_path=base_path).as_file()
self.key_file = FileOrData(
self._user, 'client-key',
file_base_path=self._config_base_path).as_file()
file_base_path=base_path).as_file()
if 'insecure-skip-tls-verify' in self._cluster:
self.verify_ssl = not self._cluster['insecure-skip-tls-verify']

Expand Down Expand Up @@ -368,9 +385,10 @@ class ConfigNode(object):
message in case of missing keys. The assumption is all access keys are
present in a well-formed kube-config."""

def __init__(self, name, value):
def __init__(self, name, value, path=None):
self.name = name
self.value = value
self.path = path

def __contains__(self, key):
return key in self.value
Expand All @@ -390,7 +408,7 @@ def __getitem__(self, key):
'Invalid kube-config file. Expected key %s in %s'
% (key, self.name))
if isinstance(v, dict) or isinstance(v, list):
return ConfigNode('%s/%s' % (self.name, key), v)
return ConfigNode('%s/%s' % (self.name, key), v, self.path)
else:
return v

Expand All @@ -415,20 +433,92 @@ def get_with_name(self, name, safe=False):
'Expected only one object with name %s in %s list'
% (name, self.name))
if result is not None:
return ConfigNode('%s[name=%s]' % (self.name, name), result)
if isinstance(result, ConfigNode):
return result
else:
return ConfigNode(
'%s[name=%s]' %
(self.name, name), result, self.path)
if safe:
return None
raise ConfigException(
'Invalid kube-config file. '
'Expected object with name %s in %s list' % (name, self.name))


def _get_kube_config_loader_for_yaml_file(filename, **kwargs):
with open(filename) as f:
return KubeConfigLoader(
config_dict=yaml.safe_load(f),
config_base_path=os.path.abspath(os.path.dirname(filename)),
**kwargs)
class KubeConfigMerger:

"""Reads and merges configuration from one or more kube-config's.
The propery `config` can be passed to the KubeConfigLoader as config_dict.
It uses a path attribute from ConfigNode to store the path to kubeconfig.
This path is required to load certs from relative paths.
A method `save_changes` updates changed kubeconfig's (it compares current
state of dicts with).
"""

def __init__(self, paths):
self.paths = []
self.config_files = {}
self.config_merged = None

for path in paths.split(ENV_KUBECONFIG_PATH_SEPARATOR):
if path:
path = os.path.expanduser(path)
if os.path.exists(path):
self.paths.append(path)
self.load_config(path)
self.config_saved = copy.deepcopy(self.config_files)

@property
def config(self):
return self.config_merged

def load_config(self, path):
with open(path) as f:
config = yaml.safe_load(f)

if self.config_merged is None:
config_merged = copy.deepcopy(config)
for item in ('clusters', 'contexts', 'users'):
config_merged[item] = []
self.config_merged = ConfigNode(path, config_merged, path)

for item in ('clusters', 'contexts', 'users'):
self._merge(item, config[item], path)
self.config_files[path] = config

def _merge(self, item, add_cfg, path):
for new_item in add_cfg:
for exists in self.config_merged.value[item]:
if exists['name'] == new_item['name']:
break
else:
self.config_merged.value[item].append(ConfigNode(
'{}/{}'.format(path, new_item), new_item, path))

def save_changes(self):
for path in self.paths:
if self.config_saved[path] != self.config_files[path]:
self.save_config(path)
self.config_saved = copy.deepcopy(self.config_files)

def save_config(self, path):
with open(path, 'w') as f:
yaml.safe_dump(self.config_files[path], f,
default_flow_style=False)


def _get_kube_config_loader_for_yaml_file(
filename, persist_config=False, **kwargs):

kcfg = KubeConfigMerger(filename)
if persist_config and 'config_persister' not in kwargs:
kwargs['config_persister'] = kcfg.save_changes()

return KubeConfigLoader(
config_dict=kcfg.config,
config_base_path=None,
**kwargs)


def list_kube_config_contexts(config_file=None):
Expand Down Expand Up @@ -456,18 +546,11 @@ async def load_kube_config(config_file=None, context=None,
"""

if config_file is None:
config_file = os.path.expanduser(KUBE_CONFIG_DEFAULT_LOCATION)

config_persister = None
if persist_config:
def _save_kube_config(config_map):
with open(config_file, 'w') as f:
yaml.safe_dump(config_map, f, default_flow_style=False)
config_persister = _save_kube_config
config_file = KUBE_CONFIG_DEFAULT_LOCATION

loader = _get_kube_config_loader_for_yaml_file(
config_file, active_context=context,
config_persister=config_persister)
persist_config=persist_config)
if client_configuration is None:
config = type.__call__(Configuration)
await loader.load_and_set(config)
Expand Down
Loading

0 comments on commit 8a65d31

Please sign in to comment.