Repository: pantsbuild/pants

Issue:
mysteriously terse engine error message when docker image_tags=[]
**Describe the bug**

Starting with https://github.com/pantsbuild/example-docker

```
$ git diff | cat
diff --git a/src/docker/hello_world/BUILD b/src/docker/hello_world/BUILD
index 4b6c1c4..83e04a9 100644
--- a/src/docker/hello_world/BUILD
+++ b/src/docker/hello_world/BUILD
@@ -4,6 +4,7 @@
 docker_image(
     name="python",
     source="Dockerfile.python",
+    image_tags = []
 )
 
 docker_image(
$ pants package src/docker/hello_world:python
15:32:12.80 [ERROR] 1 Exception encountered:

Engine traceback:
  in `package` goal

IndexError: tuple index out of range
$ ls -lsthra .pants.d/
total 16K
 512 -rw-r----- 1 ecsb ecsb    0 Oct  4 15:30 exceptions.log
 512 -rw-r----- 1 ecsb ecsb    0 Oct  4 15:30 exceptions.4637.log
1.5K drwxr-x--- 3 ecsb ecsb    6 Oct  4 15:30 .
1.5K drwxr-x--- 8 ecsb ecsb   15 Oct  4 15:30 ..
 11K drwxr-x--- 6 ecsb ecsb    6 Oct  4 15:32 run-tracker
1.5K -rw-r----- 1 ecsb ecsb 2.7K Oct  4 15:32 pants.log
$ pants package -ltrace src/docker/hello_world:python
15:32:41.40 [INFO] Initialization options changed: reinitializing scheduler...
15:32:47.64 [INFO] Scheduler initialized.
15:32:49.55 [ERROR] 1 Exception encountered:

Engine traceback:
  in `package` goal

IndexError: tuple index out of range
```

Note that the exception log files are empty and logging doesn't give any additional clues.


**Pants version**
2.17.0

**OS**
Linux

**Additional info**
I ran into this with a macro that generated image tags and left them blank in some not-in-CI cases. python tests would then throw the above error locally due to a `runtime_package_dependencies` on said image.  Which is all to say that the cause was not obvious.


Discussion:
Sorry for the trouble, and thanks for the report. I guess an additional venue to address this is to ensure a better error message for a case like this.

My fix merely patches the bug at hand, but if we hit another code path that is flawed in a similar way we'd still be throwing a terrible error message.
As a tip for future, the `--print-stacktrace` flag is handy for getting more info about problems like this (https://www.pantsbuild.org/docs/troubleshooting has a few more tips too, if you haven't seen it), and provides a few more hints about where the problem originated (docker):

```
Traceback (most recent call last):
  File ".../site-packages/pants/engine/internals/selectors.py", line 623, in native_engine_generator_send
    res = rule.send(arg) if err is None else rule.throw(throw or err)
  File ".../site-packages/pants/core/goals/package.py", line 157, in package_asset
    packages = await MultiGet(
  File ".../site-packages/pants/engine/internals/selectors.py", line 358, in MultiGet
    return await _MultiGet(tuple(__arg0))
  File ".../site-packages/pants/engine/internals/selectors.py", line 165, in __await__
    result = yield self.gets
  File ".../site-packages/pants/engine/internals/selectors.py", line 623, in native_engine_generator_send
    res = rule.send(arg) if err is None else rule.throw(throw or err)
  File ".../site-packages/pants/core/goals/package.py", line 108, in environment_aware_package
    package = await Get(
  File ".../site-packages/pants/engine/internals/selectors.py", line 118, in __await__
    result = yield self
  File ".../site-packages/pants/engine/internals/selectors.py", line 623, in native_engine_generator_send
    res = rule.send(arg) if err is None else rule.throw(throw or err)
  File ".../site-packages/pants/backend/docker/goals/package_image.py", line 404, in build_docker_image
    process = docker.build_image(
  File ".../site-packages/pants/backend/docker/util_rules/docker_binary.py", line 84, in build_image
    f"Building docker image {tags[0]}"
IndexError: tuple index out of range
```

(Of course, not minimising that it would be nice for the diagnostics to be better by default, without requiring extra flags. Plus the `-l` vs. `--print-stacktrace` flag difference is a bit annoying!)

Resolving patch:
diff --git a/src/python/pants/backend/docker/goals/package_image.py b/src/python/pants/backend/docker/goals/package_image.py
--- a/src/python/pants/backend/docker/goals/package_image.py
+++ b/src/python/pants/backend/docker/goals/package_image.py
@@ -42,10 +42,10 @@
 from pants.engine.fs import CreateDigest, Digest, FileContent
 from pants.engine.process import FallibleProcessResult, Process, ProcessExecutionFailure
 from pants.engine.rules import Get, MultiGet, collect_rules, rule
-from pants.engine.target import Target, WrappedTarget, WrappedTargetRequest
+from pants.engine.target import InvalidFieldException, Target, WrappedTarget, WrappedTargetRequest
 from pants.engine.unions import UnionMembership, UnionRule
 from pants.option.global_options import GlobalOptions, KeepSandboxes
-from pants.util.strutil import bullet_list
+from pants.util.strutil import bullet_list, softwrap
 from pants.util.value_interpolation import InterpolationContext, InterpolationError
 
 logger = logging.getLogger(__name__)
@@ -392,6 +392,16 @@ async def build_docker_image(
         )
     )
     tags = tuple(tag.full_name for registry in image_refs for tag in registry.tags)
