Repository: docker/docker-py

Issue:
docker-py v2.0.0  Can't create container data volumes using client.containers.run & create
[https://docker-py.readthedocs.io/en/latest/containers.html](url): **run** method: for **volumes** it says:
```
volumes (dict or list) –

A dictionary to configure volumes mounted inside the container. The key is either the host path or a volume name, and the value is a dictionary with the keys:

    bind The path to mount the volume inside the container
    mode Either rw to mount the volume read/write, or ro to mount it read-only.

For example:

{'/home/user1/': {'bind': '/mnt/vol2', 'mode': 'rw'},
 '/var/www': {'bind': '/mnt/vol1', 'mode': 'ro'}}
```

This effectively means that you can't create container data volumes, since you either have to create a volume 1st using **docker create volume** or use a host directory.
There is no option to just specify a container folder.

The  low-level API ([https://docker-py.readthedocs.io/en/latest/api.html#module-docker.api.container](url)) **create_container** still allows you to do this (since I was able to do this using the v1 of docker-py

From the **docker run** documentation [https://docs.docker.com/engine/reference/commandline/run/](url):
```
  -v, --volume value                Bind mount a volume (default []). The format
                                    is `[host-src:]container-dest[:<options>]`.
                                    The comma-delimited `options` are [rw|ro],
                                    [z|Z], [[r]shared|[r]slave|[r]private], and
                                    [nocopy]. The 'host-src' is an absolute path
                                    or a name value.
```

Am I correct here that logic to support container data volumes is missing from **client.containers.run & create**?
Or is there another way how to do this? Or are container data volumes being replaced by data volumes?





Discussion:
Thank you for the report. It looks like this is an issue we overlooked.

cc @bfirsh It seems like we're passing the `volumes` data to `host_config` no matter what, so even using the list syntax (`volumes=['/hello/world:rw']`) will result in an error from the engine because we're passing an invalid bind mount spec.
I believe there is also a bug with named volumes not being supported by `DockerClient.containers.run`:

```
>>> print(docker.__version__)
2.0.1
>>> client.containers.run('alpine', 'ls', volumes={'my-named-volume': {'bind': '/data', 'mode': 'ro'}})
Traceback (most recent call last):
[...]
docker.errors.APIError: 500 Server Error: Internal Server Error for url: http+docker://localunixsocket/v1.24/containers/create
("Invalid volume spec "my-named-volume": Invalid volume destination path: 'my-named-volume' mount path must beabsolute.")
```

(The volume had already been created with `docker volume create --name my-named-volume`.) 
I had the same problem that @RobbieClarken  mentioned above. And I solved it by doing a tiny modification on the line 882 in docker/models/containers.py.(on branch 2.0.2-release)
The original line of codes is
`create_kwargs['volumes'] = [v.split(':')[0] for v in binds]`
And I change it as below:
`create_kwargs['volumes'] = [v.split(':')[1] for v in binds]`

Which I believe might be a bug to fix because the definition of volume parameter for docker engine api is

> An object mapping mount point paths **inside the container** to empty objects.

Hence, the volume parameter should be mapping to `v.split(':')[1]` instead `v.split(':')[0]` because former one means mount point paths inside the container and later one means host path or volume name.
@milk4candy It works on docker-py version 2.0.2! Thanks a lot.


Resolving patch:
diff --git a/docker/api/container.py b/docker/api/container.py
--- a/docker/api/container.py
+++ b/docker/api/container.py
@@ -313,9 +313,10 @@ def create_container(self, image, command=None, hostname=None, user=None,
 
         **Using volumes**
 
-        Volume declaration is done in two parts. Provide a list of mountpoints
-        to the with the ``volumes`` parameter, and declare mappings in the
-        ``host_config`` section.
+        Volume declaration is done in two parts. Provide a list of
+        paths to use as mountpoints inside the container with the
+        ``volumes`` parameter, and declare mappings from paths on the host
+        in the ``host_config`` section.
 
         .. code-block:: python
 
@@ -392,7 +393,8 @@ def create_container(self, image, command=None, hostname=None, user=None,
                 version 1.10. Use ``host_config`` instead.
             dns_opt (:py:class:`list`): Additional options to be added to the
                 container's ``resolv.conf`` file
-            volumes (str or list):
+            volumes (str or list): List of paths inside the container to use
+                as volumes.
             volumes_from (:py:class:`list`): List of container names or Ids to
                 get volumes from.
             network_disabled (bool): Disable networking
diff --git a/docker/models/containers.py b/docker/models/containers.py
--- a/docker/models/containers.py
+++ b/docker/models/containers.py
@@ -885,5 +885,15 @@ def _create_container_args(kwargs):
                                   for p in sorted(port_bindings.keys())]
     binds = create_kwargs['host_config'].get('Binds')
     if binds:
-        create_kwargs['volumes'] = [v.split(':')[0] for v in binds]
+        create_kwargs['volumes'] = [_host_volume_from_bind(v) for v in binds]
     return create_kwargs
+
+
+def _host_volume_from_bind(bind):
+    bits = bind.split(':')
+    if len(bits) == 1:
+        return bits[0]
+    elif len(bits) == 2 and bits[1] in ('ro', 'rw'):
+        return bits[0]
+    else:
+        return bits[1]


Test patch:
diff --git a/tests/integration/models_containers_test.py b/tests/integration/models_containers_test.py
--- a/tests/integration/models_containers_test.py
+++ b/tests/integration/models_containers_test.py
@@ -1,4 +1,5 @@
 import docker
+import tempfile
 from .base import BaseIntegrationTest, TEST_API_VERSION
 
 
@@ -32,6 +33,42 @@ def test_run_with_image_that_does_not_exist(self):
         with self.assertRaises(docker.errors.ImageNotFound):
             client.containers.run("dockerpytest_does_not_exist")
 
+    def test_run_with_volume(self):
+        client = docker.from_env(version=TEST_API_VERSION)
+        path = tempfile.mkdtemp()
+
+        container = client.containers.run(
+            "alpine", "sh -c 'echo \"hello\" > /insidecontainer/test'",
+            volumes=["%s:/insidecontainer" % path],
+            detach=True
+        )
+        self.tmp_containers.append(container.id)
+        container.wait()
+
+        out = client.containers.run(
+            "alpine", "cat /insidecontainer/test",
+            volumes=["%s:/insidecontainer" % path]
+        )
+        self.assertEqual(out, b'hello\n')
+
+    def test_run_with_named_volume(self):
+        client = docker.from_env(version=TEST_API_VERSION)
+        client.volumes.create(name="somevolume")
+
+        container = client.containers.run(
+            "alpine", "sh -c 'echo \"hello\" > /insidecontainer/test'",
+            volumes=["somevolume:/insidecontainer"],
+            detach=True
+        )
+        self.tmp_containers.append(container.id)
+        container.wait()
+
+        out = client.containers.run(
+            "alpine", "cat /insidecontainer/test",
+            volumes=["somevolume:/insidecontainer"]
+        )
+        self.assertEqual(out, b'hello\n')
+
     def test_get(self):
         client = docker.from_env(version=TEST_API_VERSION)
         container = client.containers.run("alpine", "sleep 300", detach=True)
diff --git a/tests/unit/models_containers_test.py b/tests/unit/models_containers_test.py
--- a/tests/unit/models_containers_test.py
+++ b/tests/unit/models_containers_test.py
@@ -100,6 +100,9 @@ def test_create_container_args(self):
             volumes=[
                 '/home/user1/:/mnt/vol2',
                 '/var/www:/mnt/vol1:ro',
+                'volumename:/mnt/vol3',
+                '/volumewithnohostpath',
+                '/anothervolumewithnohostpath:ro',
             ],
             volumes_from=['container'],
             working_dir='/code'
@@ -116,6 +119,9 @@ def test_create_container_args(self):
                 'Binds': [
                     '/home/user1/:/mnt/vol2',
                     '/var/www:/mnt/vol1:ro',
+                    'volumename:/mnt/vol3',
+                    '/volumewithnohostpath',
+                    '/anothervolumewithnohostpath:ro'
                 ],
                 'BlkioDeviceReadBps': [{'Path': 'foo', 'Rate': 3}],
                 'BlkioDeviceReadIOps': [{'Path': 'foo', 'Rate': 3}],
@@ -181,7 +187,13 @@ def test_create_container_args(self):
             tty=True,
             user='bob',
             volume_driver='some_driver',
-            volumes=['/home/user1/', '/var/www'],
+            volumes=[
+                '/mnt/vol2',
+                '/mnt/vol1',
+                '/mnt/vol3',
+                '/volumewithnohostpath',
+                '/anothervolumewithnohostpath'
+            ],
             working_dir='/code'
         )