From 58d9bc13bb9df9b063c8efc7afee6620a42e3565 Mon Sep 17 00:00:00 2001 From: Tristan Cacqueray Date: Thu, 6 Feb 2020 20:03:41 +0000 Subject: [PATCH 01/12] Add k8s_exec module to execute command through the API This change adds a new module to execute command in a container through the API: https://docs.okd.io/latest/dev_guide/executing_remote_commands.html#protocol Related: https://github.com/ansible/ansible/pull/55029 Closes: https://github.com/ansible-collections/kubernetes/issues/7 --- plugins/modules/k8s_exec.py | 142 ++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 plugins/modules/k8s_exec.py diff --git a/plugins/modules/k8s_exec.py b/plugins/modules/k8s_exec.py new file mode 100644 index 00000000..1b12fb53 --- /dev/null +++ b/plugins/modules/k8s_exec.py @@ -0,0 +1,142 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2020, Red Hat +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +module: k8s_exec +short_description: Execute command in Pod +version_added: "2.10" +author: "Tristan de Cacqueray (@tristanC)" +description: + - Use the Kubernetes Python client to execute command on K8s pods. +extends_documentation_fragment: + - k8s_auth_options +requirements: + - "python >= 2.7" + - "openshift == 0.4.3" + - "PyYAML >= 3.11" +options: + proxy: + description: + - The URL of an HTTP proxy to use for the connection. Can also be specified via K8S_AUTH_PROXY environment variable. + - Please note that this module does not pick up typical proxy settings from the environment (e.g. HTTP_PROXY). + type: str + namespace: + description: + - The pod namespace name + type: str + required: yes + pod: + description: + - The pod name + type: str + required: yes + container: + description: + - The name of the container in the pod to connect to. Defaults to only container if there is only one container in the pod. + type: str + required: no + command: + description: + - The command to execute + type: str + required: yes +''' + +EXAMPLES = ''' +- name: Execute a command + k8s_exec: + namespace: myproject + pod: zuul-scheduler + command: zuul-scheduler full-reconfigure +''' + +RETURN = ''' +result: + description: + - The command object + returned: success + type: complex + contains: + stdout: + description: The command stdout + type: str + stdout_lines: + description: The command stdout + type: str + stderr: + description: The command stderr + type: str + stderr_lines: + description: The command stderr + type: str +''' + +import copy +import shlex +from ansible_collections.community.kubernetes.plugins.module_utils.common import KubernetesAnsibleModule +from ansible_collections.community.kubernetes.plugins.module_utils.common import AUTH_ARG_SPEC + +try: + from kubernetes.client.apis import core_v1_api + from kubernetes.stream import stream +except ImportError: + # ImportError are managed by the common module already. + pass + + +class KubernetesExecCommand(KubernetesAnsibleModule): + @property + def argspec(self): + spec = copy.deepcopy(AUTH_ARG_SPEC) + spec['namespace'] = {'type': 'str'} + spec['pod'] = {'type': 'str'} + spec['container'] = {'type': 'str'} + spec['command'] = {'type': 'str'} + return spec + + +def main(): + module = KubernetesExecCommand() + # Load kubernetes.client.Configuration + module.get_api_client() + api = core_v1_api.CoreV1Api() + + # hack because passing the container as None breaks things + optional_kwargs = {} + if module.params.get('container'): + optional_kwargs['container'] = module.params['container'] + resp = stream( + api.connect_get_namespaced_pod_exec, + module.params["pod"], + module.params["namespace"], + command=shlex.split(module.params["command"]), + stdout=True, + stderr=True, + stdin=False, + tty=False, + _preload_content=False, **optional_kwargs) + stdout, stderr = [], [] + while resp.is_open(): + resp.update(timeout=1) + if resp.peek_stdout(): + stdout.append(resp.read_stdout()) + if resp.peek_stderr(): + stderr.append(resp.read_stderr()) + module.exit_json( + changed=True, stdout="".join(stdout), stderr="".join(stderr)) + + +if __name__ == '__main__': + main() From ad48d910fc6e4f8cf2770a9540969b94c7bde914 Mon Sep 17 00:00:00 2001 From: Tristan Cacqueray Date: Thu, 6 Feb 2020 20:19:17 +0000 Subject: [PATCH 02/12] Add k8s_exec test --- molecule/default/tasks/full.yml | 35 +++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/molecule/default/tasks/full.yml b/molecule/default/tasks/full.yml index b99f0d3a..2a0c1f9d 100644 --- a/molecule/default/tasks/full.yml +++ b/molecule/default/tasks/full.yml @@ -37,6 +37,41 @@ - "'resources' in k8s_info" - not k8s_info.resources + - name: Create a pod + k8s: + definition: + apiVersion: v1 + kind: Pod + metadata: + name: "{{ k8s_pod_name }}" + namespace: testing + spec: "{{ k8s_pod_spec }}" + wait: yes + wait_sleep: 1 + wait_timeout: 30 + vars: + k8s_pod_name: sleep-pod + k8s_pod_image: alpine:3.8 + k8s_pod_command: + - /bin/sleep + - infinity + + - name: Execute a command + k8s_exec: + namespace: testing + pod: sleep-pod + command: cat /etc/os-release + register: output + + - name: Show k8s_exec output + debug: + var: output + + - name: Assert k8s_exec output is correct + assert: + that: + - "'alpine' in output.stdout" + - name: Create a service k8s: state: present From c067a93886d8aacfc1d52c16e07dcfe991d8f65e Mon Sep 17 00:00:00 2001 From: Tristan Cacqueray Date: Fri, 7 Feb 2020 17:25:43 +0000 Subject: [PATCH 03/12] Move k8s_exec test to a dedicated file --- molecule/default/tasks/full.yml | 35 ---------------- .../kubernetes/tasks/test_k8s_exec.yml | 41 +++++++++++++++++++ 2 files changed, 41 insertions(+), 35 deletions(-) create mode 100644 tests/integration/targets/kubernetes/tasks/test_k8s_exec.yml diff --git a/molecule/default/tasks/full.yml b/molecule/default/tasks/full.yml index 2a0c1f9d..b99f0d3a 100644 --- a/molecule/default/tasks/full.yml +++ b/molecule/default/tasks/full.yml @@ -37,41 +37,6 @@ - "'resources' in k8s_info" - not k8s_info.resources - - name: Create a pod - k8s: - definition: - apiVersion: v1 - kind: Pod - metadata: - name: "{{ k8s_pod_name }}" - namespace: testing - spec: "{{ k8s_pod_spec }}" - wait: yes - wait_sleep: 1 - wait_timeout: 30 - vars: - k8s_pod_name: sleep-pod - k8s_pod_image: alpine:3.8 - k8s_pod_command: - - /bin/sleep - - infinity - - - name: Execute a command - k8s_exec: - namespace: testing - pod: sleep-pod - command: cat /etc/os-release - register: output - - - name: Show k8s_exec output - debug: - var: output - - - name: Assert k8s_exec output is correct - assert: - that: - - "'alpine' in output.stdout" - - name: Create a service k8s: state: present diff --git a/tests/integration/targets/kubernetes/tasks/test_k8s_exec.yml b/tests/integration/targets/kubernetes/tasks/test_k8s_exec.yml new file mode 100644 index 00000000..cc467a47 --- /dev/null +++ b/tests/integration/targets/kubernetes/tasks/test_k8s_exec.yml @@ -0,0 +1,41 @@ +--- +- vars: + ns: k8s-exec + pod: sleep-pod + block: + - name: ensure that k8s-log namespace exists + k8s: + kind: Namespace + name: "{{ ns }}" + + - name: Create a pod + k8s: + definition: + apiVersion: v1 + kind: Pod + metadata: + name: "{{ pod }}" + namespace: "{{ ns }}" + spec: + containers: + - image: busybox + command: ["sleep", "infinity"] + wait: yes + wait_sleep: 1 + wait_timeout: 30 + + - name: Execute a command + k8s_exec: + pod: "{{ pod }}" + namespace: "{{ ns }}" + command: cat /etc/resolv.conf + register: output + + - name: Show k8s_exec output + debug: + var: output + + - name: Assert k8s_exec output is correct + assert: + that: + - "'nameserver' in output.stdout" From c4f5d254ca1ded576ebd00e4b1bdeb8ca7661631 Mon Sep 17 00:00:00 2001 From: Tristan Cacqueray Date: Wed, 12 Feb 2020 11:45:54 +0000 Subject: [PATCH 04/12] Fix doc_fragments reference --- plugins/modules/k8s_exec.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) mode change 100644 => 100755 plugins/modules/k8s_exec.py diff --git a/plugins/modules/k8s_exec.py b/plugins/modules/k8s_exec.py old mode 100644 new mode 100755 index 1b12fb53..b2a3a8b0 --- a/plugins/modules/k8s_exec.py +++ b/plugins/modules/k8s_exec.py @@ -21,7 +21,7 @@ author: "Tristan de Cacqueray (@tristanC)" description: - Use the Kubernetes Python client to execute command on K8s pods. extends_documentation_fragment: - - k8s_auth_options + - community.kubernetes.k8s_auth_options requirements: - "python >= 2.7" - "openshift == 0.4.3" From 3bab767762e26ba33c49ae500709225b222047b6 Mon Sep 17 00:00:00 2001 From: Tristan Cacqueray Date: Wed, 12 Feb 2020 12:04:45 +0000 Subject: [PATCH 05/12] Fix doc-required-mismatch --- plugins/modules/k8s_exec.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/modules/k8s_exec.py b/plugins/modules/k8s_exec.py index b2a3a8b0..cf6ae206 100755 --- a/plugins/modules/k8s_exec.py +++ b/plugins/modules/k8s_exec.py @@ -100,10 +100,10 @@ class KubernetesExecCommand(KubernetesAnsibleModule): @property def argspec(self): spec = copy.deepcopy(AUTH_ARG_SPEC) - spec['namespace'] = {'type': 'str'} - spec['pod'] = {'type': 'str'} - spec['container'] = {'type': 'str'} - spec['command'] = {'type': 'str'} + spec['namespace'] = dict(type='str', required=True) + spec['pod'] = dict(type='str', required=True) + spec['container'] = dict(type='str', required=True) + spec['command'] = dict(type='str', required=True) return spec From 42cf348eeb64bb7f01b05d0a4802ad1943dc7223 Mon Sep 17 00:00:00 2001 From: Tristan Cacqueray Date: Wed, 12 Feb 2020 12:17:53 +0000 Subject: [PATCH 06/12] Fix doc required and remove exec bit --- plugins/modules/k8s_exec.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) mode change 100755 => 100644 plugins/modules/k8s_exec.py diff --git a/plugins/modules/k8s_exec.py b/plugins/modules/k8s_exec.py old mode 100755 new mode 100644 index cf6ae206..bd7b1b9e --- a/plugins/modules/k8s_exec.py +++ b/plugins/modules/k8s_exec.py @@ -102,7 +102,7 @@ class KubernetesExecCommand(KubernetesAnsibleModule): spec = copy.deepcopy(AUTH_ARG_SPEC) spec['namespace'] = dict(type='str', required=True) spec['pod'] = dict(type='str', required=True) - spec['container'] = dict(type='str', required=True) + spec['container'] = dict(type='str') spec['command'] = dict(type='str', required=True) return spec From 9c80a5756c8600d43266a7ba43a971d54288a8cd Mon Sep 17 00:00:00 2001 From: Tristan Cacqueray Date: Wed, 12 Feb 2020 12:40:02 +0000 Subject: [PATCH 07/12] Integrate the integration test --- molecule/default/tasks/full.yml | 1 + .../kubernetes/tasks/test_k8s_exec.yml | 30 ++++++++++++------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/molecule/default/tasks/full.yml b/molecule/default/tasks/full.yml index b99f0d3a..d6216e48 100644 --- a/molecule/default/tasks/full.yml +++ b/molecule/default/tasks/full.yml @@ -326,6 +326,7 @@ register: k8s_info_testing6 failed_when: not k8s_info_testing6.resources or k8s_info_testing6.resources[0].status.phase != "Active" + - include_tasks: test_k8s_exec.yml - include_tasks: crd.yml - include_tasks: lists.yml - include_tasks: append_hash.yml diff --git a/tests/integration/targets/kubernetes/tasks/test_k8s_exec.yml b/tests/integration/targets/kubernetes/tasks/test_k8s_exec.yml index cc467a47..99ec5913 100644 --- a/tests/integration/targets/kubernetes/tasks/test_k8s_exec.yml +++ b/tests/integration/targets/kubernetes/tasks/test_k8s_exec.yml @@ -2,24 +2,26 @@ - vars: ns: k8s-exec pod: sleep-pod + pod_def: + apiVersion: v1 + kind: Pod + metadata: + name: "{{ pod }}" + namespace: "{{ ns }}" + spec: + containers: + - image: busybox + command: ["sleep", "infinity"] + block: - - name: ensure that k8s-log namespace exists + - name: Ensure that k8s-log namespace exists k8s: kind: Namespace name: "{{ ns }}" - name: Create a pod k8s: - definition: - apiVersion: v1 - kind: Pod - metadata: - name: "{{ pod }}" - namespace: "{{ ns }}" - spec: - containers: - - image: busybox - command: ["sleep", "infinity"] + definition: "{{ pod_def }}" wait: yes wait_sleep: 1 wait_timeout: 30 @@ -39,3 +41,9 @@ assert: that: - "'nameserver' in output.stdout" + + - name: Cleanup namespace + k8s: + kind: Namespace + name: "{{ ns }}" + state: absent From b4c4a525aab602a4934f393c11425b6ed81f6d5b Mon Sep 17 00:00:00 2001 From: Tristan Cacqueray Date: Wed, 12 Feb 2020 12:59:35 +0000 Subject: [PATCH 08/12] Fix integration test --- tests/integration/targets/kubernetes/tasks/test_k8s_exec.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/integration/targets/kubernetes/tasks/test_k8s_exec.yml b/tests/integration/targets/kubernetes/tasks/test_k8s_exec.yml index 99ec5913..39c7e997 100644 --- a/tests/integration/targets/kubernetes/tasks/test_k8s_exec.yml +++ b/tests/integration/targets/kubernetes/tasks/test_k8s_exec.yml @@ -10,7 +10,8 @@ namespace: "{{ ns }}" spec: containers: - - image: busybox + - name: sleeper + image: busybox command: ["sleep", "infinity"] block: From fe88bc42cc309109a004af8851b2f13640b64797 Mon Sep 17 00:00:00 2001 From: Tristan Cacqueray Date: Wed, 12 Feb 2020 15:14:32 +0000 Subject: [PATCH 09/12] Remove integration of integration test --- molecule/default/tasks/full.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/molecule/default/tasks/full.yml b/molecule/default/tasks/full.yml index d6216e48..b99f0d3a 100644 --- a/molecule/default/tasks/full.yml +++ b/molecule/default/tasks/full.yml @@ -326,7 +326,6 @@ register: k8s_info_testing6 failed_when: not k8s_info_testing6.resources or k8s_info_testing6.resources[0].status.phase != "Active" - - include_tasks: test_k8s_exec.yml - include_tasks: crd.yml - include_tasks: lists.yml - include_tasks: append_hash.yml From ea0326eab1d2d8cd4f42fa41e37ded298447cd32 Mon Sep 17 00:00:00 2001 From: Tristan Cacqueray Date: Fri, 14 Feb 2020 00:15:14 +0000 Subject: [PATCH 10/12] Add integration test to molecule --- molecule/default/playbook.yml | 1 + .../tasks/test_k8s_exec.yml => molecule/default/tasks/exec.yml | 0 2 files changed, 1 insertion(+) rename tests/integration/targets/kubernetes/tasks/test_k8s_exec.yml => molecule/default/tasks/exec.yml (100%) diff --git a/molecule/default/playbook.yml b/molecule/default/playbook.yml index 8a85279c..d8c04705 100644 --- a/molecule/default/playbook.yml +++ b/molecule/default/playbook.yml @@ -25,3 +25,4 @@ - include_tasks: tasks/apply.yml - include_tasks: tasks/waiter.yml - include_tasks: tasks/full.yml + - include_tasks: tasks/exec.yml diff --git a/tests/integration/targets/kubernetes/tasks/test_k8s_exec.yml b/molecule/default/tasks/exec.yml similarity index 100% rename from tests/integration/targets/kubernetes/tasks/test_k8s_exec.yml rename to molecule/default/tasks/exec.yml From 5f1b88ba6fdc81475c83a4cba71ee5fea5fabecb Mon Sep 17 00:00:00 2001 From: Tristan Cacqueray Date: Fri, 14 Feb 2020 18:28:16 +0000 Subject: [PATCH 11/12] Update test variable name and k8s_exec version_added attribute --- molecule/default/tasks/exec.yml | 27 ++++++++++++++------------- plugins/modules/k8s_exec.py | 2 +- 2 files changed, 15 insertions(+), 14 deletions(-) mode change 100644 => 100755 plugins/modules/k8s_exec.py diff --git a/molecule/default/tasks/exec.yml b/molecule/default/tasks/exec.yml index 39c7e997..77a69f3e 100644 --- a/molecule/default/tasks/exec.yml +++ b/molecule/default/tasks/exec.yml @@ -1,13 +1,13 @@ --- - vars: - ns: k8s-exec + exec_namespace: k8s-exec pod: sleep-pod - pod_def: + exec_pod_definition: apiVersion: v1 kind: Pod metadata: name: "{{ pod }}" - namespace: "{{ ns }}" + namespace: "{{ exec_namespace }}" spec: containers: - name: sleeper @@ -15,36 +15,37 @@ command: ["sleep", "infinity"] block: - - name: Ensure that k8s-log namespace exists + - name: "Ensure that {{ exec_namespace }} namespace exists" k8s: kind: Namespace - name: "{{ ns }}" + name: "{{ exec_namespace }}" - - name: Create a pod + - name: "Create a pod" k8s: - definition: "{{ pod_def }}" + definition: "{{ exec_pod_definition }}" wait: yes wait_sleep: 1 wait_timeout: 30 - - name: Execute a command + - name: "Execute a command" k8s_exec: pod: "{{ pod }}" - namespace: "{{ ns }}" + namespace: "{{ exec_namespace }}" command: cat /etc/resolv.conf register: output - - name: Show k8s_exec output + - name: "Show k8s_exec output" debug: var: output - - name: Assert k8s_exec output is correct + - name: "Assert k8s_exec output is correct" assert: that: - "'nameserver' in output.stdout" - - name: Cleanup namespace + always: + - name: "Cleanup namespace" k8s: kind: Namespace - name: "{{ ns }}" + name: "{{ exec_namespace }}" state: absent diff --git a/plugins/modules/k8s_exec.py b/plugins/modules/k8s_exec.py old mode 100644 new mode 100755 index bd7b1b9e..f91a7466 --- a/plugins/modules/k8s_exec.py +++ b/plugins/modules/k8s_exec.py @@ -16,7 +16,7 @@ ANSIBLE_METADATA = {'metadata_version': '1.1', DOCUMENTATION = ''' module: k8s_exec short_description: Execute command in Pod -version_added: "2.10" +version_added: "1.0.0" author: "Tristan de Cacqueray (@tristanC)" description: - Use the Kubernetes Python client to execute command on K8s pods. From eb50e6f6cda1a9c62443cc6d4881aabc367216bf Mon Sep 17 00:00:00 2001 From: Tristan Cacqueray Date: Fri, 14 Feb 2020 14:13:49 -0500 Subject: [PATCH 12/12] Fix k8s_exec mode --- plugins/modules/k8s_exec.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 plugins/modules/k8s_exec.py diff --git a/plugins/modules/k8s_exec.py b/plugins/modules/k8s_exec.py old mode 100755 new mode 100644