Repository: XD-embedded/xd-docker

Issue:
client.container_kill()
Client API command to kill a container.

Discussion:

Patch:
diff --git a/xd/docker/client.py b/xd/docker/client.py
index a0c7398..fdff2db 100644
--- a/xd/docker/client.py
+++ b/xd/docker/client.py
@@ -17,7 +17,7 @@ from typing import Optional, Union, Sequence, Dict, Tuple, List
 from xd.docker.container import Container
 from xd.docker.image import Image
 from xd.docker.parameters import ContainerConfig, HostConfig, ContainerName, \
-    Repository, RegistryAuthConfig, VolumeMount, json_update
+    Repository, RegistryAuthConfig, VolumeMount, Signal, json_update
 
 import logging
 log = logging.getLogger(__name__)
@@ -572,3 +572,29 @@ class DockerClient(object):
 
         self._post('/containers/{}/restart'.format(id_or_name),
                    params=params)
+
+    def container_kill(self,
+                       container: Union[Container, ContainerName, str],
+                       signal: Optional[Signal]=None):
+        """Kill container.
+
+        Send signal to container, and (maybe) wait for the container to exit.
+
+        Arguments:
+          container: The container to remove (id or name).
+          signal: Signal to send to container.
+        """
+
+        # Handle convenience argument types
+        if isinstance(container, str):
+            id_or_name = container
+        elif isinstance(container, ContainerName):
+            id_or_name = container.name
+        else:
+            id_or_name = container.id or container.name
+
+        params = {}
+        if signal is not None:
+            params['signal'] = signal
+
+        self._post('/containers/{}/kill'.format(id_or_name), params=params)


Test patch:
diff --git a/tests/integration/container_kill_test.py b/tests/integration/container_kill_test.py
new file mode 100644
index 0000000..bb9dfbc
--- /dev/null
+++ b/tests/integration/container_kill_test.py
@@ -0,0 +1,38 @@
+import pytest
+import os
+
+from xd.docker.client import *
+
+
+def test_kill(docker, stdout):
+    os.system("docker run -d --name xd-docker-test busybox:latest sleep 10")
+    docker.container_kill('xd-docker-test')
+
+
+def test_already_stopped(docker, stdout):
+    os.system("docker run --name xd-docker-test busybox:latest true")
+    # Prior to Docker 1.8, kill silently ignores stopped containers, and
+    # beginning with 1.8, they return HTTP 500 (ServerError)
+    if docker.api_version > (1, 19):
+        with pytest.raises(ServerError) as servererror:
+            docker.container_kill('xd-docker-test')
+    else:
+        docker.container_kill('xd-docker-test')
+
+
+def test_not_started(docker, stdout):
+    os.system("docker create --name xd-docker-test busybox:latest true")
+    # Prior to Docker 1.8, kill silently ignores stopped containers, and
+    # beginning with 1.8, they return HTTP 500 (ServerError)
+    if docker.api_version > (1, 19):
+        with pytest.raises(ServerError) as servererror:
+            docker.container_kill('xd-docker-test')
+    else:
+        docker.container_kill('xd-docker-test')
+
+
+def test_no_such_container(docker, stdout):
+    with pytest.raises(ClientError) as clienterror:
+        with stdout.redirect():
+            docker.container_kill('xd-docker-test')
+        assert clienterror.value.code == 404
diff --git a/tests/unit/client_test.py b/tests/unit/client_test.py
index e890e2d..c2f4f95 100644
--- a/tests/unit/client_test.py
+++ b/tests/unit/client_test.py
@@ -1354,3 +1354,46 @@ class container_restart_tests(ContextClientTestCase):
         params = post_mock.call_args[1]['params']
         assert 't' in params
         assert params['t'] == 42
+
+
+class container_kill_tests(ContextClientTestCase):
+
+    @mock.patch('requests.post')
+    def test_normal(self, post_mock):
+        post_mock.return_value = requests_mock.Response(None, 204)
+        self.client.container_kill("foobar")
+
+    @mock.patch('requests.post')
+    def test_no_such_container(self, post_mock):
+        post_mock.return_value = requests_mock.Response(None, 404)
+        with pytest.raises(ClientError) as clienterror:
+            self.client.container_kill('foobar')
+        assert clienterror.value.code == 404
+
+    @mock.patch('requests.post')
+    def test_containername(self, post_mock):
+        post_mock.return_value = requests_mock.Response(None, 204)
+        self.client.container_kill(ContainerName("foobar"))
+
+    @mock.patch('requests.post')
+    def test_container(self, post_mock):
+        post_mock.return_value = requests_mock.Response(None, 204)
+        self.client.container_kill(Container(self.client,name="foobar"))
+
+    @mock.patch('requests.post')
+    def test_sigint(self, post_mock):
+        post_mock.return_value = requests_mock.Response(None, 204)
+        self.client.container_kill("foobar", signal='SIGINT')
+        assert 'params' in post_mock.call_args[1]
+        params = post_mock.call_args[1]['params']
+        assert 'signal' in params
+        assert params['signal'] == 'SIGINT'
+
+    @mock.patch('requests.post')
+    def test_sighup(self, post_mock):
+        post_mock.return_value = requests_mock.Response(None, 204)
+        self.client.container_kill("foobar", signal='SIGHUP')
+        assert 'params' in post_mock.call_args[1]
+        params = post_mock.call_args[1]['params']
+        assert 'signal' in params
+        assert params['signal'] == 'SIGHUP'