Repository: docker/docker-py

Issue:
ImageNotFound error if no :latest tag exists (version 2.2.1)
Steps to reproduce:

```
import docker
client = docker.from_env()
target_img_tags = client.images.pull("deis/store-daemon").tags
```

Raises:
`docker.errors.ImageNotFound: 404 Client Error: Not Found ("No such image: deis/store-daemon")`

doing a `docker images | grep deis` gives us:

```
deis/store-daemon                 v1.13.4                                    eb4b06970e51        2 months ago        120 MB
deis/store-daemon                 v1.13.3                                    68b109613922        7 months ago        394 MB
deis/store-daemon                 v1.13.2                                    f1d1a8456bba        9 months ago        394 MB
deis/store-daemon                 v1.13.1                                    ea7d19df6386        11 months ago       393 MB
deis/store-daemon                 v1.13.0                                    805a931c762a        12 months ago       393 MB
deis/store-daemon                 v1.12.3                                    6578514c16a4        13 months ago       380 MB
deis/store-daemon                 v1.12.2                                    eefcc52df297        16 months ago       380 MB
deis/store-daemon                 v1.12.1                                    1e071363779f        17 months ago       380 MB
deis/store-daemon                 v1.12.0                                    6260dcca6ebc        17 months ago       380 MB
deis/store-daemon                 v1.12.0-rc3                                6260dcca6ebc        17 months ago       380 MB
deis/store-daemon                 v1.12.0-rc2                                2933f901c9bc        17 months ago       380 MB
deis/store-daemon                 v1.12.0-rc1                                8e1c91b7f55f        17 months ago       380 MB
deis/store-daemon                 v1.11.2                                    62f342b7b23d        17 months ago       380 MB
deis/store-daemon                 v1.11.1                                    d6615a82e85f        18 months ago       380 MB
deis/store-daemon                 v1.11.0                                    352abf8cd7ed        18 months ago       379 MB
deis/store-daemon                 v1.11.0-rc1                                352abf8cd7ed        18 months ago       379 MB
deis/store-daemon                 v1.10.1                                    0e676a599ff9        18 months ago       380 MB
deis/store-daemon                 v1.10.0                                    b9b8f74b62fd        19 months ago       380 MB
deis/store-daemon                 v1.9.1                                     b86f33c1a7ff        19 months ago       379 MB
deis/store-daemon                 v1.9.0                                     e3132889f86d        20 months ago       379 MB
deis/store-daemon                 v1.8.0                                     f8197dfe2e41        21 months ago       379 MB
deis/store-daemon                 v1.7.3                                     2622ab04aa3a        22 months ago       375 MB
deis/store-daemon                 v1.7.2                                     ad79dfb60604        22 months ago       375 MB
deis/store-daemon                 v1.7.1                                     9e02aa838319        22 months ago       375 MB
deis/store-daemon                 v1.7.0                                     8a61029b06c1        22 months ago       363 MB
deis/store-daemon                 v1.6.1                                     34c0ebfd0849        23 months ago       365 MB
deis/store-daemon                 v1.6.0                                     cc3910403925        23 months ago       365 MB
deis/store-daemon                 v1.5.2                                     479356b8bd80        23 months ago       309 MB
deis/store-daemon                 v1.5.1                                     b2166ae3543a        24 months ago       309 MB
deis/store-daemon                 v1.5.0                                     ace85e5377ee        24 months ago       309 MB
deis/store-daemon                 v1.4.1                                     5a49fe94604b        2 years ago         309 MB
deis/store-daemon                 v1.4.0                                     642240140452        2 years ago         309 MB
deis/store-daemon                 v1.3.1                                     01362bcb8a76        2 years ago         309 MB
deis/store-daemon                 v1.3.0                                     05fb0710a65a        2 years ago         309 MB
deis/store-daemon                 v1.2.2                                     d9bcf549284c        2 years ago         309 MB
deis/store-daemon                 v1.2.0                                     5d1105c57523        2 years ago         309 MB
deis/store-daemon                 v1.2.1                                     5d1105c57523        2 years ago         309 MB
deis/store-daemon                 v1.1.1                                     13d86418a872        2 years ago         309 MB
deis/store-daemon                 v1.1.0                                     e3a488647436        2 years ago         309 MB
deis/store-daemon                 v1.0.2                                     93ff83b6fb02        2 years ago         309 MB
deis/store-daemon                 v1.0.1                                     e78fd233bfe0        2 years ago         309 MB
deis/store-daemon                 v1.0.0                                     8f53a2132059        2 years ago         309 MB
deis/store-daemon                 v0.15.1                                    0907a8407563        2 years ago         290 MB
deis/store-daemon                 v0.15.0                                    0217efa1a137        2 years ago         290 MB
deis/store-daemon                 v0.14.0                                    036230c75c5e        2 years ago         322 MB
deis/store-daemon                 v0.14.1                                    036230c75c5e        2 years ago         322 MB
deis/store-daemon                 v0.13.1                                    8af905a5d2ce        2 years ago         439 MB
deis/store-daemon                 v0.13.0                                    b48548def258        2 years ago         442 MB
```


So what's the best way to get a list of image tags, if there's no :latest tag?  The pull downloads all this stuff, but it's inaccessible via the Python SDK.


Discussion:
`client.images.list(name='deis/store-daemon')`

http://docker-py.readthedocs.io/en/stable/images.html#docker.models.images.ImageCollection.list
I'm having a similar issue - I try and pull an image using the following:

`import docker`
`client = docker.from_env()`
`client.images.pull('bsidescbrctf/2017-crypto-docuprotect', tag='1')`

