Skip to content

Commit

Permalink
add support for in-memory kubeconfig (#212)
Browse files Browse the repository at this point in the history
add support for in-memory kubeconfig

SUMMARY

k8s module support now authentication with kubeconfig parameter as file and dict.

Closes #139
ISSUE TYPE


Feature Pull Request

COMPONENT NAME

ADDITIONAL INFORMATION

Reviewed-by: Mike Graves <[email protected]>
Reviewed-by: None <None>
  • Loading branch information
abikouo authored Aug 30, 2021
1 parent e21ad02 commit d78b64d
Show file tree
Hide file tree
Showing 7 changed files with 85 additions and 17 deletions.
2 changes: 2 additions & 0 deletions changelogs/fragments/212-in-memory-kubeconfig.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
minor_changes:
- add support for in-memory kubeconfig in addition to file for k8s modules. (https://github.com/ansible-collections/kubernetes.core/pull/212).
6 changes: 6 additions & 0 deletions molecule/default/tasks/full.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@
environment:
K8S_AUTH_KUBECONFIG: ~/.kube/customconfig

- name: Using in-memory kubeconfig should succeed
kubernetes.core.k8s:
name: testing
kind: Namespace
kubeconfig: "{{ lookup('file', '~/.kube/customconfig') | from_yaml }}"

always:
- name: Return kubeconfig
copy:
Expand Down
37 changes: 35 additions & 2 deletions molecule/default/tasks/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -177,10 +177,43 @@
that:
- result is successful
- "'warnings' in result"


vars:
ansible_python_interpreter: "{{ virtualenv_interpreter }}"

- name: stat default kube config
stat:
path: "~/.kube/config"
register: _stat

- name: validate that in-memory kubeconfig authentication failed for kubernetes < 17.17.0
block:
- set_fact:
virtualenv_kubeconfig: "{{ remote_tmp_dir }}/kubeconfig"

- pip:
name:
- "kubernetes<17.17.0"
virtualenv: "{{ virtualenv_kubeconfig }}"
virtualenv_command: "{{ virtualenv_command }}"
virtualenv_site_packages: false

- name: list namespace using in-memory kubeconfig
k8s_info:
kind: Namespace
kubeconfig: "{{ lookup('file', '~/.kube/config') | from_yaml }}"
register: _result
ignore_errors: true
vars:
ansible_python_interpreter: "{{ virtualenv_kubeconfig }}/bin/python"

- name: assert that task failed with proper message
assert:
that:
- '"kubernetes >= 17.17.0 is required" in _result.msg'
when:
- _stat.stat.exists
- _stat.stat.readable
- _stat.stat.isreg
always:
- name: Remove temp directory
file:
Expand Down
29 changes: 20 additions & 9 deletions plugins/action/k8s_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,24 @@ def get_file_realpath(self, local_path):
except AnsibleError:
raise AnsibleActionFail("%s does not exist in local filesystem" % local_path)

def get_kubeconfig(self, kubeconfig, remote_transport, new_module_args):
if isinstance(kubeconfig, string_types):
# find the kubeconfig in the expected search path
if not remote_transport:
# kubeconfig is local
# find in expected paths
kubeconfig = self._find_needle('files', kubeconfig)

# decrypt kubeconfig found
actual_file = self._loader.get_real_file(kubeconfig, decrypt=True)
new_module_args['kubeconfig'] = actual_file

elif isinstance(kubeconfig, dict):
new_module_args['kubeconfig'] = kubeconfig
else:
raise AnsibleActionFail("Error while reading kubeconfig parameter - "
"a string or dict expected, but got %s instead" % type(kubeconfig))

def run(self, tmp=None, task_vars=None):
''' handler for k8s options '''
if task_vars is None:
Expand All @@ -211,22 +229,15 @@ def run(self, tmp=None, task_vars=None):
new_module_args = copy.deepcopy(self._task.args)

kubeconfig = self._task.args.get('kubeconfig', None)
# find the kubeconfig in the expected search path
if kubeconfig and not remote_transport:
# kubeconfig is local
if kubeconfig:
try:
# find in expected paths
kubeconfig = self._find_needle('files', kubeconfig)
self.get_kubeconfig(kubeconfig, remote_transport, new_module_args)
except AnsibleError as e:
result['failed'] = True
result['msg'] = to_text(e)
result['exception'] = traceback.format_exc()
return result

# decrypt kubeconfig found
actual_file = self._loader.get_real_file(kubeconfig, decrypt=True)
new_module_args['kubeconfig'] = actual_file

# find the file in the expected search path
src = self._task.args.get('src', None)

Expand Down
3 changes: 2 additions & 1 deletion plugins/doc_fragments/k8s_auth_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ class ModuleDocFragment(object):
options are provided, the Kubernetes client will attempt to load the default
configuration file from I(~/.kube/config). Can also be specified via K8S_AUTH_KUBECONFIG environment
variable.
type: path
- The kubernetes configuration can be provided as dictionary. This feature requires a python kubernetes client version >= 17.17.0. Added in version 2.2.0.
type: raw
context:
description:
- The name of a context found in the config file. Can also be specified via K8S_AUTH_CONTEXT environment variable.
Expand Down
2 changes: 1 addition & 1 deletion plugins/module_utils/args_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def list_dict_str(value):

AUTH_ARG_SPEC = {
'kubeconfig': {
'type': 'path',
'type': 'raw',
},
'context': {},
'host': {},
Expand Down
23 changes: 19 additions & 4 deletions plugins/module_utils/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,8 @@ def get_api_client(module=None, **kwargs):

def _raise_or_fail(exc, msg):
if module:
module.fail_json(msg % to_native(exc))
module.fail_json(msg=msg % to_native(exc))
raise exc

# If authorization variables aren't defined, look for them in environment variables
for true_name, arg_name in AUTH_ARG_MAP.items():
if module and module.params.get(arg_name) is not None:
Expand Down Expand Up @@ -150,6 +149,22 @@ def _raise_or_fail(exc, msg):
def auth_set(*names):
return all(auth.get(name) for name in names)

def _load_config():
kubeconfig = auth.get('kubeconfig')
optional_arg = {
'context': auth.get('context'),
'persist_config': auth.get('persist_config'),
}
if kubeconfig:
if isinstance(kubeconfig, string_types):
kubernetes.config.load_kube_config(config_file=kubeconfig, **optional_arg)
elif isinstance(kubeconfig, dict):
if LooseVersion(kubernetes.__version__) < LooseVersion("17.17"):
_raise_or_fail(Exception("kubernetes >= 17.17.0 is required to use in-memory kubeconfig."), 'Failed to load kubeconfig due to: %s')
kubernetes.config.load_kube_config_from_dict(config_dict=kubeconfig, **optional_arg)
else:
kubernetes.config.load_kube_config(config_file=None, **optional_arg)

if auth_set('host'):
# Removing trailing slashes if any from hostname
auth['host'] = auth.get('host').rstrip('/')
Expand All @@ -159,7 +174,7 @@ def auth_set(*names):
pass
elif auth_set('kubeconfig') or auth_set('context'):
try:
kubernetes.config.load_kube_config(auth.get('kubeconfig'), auth.get('context'), persist_config=auth.get('persist_config'))
_load_config()
except Exception as err:
_raise_or_fail(err, 'Failed to load kubeconfig due to %s')

Expand All @@ -169,7 +184,7 @@ def auth_set(*names):
kubernetes.config.load_incluster_config()
except kubernetes.config.ConfigException:
try:
kubernetes.config.load_kube_config(auth.get('kubeconfig'), auth.get('context'), persist_config=auth.get('persist_config'))
_load_config()
except Exception as err:
_raise_or_fail(err, 'Failed to load kubeconfig due to %s')

Expand Down

0 comments on commit d78b64d

Please sign in to comment.