Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat] Load kubeconfig from dict #169

Merged
merged 1 commit into from
Dec 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions kubernetes_asyncio/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@
from .config_exception import ConfigException
from .incluster_config import load_incluster_config
from .kube_config import (
list_kube_config_contexts, load_kube_config, new_client_from_config,
refresh_token,
list_kube_config_contexts, load_kube_config, load_kube_config_from_dict,
new_client_from_config, new_client_from_config_dict, refresh_token,
)
111 changes: 85 additions & 26 deletions kubernetes_asyncio/config/kube_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,21 +50,6 @@ def _cleanup_temp_files():
_temp_files = {}


def _create_temp_file_with_content(content):
if len(_temp_files) == 0:
atexit.register(_cleanup_temp_files)
# Because we may change context several times, try to remember files we
# created and reuse them at a small memory cost.
content_key = str(content)
if content_key in _temp_files:
return _temp_files[content_key]
_, name = tempfile.mkstemp()
_temp_files[content_key] = name
with open(name, 'wb') as fd:
fd.write(content.encode() if isinstance(content, str) else content)
return name


def _is_expired(expiry):
return ((parse_rfc3339(expiry) - EXPIRY_SKEW_PREVENTION_DELAY)
<= datetime.datetime.utcnow().replace(tzinfo=UTC))
Expand All @@ -81,18 +66,36 @@ class FileOrData(object):
result in base64 encode of the file content after read."""

def __init__(self, obj, file_key_name, data_key_name=None,
file_base_path="", base64_file_content=True):
file_base_path="", base64_file_content=True,
temp_file_path=None):
if not data_key_name:
data_key_name = file_key_name + "-data"
self._file = None
self._data = None
self._base64_file_content = base64_file_content
self._temp_file_path = temp_file_path
if temp_file_path:
os.makedirs(name=temp_file_path, exist_ok=True)
if data_key_name in obj:
self._data = obj[data_key_name]
elif file_key_name in obj:
self._file = os.path.normpath(
os.path.join(file_base_path, obj[file_key_name]))

def _create_temp_file_with_content(self, content):
if len(_temp_files) == 0:
atexit.register(_cleanup_temp_files)
# Because we may change context several times, try to remember files we
# created and reuse them at a small memory cost.
content_key = str(content)
if content_key in _temp_files:
return _temp_files[content_key]
_, name = tempfile.mkstemp(dir=self._temp_file_path)
_temp_files[content_key] = name
with open(name, 'wb') as fd:
fd.write(content.encode() if isinstance(content, str) else content)
return name

def as_file(self):
"""If obj[%data_key_name] exists, return name of a file with base64
decoded obj[%data_key_name] content otherwise obj[%file_key_name]."""
Expand All @@ -103,10 +106,10 @@ def as_file(self):
content = self._data.encode()
else:
content = self._data
self._file = _create_temp_file_with_content(
self._file = self._create_temp_file_with_content(
base64.standard_b64decode(content))
else:
self._file = _create_temp_file_with_content(self._data)
self._file = self._create_temp_file_with_content(self._data)
if self._file and not os.path.isfile(self._file):
raise ConfigException("File does not exists: %s" % self._file)
return self._file
Expand All @@ -130,7 +133,8 @@ class KubeConfigLoader(object):
def __init__(self, config_dict, active_context=None,
get_google_credentials=None,
config_base_path="",
config_persister=None):
config_persister=None,
temp_file_path=None):

if isinstance(config_dict, ConfigNode):
self._config = config_dict
Expand All @@ -144,6 +148,7 @@ def __init__(self, config_dict, active_context=None,
self.set_active_context(active_context)
self._config_base_path = config_base_path
self._config_persister = config_persister
self._temp_file_path = temp_file_path
if get_google_credentials:
self._get_google_credentials = get_google_credentials
else:
Expand Down Expand Up @@ -346,13 +351,16 @@ def _load_cluster_info(self):
base_path = self._get_base_path(self._cluster.path)
self.ssl_ca_cert = FileOrData(
self._cluster, 'certificate-authority',
file_base_path=base_path).as_file()
file_base_path=base_path,
temp_file_path=self._temp_file_path).as_file()
self.cert_file = FileOrData(
self._user, 'client-certificate',
file_base_path=base_path).as_file()
file_base_path=base_path,
temp_file_path=self._temp_file_path).as_file()
self.key_file = FileOrData(
self._user, 'client-key',
file_base_path=base_path).as_file()
file_base_path=base_path,
temp_file_path=self._temp_file_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 @@ -536,7 +544,8 @@ def list_kube_config_contexts(config_file=None):

async def load_kube_config(config_file=None, context=None,
client_configuration=None,
persist_config=True):
persist_config=True,
temp_file_path=None):
"""Loads authentication and cluster information from kube-config file
and stores them in kubernetes.client.configuration.

