Repository: PrefectHQ/prefect

Issue:
Docker buildimage intermittently builds image
Hello, I am playing around with prefect and trying to use the buildimage for docker

```
from prefect import task, Flow
from prefect.tasks.docker import images


@task
def build_image():
    images.BuildImage(path='.').run()


with Flow("ETL") as flow:
    e = build_image()

flow.run()
```

```
[2019-07-16 19:36:37,341] INFO - prefect.FlowRunner | Beginning Flow run for 'ETL'
[2019-07-16 19:36:37,341] INFO - prefect.FlowRunner | Starting flow run.
[2019-07-16 19:36:37,342] INFO - prefect.TaskRunner | Task 'build_image': Starting task run...
[2019-07-16 19:36:41,274] INFO - prefect.TaskRunner | Task 'build_image': finished task run for task with final state: 'Success'
[2019-07-16 19:36:41,275] INFO - prefect.FlowRunner | Flow run SUCCESS: all reference tasks succeeded
```
I noticed that this intermittently builds an image once about ever 4-5 times I run it.
Anyone have an idea what is going on?


Discussion:
Hi @richard-avelar ! 

It's possible this is related to docker caching the images, especially if you haven't changed your Dockerfile at all in between runs.  To test this, could you try using `nocache` keyword to `BuildImage`?

```python
  from prefect import task, Flow
from prefect.tasks.docker import images


build_image = images.BuildImage(path='.', nocache=True)


with Flow("ETL") as flow:
    e = build_image()

flow.run()
```

Also, just an FYI: your current implementation creates a Task which calls _another_ task's `run` method, which could lead to unexpected issues for certain tasks.  I'd recommend using the pattern I've included above, where you instead instantiate the `BuildImage` task as `build_image` directly, without needing to use the `@task` decorator or anything else.
Hey @cicdw, thanks for the reply!
Wouldn't nocache only come into play after the image was created at least once and stored in my local registry? Sorry for the misunderstanding in the original post, I delete images before each run as well.
In any case, I tried to make the changes you mentioned to no avail unfortunately.
Here is the python file
```
from prefect import Flow
from prefect.tasks.docker import images


build_image = images.BuildImage(path='.', nocache=True)


with Flow("ETL") as flow:
    e = build_image()

flow.run()
```
and the output displayed
```
~ python3 prefect.py
[2019-07-17 02:20:19,433] INFO - prefect.FlowRunner | Beginning Flow run for 'ETL'
[2019-07-17 02:20:19,433] INFO - prefect.FlowRunner | Starting flow run.
[2019-07-17 02:20:19,434] INFO - prefect.TaskRunner | Task 'BuildImage': Starting task run...
[2019-07-17 02:20:22,891] INFO - prefect.TaskRunner | Task 'BuildImage': finished task run for task with final state: 'Success'
[2019-07-17 02:20:22,892] INFO - prefect.FlowRunner | Flow run SUCCESS: all reference tasks succeeded
~ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
~ ls
Dockerfile main.py prefect.py
```
Oooo I'm sorry, yea - so your image isn't even being _built_!  So, if we strip out the Prefect logic, the `BuildImage` task only runs this code:
```python
import docker


client = docker.APIClient(base_url="unix:///var/run/docker.sock", version="auto")
client.build(path=".", tag=None, nocache=False, rm=True, forcerm=False)
```
So I think we want to understand why this doesn't do what we think it should.  Maybe try:
```python
response = [line for line in client.build(path=".", tag=None, nocache=False, rm=True, forcerm=False)]
print(response)
```
and we can see if there's anything informative in that output.
Interesting, that seems to work 😅 
The file
```
import docker


client = docker.APIClient(base_url="unix:///var/run/docker.sock", version="auto")
client.build(path=".", tag=None, nocache=False, rm=True, forcerm=False)
response = [line for line in client.build(path=".", tag=None, nocache=False, rm=True, forcerm=False)]
print(response)
```

The output being a very long list of docker build output steps, progress, etc. 
And the resulting images showing up in my docker images list

