From 257bb53b6204d151c0530c7acafb4cbee260b7e1 Mon Sep 17 00:00:00 2001 From: "Benjamin R. J. Schwedler" Date: Thu, 19 Feb 2026 09:35:44 -0600 Subject: [PATCH] Update cache tags to prevent collisions --- posit-bakery/README.md | 11 +------- posit-bakery/posit_bakery/config/config.py | 6 ++-- posit-bakery/posit_bakery/image/bake/bake.py | 28 ++++++++++++++----- .../posit_bakery/image/image_target.py | 26 +++++++++++------ .../cache_registry/barebones_plan.json | 4 +-- .../testdata/cache_registry/basic_plan.json | 8 +++--- .../cache_registry/multiplatform_plan.json | 8 +++--- posit-bakery/test/image/test_image_target.py | 5 ++-- 8 files changed, 57 insertions(+), 39 deletions(-) diff --git a/posit-bakery/README.md b/posit-bakery/README.md index a6e0237e..28c551d2 100644 --- a/posit-bakery/README.md +++ b/posit-bakery/README.md @@ -10,8 +10,6 @@ The [bakery](./posit-bakery/) command line interface (CLI) binds together variou * [pipx](https://pipx.pypa.io/stable/installation/) * [docker buildx bake](https://github.com/docker/buildx#installing) * [just](https://just.systems/man/en/prerequisites.html) -* [gh](https://github.com/cli/cli#installation) (required while repositories are private) - ### 3rd Party Tools | Tool | Used By | Purpose | @@ -28,14 +26,7 @@ The [bakery](./posit-bakery/) command line interface (CLI) binds together variou ### CLI Installation -1. Authorize w/ GitHub, since the repository is private - - ```bash - gh auth login - gh auth setup-git - ``` - -2. Install `bakery` using `pipx` +1. Install `bakery` using `pipx` ```bash pipx install 'git+https://github.com/posit-dev/images-shared.git@main#subdirectory=posit-bakery&egg=posit-bakery' diff --git a/posit-bakery/posit_bakery/config/config.py b/posit-bakery/posit_bakery/config/config.py index b7ec603c..9ade6d1c 100644 --- a/posit-bakery/posit_bakery/config/config.py +++ b/posit-bakery/posit_bakery/config/config.py @@ -892,7 +892,9 @@ def build_targets( :param fail_fast: If True, stop building targets on the first failure. """ if strategy == ImageBuildStrategy.BAKE: - bake_plan = BakePlan.from_image_targets(context=self.base_path, image_targets=self.targets) + bake_plan = BakePlan.from_image_targets( + context=self.base_path, image_targets=self.targets, platforms=platforms + ) set_opts = None if self.settings.temp_registry is not None and push: set_opts = { @@ -955,7 +957,7 @@ def clean_caches( :param remove_older_than: Optional timedelta to remove caches older than the specified duration. :param dry_run: If True, print what would be deleted without actually deleting anything. """ - target_caches = list(set([target.cache_name.split(":")[0] for target in self.targets])) + target_caches = list(set([cn.split(":")[0] for target in self.targets if (cn := target.cache_name())])) errors = [] for target_cache in target_caches: diff --git a/posit-bakery/posit_bakery/image/bake/bake.py b/posit-bakery/posit_bakery/image/bake/bake.py index c014d7ef..cf343bc5 100644 --- a/posit-bakery/posit_bakery/image/bake/bake.py +++ b/posit-bakery/posit_bakery/image/bake/bake.py @@ -91,12 +91,22 @@ def serialize_path(value: Path | str) -> str: return str(value) @classmethod - def from_image_target(cls, image_target: ImageTarget) -> "BakeTarget": - """Create a BakeTarget from an ImageTarget.""" + def from_image_target(cls, image_target: ImageTarget, platforms: list[str] | None = None) -> "BakeTarget": + """Create a BakeTarget from an ImageTarget. + + :param image_target: The ImageTarget to create a BakeTarget from. + :param platforms: Optional platform override (e.g., from CLI --platform flag). When provided, this takes + precedence over the image target's OS platform configuration for cache tag generation. + """ kwargs = {"tags": image_target.tags} - if image_target.cache_name is not None: - kwargs["cache_from"] = [{"type": "registry", "ref": image_target.cache_name}] - kwargs["cache_to"] = [{"type": "registry", "ref": image_target.cache_name, "mode": "max"}] + effective_platforms = platforms or ( + image_target.image_os.platforms if image_target.image_os is not None else DEFAULT_PLATFORMS + ) + cache_platform = effective_platforms[0] if len(effective_platforms) == 1 else None + cache_name = image_target.cache_name(platform=cache_platform) + if cache_name is not None: + kwargs["cache_from"] = [{"type": "registry", "ref": cache_name}] + kwargs["cache_to"] = [{"type": "registry", "ref": cache_name, "mode": "max"}] if image_target.temp_name is not None: kwargs["tags"] = [image_target.temp_name.rsplit(":", 1)[0]] @@ -153,11 +163,15 @@ def update_groups( return groups @classmethod - def from_image_targets(cls, context: Path, image_targets: list[ImageTarget]) -> "BakePlan": + def from_image_targets( + cls, context: Path, image_targets: list[ImageTarget], platforms: list[str] | None = None + ) -> "BakePlan": """Create a BakePlan from a list of ImageTarget objects. :param context: The absolute path to the build context directory. :param image_targets: A list of ImageTarget objects to include in the bake plan. + :param platforms: Optional platform override (e.g., from CLI --platform flag). When provided, this takes + precedence over each image target's OS platform configuration for cache tag generation. :return: A BakePlan object containing the context, groups, and targets. """ @@ -167,7 +181,7 @@ def from_image_targets(cls, context: Path, image_targets: list[ImageTarget]) -> targets: dict[str, BakeTarget] = {} for image_target in image_targets: - bake_target = BakeTarget.from_image_target(image_target=image_target) + bake_target = BakeTarget.from_image_target(image_target=image_target, platforms=platforms) groups = cls.update_groups( groups=groups, uid=image_target.uid, diff --git a/posit-bakery/posit_bakery/image/image_target.py b/posit-bakery/posit_bakery/image/image_target.py index d4d5a6d8..87253b01 100644 --- a/posit-bakery/posit_bakery/image/image_target.py +++ b/posit-bakery/posit_bakery/image/image_target.py @@ -11,6 +11,7 @@ from python_on_whales.components.buildx.imagetools.models import Manifest from posit_bakery.config.image import ImageVersion, ImageVariant, ImageVersionOS +from posit_bakery.config.image.build_os import DEFAULT_PLATFORMS from posit_bakery.config.repository import Repository from posit_bakery.config.tag import TagPattern, TagPatternFilter from posit_bakery.const import OCI_LABEL_PREFIX, POSIT_LABEL_PREFIX, REGEX_IMAGE_TAG_SUFFIX_ALLOWED_CHARACTERS_PATTERN @@ -295,21 +296,27 @@ def labels(self) -> dict[str, str]: return labels - @property - def cache_name(self) -> str | None: - """Generate the image name and tag to use for a build cache.""" + def cache_name(self, platform: str | None = None) -> str | None: + """Generate the image name and tag to use for a build cache. + + :param platform: Optional platform string (e.g., "linux/amd64") to include in the cache tag + for platform-specific cache differentiation. + """ if not self.settings.cache_registry: return None - tag = re.sub(r"[+-].*", "", self.image_version.name) + tag = re.sub(r"\+.*", "", self.image_version.name) + tag = re.sub(REGEX_IMAGE_TAG_SUFFIX_ALLOWED_CHARACTERS_PATTERN, "-", tag).strip("-._") if self.image_os: tag += f"-{self.image_os.tagDisplayName}" if self.image_variant: tag += f"-{self.image_variant.tagDisplayName}" - cache_name = f"{self.settings.cache_registry}/{self.image_name}/cache:{tag}" + if platform: + platform_suffix = platform.removeprefix("linux/").replace("/", "-") + tag += f"-{platform_suffix}" - return cache_name + return f"{self.settings.cache_registry}/{self.image_name}/cache:{tag}" @property def temp_name(self) -> str | None: @@ -348,8 +355,11 @@ def build( cache_from = None cache_to = None - if self.cache_name is not None: - cache_from = f"type=registry,ref={self.cache_name}" + build_platforms = platforms or (self.image_os.platforms if self.image_os else DEFAULT_PLATFORMS) + cache_platform = build_platforms[0] if build_platforms and len(build_platforms) == 1 else None + cache_name = self.cache_name(platform=cache_platform) + if cache_name is not None: + cache_from = f"type=registry,ref={cache_name}" cache_to = f"{cache_from},mode=max" if isinstance(metadata_file, bool) and metadata_file: diff --git a/posit-bakery/test/image/bake/testdata/cache_registry/barebones_plan.json b/posit-bakery/test/image/bake/testdata/cache_registry/barebones_plan.json index b6ea57cb..8c7363d4 100644 --- a/posit-bakery/test/image/bake/testdata/cache_registry/barebones_plan.json +++ b/posit-bakery/test/image/bake/testdata/cache_registry/barebones_plan.json @@ -41,13 +41,13 @@ "cache_from": [ { "type": "registry", - "ref": "ghcr.io/posit-dev/scratch/cache:1.0.0-scratch" + "ref": "ghcr.io/posit-dev/scratch/cache:1.0.0-scratch-amd64" } ], "cache_to": [ { "type": "registry", - "ref": "ghcr.io/posit-dev/scratch/cache:1.0.0-scratch", + "ref": "ghcr.io/posit-dev/scratch/cache:1.0.0-scratch-amd64", "mode": "max" } ] diff --git a/posit-bakery/test/image/bake/testdata/cache_registry/basic_plan.json b/posit-bakery/test/image/bake/testdata/cache_registry/basic_plan.json index d66d65ef..e4cd858e 100644 --- a/posit-bakery/test/image/bake/testdata/cache_registry/basic_plan.json +++ b/posit-bakery/test/image/bake/testdata/cache_registry/basic_plan.json @@ -59,13 +59,13 @@ "cache_from": [ { "type": "registry", - "ref": "ghcr.io/posit-dev/test-image/cache:1.0.0-ubuntu-22.04-min" + "ref": "ghcr.io/posit-dev/test-image/cache:1.0.0-ubuntu-22.04-min-amd64" } ], "cache_to": [ { "type": "registry", - "ref": "ghcr.io/posit-dev/test-image/cache:1.0.0-ubuntu-22.04-min", + "ref": "ghcr.io/posit-dev/test-image/cache:1.0.0-ubuntu-22.04-min-amd64", "mode": "max" } ] @@ -113,13 +113,13 @@ "cache_from": [ { "type": "registry", - "ref": "ghcr.io/posit-dev/test-image/cache:1.0.0-ubuntu-22.04-std" + "ref": "ghcr.io/posit-dev/test-image/cache:1.0.0-ubuntu-22.04-std-amd64" } ], "cache_to": [ { "type": "registry", - "ref": "ghcr.io/posit-dev/test-image/cache:1.0.0-ubuntu-22.04-std", + "ref": "ghcr.io/posit-dev/test-image/cache:1.0.0-ubuntu-22.04-std-amd64", "mode": "max" } ] diff --git a/posit-bakery/test/image/bake/testdata/cache_registry/multiplatform_plan.json b/posit-bakery/test/image/bake/testdata/cache_registry/multiplatform_plan.json index 67f208d7..ab90931c 100644 --- a/posit-bakery/test/image/bake/testdata/cache_registry/multiplatform_plan.json +++ b/posit-bakery/test/image/bake/testdata/cache_registry/multiplatform_plan.json @@ -59,13 +59,13 @@ "cache_from": [ { "type": "registry", - "ref": "ghcr.io/posit-dev/test-multi/cache:1.0.0-ubuntu-22.04-min" + "ref": "ghcr.io/posit-dev/test-multi/cache:1.0.0-ubuntu-22.04-min-amd64" } ], "cache_to": [ { "type": "registry", - "ref": "ghcr.io/posit-dev/test-multi/cache:1.0.0-ubuntu-22.04-min", + "ref": "ghcr.io/posit-dev/test-multi/cache:1.0.0-ubuntu-22.04-min-amd64", "mode": "max" } ] @@ -144,13 +144,13 @@ "cache_from": [ { "type": "registry", - "ref": "ghcr.io/posit-dev/test-multi/cache:1.0.0-ubuntu-22.04-std" + "ref": "ghcr.io/posit-dev/test-multi/cache:1.0.0-ubuntu-22.04-std-amd64" } ], "cache_to": [ { "type": "registry", - "ref": "ghcr.io/posit-dev/test-multi/cache:1.0.0-ubuntu-22.04-std", + "ref": "ghcr.io/posit-dev/test-multi/cache:1.0.0-ubuntu-22.04-std-amd64", "mode": "max" } ] diff --git a/posit-bakery/test/image/test_image_target.py b/posit-bakery/test/image/test_image_target.py index 0b7a72b8..e13b2cfa 100644 --- a/posit-bakery/test/image/test_image_target.py +++ b/posit-bakery/test/image/test_image_target.py @@ -524,6 +524,7 @@ def test_build_args_with_build_args(self, basic_standard_image_target): def test_build_args_cache_registry(self, basic_standard_image_target): """Test the build property of an ImageTarget.""" basic_standard_image_target.settings = ImageTargetSettings(cache_registry="ghcr.io/posit-dev") + cache_name = basic_standard_image_target.cache_name(platform="linux/amd64") expected_build_args = { "context_path": basic_standard_image_target.context.base_path, "file": basic_standard_image_target.containerfile, @@ -534,8 +535,8 @@ def test_build_args_cache_registry(self, basic_standard_image_target): "push": False, "output": {}, "cache": True, - "cache_from": f"type=registry,ref={basic_standard_image_target.cache_name}", - "cache_to": f"type=registry,ref={basic_standard_image_target.cache_name},mode=max", + "cache_from": f"type=registry,ref={cache_name}", + "cache_to": f"type=registry,ref={cache_name},mode=max", "metadata_file": None, "platforms": ["linux/amd64"], }