Repository: pantsbuild/pants

Issue:
Docker repository from build args
**Is your feature request related to a problem? Please describe.**

As we build images for our internal registry, the last part of the image name is usually derived from the git branch. Currently there is no easy way to incorporate this value into the built image name, besides the version tag.

**Describe the solution you'd like**

By allowing to interpolate build args into the repository field value, in the same manner as for image tags, we solve this problem.

**Describe alternatives you've considered**

Discarded the idea to have more predefined values based on environmental facts from things like git etc, in favour of a general solution where you can provide the required information as environment variable values.




Discussion:

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
@@ -5,6 +5,7 @@
 import logging
 from dataclasses import dataclass
 from os import path
+from typing import Any, Mapping
 
 from pants.backend.docker.registries import DockerRegistries
 from pants.backend.docker.subsystems.docker_options import DockerOptions
@@ -73,7 +74,7 @@ def format_tag(self, tag: str, version_context: DockerVersionContext) -> str:
             if isinstance(e, KeyError):
                 msg += f"The placeholder {e} is unknown."
                 if version_context:
-                    msg += f' Try with one of: {", ".join(version_context.keys())}.'
+                    msg += f' Try with one of: {", ".join(sorted(version_context.keys()))}.'
                 else:
                     msg += (
                         " There are currently no known placeholders to use. These placeholders "
@@ -84,26 +85,35 @@ def format_tag(self, tag: str, version_context: DockerVersionContext) -> str:
                 msg += str(e)
             raise DockerImageTagValueError(msg) from e
 
-    def format_repository(self, default_repository: str) -> str:
-        directory = path.basename(self.address.spec_path)
-        parent_directory = path.basename(path.dirname(self.address.spec_path))
+    def format_repository(
+        self, default_repository: str, repository_context: Mapping[str, Any]
+    ) -> str:
+        fmt_context = dict(
+            directory=path.basename(self.address.spec_path),
+            name=self.address.target_name,
+            parent_directory=path.basename(path.dirname(self.address.spec_path)),
+            **repository_context,
+        )
         repository_fmt = self.repository.value or default_repository
+
         try:
-            return repository_fmt.format(
-                name=self.address.target_name,
-                directory=directory,
-                parent_directory=parent_directory,
-            )
-        except KeyError as e:
+            return repository_fmt.format(**fmt_context)
+        except (KeyError, ValueError) as e:
             if self.repository.value:
-                source = "`repository` field of the `docker_image` target " f"at {self.address}"
+                source = f"`repository` field of the `docker_image` target at {self.address}"
             else:
                 source = "`[docker].default_repository` configuration option"
 
-            raise DockerRepositoryNameError(
-                f"Invalid value for the {source}: {repository_fmt!r}. Unknown placeholder: {e}.\n\n"
-                f"You may only reference any of `name`, `directory` or `parent_directory`."
-            ) from e
+            msg = f"Invalid value for the {source}: {repository_fmt!r}.\n\n"
+
+            if isinstance(e, KeyError):
+                msg += (
+                    f"The placeholder {e} is unknown. "
+                    f'Try with one of: {", ".join(sorted(fmt_context.keys()))}.'
+                )
+            else:
+                msg += str(e)
+            raise DockerRepositoryNameError(msg) from e
 
     def image_refs(
         self,
@@ -122,13 +132,17 @@ def image_refs(
 
             [<registry>/]<repository-name>[:<tag>]
 
-        Where the `<repository-name>` may have contain any number of separating slashes `/`,
-        depending on the `default_repository` from configuration or the `repository` field
-        on the target `docker_image`.
+        Where the `<repository-name>` may contain any number of separating slashes `/`, depending on
+        the `default_repository` from configuration or the `repository` field on the target
+        `docker_image`.
 
         This method will always return a non-empty tuple.
         """
-        repository = self.format_repository(default_repository)
+        repository_context = {}
+        if "build_args" in version_context:
+            repository_context["build_args"] = version_context["build_args"]
+
+        repository = self.format_repository(default_repository, repository_context)
         image_names = tuple(
             ":".join(s for s in [repository, self.format_tag(tag, version_context)] if s)
             for tag in self.tags.value or ()


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
@@ -206,8 +206,8 @@ def test_build_docker_image(rule_runner: RuleRunner) -> None:
 
     err1 = (
         r"Invalid value for the `repository` field of the `docker_image` target at "
-        r"docker/test:err1: '{bad_template}'\. Unknown placeholder: 'bad_template'\.\n\n"
-        r"You may only reference any of `name`, `directory` or `parent_directory`\."
+        r"docker/test:err1: '{bad_template}'\.\n\nThe placeholder 'bad_template' is unknown\. "
+        r"Try with one of: directory, name, parent_directory\."
     )
     with pytest.raises(DockerRepositoryNameError, match=err1):
         assert_build(
@@ -347,8 +347,8 @@ def assert_tags(name: str, *expect_tags: str) -> None:
     err_1 = (
         r"Invalid tag value for the `image_tags` field of the `docker_image` target at "
         r"docker/test:err_1: '{unknown_stage}'\.\n\n"
-        r"The placeholder 'unknown_stage' is unknown\. Try with one of: baseimage, stage0, "
-        r"interim, stage2, output\."
+        r"The placeholder 'unknown_stage' is unknown\. Try with one of: baseimage, interim, "
+        r"output, stage0, stage2\."
     )
     with pytest.raises(DockerImageTagValueError, match=err_1):
         assert_tags("err_1")
@@ -457,7 +457,25 @@ def test_docker_image_version_from_build_arg(rule_runner: RuleRunner) -> None:
     )
 
 
-def test_docker_build_args_field(rule_runner: RuleRunner) -> None:
+def test_docker_repository_from_build_arg(rule_runner: RuleRunner) -> None:
+    rule_runner.write_files(
+        {"docker/test/BUILD": 'docker_image(name="image", repository="{build_args.REPO}")'}
+    )
+    rule_runner.set_options(
+        [],
+        env={
+            "PANTS_DOCKER_BUILD_ARGS": '["REPO=test/image"]',
+        },
+    )
+
+    assert_build(
+        rule_runner,
+        Address("docker/test", target_name="image"),
+        "Built docker image: test/image:latest",
+    )
+
+
+def test_docker_extra_build_args_field(rule_runner: RuleRunner) -> None:
     rule_runner.write_files(
         {
             "docker/test/BUILD": dedent(