Hmm, The docker specific code seems to work then but I just cant seem to get it to work in prefect for some reason. The docker specific code took about a full 2 minutes to finish running (as is typical for this image to take to complete building), but the prefect code takes a couple of seconds to run and complete execution which also seemed off to me like there was never a serious attempt to build or it silently failed the build attempt and exited 
Great!

So I have a guess here -> it's possible that `client.build` is an asynchronous / deferred call of some kind, and whenever the Prefect Task exits it prevents the build from actually occurring.  What we should do is update the Prefect Task to actually iterate over the `client.build` generator to ensure it completes before returning from the Task.

In the meantime, I recommend you implement a custom Task like so:
```python
import docker


@task
def build_image(path):
    client = docker.APIClient(base_url="unix:///var/run/docker.sock", version="auto")
    client.build(path=path, tag=None, nocache=False, rm=True, forcerm=False)
    response = [line for line in client.build(path=".", tag=None, nocache=False, rm=True, forcerm=False)]
    return 
```

And I'd bet this works.  Looks like I need to audit our Docker Tasks for this behavior!
Sounds good to me!
Thanks for prompt feedback!

Resolving patch:
diff --git a/src/prefect/tasks/docker/images.py b/src/prefect/tasks/docker/images.py
--- a/src/prefect/tasks/docker/images.py
+++ b/src/prefect/tasks/docker/images.py
@@ -1,6 +1,7 @@
 from typing import Any
 
 import docker
+import json
 
 from prefect import Task
 from prefect.utilities.tasks import defaults_from_attrs
@@ -370,6 +371,9 @@ def run(
                 `unix:///var/run/docker.sock` however other hosts such as `tcp://0.0.0.0:2375`
                 can be provided
 
+        Returns:
+            - List[dict]: a cleaned dictionary of the output of `client.build`
+
         Raises:
             - ValueError: if either `path` is `None`
         """
@@ -380,4 +384,16 @@ def run(
 
         client = docker.APIClient(base_url=docker_server_url, version="auto")
 
-        return client.build(path=path, tag=tag, nocache=nocache, rm=rm, forcerm=forcerm)
+        payload = [
+            line
+            for line in client.build(
+                path=path, tag=tag, nocache=nocache, rm=rm, forcerm=forcerm
+            )
+        ]
+        output = [
+            json.loads(line.decode("utf-8"))
+            for resp in payload
+            for line in resp.split(b"\r\n")
+            if line
+        ]
+        return output


Test patch:
diff --git a/tests/tasks/docker/test_images.py b/tests/tasks/docker/test_images.py
--- a/tests/tasks/docker/test_images.py
+++ b/tests/tasks/docker/test_images.py
@@ -326,6 +326,23 @@ def test_image_run_value_is_used(self, monkeypatch):
         task.run(path="test")
         assert api.return_value.build.call_args[1]["path"] == "test"
 
+    def test_image_cleans_docker_output(self, monkeypatch):
+        task = BuildImage()
+
+        output = [
+            b'{"stream":"Step 1/2 : FROM busybox"}\r\n{"stream":"\\n"}\r\n',
+            b'{"stream":" ---\\u003e db8ee88ad75f\\n"}\r\n{"stream":"Step 2/2 : CMD [\\"echo\\", \\"foo\\"]"}\r\n{"stream":"\\n"}\r\n',
+            b'{"stream":" ---\\u003e Using cache\\n"}\r\n{"stream":" ---\\u003e 0be8805a7d08\\n"}\r\n{"aux":{"ID":"sha256:0be8805a7d0828bbfcd876e8aa65cf6ee1ee016117a2d3f3aa4f21835ee1c69f"}}\r\n{"stream":"Successfully built 0be8805a7d08\\n"}\r\n',
+        ]
+        api = MagicMock()
+        api.return_value.build.return_value = output
+        monkeypatch.setattr("prefect.tasks.docker.containers.docker.APIClient", api)
+
+        return_value = task.run(path="test")
+        assert all([isinstance(value, dict) for value in return_value])
+        assert all([len(value) >= 1 for value in return_value])
+        assert all([key for value in return_value for key in value])
+
     def test_image_is_replaced(self, monkeypatch):
         task = BuildImage(path="original")