Raises the following:

`docker.errors.ImageNotFound: 404 Client Error: Not Found ("no such image: bsidescbrctf/2017-crypto-docuprotect: No such image: bsidescbrctf/2017-crypto-docuprotect:latest")`

Not sure why the 1 tag is being replaced by latest.

I have found a way around it by doing:

`test = client.images.list('bsidescbrctf/2017-crypto-docuprotect')`
`actual_image = test[0]`

But I would prefer to be able to pull the actual image itself.
@joebloggs12 Thanks for the report. FWIW, `client.images.pull('bsidescbrctf/2017-crypto-docuprotect:1')` should work as well.
@shin- Thanks for the workaround, but to be able to pull the actual image itself is better.
I agree! PRs are welcome :)

Resolving patch:
diff --git a/docker/models/images.py b/docker/models/images.py
--- a/docker/models/images.py
+++ b/docker/models/images.py
@@ -5,6 +5,7 @@
 
 from ..api import APIClient
 from ..errors import BuildError, ImageLoadError
+from ..utils import parse_repository_tag
 from ..utils.json_stream import json_stream
 from .resource import Collection, Model
 
@@ -269,6 +270,8 @@ def pull(self, name, tag=None, **kwargs):
         """
         Pull an image of the given name and return it. Similar to the
         ``docker pull`` command.
+        If no tag is specified, all tags from that repository will be
+        pulled.
 
         If you want to get the raw pull output, use the
         :py:meth:`~docker.api.image.ImageApiMixin.pull` method in the
@@ -285,7 +288,9 @@ def pull(self, name, tag=None, **kwargs):
             platform (str): Platform in the format ``os[/arch[/variant]]``
 
         Returns:
-            (:py:class:`Image`): The image that has been pulled.
+            (:py:class:`Image` or list): The image that has been pulled.
+                If no ``tag`` was specified, the method will return a list
+                of :py:class:`Image` objects belonging to this repository.
 
         Raises:
             :py:class:`docker.errors.APIError`
@@ -293,10 +298,19 @@ def pull(self, name, tag=None, **kwargs):
 
         Example:
 
-            >>> image = client.images.pull('busybox')
+            >>> # Pull the image tagged `latest` in the busybox repo
+            >>> image = client.images.pull('busybox:latest')
+
+            >>> # Pull all tags in the busybox repo
+            >>> images = client.images.pull('busybox')
         """
+        if not tag:
+            name, tag = parse_repository_tag(name)
+
         self.client.api.pull(name, tag=tag, **kwargs)
-        return self.get('{0}:{1}'.format(name, tag) if tag else name)
+        if tag:
+            return self.get('{0}:{1}'.format(name, tag))
+        return self.list(name)
 
     def push(self, repository, tag=None, **kwargs):
         return self.client.api.push(repository, tag=tag, **kwargs)


Test patch:
diff --git a/tests/integration/models_images_test.py b/tests/integration/models_images_test.py
--- a/tests/integration/models_images_test.py
+++ b/tests/integration/models_images_test.py
@@ -74,6 +74,12 @@ def test_pull_with_tag(self):
         image = client.images.pull('alpine', tag='3.3')
         assert 'alpine:3.3' in image.attrs['RepoTags']
 
+    def test_pull_multiple(self):
+        client = docker.from_env(version=TEST_API_VERSION)
+        images = client.images.pull('hello-world')
+        assert len(images) == 1
+        assert 'hello-world:latest' in images[0].attrs['RepoTags']
+
     def test_load_error(self):
         client = docker.from_env(version=TEST_API_VERSION)
         with pytest.raises(docker.errors.ImageLoadError):
diff --git a/tests/integration/models_services_test.py b/tests/integration/models_services_test.py
--- a/tests/integration/models_services_test.py
+++ b/tests/integration/models_services_test.py
@@ -1,12 +1,12 @@
 import unittest
 
 import docker
+import pytest
 
 from .. import helpers
 from .base import TEST_API_VERSION
 from docker.errors import InvalidArgument
 from docker.types.services import ServiceMode
-import pytest
 
 
 class ServiceTest(unittest.TestCase):
@@ -182,6 +182,7 @@ def test_update_remove_service_labels(self):
         service.reload()
         assert not service.attrs['Spec'].get('Labels')
 
+    @pytest.mark.xfail(reason='Flaky test')
     def test_update_retains_networks(self):
         client = docker.from_env(version=TEST_API_VERSION)
         network_name = helpers.random_name()
diff --git a/tests/unit/models_images_test.py b/tests/unit/models_images_test.py
--- a/tests/unit/models_images_test.py
+++ b/tests/unit/models_images_test.py
@@ -41,9 +41,22 @@ def test_load(self):
 
     def test_pull(self):
         client = make_fake_client()
-        image = client.images.pull('test_image')
+        image = client.images.pull('test_image:latest')
+        client.api.pull.assert_called_with('test_image', tag='latest')
+        client.api.inspect_image.assert_called_with('test_image:latest')
+        assert isinstance(image, Image)
+        assert image.id == FAKE_IMAGE_ID
+
+    def test_pull_multiple(self):
+        client = make_fake_client()
+        images = client.images.pull('test_image')
         client.api.pull.assert_called_with('test_image', tag=None)
-        client.api.inspect_image.assert_called_with('test_image')
+        client.api.images.assert_called_with(
+            all=False, name='test_image', filters=None
+        )
+        client.api.inspect_image.assert_called_with(FAKE_IMAGE_ID)
+        assert len(images) == 1
+        image = images[0]
         assert isinstance(image, Image)
         assert image.id == FAKE_IMAGE_ID