Repository: ReproNim/reproman

Issue:
create: Default to :latest tag when pulling Docker image
`reproman create` callers that are familiar with the command-line interface to `docker pull` would reasonably expect that, if they omit a tag, the default `:latest` will be used.  Instead we download all tags:

```
$ docker images busybox
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE

$ reproman create bb -t docker-container -b image=busybox >notag-out 2>&1

$ wc -l notag-out                      
84 notag-out                                    

$ grep -c "Download complete" notag-out
7                                               

$ tail -n3 notag-out                   
2019-11-05 15:14:27,259 [INFO] Downloading [====================>                              ]  924.4kB/2.202MB 
ERROR: 
Interrupted by user while doing magic

$ docker images busybox
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
busybox             1-musl              ff773d70e0ec        5 days ago          1.44MB
busybox             1-glibc             7636bfb4b772        5 days ago          5.2MB
busybox             1-uclibc            020584afccce        5 days ago          1.22MB
busybox             1-ubuntu            d34ea343a882        3 years ago         4.35MB
busybox             1.21-ubuntu         d34ea343a882        3 years ago         4.35MB
busybox             1.21.0-ubuntu       d34ea343a882        3 years ago         4.35MB
busybox             1.23                a84c36ecc374        4 years ago         1.1MB
```

Discussion:

Patch:
diff --git a/reproman/resource/docker_container.py b/reproman/resource/docker_container.py
index 080d6e6..d9d0b2e 100644
--- a/reproman/resource/docker_container.py
+++ b/reproman/resource/docker_container.py
@@ -30,6 +30,16 @@ import logging
 lgr = logging.getLogger('reproman.resource.docker_container')
 
 
+def _image_latest_default(image):
+    # Given the restricted character set for names, the presence of ":" or "@"
+    # should be a reliable indication of a tag or digest, respectively. See
+    # - https://docs.docker.com/engine/reference/commandline/tag/#extended-description
+    # - vendor/github.com/docker/distribution/reference/regexp.go
+    if ":" not in image and "@" not in image:
+        image += ":latest"
+    return image
+
+
 @attr.s
 class DockerContainer(Resource):
     """
@@ -43,8 +53,10 @@ class DockerContainer(Resource):
     id = attrib()
     type = attrib(default='docker-container')
 
-    image = attrib(default='ubuntu:latest',
-        doc="Docker base image ID from which to create the running instance")
+    image = attrib(
+        default='ubuntu:latest',
+        doc="Docker base image ID from which to create the running instance",
+        converter=_image_latest_default)
     engine_url = attrib(default='unix:///var/run/docker.sock',
         doc="Docker server URL where engine is listening for connections")
     seccomp_unconfined = attrib(default=False,


Test patch:
diff --git a/reproman/resource/tests/test_docker_container.py b/reproman/resource/tests/test_docker_container.py
index 1a9b5a7..3e7238b 100644
--- a/reproman/resource/tests/test_docker_container.py
+++ b/reproman/resource/tests/test_docker_container.py
@@ -175,3 +175,12 @@ def test_container_exists(setup_ubuntu):
     from ..docker_container import DockerContainer
     assert DockerContainer.is_container_running(setup_ubuntu['name'])
     assert not DockerContainer.is_container_running('foo')
+
+
+@mark.skipif_no_docker_dependencies
+def test_image_name_latest_default():
+    from ..docker_container import DockerContainer
+    for img, expected in [("debian:buster", "debian:buster"),
+                          ("busybox@ddeeaa", "busybox@ddeeaa"),
+                          ("busybox", "busybox:latest")]:
+        assert DockerContainer(name="cname", image=img).image == expected