+    if not tags:
+        raise InvalidFieldException(
+            softwrap(
+                f"""
+                The `{DockerImageTagsField.alias}` field in target {field_set.address} must not be
+                empty, unless there is a custom plugin providing additional tags using the
+                `DockerImageTagsRequest` union type.
+                """
+            )
+        )
 
     # Mix the upstream image ids into the env to ensure that Pants invalidates this
     # image-building process correctly when an upstream image changes, even though the


Test patch:
diff --git a/src/python/pants/backend/docker/goals/package_image_test.py b/src/python/pants/backend/docker/goals/package_image_test.py
--- a/src/python/pants/backend/docker/goals/package_image_test.py
+++ b/src/python/pants/backend/docker/goals/package_image_test.py
@@ -1624,18 +1624,40 @@ def test_image_ref_formatting(test: ImageRefTest) -> None:
         assert tuple(image_refs) == test.expect_refs
 
 
-def test_docker_image_tags_from_plugin_hook(rule_runner: RuleRunner) -> None:
-    rule_runner.write_files({"docker/test/BUILD": 'docker_image(name="plugin")'})
+@pytest.mark.parametrize(
+    "BUILD, plugin_tags, tag_flags",
+    [
+        (
+            'docker_image(name="plugin")',
+            ("1.2.3",),
+            (
+                "--tag",
+                "plugin:latest",
+                "--tag",
+                "plugin:1.2.3",
+            ),
+        ),
+        (
+            'docker_image(name="plugin", image_tags=[])',
+            ("1.2.3",),
+            (
+                "--tag",
+                "plugin:1.2.3",
+            ),
+        ),
+    ],
+)
+def test_docker_image_tags_from_plugin_hook(
+    rule_runner: RuleRunner, BUILD: str, plugin_tags: tuple[str, ...], tag_flags: tuple[str, ...]
+) -> None:
+    rule_runner.write_files({"docker/test/BUILD": BUILD})
 
     def check_docker_proc(process: Process):
         assert process.argv == (
             "/dummy/docker",
             "build",
             "--pull=False",
-            "--tag",
-            "plugin:latest",
-            "--tag",
-            "plugin:1.2.3",
+            *tag_flags,
             "--file",
             "docker/test/Dockerfile",
             ".",
@@ -1645,10 +1667,21 @@ def check_docker_proc(process: Process):
         rule_runner,
         Address("docker/test", target_name="plugin"),
         process_assertions=check_docker_proc,
-        plugin_tags=("1.2.3",),
+        plugin_tags=plugin_tags,
     )
 
 
+def test_docker_image_tags_defined(rule_runner: RuleRunner) -> None:
+    rule_runner.write_files({"docker/test/BUILD": 'docker_image(name="no-tags", image_tags=[])'})
+
+    err = "The `image_tags` field in target docker/test:no-tags must not be empty, unless"
+    with pytest.raises(InvalidFieldException, match=err):
+        assert_build(
+            rule_runner,
+            Address("docker/test", target_name="no-tags"),
+        )
+
+
 def test_docker_info_serialize() -> None:
     image_id = "abc123"
     # image refs with unique strings (i.e. not actual templates/names etc.), to make sure they're