diff --git a/ansible_mitogen/connection.py b/ansible_mitogen/connection.py index 12626485..60724fa3 100644 --- a/ansible_mitogen/connection.py +++ b/ansible_mitogen/connection.py @@ -125,6 +125,22 @@ def _connect_docker(spec): } +def _connect_kubectl(spec): + """ + Return ContextService arguments for a Kubernetes connection. + """ + return { + 'method': 'kubectl', + 'kwargs': { + 'username': spec['remote_user'], + 'pod': spec['remote_addr'], + #'container': spec['container'], + 'python_path': spec['python_path'], + 'connect_timeout': spec['ansible_ssh_timeout'] or spec['timeout'], + } + } + + def _connect_jail(spec): """ Return ContextService arguments for a FreeBSD jail connection. @@ -187,6 +203,7 @@ def _connect_setns(spec): 'python_path': spec['python_path'], 'kind': spec['mitogen_kind'], 'docker_path': spec['mitogen_docker_path'], + 'kubectl_path': spec['mitogen_kubectl_path'], 'lxc_info_path': spec['mitogen_lxc_info_path'], 'machinectl_path': spec['mitogen_machinectl_path'], } @@ -299,6 +316,7 @@ def _connect_mitogen_doas(spec): #: specification. CONNECTION_METHOD = { 'docker': _connect_docker, + 'kubectl': _connect_kubectl, 'jail': _connect_jail, 'local': _connect_local, 'lxc': _connect_lxc, @@ -366,6 +384,8 @@ def config_from_play_context(transport, inventory_name, connection): connection.get_task_var('mitogen_kind'), 'mitogen_docker_path': connection.get_task_var('mitogen_docker_path'), + 'mitogen_kubectl_path': + connection.get_task_var('mitogen_kubectl_path'), 'mitogen_lxc_info_path': connection.get_task_var('mitogen_lxc_info_path'), 'mitogen_machinectl_path': @@ -398,6 +418,7 @@ def config_from_hostvars(transport, inventory_name, connection, 'mitogen_via': hostvars.get('mitogen_via'), 'mitogen_kind': hostvars.get('mitogen_kind'), 'mitogen_docker_path': hostvars.get('mitogen_docker_path'), + 'mitogen_kubectl_path': hostvars.get('mitogen_kubectl_path'), 'mitogen_lxc_info_path': hostvars.get('mitogen_lxc_info_path'), 'mitogen_machinectl_path': hostvars.get('mitogen_machinctl_path'), }) diff --git a/ansible_mitogen/plugins/connection/mitogen_kubectl.py b/ansible_mitogen/plugins/connection/mitogen_kubectl.py new file mode 100644 index 00000000..43d162f8 --- /dev/null +++ b/ansible_mitogen/plugins/connection/mitogen_kubectl.py @@ -0,0 +1,45 @@ +# coding: utf-8 +# Copyright 2018, Yannig Perré +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from __future__ import absolute_import +import os.path +import sys + +try: + import ansible_mitogen +except ImportError: + base_dir = os.path.dirname(__file__) + sys.path.insert(0, os.path.abspath(os.path.join(base_dir, '../../..'))) + del base_dir + +import ansible_mitogen.connection + + +class Connection(ansible_mitogen.connection.Connection): + transport = 'kubectl' diff --git a/ansible_mitogen/strategy.py b/ansible_mitogen/strategy.py index fbe23ef7..e105984c 100644 --- a/ansible_mitogen/strategy.py +++ b/ansible_mitogen/strategy.py @@ -59,7 +59,7 @@ def wrap_connection_loader__get(name, *args, **kwargs): While the strategy is active, rewrite connection_loader.get() calls for some transports into requests for a compatible Mitogen transport. """ - if name in ('docker', 'jail', 'local', 'lxc', + if name in ('docker', 'kubectl', 'jail', 'local', 'lxc', 'lxd', 'machinectl', 'setns', 'ssh'): name = 'mitogen_' + name return connection_loader__get(name, *args, **kwargs) diff --git a/mitogen/core.py b/mitogen/core.py index a6ee1896..d829d624 100644 --- a/mitogen/core.py +++ b/mitogen/core.py @@ -717,6 +717,7 @@ class Importer(object): 'debug', 'doas', 'docker', + 'kubectl', 'fakessh', 'fork', 'jail', diff --git a/mitogen/kubectl.py b/mitogen/kubectl.py new file mode 100644 index 00000000..2dfaa232 --- /dev/null +++ b/mitogen/kubectl.py @@ -0,0 +1,78 @@ +# coding: utf-8 +# Copyright 2018, Yannig Perré +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +import logging + +import mitogen.core +import mitogen.parent + + +LOG = logging.getLogger(__name__) + +class Stream(mitogen.parent.Stream): + child_is_immediate_subprocess = True + + pod = None + container = None + username = None + kubectl_path = 'kubectl' + + # TODO: better way of capturing errors such as "No such container." + create_child_args = { + 'merge_stdio': True + } + + def construct(self, pod = None, container=None, + kubectl_path=None, username=None, + **kwargs): + assert pod + super(Stream, self).construct(**kwargs) + if pod: + self.pod = pod + if container: + self.container = container + if kubectl_path: + self.kubectl_path = kubectl_path + if username: + self.username = username + + def connect(self): + super(Stream, self).connect() + self.name = u'kubectl.' + (self.pod) + str(self.container) + + def get_boot_command(self): + args = ['exec', '-it', self.pod] + if self.username: + args += ['--username=' + self.username] + + if self.container: + args += ['--container=' + self.container] + bits = [self.kubectl_path] + + return bits + args + [ "--" ] + super(Stream, self).get_boot_command() diff --git a/mitogen/parent.py b/mitogen/parent.py index bb2b5d1e..fe5e6889 100644 --- a/mitogen/parent.py +++ b/mitogen/parent.py @@ -1596,6 +1596,9 @@ class Router(mitogen.core.Router): def docker(self, **kwargs): return self.connect(u'docker', **kwargs) + def kubectl(self, **kwargs): + return self.connect(u'kubectl', **kwargs) + def fork(self, **kwargs): return self.connect(u'fork', **kwargs) diff --git a/tests/ansible/test-kubectl.yml b/tests/ansible/test-kubectl.yml new file mode 100644 index 00000000..05fc6517 --- /dev/null +++ b/tests/ansible/test-kubectl.yml @@ -0,0 +1,91 @@ +--- + +- name: "Create pod" + tags: always + hosts: localhost + gather_facts: no + tasks: + - name: Create a test pod + k8s: + state: present + definition: + apiVersion: v1 + kind: Pod + metadata: + name: test-pod-{{item}} + namespace: default + spec: + containers: + - name: python2 + image: python:2 + args: [ "sleep", "100000" ] + loop: "{{ range(10)|list }}" + + - name: "Wait pod to be running" + debug: { msg: "pod is running" } + # status and availableReplicas might not be there. Using default value (d(default_value)) + until: "pod_def.status.containerStatuses[0].ready" + # Waiting 100 s + retries: 50 + delay: 2 + vars: + pod_def: "{{lookup('k8s', kind='Pod', namespace='default', resource_name='test-pod-' ~ item)}}" + loop: "{{ range(10)|list }}" + + - name: "Add pod to pods group" + add_host: + name: "test-pod-{{item}}" + groups: [ "pods" ] + ansible_connection: "kubectl" + changed_when: no + tags: "always" + loop: "{{ range(10)|list }}" + +- name: "Test kubectl connection (default strategy)" + tags: default + hosts: pods + strategy: "linear" + gather_facts: no + tasks: + - name: "Simple shell with linear" + shell: ls /tmp + loop: [ 1, 2, 3, 4, 5 ] + + - name: "Simple file with linear" + file: + path: "/etc" + state: directory + loop: [ 1, 2, 3, 4, 5 ] + +- name: "Test kubectl connection (mitogen strategy)" + tags: mitogen + hosts: pods + strategy: "mitogen_linear" + gather_facts: no + tasks: + - name: "Simple shell with mitogen" + shell: ls /tmp + loop: [ 1, 2, 3, 4, 5 ] + + - name: "Simple file with mitogen" + file: + path: "/etc" + state: directory + loop: [ 1, 2, 3, 4, 5 ] + register: _ + +- name: "Destroy pod" + tags: cleanup + hosts: localhost + gather_facts: no + tasks: + - name: Destroy pod + k8s: + state: absent + definition: + apiVersion: v1 + kind: Pod + metadata: + name: test-pod-{{item}} + namespace: default + loop: "{{ range(10)|list }}"