Expand All @@ -547,14 +556,48 @@ async def load_kube_config(config_file=None, context=None,
set configs to.
:param persist_config: If True, config file will be updated when changed
(e.g GCP token refresh).
:param temp_file_path: directory where temp files are stored
(default - system temp dir).
"""

if config_file is None:
config_file = KUBE_CONFIG_DEFAULT_LOCATION

loader = _get_kube_config_loader_for_yaml_file(
config_file, active_context=context,
persist_config=persist_config)
persist_config=persist_config,
temp_file_path=temp_file_path)
if client_configuration is None:
config = type.__call__(Configuration)
await loader.load_and_set(config)
Configuration.set_default(config)
else:
await loader.load_and_set(client_configuration)

return loader


async def load_kube_config_from_dict(config_dict, context=None,
client_configuration=None,
temp_file_path=None):
"""Loads authentication and cluster information from config_dict
and stores them in kubernetes.client.configuration.

:param config_dict: Takes the config file as a dict.
:param context: set the active context. If is set to None, current_context
from config file will be used.
:param client_configuration: The kubernetes_asyncio.client.Configuration to
set configs to.
:param temp_file_path: directory where temp files are stored
(default - system temp dir).
"""

loader = KubeConfigLoader(
config_dict=config_dict,
config_base_path=None,
active_context=context,
temp_file_path=temp_file_path)

if client_configuration is None:
config = type.__call__(Configuration)
await loader.load_and_set(config)
Expand Down Expand Up @@ -586,14 +629,30 @@ async def refresh_token(loader, client_configuration=None, interval=60):
client_configuration.api_key['authorization'] = loader.token


async def new_client_from_config(config_file=None, context=None, persist_config=True):
async def new_client_from_config(config_file=None, context=None, persist_config=True,
temp_file_path=None):
"""Loads configuration the same as load_kube_config but returns an ApiClient
to be used with any API object. This will allow the caller to concurrently
talk with multiple clusters."""
client_config = type.__call__(Configuration)

await load_kube_config(config_file=config_file, context=context,
client_configuration=client_config,
persist_config=persist_config)
persist_config=persist_config,
temp_file_path=temp_file_path)

return ApiClient(configuration=client_config)


async def new_client_from_config_dict(config_dict=None, context=None,
temp_file_path=None):
"""Loads configuration the same as load_kube_config_dict but returns an ApiClient
to be used with any API object. This will allow the caller to concurrently
talk with multiple clusters."""
client_config = type.__call__(Configuration)

await load_kube_config_from_dict(config_dict=config_dict, context=context,
client_configuration=client_config,
temp_file_path=temp_file_path)

return ApiClient(configuration=client_config)
53 changes: 42 additions & 11 deletions kubernetes_asyncio/config/kube_config_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@
from .config_exception import ConfigException
from .kube_config import (
ENV_KUBECONFIG_PATH_SEPARATOR, ConfigNode, FileOrData, KubeConfigLoader,
KubeConfigMerger, _cleanup_temp_files, _create_temp_file_with_content,
list_kube_config_contexts, load_kube_config, new_client_from_config,
refresh_token,
KubeConfigMerger, list_kube_config_contexts, load_kube_config,
load_kube_config_from_dict, new_client_from_config,
new_client_from_config_dict, refresh_token,
)

BEARER_TOKEN_FORMAT = "Bearer %s"
Expand Down Expand Up @@ -123,8 +123,8 @@ def get_file_content(filename):
return f.read()

def test_file_given_file(self):
temp_filename = _create_temp_file_with_content(TEST_DATA)
obj = {TEST_FILE_KEY: temp_filename}
obj = {
TEST_FILE_KEY: self._create_temp_file(content=TEST_DATA)}
t = FileOrData(obj=obj, file_key_name=TEST_FILE_KEY)
self.assertEqual(TEST_DATA, self.get_file_content(t.as_file()))

Expand Down Expand Up @@ -192,12 +192,6 @@ def test_file_with_custom_dirname(self):
file_base_path=tempfile_dir)
self.assertEqual(TEST_DATA, self.get_file_content(t.as_file()))

def test_create_temp_file_with_content(self):
self.assertEqual(TEST_DATA,
self.get_file_content(
_create_temp_file_with_content(TEST_DATA)))
_cleanup_temp_files()

def test_file_given_data_bytes(self):
obj = {TEST_DATA_KEY: TEST_DATA_BASE64.encode()}
t = FileOrData(obj=obj, file_key_name=TEST_FILE_KEY,
Expand Down Expand Up @@ -861,6 +855,36 @@ async def test_load_kube_config(self):
client_configuration=actual)
self.assertEqual(expected, actual)

async def test_load_kube_config_from_dict(self):
expected = FakeConfig(host=TEST_HOST,
token=BEARER_TOKEN_FORMAT % TEST_DATA_BASE64)
actual = FakeConfig()
await load_kube_config_from_dict(config_dict=self.TEST_KUBE_CONFIG,
context="simple_token",
client_configuration=actual)
self.assertEqual(expected, actual)

async def test_load_kube_config_from_dict_with_temp_file_path(self):
expected = FakeConfig(
host=TEST_SSL_HOST,
token=BEARER_TOKEN_FORMAT % TEST_DATA_BASE64,
cert_file=self._create_temp_file(TEST_CLIENT_CERT),
key_file=self._create_temp_file(TEST_CLIENT_KEY),
ssl_ca_cert=self._create_temp_file(TEST_CERTIFICATE_AUTH)
)
actual = FakeConfig()

tmp_path = tempfile.mkdtemp('test_temp_file_path')

await load_kube_config_from_dict(config_dict=self.TEST_KUBE_CONFIG,
context="ssl",
client_configuration=actual,
temp_file_path=tmp_path)
self.assertEqual(expected, actual)

# 3 files has to be created within temp_file_path
self.assertEqual(len(os.listdir(tmp_path)), 3)

def test_list_kube_config_contexts(self):
config_file = self._create_temp_file(yaml.safe_dump(self.TEST_KUBE_CONFIG))
contexts, active_context = list_kube_config_contexts(
Expand All @@ -882,6 +906,13 @@ async def test_new_client_from_config(self):
self.assertEqual(BEARER_TOKEN_FORMAT % TEST_DATA_BASE64,
client.configuration.api_key['authorization'])

async def test_new_client_from_config_dict(self):
client = await new_client_from_config_dict(
config_dict=self.TEST_KUBE_CONFIG, context="simple_token")
self.assertEqual(TEST_HOST, client.configuration.host)
self.assertEqual(BEARER_TOKEN_FORMAT % TEST_DATA_BASE64,
client.configuration.api_key['authorization'])

async def test_no_users_section(self):
expected = FakeConfig(host=TEST_HOST)
actual = FakeConfig()
Expand Down