Skip to content

Commit f18e4b0

Browse files
committed
Extract registry cleanup from BakeryConfig
Move clean_caches() and clean_temporary() out of config/config.py into posit_bakery/registry_management/clean.py. Both functions now accept a list of ImageTarget objects instead of reading from self.targets. config/config.py drops its last imports of ghcr, timedelta, ImageBuildStrategy, python_on_whales, and DockerException. It no longer depends on registry_management or build internals. - cli/clean.py calls the new standalone functions - Test mocks updated to patch the new function locations - _merge_sequential_build_metadata_files test removed (logic was inlined into build_targets in previous commit) - Build retry test updated to import from image.build
1 parent c12cbfc commit f18e4b0

6 files changed

Lines changed: 103 additions & 87 deletions

File tree

posit-bakery/posit_bakery/cli/clean.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
from posit_bakery.config.config import BakerySettings, BakeryConfigFilter
1212
from posit_bakery.config.image.posit_product.const import ReleaseStreamEnum
1313
from posit_bakery.const import DevVersionInclusionEnum, MatrixVersionInclusionEnum
14+
from posit_bakery.registry_management.clean import clean_caches as do_clean_caches
15+
from posit_bakery.registry_management.clean import clean_temporary as do_clean_temporary
1416
from posit_bakery.util import auto_path
1517

