Repository: pantsbuild/pants

Issue:
pants package does not build missing docker images if previous build was cached.
**Describe the bug**
Pant's caching of build targets does not take into consideration that the final target does not exist.

Take this example: https://www.pantsbuild.org/v2.8/docs/docker#example

```
$ ./pants package src/docker/hw/Dockerfile
[...]
18:07:29.66 [INFO] Completed: Building src.python.hw/bin.pex
18:07:31.83 [INFO] Completed: Building docker image helloworld:latest
18:07:31.83 [INFO] Built docker image: helloworld:latest

$ docker images
REPOSITORY               TAG        IMAGE ID       CREATED         SIZE
helloworld               latest     abcdefabcdef   6 seconds ago   420MB

$ docker rmi helloworld:latest

$ docker images
REPOSITORY               TAG        IMAGE ID       CREATED         SIZE

$ ./pants package src/docker/hw/Dockerfile
19:07:31.83 [INFO] Built docker image: helloworld:latest

$ docker images
REPOSITORY               TAG        IMAGE ID       CREATED         SIZE
```
If you did the equivalent commands for the `helloworld.pex` files, `pants package` would replace the missing file in the `dist/` folder.

**Pants version**
2.8rc1

**OS**
Linux



Discussion:
Mm... good find. Processes interacting with `docker` should probably be marked [`PER_SESSION`](https://github.com/pantsbuild/pants/blob/372fa36efb09d7d477436dbf927edb28537bc2f5/src/python/pants/engine/process.py#L46-L48).
Hah, nice gotcha!

I've been pondering about similar cases in my head, where you may want to not rebuild images in case they already exist in your registry, or locally, etc. So there is several levels to check here for when to reuse what's already existing, or to force build even when there is.

But this case is clearly a bug, rather than a missing feature ;) 
> I've been pondering about similar cases in my head, where you may want to not rebuild images in case they already exist in your registry, or locally, etc. 

I'm not sure that this is much of an issue. e.g. `docker-compose build` will always build over the top of existing images. Existing containers will always use the image that they're running on.

Possibly the only gotcha would be if you pulled out all the stops of "don't do this" and just used the `:latest` tag in all your orchestration.
I don't see what `docker-compose` does as an argument either way here. If your image is costly to build (as one possible example case), and if not rebuilding it while already existing somewhere is something that you would be OK with, then that could be time and energy saved.

Resolving patch:
diff --git a/src/python/pants/backend/docker/util_rules/docker_binary.py b/src/python/pants/backend/docker/util_rules/docker_binary.py
--- a/src/python/pants/backend/docker/util_rules/docker_binary.py
+++ b/src/python/pants/backend/docker/util_rules/docker_binary.py
@@ -15,6 +15,7 @@
     BinaryPaths,
     BinaryPathTest,
     Process,
+    ProcessCacheScope,
     SearchPath,
 )
 from pants.engine.rules import Get, collect_rules, rule
@@ -58,6 +59,7 @@ def build_image(
             ),
             env=env,
             input_digest=digest,
+            cache_scope=ProcessCacheScope.PER_SESSION,
         )
 
     def push_image(self, tags: tuple[str, ...]) -> Process | None:
@@ -65,7 +67,9 @@ def push_image(self, tags: tuple[str, ...]) -> Process | None:
             return None
 
         return Process(
-            argv=(self.path, "push", *tags), description="Pushing docker image {tags[0]}"
+            argv=(self.path, "push", *tags),
+            cache_scope=ProcessCacheScope.PER_SESSION,
+            description=f"Pushing docker image {tags[0]}",
         )
 
 


Test patch:
diff --git a/src/python/pants/backend/docker/util_rules/docker_binary_test.py b/src/python/pants/backend/docker/util_rules/docker_binary_test.py
--- a/src/python/pants/backend/docker/util_rules/docker_binary_test.py
+++ b/src/python/pants/backend/docker/util_rules/docker_binary_test.py
@@ -3,22 +3,49 @@
 
 from hashlib import sha256
 
+import pytest
+
 from pants.backend.docker.util_rules.docker_binary import DockerBinary
 from pants.engine.fs import Digest
-from pants.engine.process import Process
+from pants.engine.process import Process, ProcessCacheScope
+
+
+@pytest.fixture
+def docker_path() -> str:
+    return "/bin/docker"
+
+
+@pytest.fixture
+def docker(docker_path: str) -> DockerBinary:
+    return DockerBinary(docker_path)
 
 
-def test_docker_binary_build_image():
-    docker_path = "/bin/docker"
+def test_docker_binary_build_image(docker_path: str, docker: DockerBinary) -> None:
     dockerfile = "src/test/repo/Dockerfile"
-    docker = DockerBinary(docker_path)
     digest = Digest(sha256().hexdigest(), 123)
-    tags = ["test:0.1.0", "test:latest"]
+    tags = (
+        "test:0.1.0",
+        "test:latest",
+    )
     build_request = docker.build_image(tags, digest, dockerfile)
 
     assert build_request == Process(
         argv=(docker_path, "build", "-t", tags[0], "-t", tags[1], "-f", dockerfile, "."),
         input_digest=digest,
-        description="",
+        cache_scope=ProcessCacheScope.PER_SESSION,
+        description="",  # The description field is marked `compare=False`
     )
     assert build_request.description == "Building docker image test:0.1.0 +1 additional tag."
+
+
+def test_docker_binary_push_image(docker_path: str, docker: DockerBinary) -> None:
+    assert docker.push_image(()) is None
+
+    image_ref = "registry/repo/name:tag"
+    push_request = docker.push_image((image_ref,))
+    assert push_request == Process(
+        argv=(docker_path, "push", image_ref),
+        cache_scope=ProcessCacheScope.PER_SESSION,
+        description="",  # The description field is marked `compare=False`
+    )
+    assert push_request.description == f"Pushing docker image {image_ref}"