Repository: XD-embedded/xd-docker

Issue:
client.container_remove()
Client API command to remove a container.

Discussion:

Patch:
diff --git a/CHANGELOG b/CHANGELOG
index 8b6bf1b..7fdaf1f 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,7 @@
-0.1.0 (in development)
+0.2.0 (in development)
 ----------------------
-Initial release
+* Add container_remove() method.
+
+0.1.0 (2016-08-13)
+------------------
+First development release
diff --git a/xd/docker/client.py b/xd/docker/client.py
index c5ad43d..4e16fa9 100644
--- a/xd/docker/client.py
+++ b/xd/docker/client.py
@@ -428,3 +428,33 @@ class DockerClient(object):
                               headers=headers, data=json.dumps(json_params))
         response_json = response.json()
         return Container(self, id=response_json['Id'])
+
+    def container_remove(self, container: Union[Container, ContainerName, str],
+                         force: Optional[bool]=None,
+                         volumes: Optional[bool]=None):
+        """Remove a container.
+
+        Remove a container and (optionally) the associated volumes.
+
+        Arguments:
+          container: The container to remove (id or name).
+          force: Kill then remove the container.
+          volumes: Remove the volumes associated to the 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
+
+        query_params = {}
+        if force is not None:
+            query_params['force'] = force
+        if volumes is not None:
+            query_params['v'] = volumes
+
+        self._delete('/containers/' + id_or_name, params=query_params)
+        return


Test patch:
diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py
index c09777c..b23c92d 100644
--- a/tests/integration/conftest.py
+++ b/tests/integration/conftest.py
@@ -13,7 +13,7 @@ DOCKER_HOST = os.environ.get('DOCKER_HOST', None)
 
 @pytest.fixture(scope="function")
 def docker(request):
-    os.system("for c in `docker ps -a -q`;do docker rm $c;done")
+    os.system("for c in `docker ps -a -q`;do docker rm -f -v $c;done")
     os.system("for i in `docker images -q`;do docker rmi $i;done")
     return DockerClient(host=DOCKER_HOST)
 
diff --git a/tests/integration/container_remove_test.py b/tests/integration/container_remove_test.py
new file mode 100644
index 0000000..948a92f
--- /dev/null
+++ b/tests/integration/container_remove_test.py
@@ -0,0 +1,38 @@
+import pytest
+import os
+import re
+
+from xd.docker.client import *
+
+
+def test_ok(docker, stdout):
+    os.system("docker create --name xd-docker-test busybox:latest")
+    docker.container_remove('xd-docker-test')
+
+    
+def test_no_such_container(docker, stdout):
+    with pytest.raises(ClientError) as clienterror:
+        with stdout.redirect():
+            docker.container_remove('xd-docker-test')
+    assert clienterror.value.code == 404
+
+    
+def test_container_running(docker, stdout):
+    os.system("docker run -d --name xd-docker-test busybox:latest sleep 600")
+    with pytest.raises(ClientError) as clienterror:
+        with stdout.redirect():
+            docker.container_remove('xd-docker-test')
+    assert clienterror.value.code == 409
+
+    
+def test_container_running_force_false(docker, stdout):
+    os.system("docker run -d --name xd-docker-test busybox:latest sleep 600")
+    with pytest.raises(ClientError) as clienterror:
+        with stdout.redirect():
+            docker.container_remove('xd-docker-test', force=False)
+    assert clienterror.value.code == 409
+
+    
+def test_container_running_force(docker, stdout):
+    os.system("docker run -d --name xd-docker-test busybox:latest sleep 600")
+    docker.container_remove('xd-docker-test', force=True)
diff --git a/tests/unit/client_test.py b/tests/unit/client_test.py
index 19a8acf..55c1443 100644
--- a/tests/unit/client_test.py
+++ b/tests/unit/client_test.py
@@ -1,5 +1,6 @@
 import unittest
 import mock
+import pytest
 import io
 import contextlib
 import tempfile
@@ -292,7 +293,7 @@ class containers_tests(SimpleClientTestCase):
         },
         "Mounts": []
     }]
-    
+
     @mock.patch('requests.get')
     def test_containers_1(self, get_mock):
         get_mock.return_value = requests_mock.Response(json.dumps(
@@ -1100,3 +1101,76 @@ class container_create_tests(ContextClientTestCase):
         assert post_mock.call_count == 1
         name, args, kwargs = post_mock.mock_calls[0]
         assert args[0].endswith('/containers/create')
+
+
+class container_remove_tests(ContextClientTestCase):
+
+    @mock.patch('requests.delete')
+    def test_ok(self, delete_mock):
+        delete_mock.return_value = requests_mock.Response("OK", 200)
+        self.client.container_remove('foobar')
+        params = delete_mock.call_args[1]['params']
+        assert 'force' not in params
+        assert 'v' not in params
+        assert delete_mock.call_args[0][0].endswith('/containers/foobar')
+
+    @mock.patch('requests.delete')
+    def test_no_such_container(self, delete_mock):
+        delete_mock.return_value = requests_mock.Response(
+            "No such container", 404)
+        with pytest.raises(ClientError) as clienterror:
+            self.client.container_remove('foobar')
+        assert clienterror.value.code == 404
+
+    @mock.patch('requests.delete')
+    def test_containername(self, delete_mock):
+        delete_mock.return_value = requests_mock.Response("OK", 200)
+        self.client.container_remove(ContainerName('foobar'))
+        assert delete_mock.call_args[0][0].endswith('/containers/foobar')
+
+    @mock.patch('requests.delete')
+    def test_container_with_name(self, delete_mock):
+        delete_mock.return_value = requests_mock.Response("OK", 200)
+        self.client.container_remove(Container(self.client, name='foobar'))
+        assert delete_mock.call_args[0][0].endswith('/containers/foobar')
+
+    @mock.patch('requests.delete')
+    def test_container_with_id(self, delete_mock):
+        delete_mock.return_value = requests_mock.Response("OK", 200)
+        self.client.container_remove(Container(self.client, id='dfeb03b02b41'))
+        assert delete_mock.call_args[0][0].endswith('/containers/dfeb03b02b41')
+
+    @mock.patch('requests.delete')
+    def test_container_with_id_and_name(self, delete_mock):
+        delete_mock.return_value = requests_mock.Response("OK", 200)
+        self.client.container_remove(Container(self.client,
+                                               id='dfeb03b02b41', name='foo'))
+        assert delete_mock.call_args[0][0].endswith('/containers/dfeb03b02b41')
+
+    @mock.patch('requests.delete')
+    def test_force_true(self, delete_mock):
+        delete_mock.return_value = requests_mock.Response("OK", 200)
+        self.client.container_remove('foobar', force=True)
+        params = delete_mock.call_args[1]['params']
+        assert 'force' in params and params['force']
+
+    @mock.patch('requests.delete')
+    def test_force_false(self, delete_mock):
+        delete_mock.return_value = requests_mock.Response("OK", 200)
+        self.client.container_remove('foobar', force=False)
+        params = delete_mock.call_args[1]['params']
+        assert 'force' in params and not params['force']
+
+    @mock.patch('requests.delete')
+    def test_volumes_true(self, delete_mock):
+        delete_mock.return_value = requests_mock.Response("OK", 200)
+        self.client.container_remove('foobar', volumes=True)
+        params = delete_mock.call_args[1]['params']
+        assert 'v' in params and params['v']
+
+    @mock.patch('requests.delete')
+    def test_volumes_false(self, delete_mock):
+        delete_mock.return_value = requests_mock.Response("OK", 200)
+        self.client.container_remove('foobar', volumes=False)
+        params = delete_mock.call_args[1]['params']
+        assert 'v' in params and not params['v']