1618
app = typer.Typer()
@@ -106,7 +108,8 @@ def cache_registry(
106108

107109
log.info(f"Cleaning cache registries in {registry}")
108110

109-
errors = config.clean_caches(
111+
errors = do_clean_caches(
112+
targets=config.targets,
110113
remove_untagged=untagged,
111114
remove_older_than=timedelta(days=older_than) if older_than else None,
112115
dry_run=dry_run,
@@ -199,7 +202,8 @@ def temp_registry(
199202

200203
log.info(f"Cleaning temporary registries in {registry}")
201204

202-
errors = config.clean_temporary(
205+
errors = do_clean_temporary(
206+
targets=config.targets,
203207
remove_untagged=untagged,
204208
remove_older_than=timedelta(days=older_than) if older_than else None,
205209
dry_run=dry_run,

posit-bakery/posit_bakery/config/config.py

Lines changed: 1 addition & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import os
66
import re
77
import shutil
8-
from datetime import timedelta
98
from pathlib import Path
109
from typing import Annotated, Self, Any
1110

@@ -33,8 +32,7 @@
3332
BakeryRenderErrorGroup,
3433
)
3534
from posit_bakery.image.image_metadata import MetadataFile
36-
from posit_bakery.image.image_target import ImageTarget, ImageBuildStrategy, ImageTargetSettings
37-
from posit_bakery.registry_management import ghcr
35+
from posit_bakery.image.image_target import ImageTarget, ImageTargetSettings
3836

3937
log = logging.getLogger(__name__)
4038

@@ -895,57 +893,3 @@ def load_build_metadata_from_file(self, metadata_file: Path) -> list[str]:
895893
log.info(f"Loaded build metadata for target '{target}' from file '{metadata_file.filepath}'.")
896894

897895
return targets_loaded
898-
899-
def clean_caches(
900-
self,
901-
remove_untagged: bool = True,
902-
remove_older_than: timedelta | None = None,
903-
dry_run: bool = False,
904-
):
905-
"""Cleans up dangling caches in the specified registry for all generated image targets.
906-
907-
:param remove_untagged: If True, remove untagged caches.
908-
:param remove_older_than: Optional timedelta to remove caches older than the specified duration.
909-
:param dry_run: If True, print what would be deleted without actually deleting anything.
910-
"""
911-
target_caches = list(set([cn.split(":")[0] for target in self.targets if (cn := target.cache_name())]))
912-
913-
errors = []
914-
for target_cache in target_caches:
915-
errors.extend(
916-
ghcr.clean_temporary_artifacts(
917-
ghcr_registry=target_cache,
918-
remove_untagged=remove_untagged,
919-
remove_older_than=remove_older_than,
920-
dry_run=dry_run,
921-
)
922-
)
923-
924-
return errors
925-
926-
def clean_temporary(
927-
self,
928-
remove_untagged: bool = True,
929-
remove_older_than: timedelta | None = None,
930-
dry_run: bool = False,
931-
):
932-
"""Cleans up temporary images in the specified registry for all generated image targets.
933-
934-
:param remove_untagged: If True, remove untagged images.
935-
:param remove_older_than: Optional timedelta to remove images older than the specified duration.
936-
:param dry_run: If True, print what would be deleted without actually deleting anything.
937-
"""
938-
target_caches = list(set([target.temp_name for target in self.targets]))
939-
940-
errors = []
941-
for target_cache in target_caches:
942-
errors.extend(
943-
ghcr.clean_temporary_artifacts(
944-
ghcr_registry=target_cache,
945-
remove_untagged=remove_untagged,
946-
remove_older_than=remove_older_than,
947-
dry_run=dry_run,
948-
)
949-
)
950-
951-
return errors
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import logging
2+
from datetime import timedelta
3+
4+
from posit_bakery.image.image_target import ImageTarget
5+
from posit_bakery.registry_management import ghcr
6+
7+
log = logging.getLogger(__name__)
8+
9+
10+
def clean_caches(
11+
targets: list[ImageTarget],
12+
remove_untagged: bool = True,
13+
remove_older_than: timedelta | None = None,
14+
dry_run: bool = False,
15+
) -> list[Exception]:
16+
"""Clean up dangling caches in the registry for the given image targets.
17+
18+
:param targets: Image targets whose cache registries should be cleaned.
19+
:param remove_untagged: If True, remove untagged caches.
20+
:param remove_older_than: Remove caches older than the specified duration.
21+
:param dry_run: If True, print what would be deleted without deleting.
22+
:return: List of errors encountered during cleanup.
23+
"""
24+
target_caches = list(set([cn.split(":")[0] for target in targets if (cn := target.cache_name())]))
25+
26+
errors = []
27+
for target_cache in target_caches:
28+
errors.extend(
29+
ghcr.clean_temporary_artifacts(
30+
ghcr_registry=target_cache,
31+
remove_untagged=remove_untagged,
32+
remove_older_than=remove_older_than,
33+
dry_run=dry_run,
34+
)
35+
)
36+
37+
return errors
38+
39+
40+
def clean_temporary(
41+
targets: list[ImageTarget],
42+
remove_untagged: bool = True,
43+
remove_older_than: timedelta | None = None,
44+
dry_run: bool = False,
45+
) -> list[Exception]:
46+
"""Clean up temporary images in the registry for the given image targets.
47+
48+
:param targets: Image targets whose temporary registries should be cleaned.
49+
:param remove_untagged: If True, remove untagged images.
50+
:param remove_older_than: Remove images older than the specified duration.
51+
:param dry_run: If True, print what would be deleted without deleting.
52+
:return: List of errors encountered during cleanup.
53+
"""
54+
target_caches = list(set([target.temp_name for target in targets]))
55+
56+
errors = []
57+
for target_cache in target_caches:
58+
errors.extend(
59+
ghcr.clean_temporary_artifacts(
60+
ghcr_registry=target_cache,
61+
remove_untagged=remove_untagged,
62+
remove_older_than=remove_older_than,
63+
dry_run=dry_run,
64+
)
65+
)
66+
67+
return errors

posit-bakery/test/cli/test_clean.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,13 @@
3131

3232
@pytest.fixture
3333
def mock_config():
34-
"""Mock BakeryConfig to capture settings without making API calls."""
35-
with patch("posit_bakery.cli.clean.BakeryConfig") as mock:
34+
"""Mock BakeryConfig and clean functions to capture settings without making API calls."""
35+
with (
36+
patch("posit_bakery.cli.clean.BakeryConfig") as mock,
37+
patch("posit_bakery.cli.clean.do_clean_caches", return_value=[]),
38+
patch("posit_bakery.cli.clean.do_clean_temporary", return_value=[]),
39+
):
3640
instance = MagicMock()
37-
instance.clean_caches.return_value = []
38-
instance.clean_temporary.return_value = []
3941
mock.from_context.return_value = instance
4042
yield mock
4143

posit-bakery/test/config/test_build_retry.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import pytest
66
from python_on_whales import DockerException
77

8-
from posit_bakery.config.config import _retry_build, _RETRY_DELAY_SECONDS
8+
from posit_bakery.image.build import _retry_build, _RETRY_DELAY_SECONDS
99
from posit_bakery.error import BakeryFileError, BakeryToolRuntimeError
1010

1111
pytestmark = [
@@ -25,7 +25,7 @@ def test_successful_build_no_retry_needed(self):
2525

2626
mock_fn.assert_called_once()
2727

28-
@patch("posit_bakery.config.config.time.sleep")
28+
@patch("posit_bakery.image.build.time.sleep")
2929
def test_retry_on_docker_exception_then_success(self, mock_sleep):
3030
"""Test that DockerException triggers retry and succeeds on second attempt."""
3131
mock_fn = MagicMock(side_effect=[DockerException(["docker", "build"], 1), None])
@@ -35,7 +35,7 @@ def test_retry_on_docker_exception_then_success(self, mock_sleep):
3535
assert mock_fn.call_count == 2
3636
mock_sleep.assert_called_once_with(_RETRY_DELAY_SECONDS)
3737

38-
@patch("posit_bakery.config.config.time.sleep")
38+
@patch("posit_bakery.image.build.time.sleep")
3939
def test_retry_on_bakery_tool_runtime_error_then_success(self, mock_sleep):
4040
"""Test that BakeryToolRuntimeError triggers retry and succeeds on second attempt."""
4141
mock_fn = MagicMock(side_effect=[BakeryToolRuntimeError("Build failed", cmd=["docker", "build"]), None])
@@ -45,7 +45,7 @@ def test_retry_on_bakery_tool_runtime_error_then_success(self, mock_sleep):
4545
assert mock_fn.call_count == 2
4646
mock_sleep.assert_called_once_with(_RETRY_DELAY_SECONDS)
4747

48-
@patch("posit_bakery.config.config.time.sleep")
48+
@patch("posit_bakery.image.build.time.sleep")
4949
def test_all_retries_exhausted_raises_exception(self, mock_sleep):
5050
"""Test that exception is raised when all retries are exhausted."""
5151
error = DockerException(["docker", "build"], 1)
@@ -68,7 +68,7 @@ def test_bakery_file_error_not_retried(self):
6868
# Should only be called once, no retries
6969
mock_fn.assert_called_once()
7070

71-
@patch("posit_bakery.config.config.time.sleep")
71+
@patch("posit_bakery.image.build.time.sleep")
7272
def test_retry_zero_means_no_retries(self, mock_sleep):
7373
"""Test that retry=0 means no retries, just one attempt."""
7474
error = DockerException(["docker", "build"], 1)
@@ -80,7 +80,7 @@ def test_retry_zero_means_no_retries(self, mock_sleep):
8080
mock_fn.assert_called_once()
8181
mock_sleep.assert_not_called()
8282

83-
@patch("posit_bakery.config.config.time.sleep")
83+
@patch("posit_bakery.image.build.time.sleep")
8484
def test_multiple_retries_then_success(self, mock_sleep):
8585
"""Test multiple failures before eventual success."""
8686
mock_fn = MagicMock(
@@ -96,7 +96,7 @@ def test_multiple_retries_then_success(self, mock_sleep):
9696
assert mock_fn.call_count == 3
9797
assert mock_sleep.call_count == 2
9898

99-
@patch("posit_bakery.config.config.time.sleep")
99+
@patch("posit_bakery.image.build.time.sleep")
100100
def test_logs_warning_on_retry(self, mock_sleep, caplog):
101101
"""Test that a warning is logged when retrying."""
102102
import logging

posit-bakery/test/config/test_config.py

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1733,19 +1733,6 @@ def test_remove_version_does_not_exist(self, get_tmpcontext):
17331733
with pytest.raises(ValueError, match=f"Version 'non-existent' does not exist for image '{image_name}'"):
17341734
config.remove_version(image_name, "non-existent")
17351735

1736-
def test__merge_sequential_build_metadata_files(self, get_config_obj):
1737-
"""Test merging sequential build metadata files."""
1738-
config = get_config_obj("basic")
1739-
for target in config.targets:
1740-
metadata_filepath = CONFIG_TESTDATA_DIR / "build_metadata" / f"{target.uid}.json"
1741-
target.build_metadata.append(BuildMetadata.model_validate_json(metadata_filepath.read_text()))
1742-
1743-
merged_metadata = config._merge_sequential_build_metadata_files()
1744-
with open(CONFIG_TESTDATA_DIR / "build_metadata" / "expected.json", "r") as f:
1745-
expected_metadata = json.load(f)
1746-
1747-
assert merged_metadata == expected_metadata
1748-
17491736
def test_load_build_metadata_file(self, get_config_obj):
17501737
"""Test loading a build metadata file."""
17511738
metadata_filepath = CONFIG_TESTDATA_DIR / "build_metadata" / "expected.json"
@@ -1807,7 +1794,10 @@ def test_clean_caches(
18071794
mock_ghcr_client_instance.get_package_versions.return_value = cache_ghcr_package_versions_data
18081795

18091796
# Clean caches
1810-
config.clean_caches(
1797+
from posit_bakery.registry_management.clean import clean_caches
1798+
1799+
clean_caches(
1800+
targets=config.targets,
18111801
remove_untagged=untagged,
18121802
remove_older_than=timedelta(days=older_than_days) if older_than_days is not None else None,
18131803
)
@@ -1839,7 +1829,10 @@ def test_clean_caches_dry_run(
18391829
mock_ghcr_client_instance.get_package_versions.return_value = cache_ghcr_package_versions_data
18401830

18411831
# Clean caches
1842-
config.clean_caches(
1832+
from posit_bakery.registry_management.clean import clean_caches
1833+
1834+
clean_caches(
1835+
targets=config.targets,
18431836
remove_untagged=True,
18441837
remove_older_than=timedelta(days=14),
18451838
dry_run=True,
@@ -1898,7 +1891,10 @@ def test_clean_temporary(
18981891
mock_ghcr_client_instance.get_package_versions.return_value = temp_ghcr_package_versions_data
18991892

19001893
# Clean temp images
1901-
config.clean_temporary(
1894+
from posit_bakery.registry_management.clean import clean_temporary
1895+
1896+
clean_temporary(
1897+
targets=config.targets,
19021898
remove_untagged=untagged,
19031899
remove_older_than=timedelta(days=older_than_days) if older_than_days is not None else None,
19041900
)
@@ -1930,7 +1926,10 @@ def test_clean_temporary_dry_run(
19301926
mock_ghcr_client_instance.get_package_versions.return_value = temp_ghcr_package_versions_data
19311927

19321928
# Clean temp images
1933-
config.clean_temporary(
1929+
from posit_bakery.registry_management.clean import clean_temporary
1930+
1931+
clean_temporary(
1932+
targets=config.targets,
19341933
remove_untagged=True,
19351934
remove_older_than=timedelta(days=14),
19361935
dry_run=True,

0 commit comments

Comments
 (0)