Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/notes/2.32.x.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ The option `[docker].push_on_package` can be used to prevent Docker images from

Running `pants publish` on a `docker_image` target with `skip_push=True` will no longer package the `docker_image`.

The option `[docker].build_extra_options` or/and the `build_extra_options` field of `docker_image` targets can now be used to pass extra options to docker build or podman build. Make sure that the options are understood by the tool.
For example, you can now set `build_extra_options=["--no-cache"]` to do not use existing cached images for the container build.

Fields that are already present in the docker image target are surpressed by the extra options when they conflict.
For example, if you set `build_extra_options=["--pull"]`, the `pull` field of the docker image target will be ignored.

#### Helm

When `pants publish` is invoked, Pants will now skip packaging for `helm_chart` targets if either `skip_push=True` or the target has no registries.
Expand Down
48 changes: 44 additions & 4 deletions src/python/pants/backend/docker/goals/package_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
DockerBuildOptionFieldMultiValueMixin,
DockerBuildOptionFieldValueMixin,
DockerBuildOptionFlagFieldMixin,
DockerImageBuildExtraOptionsField,
DockerImageBuildImageOutputField,
DockerImageContextRootField,
DockerImageRegistriesField,
Expand Down Expand Up @@ -323,6 +324,20 @@ class DockerInfoV1ImageTag:
name: str


def _extra_options_flag_names(extra_options: tuple[str, ...]) -> frozenset[str]:
"""Return the set of flag names (e.g. ``--pull``) present in *extra_options*.

Handles both ``--flag=value`` and ``--flag value`` styles so that any structured field whose
``docker_build_option`` class variable matches one of these names will be suppressed in favour
of the caller-supplied value, avoiding duplicate or conflicting flags on the command line.
Comment thread
tim-werner marked this conversation as resolved.
Outdated
"""
names: set[str] = set()
for opt in extra_options:
if opt.startswith("-"):
names.add(opt.split("=")[0])
return frozenset(names)


def get_build_options(
context: DockerBuildContext,
field_set: DockerPackageFieldSet,
Expand All @@ -331,7 +346,23 @@ def get_build_options(
global_build_no_cache_option: bool | None,
use_buildx_option: bool,
target: Target,
global_extra_options: tuple[str, ...] = (),
) -> Iterator[str]:
target_extra = tuple(target[DockerImageBuildExtraOptionsField].value or ())
target_flag_names = _extra_options_flag_names(target_extra)

# Per-target wins: drop any global entry whose flag is already covered by the per-target list.
filtered_global = tuple(
opt
for opt in global_extra_options
if opt.startswith("-") and opt.split("=")[0] not in target_flag_names
Comment thread
tim-werner marked this conversation as resolved.
Outdated
)

extra_options: tuple[str, ...] = (*filtered_global, *target_extra)

# Compute the set of flag names that are provided by global and target extra options.
overridden_flags = _extra_options_flag_names(extra_options)

# Build options from target fields inheriting from DockerBuildOptionFieldMixin
for field_type in target.field_types:
if issubclass(field_type, DockerBuildKitOptionField):
Expand All @@ -356,6 +387,12 @@ def get_build_options(
DockerBuildOptionFlagFieldMixin,
),
):
flag = getattr(
field_type, "docker_build_option", None
) # get the flag name if it exists such as --pull or --network, etc.
if flag and flag in overridden_flags:
continue # skip this field since its flag is already covered by extra_options
Comment thread
tim-werner marked this conversation as resolved.
Outdated

source = InterpolationContext.TextSource(
address=target.address, target_alias=target.alias, field_alias=field_type.alias
)
Expand Down Expand Up @@ -386,11 +423,13 @@ def get_build_options(
)
)

if target_stage:
yield from ("--target", target_stage)
if target_stage and "--target" not in overridden_flags:
extra_options = extra_options + ("--target", target_stage)

if global_build_no_cache_option and "--no-cache" not in overridden_flags:
extra_options = extra_options + ("--no-cache",)

