Skip to content

Commit

Permalink
Support interpolating Docker build args into the repository field o…
Browse files Browse the repository at this point in the history
…f `docker_image` targets. (#13721)

This is to support image names like
```
project/develop:1.2.3
project/feature_b:1.3.0
project/main:2.0.0
```

All from the same `docker_image` target, based on which branch was checked out.
  • Loading branch information
kaos authored Nov 27, 2021
1 parent 58df809 commit 5656af7
Show file tree
Hide file tree
Showing 2 changed files with 56 additions and 24 deletions.
52 changes: 33 additions & 19 deletions src/python/pants/backend/docker/goals/package_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 "
Expand All @@ -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,
Expand All @@ -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 ()
Expand Down
28 changes: 23 additions & 5 deletions src/python/pants/backend/docker/goals/package_image_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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(
Expand Down

0 comments on commit 5656af7

Please sign in to comment.