From d78b64d792f82bf70692787c082542a59f92c9d5 Mon Sep 17 00:00:00 2001 From: abikouo <79859644+abikouo@users.noreply.github.com> Date: Mon, 30 Aug 2021 11:31:07 +0200 Subject: [PATCH] add support for in-memory kubeconfig (#212) 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 Reviewed-by: None --- .../fragments/212-in-memory-kubeconfig.yml | 2 + molecule/default/tasks/full.yml | 6 +++ molecule/default/tasks/validate.yml | 37 ++++++++++++++++++- plugins/action/k8s_info.py | 29 ++++++++++----- plugins/doc_fragments/k8s_auth_options.py | 3 +- plugins/module_utils/args_common.py | 2 +- plugins/module_utils/common.py | 23 ++++++++++-- 7 files changed, 85 insertions(+), 17 deletions(-) create mode 100644 changelogs/fragments/212-in-memory-kubeconfig.yml diff --git a/changelogs/fragments/212-in-memory-kubeconfig.yml b/changelogs/fragments/212-in-memory-kubeconfig.yml new file mode 100644 index 00000000..82e2560b --- /dev/null +++ b/changelogs/fragments/212-in-memory-kubeconfig.yml @@ -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). diff --git a/molecule/default/tasks/full.yml b/molecule/default/tasks/full.yml index 554b3efb..03dfa402 100644 --- a/molecule/default/tasks/full.yml +++ b/molecule/default/tasks/full.yml @@ -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: diff --git a/molecule/default/tasks/validate.yml b/molecule/default/tasks/validate.yml index ff5c3467..9c25c677 100644 --- a/molecule/default/tasks/validate.yml +++ b/molecule/default/tasks/validate.yml @@ -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: diff --git a/plugins/action/k8s_info.py b/plugins/action/k8s_info.py index c3e2cb57..bb4cf8c2 100644 --- a/plugins/action/k8s_info.py +++ b/plugins/action/k8s_info.py @@ -194,6 +194,24 @@ class ActionModule(ActionBase): 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: @@ -211,22 +229,15 @@ class ActionModule(ActionBase): 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) diff --git a/plugins/doc_fragments/k8s_auth_options.py b/plugins/doc_fragments/k8s_auth_options.py index 0012a40d..f42dfd2d 100644 --- a/plugins/doc_fragments/k8s_auth_options.py +++ b/plugins/doc_fragments/k8s_auth_options.py @@ -27,7 +27,8 @@ options: 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. diff --git a/plugins/module_utils/args_common.py b/plugins/module_utils/args_common.py index 67c183db..b8bc0bcc 100644 --- a/plugins/module_utils/args_common.py +++ b/plugins/module_utils/args_common.py @@ -19,7 +19,7 @@ AUTH_PROXY_HEADERS_SPEC = dict( AUTH_ARG_SPEC = { 'kubeconfig': { - 'type': 'path', + 'type': 'raw', }, 'context': {}, 'host': {}, diff --git a/plugins/module_utils/common.py b/plugins/module_utils/common.py index b091e741..1326a4fb 100644 --- a/plugins/module_utils/common.py +++ b/plugins/module_utils/common.py @@ -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: @@ -150,6 +149,22 @@ def get_api_client(module=None, **kwargs): 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('/') @@ -159,7 +174,7 @@ def get_api_client(module=None, **kwargs): 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') @@ -169,7 +184,7 @@ def get_api_client(module=None, **kwargs): 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')