if global_build_no_cache_option:
yield "--no-cache"
yield from extra_options


@dataclass(frozen=True)
Expand Down Expand Up @@ -510,6 +549,7 @@ async def get_docker_image_build_process(
global_build_no_cache_option=options.build_no_cache,
use_buildx_option=options.use_buildx,
target=wrapped_target.target,
global_extra_options=options.build_extra_options,
)
),
)
Expand Down
191 changes: 191 additions & 0 deletions src/python/pants/backend/docker/goals/package_image_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ def _setup_docker_options(rule_runner: RuleRunner, options: dict | None) -> Dock
opts.setdefault("build_hosts", None)
opts.setdefault("build_verbose", False)
opts.setdefault("build_no_cache", False)
opts.setdefault("build_extra_options", [])
opts.setdefault("use_buildx", False)
opts.setdefault("env_vars", [])
opts.setdefault("suggest_renames", True)
Expand Down Expand Up @@ -2972,3 +2973,193 @@ def test_field_set_pushes_on_package(output: dict | None, expected: bool) -> Non
rule_runner.get_target(Address("", target_name="image"))
)
assert field_set.pushes_on_package() is expected


def test_global_build_extra_options(rule_runner: RuleRunner) -> None:
"""Global build_extra_options from [docker] should be passed to the docker build command."""
Comment thread
tim-werner marked this conversation as resolved.
Outdated
rule_runner.set_options(
[],
env={
"PANTS_DOCKER_BUILD_EXTRA_OPTIONS": '["--compress"]',
},
)
rule_runner.write_files(
{
"docker/test/BUILD": dedent(
"""\
docker_image(
name="img1",
)
"""
),
}
)

def check_build_process(result: DockerImageBuildProcess):
assert "--compress" in result.process.argv

assert_build_process(
rule_runner,
Address("docker/test", target_name="img1"),
build_process_assertions=check_build_process,
)


def test_extra_build_options_global_and_target_merged(rule_runner: RuleRunner) -> None:
"""Global options and per-target options should be merged correctly."""
rule_runner.set_options(
[],
env={
"PANTS_DOCKER_BUILD_EXTRA_OPTIONS": '["--compress"]',
},
)
rule_runner.write_files(
{
"docker/test/BUILD": dedent(
"""\
docker_image(
name="img1",
extra_build_options=["--pull=always"],
)
"""
),
}
)

def check_build_process(result: DockerImageBuildProcess):
argv = result.process.argv
assert "--compress" in argv
assert "--pull=always" in argv

assert_build_process(
rule_runner,
Address("docker/test", target_name="img1"),
build_process_assertions=check_build_process,
)


def test_extra_build_options_overrides_pull_field(rule_runner: RuleRunner) -> None:
"""When extra_build_options contains --pull, the docker_image target pull argument is suppressed."""
rule_runner.write_files(
{
"docker/test/BUILD": dedent(
"""\
docker_image(
name="img1",
pull=True,
extra_build_options=["--pull=always"],
)
"""
),
}
)

def check_build_process(result: DockerImageBuildProcess):
argv = result.process.argv
# Only the extra_build_options value should appear; the structured field's
# --pull=True must NOT be present to avoid duplicate flags.
assert "--pull=always" in argv
assert "--pull=True" not in argv

assert_build_process(
rule_runner,
Address("docker/test", target_name="img1"),
build_process_assertions=check_build_process,
)


def test_global_extra_options_overridden_by_target_extra_options(
rule_runner: RuleRunner,
) -> None:
"""When both global and target extra options specify the same flag, the target wins
and the global does not appear."""
rule_runner.set_options(
[],
env={
"PANTS_DOCKER_BUILD_EXTRA_OPTIONS": '["--pull=missing"]',
},
)
rule_runner.write_files(
{
"docker/test/BUILD": dedent(
"""\
docker_image(
name="img1",
extra_build_options=["--pull=always"],
)
"""
),
}
)

def check_build_process(result: DockerImageBuildProcess):
argv = result.process.argv
# Both global and target specify --pull; the per-target value wins and the global
# entry is dropped at the flag-level dedup stage.
assert "--pull=always" in argv
assert "--pull=missing" not in argv # global dropped because target specifies --pull

assert_build_process(
rule_runner,
Address("docker/test", target_name="img1"),
build_process_assertions=check_build_process,
)


def test_global_build_no_cache_options_exits(
rule_runner: RuleRunner,
) -> None:
rule_runner.set_options(
[],
env={
"PANTS_DOCKER_BUILD_EXTRA_OPTIONS": '["--no-cache"]',
},
)
rule_runner.write_files(
{
"docker/test/BUILD": dedent(
"""\
docker_image(
name="img1",
)
"""
),
}
)

def check_build_process(result: DockerImageBuildProcess):
argv = result.process.argv
assert "--no-cache" in argv

assert_build_process(
rule_runner,
Address("docker/test", target_name="img1"),
build_process_assertions=check_build_process,
)


def test_global_build_no_cache_options_overridden_by_target_extra_options(
rule_runner: RuleRunner,
) -> None:
rule_runner.write_files(
{
"docker/test/BUILD": dedent(
"""\
docker_image(
name="img1",
extra_build_options=["--no-cache"],
)
"""
),
}
)

def check_build_process(result: DockerImageBuildProcess):
argv = result.process.argv
assert "--no-cache" in argv

assert_build_process(
rule_runner,
Address("docker/test", target_name="img1"),
build_process_assertions=check_build_process,
)
22 changes: 22 additions & 0 deletions src/python/pants/backend/docker/subsystems/docker_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,24 @@ def env_vars(self) -> tuple[str, ...]:
default=False,
help="Do not use the Docker cache when building images.",
)
_build_extra_options = ShellStrListOption(
help=softwrap(
f"""
Global extra options to pass to the `docker build` / `podman build` command.

These are appended after all other computed flags. If any option specified here uses a
flag that is already covered by a dedicated field (e.g. `--pull`, `--network`,
`--platform`), the dedicated field's contribution is suppressed in favour of
the value given here, to avoid duplicate or conflicting flags.

Example:

[{options_scope}]
build_extra_options = ["--pull=always"]

"""
),
)
build_verbose = BoolOption(
default=False,
help="Whether to log the Docker output to the console. If false, only the image ID is logged.",
Expand Down Expand Up @@ -302,6 +320,10 @@ def env_vars(self) -> tuple[str, ...]:
def build_args(self) -> tuple[str, ...]:
return tuple(sorted(set(self._build_args)))

@property
def build_extra_options(self) -> tuple[str, ...]:
return tuple(self._build_extra_options)

@property
def tools(self) -> tuple[str, ...]:
return tuple(sorted(set(self._tools)))
Expand Down
25 changes: 25 additions & 0 deletions src/python/pants/backend/docker/target_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,30 @@ class DockerImageRunExtraArgsField(StringSequenceField):
)


class DockerImageBuildExtraOptionsField(StringSequenceField):
alias = "extra_build_options"
default = ()
help = help_text(
lambda: f"""
Additional options to pass directly to the `docker build` (or `podman build`) command.

These are appended after all other computed flags. If any option specified here uses a
flag that is already covered by a dedicated field (e.g. `--pull`, `--network`,
`--platform`), the dedicated field's contribution is suppressed in favour of
the value given here, to avoid duplicate or conflicting flags on the command line.

Example:

docker_image(
extra_build_options=["--pull=always", "--compress"],
)

Use `[{DockerOptions.options_scope}].build_extra_options` to set default options for all
images. Per-target option overrides a conflicting global one.
"""
)


class DockerImageTarget(Target):
alias = "docker_image"
core_fields = (
Expand All @@ -622,6 +646,7 @@ class DockerImageTarget(Target):
DockerImageBuildImageCacheFromField,
DockerImageBuildImageOutputField,
DockerImageRunExtraArgsField,
DockerImageBuildExtraOptionsField,
OutputPathField,
RestartableField,
)
Expand Down
Loading