From 60e9bfde2a25f107d5075eac4b3b76e0348a41ad Mon Sep 17 00:00:00 2001 From: Tim Werner Date: Fri, 23 Jan 2026 11:59:09 +0100 Subject: [PATCH 01/13] support pull for podman --- .../backend/docker/goals/package_image.py | 6 +- .../goals/package_image_podman_pull_test.py | 217 ++++++++++++++++++ .../pants/backend/docker/target_types.py | 71 +++++- 3 files changed, 288 insertions(+), 6 deletions(-) create mode 100644 src/python/pants/backend/docker/goals/package_image_podman_pull_test.py diff --git a/src/python/pants/backend/docker/goals/package_image.py b/src/python/pants/backend/docker/goals/package_image.py index 801351d4972..2c62ee674ce 100644 --- a/src/python/pants/backend/docker/goals/package_image.py +++ b/src/python/pants/backend/docker/goals/package_image.py @@ -331,6 +331,7 @@ def get_build_options( global_build_no_cache_option: bool | None, use_buildx_option: bool, target: Target, + docker: DockerBinary | None = None, ) -> Iterator[str]: # Build options from target fields inheriting from DockerBuildOptionFieldMixin for field_type in target.field_types: @@ -355,7 +356,7 @@ def get_build_options( DockerBuildOptionFieldMultiValueMixin, DockerBuildOptionFlagFieldMixin, ), - ): + ) or field_type.__name__ == "DockerImageBuildPullOptionField": source = InterpolationContext.TextSource( address=target.address, target_alias=target.alias, field_alias=field_type.alias ) @@ -365,7 +366,7 @@ def get_build_options( error_cls=DockerImageOptionValueError, ) yield from target[field_type].options( - format, global_build_hosts_options=global_build_hosts_options + format, global_build_hosts_options=global_build_hosts_options, docker=docker ) # Target stage @@ -510,6 +511,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, + docker=docker, ) ), ) diff --git a/src/python/pants/backend/docker/goals/package_image_podman_pull_test.py b/src/python/pants/backend/docker/goals/package_image_podman_pull_test.py new file mode 100644 index 00000000000..5936d91e9ab --- /dev/null +++ b/src/python/pants/backend/docker/goals/package_image_podman_pull_test.py @@ -0,0 +1,217 @@ +# Copyright 2021 Pants project contributors (see CONTRIBUTORS.md). +# Licensed under the Apache License, Version 2.0 (see LICENSE). + +"""Integration tests for Podman-specific pull policy behavior in DockerImageBuildPullOptionField.""" + +from __future__ import annotations + +import pytest + +from pants.backend.docker.goals.package_image import ( + DockerPackageFieldSet, + build_docker_image, +) +from pants.backend.docker.goals.package_image import rules +from pants.backend.docker.subsystems.docker_options import DockerOptions +from pants.backend.docker.target_types import DockerImageTarget +from pants.backend.docker.util_rules.docker_binary import DockerBinary +from pants.backend.docker.util_rules.docker_build_args import rules as build_args_rules +from pants.backend.docker.util_rules.docker_build_env import rules as build_env_rules +from pants.engine.addresses import Address +from pants.engine.fs import EMPTY_DIGEST, CreateDigest +from pants.engine.process import Process, ProcessExecutionEnvironment +from pants.engine.target import InvalidFieldException, WrappedTarget +from pants.engine.unions import UnionMembership +from pants.option.global_options import GlobalOptions, KeepSandboxes +from pants.testutil.option_util import create_subsystem +from pants.testutil.rule_runner import QueryRule, RuleRunner, run_rule_with_mocks + + +@pytest.fixture +def rule_runner() -> RuleRunner: + return RuleRunner( + rules=[ + *rules(), + *build_args_rules(), + *build_env_rules(), + QueryRule(GlobalOptions, []), + QueryRule(DockerOptions, []), + ], + target_types=[DockerImageTarget], + ) + + +def create_test_context(rule_runner: RuleRunner, pull_value=None): + """Helper to create a mock build context and target with specific pull value.""" + from pants.backend.docker.util_rules.docker_build_context import DockerBuildContext + from pants.backend.docker.util_rules.docker_build_args import DockerBuildArgs + from pants.backend.docker.util_rules.docker_build_env import DockerBuildEnvironment + from pants.util.value_interpolation import InterpolationContext, InterpolationValue + + # Create BUILD file with optional pull value + # Python booleans need to be capitalized (True/False) in BUILD files + build_content = "docker_image(name='test'" + if pull_value is not None: + if isinstance(pull_value, str): + build_content += f", pull='{pull_value}'" + else: + # Convert bool to string with proper capitalization + build_content += f", pull={str(pull_value)}" + build_content += ")" + + rule_runner.write_files({ + "test/BUILD": build_content, + "test/Dockerfile": "FROM alpine:3.16\n", + }) + + tgt = rule_runner.get_target(Address("test")) + + # Mock build context + build_context = DockerBuildContext( + build_args=DockerBuildArgs(), + digest=EMPTY_DIGEST, + dockerfile="test/Dockerfile", + build_env=DockerBuildEnvironment(environment={}), + interpolation_context=InterpolationContext.from_dict({ + "tags": InterpolationValue({}), + }), + copy_source_vs_context_source=(("test/Dockerfile", ""),), + stages=(), + upstream_image_ids=(), + ) + + return tgt, build_context + + +@pytest.mark.parametrize( + "policy", + ["always", "missing", "never", "newer"], +) +def test_podman_pull_string_policies(rule_runner: RuleRunner, policy: str) -> None: + """Test that Podman accepts all valid string pull policies.""" + tgt, build_context = create_test_context(rule_runner, pull_value=policy) + + process_args = [] + + def capture_process(process: Process): + process_args.append(process.argv) + from pants.engine.process import FallibleProcessResult, ProcessResultMetadata + return FallibleProcessResult( + stdout=b"Successfully built abc123\n", + stdout_digest=EMPTY_DIGEST, + stderr=b"", + stderr_digest=EMPTY_DIGEST, + exit_code=0, + output_digest=EMPTY_DIGEST, + metadata=ProcessResultMetadata(0, ProcessExecutionEnvironment(environment_name=None, platform="linux_x86_64", docker_image=None, remote_execution=False, remote_execution_extra_platform_properties=[], execute_in_workspace=False, keep_sandboxes="never"), "ran_locally", 0), + ) + + def mock_digest(_: CreateDigest): + return EMPTY_DIGEST + + docker_options = create_subsystem( + DockerOptions, + registries={}, + default_repository="{name}", + default_context_root="", + build_args=[], + build_target_stage=None, + build_hosts=None, + build_verbose=False, + build_no_cache=False, + use_buildx=False, + env_vars=[], + ) + + global_options = rule_runner.request(GlobalOptions, []) + + # Use Podman binary + podman_binary = DockerBinary( + path="/bin/podman", + fingerprint="test", + extra_env={}, + extra_input_digests=None, + is_podman=True, + ) + + result = run_rule_with_mocks( + build_docker_image, + rule_args=[ + DockerPackageFieldSet.create(tgt), + docker_options, + global_options, + podman_binary, + KeepSandboxes.never, + UnionMembership.from_rules([]), + ], + mock_calls={ + "pants.backend.docker.util_rules.docker_build_context.create_docker_build_context": lambda _req: build_context, + "pants.engine.internals.graph.resolve_target": lambda _: WrappedTarget(tgt), + "pants.engine.intrinsics.execute_process": capture_process, + "pants.engine.intrinsics.create_digest": mock_digest, + }, + union_membership=UnionMembership.from_rules([]), + show_warnings=False, + ) + + # Verify that the correct policy was used + assert len(process_args) == 1 + argv = process_args[0] + expected_flag = f"--pull={policy}" + assert expected_flag in argv, f"Expected '{expected_flag}' in {argv}" + + +def test_docker_pull_string_raises_error(rule_runner: RuleRunner) -> None: + """Test that Docker backend raises error when given a string pull policy.""" + tgt, build_context = create_test_context(rule_runner, pull_value="always") + + docker_options = create_subsystem( + DockerOptions, + registries={}, + default_repository="{name}", + default_context_root="", + build_args=[], + build_target_stage=None, + build_hosts=None, + build_verbose=False, + build_no_cache=False, + use_buildx=False, + env_vars=[], + ) + + global_options = rule_runner.request(GlobalOptions, []) + + # Use Docker binary (not Podman) + docker_binary = DockerBinary( + path="/bin/docker", + fingerprint="test", + extra_env={}, + extra_input_digests=None, + is_podman=False, + ) + + def mock_digest(_: CreateDigest): + return EMPTY_DIGEST + + # Should raise InvalidFieldException + with pytest.raises(InvalidFieldException) as exc_info: + run_rule_with_mocks( + build_docker_image, + rule_args=[ + DockerPackageFieldSet.create(tgt), + docker_options, + global_options, + docker_binary, + KeepSandboxes.never, + UnionMembership.from_rules([]), + ], + mock_calls={ + "pants.backend.docker.util_rules.docker_build_context.create_docker_build_context": lambda _req: build_context, + "pants.engine.internals.graph.resolve_target": lambda _: WrappedTarget(tgt), + "pants.engine.intrinsics.create_digest": mock_digest, + }, + union_membership=UnionMembership.from_rules([]), + show_warnings=False, + ) + + assert "string pull policies are only supported by Podman" in str(exc_info.value) \ No newline at end of file diff --git a/src/python/pants/backend/docker/target_types.py b/src/python/pants/backend/docker/target_types.py index 47a24701821..649d46c2876 100644 --- a/src/python/pants/backend/docker/target_types.py +++ b/src/python/pants/backend/docker/target_types.py @@ -5,6 +5,7 @@ import os import re +import warnings from abc import ABC, abstractmethod from collections.abc import Callable, Iterator from dataclasses import dataclass @@ -252,7 +253,7 @@ def option_values( @final def options( - self, value_formatter: OptionValueFormatter, global_build_hosts_options + self, value_formatter: OptionValueFormatter, global_build_hosts_options, **kwargs ) -> Iterator[str]: for value in self.option_values( value_formatter=value_formatter, global_build_hosts_options=global_build_hosts_options @@ -523,12 +524,17 @@ def options(self, *args, **kwargs) -> Iterator[str]: yield f"{self.docker_build_option}={','.join(list(self.value))}" -class DockerImageBuildPullOptionField(DockerBuildOptionFieldValueMixin, BoolField): +class DockerImageBuildPullOptionField(Field): alias = "pull" - default = False + default = None help = help_text( """ - If true, then docker will always attempt to pull a newer version of the image. + Pull policy for the image. + + For Docker: accepts boolean (true to always pull, false to use cached). + For Podman: accepts boolean or string policy ("always", "missing", "never", "newer"). + + Default: false for Docker, "missing" for Podman. NOTE: This option cannot be used on images that build off of "transitive" base images referenced by address (i.e. `FROM path/to/your/base/Dockerfile`). @@ -536,6 +542,63 @@ class DockerImageBuildPullOptionField(DockerBuildOptionFieldValueMixin, BoolFiel ) docker_build_option = "--pull" + @classmethod + def compute_value(cls, raw_value, address): + value_or_default = super().compute_value(raw_value, address=address) + if value_or_default is None: + return None + if not isinstance(value_or_default, (bool, str)): + raise InvalidFieldException( + f"The {cls.alias!r} field in target {address} must be a boolean or a string, " + f"but was {type(value_or_default).__name__}." + ) + if isinstance(value_or_default, str): + valid_policies = ("always", "missing", "never", "newer") + if value_or_default not in valid_policies: + raise InvalidFieldException( + f"The {cls.alias!r} field in target {address} must be one of {valid_policies}, " + f"but was {value_or_default!r}." + ) + return value_or_default + + def options(self, value_formatter, global_build_hosts_options=None, **kwargs): + # Determine backend type from DockerBinary (which is resolved based on + # the [docker].experimental_enable_podman option). When experimental_enable_podman=true, + # the docker_binary will be 'podman' and is_podman will be True. + docker_binary = kwargs.get("docker") or kwargs.get("docker_binary") + is_podman = getattr(docker_binary, "is_podman", False) if docker_binary is not None else False + + val = self.value + if val is None: + # Use defaults based on backend + val = "missing" if is_podman else False + + if isinstance(val, str): + # String policies are only supported by Podman + if not is_podman: + raise InvalidFieldException( + f"The {self.alias!r} field was set to string value {val!r}, " + f"but string pull policies are only supported by Podman, not Docker. " + f"Use a boolean value (true/false) for Docker." + ) + yield f"{self.docker_build_option}={value_formatter(val)}" + else: + # Boolean value + if is_podman: + # Convert boolean to Podman policy string + warnings.warn( + f"Using boolean values for the 'pull' field with Podman is deprecated. " + f"Please use string values instead: 'always', 'missing', 'never', or 'newer'. " + f"Boolean {val} is being converted to 'always' if val else 'missing' policy.", + DeprecationWarning, + stacklevel=2, + ) + policy = "always" if val else "missing" + yield f"{self.docker_build_option}={policy}" + else: + # Docker: emit explicit boolean value with capital first letter + yield f"{self.docker_build_option}={str(val).capitalize()}" + class DockerBuildOptionFlagFieldMixin(BoolField, ABC): """Inherit this mixin class to provide optional flags (i.e. add `--flag` only when the value is From be23b6739c23d62d6324ca66024e1b2db547aaa8 Mon Sep 17 00:00:00 2001 From: Tim Werner Date: Fri, 23 Jan 2026 14:19:52 +0100 Subject: [PATCH 02/13] add new target type --- .../pants/backend/docker/target_types.py | 4 ++- src/python/pants/engine/target.py | 31 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/python/pants/backend/docker/target_types.py b/src/python/pants/backend/docker/target_types.py index 649d46c2876..3ba4ffa6e6a 100644 --- a/src/python/pants/backend/docker/target_types.py +++ b/src/python/pants/backend/docker/target_types.py @@ -33,6 +33,7 @@ ListOfDictStringToStringField, OptionalSingleSourceField, StringField, + StringOrBoolField, StringSequenceField, Target, Targets, @@ -524,7 +525,7 @@ def options(self, *args, **kwargs) -> Iterator[str]: yield f"{self.docker_build_option}={','.join(list(self.value))}" -class DockerImageBuildPullOptionField(Field): +class DockerImageBuildPullOptionField(DockerBuildOptionFieldValueMixin, StringOrBoolField): alias = "pull" default = None help = help_text( @@ -547,6 +548,7 @@ def compute_value(cls, raw_value, address): value_or_default = super().compute_value(raw_value, address=address) if value_or_default is None: return None + if not isinstance(value_or_default, (bool, str)): raise InvalidFieldException( f"The {cls.alias!r} field in target {address} must be a boolean or a string, " diff --git a/src/python/pants/engine/target.py b/src/python/pants/engine/target.py index 452dbf701ee..12d06220aa4 100644 --- a/src/python/pants/engine/target.py +++ b/src/python/pants/engine/target.py @@ -1900,6 +1900,37 @@ def compute_value(cls, raw_value: str | None, address: Address) -> str | None: return value_or_default +class StringOrBoolField(Field): + """A field whose value can be either a string or a boolean. + + This is useful for fields that need to accept both boolean flags and string options. + Subclasses must either set `default: str | bool` or `required = True` so that the value is + always defined. + + If you expect the string to only be one of several values, set the class property + `valid_choices`. + """ + + value: str | bool | None + default: ClassVar[str | bool | None] = None + valid_choices: ClassVar[type[Enum] | tuple[str, ...] | None] = None + + @classmethod + def compute_value(cls, raw_value: str | bool | None, address: Address) -> str | bool | None: + value_or_default = super().compute_value(raw_value, address) + if value_or_default is not None: + if not isinstance(value_or_default, (str, bool)): + raise InvalidFieldTypeException( + address, cls.alias, raw_value, expected_type="a string or boolean" + ) + # Validate string choices if provided + if isinstance(value_or_default, str) and cls.valid_choices is not None: + _validate_choices( + address, cls.alias, [value_or_default], valid_choices=cls.valid_choices + ) + return value_or_default + + class SequenceField(Generic[T], Field): """A field whose value is a homogeneous sequence. From cc856b1738a0d0eea7d8685afaf8148771bbd9ed Mon Sep 17 00:00:00 2001 From: Tim Werner Date: Fri, 23 Jan 2026 14:39:46 +0100 Subject: [PATCH 03/13] add type to help info extracter --- .../pants/backend/docker/target_types.py | 23 ++----------------- src/python/pants/help/help_info_extracter.py | 10 +++++--- 2 files changed, 9 insertions(+), 24 deletions(-) diff --git a/src/python/pants/backend/docker/target_types.py b/src/python/pants/backend/docker/target_types.py index 3ba4ffa6e6a..ec8dce48b91 100644 --- a/src/python/pants/backend/docker/target_types.py +++ b/src/python/pants/backend/docker/target_types.py @@ -525,9 +525,10 @@ def options(self, *args, **kwargs) -> Iterator[str]: yield f"{self.docker_build_option}={','.join(list(self.value))}" -class DockerImageBuildPullOptionField(DockerBuildOptionFieldValueMixin, StringOrBoolField): +class DockerImageBuildPullOptionField(StringOrBoolField): alias = "pull" default = None + valid_choices = ("always", "missing", "never", "newer") help = help_text( """ Pull policy for the image. @@ -543,26 +544,6 @@ class DockerImageBuildPullOptionField(DockerBuildOptionFieldValueMixin, StringOr ) docker_build_option = "--pull" - @classmethod - def compute_value(cls, raw_value, address): - value_or_default = super().compute_value(raw_value, address=address) - if value_or_default is None: - return None - - if not isinstance(value_or_default, (bool, str)): - raise InvalidFieldException( - f"The {cls.alias!r} field in target {address} must be a boolean or a string, " - f"but was {type(value_or_default).__name__}." - ) - if isinstance(value_or_default, str): - valid_policies = ("always", "missing", "never", "newer") - if value_or_default not in valid_policies: - raise InvalidFieldException( - f"The {cls.alias!r} field in target {address} must be one of {valid_policies}, " - f"but was {value_or_default!r}." - ) - return value_or_default - def options(self, value_formatter, global_build_hosts_options=None, **kwargs): # Determine backend type from DockerBinary (which is resolved based on # the [docker].experimental_enable_podman option). When experimental_enable_podman=true, diff --git a/src/python/pants/help/help_info_extracter.py b/src/python/pants/help/help_info_extracter.py index 6b9a5b1d69b..025e694d168 100644 --- a/src/python/pants/help/help_info_extracter.py +++ b/src/python/pants/help/help_info_extracter.py @@ -27,7 +27,7 @@ from pants.engine.goal import GoalSubsystem from pants.engine.internals.parser import BuildFileSymbolInfo, BuildFileSymbolsInfo, Registrar from pants.engine.rules import Rule, TaskRule -from pants.engine.target import Field, RegisteredTargetTypes, StringField, Target, TargetGenerator +from pants.engine.target import Field, RegisteredTargetTypes, StringField, Target, TargetGenerator, StringOrBoolField from pants.engine.unions import UnionMembership, UnionRule, is_union from pants.option.native_options import NativeOptionParser, parse_dest from pants.option.option_types import OptionInfo @@ -205,13 +205,17 @@ def create(cls, field: type[Field], *, provider: str) -> TargetFieldHelpInfo: type_hint = pretty_print_type_hint(raw_value_type) # Check if the field only allows for certain choices. - if issubclass(field, StringField) and field.valid_choices is not None: + if issubclass(field, (StringField, StringOrBoolField)) and field.valid_choices is not None: valid_choices = sorted( field.valid_choices if isinstance(field.valid_choices, tuple) else (choice.value for choice in field.valid_choices) ) - type_hint = " | ".join([*(repr(c) for c in valid_choices), "None"]) + if issubclass(field, StringOrBoolField): + type_hint = " | ".join([*(repr(c) for c in valid_choices), "bool", "None"]) + else: + type_hint = " | ".join([*(repr(c) for c in valid_choices), "None"]) + if field.required: # We hackily remove `None` as a valid option for the field when it's required. This From 38dcb98e0b88e7ab599dd04d0466a896aa08937a Mon Sep 17 00:00:00 2001 From: Tim Werner Date: Fri, 23 Jan 2026 14:56:54 +0100 Subject: [PATCH 04/13] lint --- .../goals/package_image_podman_pull_test.py | 80 ++++++++++++------- 1 file changed, 49 insertions(+), 31 deletions(-) diff --git a/src/python/pants/backend/docker/goals/package_image_podman_pull_test.py b/src/python/pants/backend/docker/goals/package_image_podman_pull_test.py index 5936d91e9ab..33abe9347c3 100644 --- a/src/python/pants/backend/docker/goals/package_image_podman_pull_test.py +++ b/src/python/pants/backend/docker/goals/package_image_podman_pull_test.py @@ -10,8 +10,8 @@ from pants.backend.docker.goals.package_image import ( DockerPackageFieldSet, build_docker_image, + rules, ) -from pants.backend.docker.goals.package_image import rules from pants.backend.docker.subsystems.docker_options import DockerOptions from pants.backend.docker.target_types import DockerImageTarget from pants.backend.docker.util_rules.docker_binary import DockerBinary @@ -43,11 +43,11 @@ def rule_runner() -> RuleRunner: def create_test_context(rule_runner: RuleRunner, pull_value=None): """Helper to create a mock build context and target with specific pull value.""" - from pants.backend.docker.util_rules.docker_build_context import DockerBuildContext from pants.backend.docker.util_rules.docker_build_args import DockerBuildArgs + from pants.backend.docker.util_rules.docker_build_context import DockerBuildContext from pants.backend.docker.util_rules.docker_build_env import DockerBuildEnvironment from pants.util.value_interpolation import InterpolationContext, InterpolationValue - + # Create BUILD file with optional pull value # Python booleans need to be capitalized (True/False) in BUILD files build_content = "docker_image(name='test'" @@ -58,28 +58,32 @@ def create_test_context(rule_runner: RuleRunner, pull_value=None): # Convert bool to string with proper capitalization build_content += f", pull={str(pull_value)}" build_content += ")" - - rule_runner.write_files({ - "test/BUILD": build_content, - "test/Dockerfile": "FROM alpine:3.16\n", - }) - + + rule_runner.write_files( + { + "test/BUILD": build_content, + "test/Dockerfile": "FROM alpine:3.16\n", + } + ) + tgt = rule_runner.get_target(Address("test")) - + # Mock build context build_context = DockerBuildContext( build_args=DockerBuildArgs(), digest=EMPTY_DIGEST, dockerfile="test/Dockerfile", build_env=DockerBuildEnvironment(environment={}), - interpolation_context=InterpolationContext.from_dict({ - "tags": InterpolationValue({}), - }), + interpolation_context=InterpolationContext.from_dict( + { + "tags": InterpolationValue({}), + } + ), copy_source_vs_context_source=(("test/Dockerfile", ""),), stages=(), upstream_image_ids=(), ) - + return tgt, build_context @@ -90,12 +94,13 @@ def create_test_context(rule_runner: RuleRunner, pull_value=None): def test_podman_pull_string_policies(rule_runner: RuleRunner, policy: str) -> None: """Test that Podman accepts all valid string pull policies.""" tgt, build_context = create_test_context(rule_runner, pull_value=policy) - + process_args = [] - + def capture_process(process: Process): process_args.append(process.argv) from pants.engine.process import FallibleProcessResult, ProcessResultMetadata + return FallibleProcessResult( stdout=b"Successfully built abc123\n", stdout_digest=EMPTY_DIGEST, @@ -103,12 +108,25 @@ def capture_process(process: Process): stderr_digest=EMPTY_DIGEST, exit_code=0, output_digest=EMPTY_DIGEST, - metadata=ProcessResultMetadata(0, ProcessExecutionEnvironment(environment_name=None, platform="linux_x86_64", docker_image=None, remote_execution=False, remote_execution_extra_platform_properties=[], execute_in_workspace=False, keep_sandboxes="never"), "ran_locally", 0), + metadata=ProcessResultMetadata( + 0, + ProcessExecutionEnvironment( + environment_name=None, + platform="linux_x86_64", + docker_image=None, + remote_execution=False, + remote_execution_extra_platform_properties=[], + execute_in_workspace=False, + keep_sandboxes="never", + ), + "ran_locally", + 0, + ), ) - + def mock_digest(_: CreateDigest): return EMPTY_DIGEST - + docker_options = create_subsystem( DockerOptions, registries={}, @@ -122,9 +140,9 @@ def mock_digest(_: CreateDigest): use_buildx=False, env_vars=[], ) - + global_options = rule_runner.request(GlobalOptions, []) - + # Use Podman binary podman_binary = DockerBinary( path="/bin/podman", @@ -133,8 +151,8 @@ def mock_digest(_: CreateDigest): extra_input_digests=None, is_podman=True, ) - - result = run_rule_with_mocks( + + run_rule_with_mocks( build_docker_image, rule_args=[ DockerPackageFieldSet.create(tgt), @@ -153,7 +171,7 @@ def mock_digest(_: CreateDigest): union_membership=UnionMembership.from_rules([]), show_warnings=False, ) - + # Verify that the correct policy was used assert len(process_args) == 1 argv = process_args[0] @@ -164,7 +182,7 @@ def mock_digest(_: CreateDigest): def test_docker_pull_string_raises_error(rule_runner: RuleRunner) -> None: """Test that Docker backend raises error when given a string pull policy.""" tgt, build_context = create_test_context(rule_runner, pull_value="always") - + docker_options = create_subsystem( DockerOptions, registries={}, @@ -178,9 +196,9 @@ def test_docker_pull_string_raises_error(rule_runner: RuleRunner) -> None: use_buildx=False, env_vars=[], ) - + global_options = rule_runner.request(GlobalOptions, []) - + # Use Docker binary (not Podman) docker_binary = DockerBinary( path="/bin/docker", @@ -189,10 +207,10 @@ def test_docker_pull_string_raises_error(rule_runner: RuleRunner) -> None: extra_input_digests=None, is_podman=False, ) - + def mock_digest(_: CreateDigest): return EMPTY_DIGEST - + # Should raise InvalidFieldException with pytest.raises(InvalidFieldException) as exc_info: run_rule_with_mocks( @@ -213,5 +231,5 @@ def mock_digest(_: CreateDigest): union_membership=UnionMembership.from_rules([]), show_warnings=False, ) - - assert "string pull policies are only supported by Podman" in str(exc_info.value) \ No newline at end of file + + assert "string pull policies are only supported by Podman" in str(exc_info.value) From dde9a9bc266ea275f8b06952d88ff41f88b965cb Mon Sep 17 00:00:00 2001 From: Tim Werner Date: Fri, 23 Jan 2026 15:14:49 +0100 Subject: [PATCH 05/13] fix linting --- .../backend/docker/goals/package_image.py | 25 +++++++++++-------- .../pants/backend/docker/target_types.py | 11 ++++---- src/python/pants/help/help_info_extracter.py | 10 ++++++-- 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/src/python/pants/backend/docker/goals/package_image.py b/src/python/pants/backend/docker/goals/package_image.py index 2c62ee674ce..9feeffa58e9 100644 --- a/src/python/pants/backend/docker/goals/package_image.py +++ b/src/python/pants/backend/docker/goals/package_image.py @@ -346,17 +346,20 @@ def get_build_options( # Case where BuildKit option has a default value - still should not be generated continue - if issubclass( - field_type, - ( - DockerBuildOptionFieldMixin, - DockerBuildOptionFieldMultiValueDictMixin, - DockerBuildOptionFieldListOfMultiValueDictMixin, - DockerBuildOptionFieldValueMixin, - DockerBuildOptionFieldMultiValueMixin, - DockerBuildOptionFlagFieldMixin, - ), - ) or field_type.__name__ == "DockerImageBuildPullOptionField": + if ( + issubclass( + field_type, + ( + DockerBuildOptionFieldMixin, + DockerBuildOptionFieldMultiValueDictMixin, + DockerBuildOptionFieldListOfMultiValueDictMixin, + DockerBuildOptionFieldValueMixin, + DockerBuildOptionFieldMultiValueMixin, + DockerBuildOptionFlagFieldMixin, + ), + ) + or field_type.__name__ == "DockerImageBuildPullOptionField" + ): source = InterpolationContext.TextSource( address=target.address, target_alias=target.alias, field_alias=field_type.alias ) diff --git a/src/python/pants/backend/docker/target_types.py b/src/python/pants/backend/docker/target_types.py index ec8dce48b91..f44b348987b 100644 --- a/src/python/pants/backend/docker/target_types.py +++ b/src/python/pants/backend/docker/target_types.py @@ -532,10 +532,9 @@ class DockerImageBuildPullOptionField(StringOrBoolField): help = help_text( """ Pull policy for the image. - + For Docker: accepts boolean (true to always pull, false to use cached). For Podman: accepts boolean or string policy ("always", "missing", "never", "newer"). - Default: false for Docker, "missing" for Podman. NOTE: This option cannot be used on images that build off of "transitive" base images @@ -549,13 +548,15 @@ def options(self, value_formatter, global_build_hosts_options=None, **kwargs): # the [docker].experimental_enable_podman option). When experimental_enable_podman=true, # the docker_binary will be 'podman' and is_podman will be True. docker_binary = kwargs.get("docker") or kwargs.get("docker_binary") - is_podman = getattr(docker_binary, "is_podman", False) if docker_binary is not None else False - + is_podman = ( + getattr(docker_binary, "is_podman", False) if docker_binary is not None else False + ) + val = self.value if val is None: # Use defaults based on backend val = "missing" if is_podman else False - + if isinstance(val, str): # String policies are only supported by Podman if not is_podman: diff --git a/src/python/pants/help/help_info_extracter.py b/src/python/pants/help/help_info_extracter.py index 025e694d168..85536263dcc 100644 --- a/src/python/pants/help/help_info_extracter.py +++ b/src/python/pants/help/help_info_extracter.py @@ -27,7 +27,14 @@ from pants.engine.goal import GoalSubsystem from pants.engine.internals.parser import BuildFileSymbolInfo, BuildFileSymbolsInfo, Registrar from pants.engine.rules import Rule, TaskRule -from pants.engine.target import Field, RegisteredTargetTypes, StringField, Target, TargetGenerator, StringOrBoolField +from pants.engine.target import ( + Field, + RegisteredTargetTypes, + StringField, + StringOrBoolField, + Target, + TargetGenerator, +) from pants.engine.unions import UnionMembership, UnionRule, is_union from pants.option.native_options import NativeOptionParser, parse_dest from pants.option.option_types import OptionInfo @@ -215,7 +222,6 @@ def create(cls, field: type[Field], *, provider: str) -> TargetFieldHelpInfo: type_hint = " | ".join([*(repr(c) for c in valid_choices), "bool", "None"]) else: type_hint = " | ".join([*(repr(c) for c in valid_choices), "None"]) - if field.required: # We hackily remove `None` as a valid option for the field when it's required. This From 8eb88d9f6d9a105b7eed6dd5e9dfa91bc09723cc Mon Sep 17 00:00:00 2001 From: Tim Werner Date: Fri, 23 Jan 2026 15:21:34 +0100 Subject: [PATCH 06/13] add release note to 2.31 --- docs/notes/2.31.x.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/notes/2.31.x.md b/docs/notes/2.31.x.md index 57ba60d9e7f..b19dd60aae7 100644 --- a/docs/notes/2.31.x.md +++ b/docs/notes/2.31.x.md @@ -31,6 +31,9 @@ This work stands on the shoulders of support from the [Science Projects](https:/ ### Backends +the `--pull` flag for `docker_image` targets now supports also podman. When podman is activated the flag can be set to `missing`, `always`,`never` and `newer` as well as False (equal to `missing`) or True (equal to `always`). +The default behavior is now `missing`, which pulls the base image only if it is not already present locally. + #### Helm #### JVM From 53f4362f104ceda809f4d57608dd6cd230f86d76 Mon Sep 17 00:00:00 2001 From: Tim Werner Date: Fri, 23 Jan 2026 16:34:42 +0100 Subject: [PATCH 07/13] fix mypy --- src/python/pants/backend/docker/goals/package_image.py | 1 - src/python/pants/engine/target.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/python/pants/backend/docker/goals/package_image.py b/src/python/pants/backend/docker/goals/package_image.py index 9feeffa58e9..57b3829849f 100644 --- a/src/python/pants/backend/docker/goals/package_image.py +++ b/src/python/pants/backend/docker/goals/package_image.py @@ -358,7 +358,6 @@ def get_build_options( DockerBuildOptionFlagFieldMixin, ), ) - or field_type.__name__ == "DockerImageBuildPullOptionField" ): source = InterpolationContext.TextSource( address=target.address, target_alias=target.alias, field_alias=field_type.alias diff --git a/src/python/pants/engine/target.py b/src/python/pants/engine/target.py index 12d06220aa4..b6ac77a0c46 100644 --- a/src/python/pants/engine/target.py +++ b/src/python/pants/engine/target.py @@ -1916,7 +1916,7 @@ class StringOrBoolField(Field): valid_choices: ClassVar[type[Enum] | tuple[str, ...] | None] = None @classmethod - def compute_value(cls, raw_value: str | bool | None, address: Address) -> str | bool | None: + def compute_value(cls, raw_value: str | bool | None, address: Address) -> str | bool | None | Any: value_or_default = super().compute_value(raw_value, address) if value_or_default is not None: if not isinstance(value_or_default, (str, bool)): From 2c46fc4edd3a548598a3e6ef6031930af7e75bae Mon Sep 17 00:00:00 2001 From: Tim Werner Date: Fri, 23 Jan 2026 17:06:50 +0100 Subject: [PATCH 08/13] add DockerImageBuildPullOptionField to build options and format compute_value method --- src/python/pants/backend/docker/goals/package_image.py | 2 ++ src/python/pants/engine/target.py | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/python/pants/backend/docker/goals/package_image.py b/src/python/pants/backend/docker/goals/package_image.py index 57b3829849f..8b826579667 100644 --- a/src/python/pants/backend/docker/goals/package_image.py +++ b/src/python/pants/backend/docker/goals/package_image.py @@ -35,6 +35,7 @@ DockerImageTagsField, DockerImageTagsRequest, DockerImageTargetStageField, + DockerImageBuildPullOptionField, get_docker_image_tags, ) from pants.backend.docker.util_rules.docker_binary import DockerBinary @@ -356,6 +357,7 @@ def get_build_options( DockerBuildOptionFieldValueMixin, DockerBuildOptionFieldMultiValueMixin, DockerBuildOptionFlagFieldMixin, + DockerImageBuildPullOptionField ), ) ): diff --git a/src/python/pants/engine/target.py b/src/python/pants/engine/target.py index b6ac77a0c46..9bbc11b001b 100644 --- a/src/python/pants/engine/target.py +++ b/src/python/pants/engine/target.py @@ -1916,7 +1916,9 @@ class StringOrBoolField(Field): valid_choices: ClassVar[type[Enum] | tuple[str, ...] | None] = None @classmethod - def compute_value(cls, raw_value: str | bool | None, address: Address) -> str | bool | None | Any: + def compute_value( + cls, raw_value: str | bool | None, address: Address + ) -> str | bool | None | Any: value_or_default = super().compute_value(raw_value, address) if value_or_default is not None: if not isinstance(value_or_default, (str, bool)): From ae42b18349bf647a1b67572e217f3fa1a18fa8a2 Mon Sep 17 00:00:00 2001 From: Tim Werner Date: Fri, 23 Jan 2026 17:10:40 +0100 Subject: [PATCH 09/13] refactor: reorder DockerImageBuildPullOptionField import and clean up subclass check --- .../backend/docker/goals/package_image.py | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/python/pants/backend/docker/goals/package_image.py b/src/python/pants/backend/docker/goals/package_image.py index 8b826579667..bbb89ca84ac 100644 --- a/src/python/pants/backend/docker/goals/package_image.py +++ b/src/python/pants/backend/docker/goals/package_image.py @@ -28,6 +28,7 @@ DockerBuildOptionFieldValueMixin, DockerBuildOptionFlagFieldMixin, DockerImageBuildImageOutputField, + DockerImageBuildPullOptionField, DockerImageContextRootField, DockerImageRegistriesField, DockerImageRepositoryField, @@ -35,7 +36,6 @@ DockerImageTagsField, DockerImageTagsRequest, DockerImageTargetStageField, - DockerImageBuildPullOptionField, get_docker_image_tags, ) from pants.backend.docker.util_rules.docker_binary import DockerBinary @@ -347,19 +347,17 @@ def get_build_options( # Case where BuildKit option has a default value - still should not be generated continue - if ( - issubclass( - field_type, - ( - DockerBuildOptionFieldMixin, - DockerBuildOptionFieldMultiValueDictMixin, - DockerBuildOptionFieldListOfMultiValueDictMixin, - DockerBuildOptionFieldValueMixin, - DockerBuildOptionFieldMultiValueMixin, - DockerBuildOptionFlagFieldMixin, - DockerImageBuildPullOptionField - ), - ) + if issubclass( + field_type, + ( + DockerBuildOptionFieldMixin, + DockerBuildOptionFieldMultiValueDictMixin, + DockerBuildOptionFieldListOfMultiValueDictMixin, + DockerBuildOptionFieldValueMixin, + DockerBuildOptionFieldMultiValueMixin, + DockerBuildOptionFlagFieldMixin, + DockerImageBuildPullOptionField, + ), ): source = InterpolationContext.TextSource( address=target.address, target_alias=target.alias, field_alias=field_type.alias From 11d42a3e288a1bae873604d20a73aea2025e68c8 Mon Sep 17 00:00:00 2001 From: Tim Werner Date: Mon, 9 Mar 2026 16:04:07 +0100 Subject: [PATCH 10/13] fix mypy issue --- .../docker/goals/package_image_podman_pull_test.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/python/pants/backend/docker/goals/package_image_podman_pull_test.py b/src/python/pants/backend/docker/goals/package_image_podman_pull_test.py index 33abe9347c3..af44e7b67fb 100644 --- a/src/python/pants/backend/docker/goals/package_image_podman_pull_test.py +++ b/src/python/pants/backend/docker/goals/package_image_podman_pull_test.py @@ -18,7 +18,8 @@ from pants.backend.docker.util_rules.docker_build_args import rules as build_args_rules from pants.backend.docker.util_rules.docker_build_env import rules as build_env_rules from pants.engine.addresses import Address -from pants.engine.fs import EMPTY_DIGEST, CreateDigest +from pants.engine.env_vars import EnvironmentVars +from pants.engine.fs import EMPTY_DIGEST, EMPTY_FILE_DIGEST, CreateDigest from pants.engine.process import Process, ProcessExecutionEnvironment from pants.engine.target import InvalidFieldException, WrappedTarget from pants.engine.unions import UnionMembership @@ -73,7 +74,7 @@ def create_test_context(rule_runner: RuleRunner, pull_value=None): build_args=DockerBuildArgs(), digest=EMPTY_DIGEST, dockerfile="test/Dockerfile", - build_env=DockerBuildEnvironment(environment={}), + build_env=DockerBuildEnvironment(environment=EnvironmentVars()), interpolation_context=InterpolationContext.from_dict( { "tags": InterpolationValue({}), @@ -103,9 +104,9 @@ def capture_process(process: Process): return FallibleProcessResult( stdout=b"Successfully built abc123\n", - stdout_digest=EMPTY_DIGEST, + stdout_digest=EMPTY_FILE_DIGEST, stderr=b"", - stderr_digest=EMPTY_DIGEST, + stderr_digest=EMPTY_FILE_DIGEST, exit_code=0, output_digest=EMPTY_DIGEST, metadata=ProcessResultMetadata( From c09ecdff4545a27044d63649aeb9e04d19e7a8f9 Mon Sep 17 00:00:00 2001 From: Tim Werner Date: Tue, 10 Mar 2026 09:29:10 +0100 Subject: [PATCH 11/13] fix tests --- .../goals/package_image_podman_pull_test.py | 103 +++++++----------- 1 file changed, 41 insertions(+), 62 deletions(-) diff --git a/src/python/pants/backend/docker/goals/package_image_podman_pull_test.py b/src/python/pants/backend/docker/goals/package_image_podman_pull_test.py index af44e7b67fb..a264c553482 100644 --- a/src/python/pants/backend/docker/goals/package_image_podman_pull_test.py +++ b/src/python/pants/backend/docker/goals/package_image_podman_pull_test.py @@ -8,8 +8,12 @@ import pytest from pants.backend.docker.goals.package_image import ( + DockerImageBuildProcess, + DockerImageRefs, DockerPackageFieldSet, - build_docker_image, + ImageRefRegistry, + ImageRefTag, + get_docker_image_build_process, rules, ) from pants.backend.docker.subsystems.docker_options import DockerOptions @@ -19,13 +23,15 @@ from pants.backend.docker.util_rules.docker_build_env import rules as build_env_rules from pants.engine.addresses import Address from pants.engine.env_vars import EnvironmentVars -from pants.engine.fs import EMPTY_DIGEST, EMPTY_FILE_DIGEST, CreateDigest -from pants.engine.process import Process, ProcessExecutionEnvironment +from pants.engine.fs import EMPTY_DIGEST from pants.engine.target import InvalidFieldException, WrappedTarget from pants.engine.unions import UnionMembership -from pants.option.global_options import GlobalOptions, KeepSandboxes from pants.testutil.option_util import create_subsystem from pants.testutil.rule_runner import QueryRule, RuleRunner, run_rule_with_mocks +from pants.backend.docker.util_rules.docker_build_args import DockerBuildArgs +from pants.backend.docker.util_rules.docker_build_context import DockerBuildContext +from pants.backend.docker.util_rules.docker_build_env import DockerBuildEnvironment +from pants.util.value_interpolation import InterpolationContext, InterpolationValue @pytest.fixture @@ -35,20 +41,34 @@ def rule_runner() -> RuleRunner: *rules(), *build_args_rules(), *build_env_rules(), - QueryRule(GlobalOptions, []), QueryRule(DockerOptions, []), ], target_types=[DockerImageTarget], ) +def _make_image_refs(address: Address) -> DockerImageRefs: + repository = address.target_name + return DockerImageRefs( + [ + ImageRefRegistry( + registry=None, + repository=repository, + tags=( + ImageRefTag( + template="latest", + formatted="latest", + full_name=f"{repository}:latest", + uses_local_alias=False, + ), + ), + ) + ] + ) + + def create_test_context(rule_runner: RuleRunner, pull_value=None): """Helper to create a mock build context and target with specific pull value.""" - from pants.backend.docker.util_rules.docker_build_args import DockerBuildArgs - from pants.backend.docker.util_rules.docker_build_context import DockerBuildContext - from pants.backend.docker.util_rules.docker_build_env import DockerBuildEnvironment - from pants.util.value_interpolation import InterpolationContext, InterpolationValue - # Create BUILD file with optional pull value # Python booleans need to be capitalized (True/False) in BUILD files build_content = "docker_image(name='test'" @@ -96,38 +116,6 @@ def test_podman_pull_string_policies(rule_runner: RuleRunner, policy: str) -> No """Test that Podman accepts all valid string pull policies.""" tgt, build_context = create_test_context(rule_runner, pull_value=policy) - process_args = [] - - def capture_process(process: Process): - process_args.append(process.argv) - from pants.engine.process import FallibleProcessResult, ProcessResultMetadata - - return FallibleProcessResult( - stdout=b"Successfully built abc123\n", - stdout_digest=EMPTY_FILE_DIGEST, - stderr=b"", - stderr_digest=EMPTY_FILE_DIGEST, - exit_code=0, - output_digest=EMPTY_DIGEST, - metadata=ProcessResultMetadata( - 0, - ProcessExecutionEnvironment( - environment_name=None, - platform="linux_x86_64", - docker_image=None, - remote_execution=False, - remote_execution_extra_platform_properties=[], - execute_in_workspace=False, - keep_sandboxes="never", - ), - "ran_locally", - 0, - ), - ) - - def mock_digest(_: CreateDigest): - return EMPTY_DIGEST - docker_options = create_subsystem( DockerOptions, registries={}, @@ -142,8 +130,6 @@ def mock_digest(_: CreateDigest): env_vars=[], ) - global_options = rule_runner.request(GlobalOptions, []) - # Use Podman binary podman_binary = DockerBinary( path="/bin/podman", @@ -153,29 +139,27 @@ def mock_digest(_: CreateDigest): is_podman=True, ) - run_rule_with_mocks( - build_docker_image, + address = Address("test") + image_refs = _make_image_refs(address) + + result: DockerImageBuildProcess = run_rule_with_mocks( + get_docker_image_build_process, rule_args=[ DockerPackageFieldSet.create(tgt), docker_options, - global_options, podman_binary, - KeepSandboxes.never, - UnionMembership.from_rules([]), ], mock_calls={ "pants.backend.docker.util_rules.docker_build_context.create_docker_build_context": lambda _req: build_context, "pants.engine.internals.graph.resolve_target": lambda _: WrappedTarget(tgt), - "pants.engine.intrinsics.execute_process": capture_process, - "pants.engine.intrinsics.create_digest": mock_digest, + "pants.backend.docker.goals.package_image.get_image_refs": lambda _: image_refs, }, union_membership=UnionMembership.from_rules([]), show_warnings=False, ) # Verify that the correct policy was used - assert len(process_args) == 1 - argv = process_args[0] + argv = result.process.argv expected_flag = f"--pull={policy}" assert expected_flag in argv, f"Expected '{expected_flag}' in {argv}" @@ -198,8 +182,6 @@ def test_docker_pull_string_raises_error(rule_runner: RuleRunner) -> None: env_vars=[], ) - global_options = rule_runner.request(GlobalOptions, []) - # Use Docker binary (not Podman) docker_binary = DockerBinary( path="/bin/docker", @@ -209,25 +191,22 @@ def test_docker_pull_string_raises_error(rule_runner: RuleRunner) -> None: is_podman=False, ) - def mock_digest(_: CreateDigest): - return EMPTY_DIGEST + address = Address("test") + image_refs = _make_image_refs(address) # Should raise InvalidFieldException with pytest.raises(InvalidFieldException) as exc_info: run_rule_with_mocks( - build_docker_image, + get_docker_image_build_process, rule_args=[ DockerPackageFieldSet.create(tgt), docker_options, - global_options, docker_binary, - KeepSandboxes.never, - UnionMembership.from_rules([]), ], mock_calls={ "pants.backend.docker.util_rules.docker_build_context.create_docker_build_context": lambda _req: build_context, "pants.engine.internals.graph.resolve_target": lambda _: WrappedTarget(tgt), - "pants.engine.intrinsics.create_digest": mock_digest, + "pants.backend.docker.goals.package_image.get_image_refs": lambda _: image_refs, }, union_membership=UnionMembership.from_rules([]), show_warnings=False, From 481cecae001398f0a524d51115ad0fb3fef0fccc Mon Sep 17 00:00:00 2001 From: Tim Werner Date: Tue, 10 Mar 2026 09:52:17 +0100 Subject: [PATCH 12/13] fix linting issue --- .../backend/docker/goals/package_image_podman_pull_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/python/pants/backend/docker/goals/package_image_podman_pull_test.py b/src/python/pants/backend/docker/goals/package_image_podman_pull_test.py index a264c553482..434a0ce2e2c 100644 --- a/src/python/pants/backend/docker/goals/package_image_podman_pull_test.py +++ b/src/python/pants/backend/docker/goals/package_image_podman_pull_test.py @@ -19,7 +19,10 @@ from pants.backend.docker.subsystems.docker_options import DockerOptions from pants.backend.docker.target_types import DockerImageTarget from pants.backend.docker.util_rules.docker_binary import DockerBinary +from pants.backend.docker.util_rules.docker_build_args import DockerBuildArgs from pants.backend.docker.util_rules.docker_build_args import rules as build_args_rules +from pants.backend.docker.util_rules.docker_build_context import DockerBuildContext +from pants.backend.docker.util_rules.docker_build_env import DockerBuildEnvironment from pants.backend.docker.util_rules.docker_build_env import rules as build_env_rules from pants.engine.addresses import Address from pants.engine.env_vars import EnvironmentVars @@ -28,9 +31,6 @@ from pants.engine.unions import UnionMembership from pants.testutil.option_util import create_subsystem from pants.testutil.rule_runner import QueryRule, RuleRunner, run_rule_with_mocks -from pants.backend.docker.util_rules.docker_build_args import DockerBuildArgs -from pants.backend.docker.util_rules.docker_build_context import DockerBuildContext -from pants.backend.docker.util_rules.docker_build_env import DockerBuildEnvironment from pants.util.value_interpolation import InterpolationContext, InterpolationValue From 4bf2e958f533690f56da882dd5dd3e2e3fca6b3f Mon Sep 17 00:00:00 2001 From: Tim Werner Date: Tue, 10 Mar 2026 10:30:55 +0100 Subject: [PATCH 13/13] move note to new release version --- docs/notes/2.31.x.md | 3 --- docs/notes/2.32.x.md | 3 +++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/notes/2.31.x.md b/docs/notes/2.31.x.md index b19dd60aae7..57ba60d9e7f 100644 --- a/docs/notes/2.31.x.md +++ b/docs/notes/2.31.x.md @@ -31,9 +31,6 @@ This work stands on the shoulders of support from the [Science Projects](https:/ ### Backends -the `--pull` flag for `docker_image` targets now supports also podman. When podman is activated the flag can be set to `missing`, `always`,`never` and `newer` as well as False (equal to `missing`) or True (equal to `always`). -The default behavior is now `missing`, which pulls the base image only if it is not already present locally. - #### Helm #### JVM diff --git a/docs/notes/2.32.x.md b/docs/notes/2.32.x.md index 7ce50fb838d..62c6173d57e 100644 --- a/docs/notes/2.32.x.md +++ b/docs/notes/2.32.x.md @@ -54,6 +54,9 @@ For `generate-lockfiles`, typos in the name of a resolve now give "Did you mean? ### Backends +The `--pull` flag for `docker_image` targets now supports also podman. When podman is activated the flag can be set to `missing`, `always`,`never` and `newer` as well as False (equal to `missing`) or True (equal to `always`). +The default behavior is now `missing`, which pulls the base image only if it is not already present locally. + #### Docker The option `[docker].push_on_package` can be used to prevent Docker images from being pushed during packaging, i.e. when `--output` contains `push=True` or `type=registry`.