Repository: PrefectHQ/prefect

Issue:
Consider a push an attribute on the Docker storage option
Currently the `push` toggle is on the build function of Docker storage. We should consider moving it to an attribute of the storage class so we can specify push during the deploy step

```
flow.deploy("my-project", registry_url="", push=False)
```

Alternative Idea: what if we set the Docker storage up to simply not attempt a push if a registry url isn't provided? If we went down this route we could also adjust the Local Agent to just not pull if the `storage.registry_url` is absent

cc @cicdw 


Discussion:
Oooo I really like your option 2 - it's a one line change to replace the `ValueError` with a `UserWarning` stating the docker container won't be pushed and will remain local, let's do that!
@cicdw Awesome I'll implement that

Resolving patch:
diff --git a/src/prefect/agent/local/agent.py b/src/prefect/agent/local/agent.py
--- a/src/prefect/agent/local/agent.py
+++ b/src/prefect/agent/local/agent.py
@@ -64,7 +64,7 @@ def deploy_flows(self, flow_runs: list) -> None:
 
             env_vars = self.populate_env_vars(flow_run=flow_run)
 
-            if not self.no_pull:
+            if not self.no_pull and storage.registry_url:
                 self.logger.debug("Pulling image {}...".format(storage.name))
                 try:
                     self.docker_client.pull(storage.name)
diff --git a/src/prefect/environments/storage/docker.py b/src/prefect/environments/storage/docker.py
--- a/src/prefect/environments/storage/docker.py
+++ b/src/prefect/environments/storage/docker.py
@@ -9,6 +9,7 @@
 import textwrap
 import uuid
 from typing import Any, Callable, Dict, Iterable, List
+import warnings
 
 import cloudpickle
 import docker
@@ -181,11 +182,11 @@ def name(self) -> str:
         """
         Full name of the Docker image.
         """
-        if None in [self.registry_url, self.image_name, self.image_tag]:
+        if None in [self.image_name, self.image_tag]:
             raise ValueError("Docker storage is missing required fields")
 
         return "{}:{}".format(
-            PurePosixPath(self.registry_url, self.image_name),  # type: ignore
+            PurePosixPath(self.registry_url or "", self.image_name),  # type: ignore
             self.image_tag,  # type: ignore
         )
 
@@ -254,9 +255,11 @@ def _build_image(self, push: bool = True) -> tuple:
             if self.registry_url:
                 full_name = str(PurePosixPath(self.registry_url, self.image_name))
             elif push is True:
-                raise ValueError(
-                    "This Docker storage object has no `registry_url`, and cannot be pushed."
+                warnings.warn(
+                    "This Docker storage object has no `registry_url`, and will not be pushed.",
+                    UserWarning,
                 )
+                full_name = self.image_name
             else:
                 full_name = self.image_name
 
@@ -275,7 +278,7 @@ def _build_image(self, push: bool = True) -> tuple:
                 )
 
             # Push the image if requested
-            if push:
+            if push and self.registry_url:
                 self.push_image(full_name, self.image_tag)
 
                 # Remove the image locally after being pushed


Test patch:
diff --git a/tests/agent/test_local_agent.py b/tests/agent/test_local_agent.py
--- a/tests/agent/test_local_agent.py
+++ b/tests/agent/test_local_agent.py
@@ -210,3 +210,35 @@ def test_local_agent_deploy_flows_no_pull(monkeypatch, runner_token):
     assert not api.pull.called
     assert api.create_container.called
     assert api.start.called
+
+
+def test_local_agent_deploy_flows_no_registry_does_not_pull(monkeypatch, runner_token):
+
+    api = MagicMock()
+    api.ping.return_value = True
+    api.create_container.return_value = {"Id": "container_id"}
+    monkeypatch.setattr(
+        "prefect.agent.local.agent.docker.APIClient", MagicMock(return_value=api)
+    )
+
+    agent = LocalAgent()
+    agent.deploy_flows(
+        flow_runs=[
+            GraphQLResult(
+                {
+                    "flow": GraphQLResult(
+                        {
+                            "storage": Docker(
+                                registry_url="", image_name="name", image_tag="tag"
+                            ).serialize()
+                        }
+                    ),
+                    "id": "id",
+                }
+            )
+        ]
+    )
+
+    assert not api.pull.called
+    assert api.create_container.called
+    assert api.start.called
diff --git a/tests/environments/storage/test_docker_storage.py b/tests/environments/storage/test_docker_storage.py
--- a/tests/environments/storage/test_docker_storage.py
+++ b/tests/environments/storage/test_docker_storage.py
@@ -275,14 +275,18 @@ def test_build_image_passes_and_pushes(monkeypatch):
     assert "reg" in remove.call_args[1]["image"]
 
 
-def test_build_image_fails_no_registry(monkeypatch):
+@pytest.mark.filterwarnings("error")
+def test_build_image_passes_but_raises_warning(monkeypatch):
     storage = Docker(base_image="python:3.6", image_name="test", image_tag="latest")
 
     client = MagicMock()
-    monkeypatch.setattr("docker.APIClient", client)
+    client.images.return_value = ["name"]
+    monkeypatch.setattr("docker.APIClient", MagicMock(return_value=client))
 
-    with pytest.raises(ValueError):
+    with pytest.raises(UserWarning):
         image_name, image_tag = storage._build_image()
+        assert image_name == "test"
+        assert image_tag == "latest"
 
 
 def test_create_dockerfile_from_base_image():
@@ -443,6 +447,16 @@ def test_docker_storage_name():
     assert storage.name == "test1/test2:test3"
 
 
+def test_docker_storage_name_registry_url_none():
+    storage = Docker(base_image="python:3.6")
+    with pytest.raises(ValueError):
+        storage.name
+
+    storage.image_name = "test2"
+    storage.image_tag = "test3"
+    assert storage.name == "test2:test3"
+
+
 def test_docker_storage_get_flow_method():
     storage = Docker(base_image="python:3.6")
     with tempfile.TemporaryDirectory() as directory: