docker-image-cleanup 0.1.2__tar.gz → 0.1.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {docker_image_cleanup-0.1.2 → docker_image_cleanup-0.1.4}/PKG-INFO +10 -1
- {docker_image_cleanup-0.1.2 → docker_image_cleanup-0.1.4}/README.md +9 -0
- {docker_image_cleanup-0.1.2 → docker_image_cleanup-0.1.4}/docker_image_cleanup/__init__.py +43 -15
- docker_image_cleanup-0.1.4/docker_image_cleanup/version.py +34 -0
- {docker_image_cleanup-0.1.2 → docker_image_cleanup-0.1.4}/pyproject.toml +21 -5
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: docker-image-cleanup
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Clean up old Docker images from local storage with retention controls
|
|
5
5
|
Keywords: docker,cleanup,images,disk-space
|
|
6
6
|
Author: Michael Bianco
|
|
@@ -14,6 +14,11 @@ Requires-Python: >=3.11
|
|
|
14
14
|
Project-URL: Repository, https://github.com/iloveitaly/docker-image-cleanup
|
|
15
15
|
Description-Content-Type: text/markdown
|
|
16
16
|
|
|
17
|
+
[](https://github.com/iloveitaly/docker-image-cleanup/releases)
|
|
18
|
+
[](https://pepy.tech/project/docker-image-cleanup)
|
|
19
|
+

|
|
20
|
+
[](https://opensource.org/licenses/MIT)
|
|
21
|
+
|
|
17
22
|
# Clean Up Images from Self-Hosted Docker Registries
|
|
18
23
|
|
|
19
24
|
If you're running a self-hosted Docker registry (using tools like [unregistry](https://github.com/psviderski/unregistry)), you'll want to clean up old images to reclaim disk space. This tool connects to your local Docker daemon and removes old images from specified repositories while keeping the ones you actually need.
|
|
@@ -123,3 +128,7 @@ The cleanup process is straightforward:
|
|
|
123
128
|
Images with multiple tags are handled intelligently. If all tags on an image would be removed, the entire image is deleted. If only some tags would be removed, those tags are untagged but the image remains.
|
|
124
129
|
|
|
125
130
|
# [MIT License](LICENSE.md)
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
*This project was created from [iloveitaly/python-package-template](https://github.com/iloveitaly/python-package-template)*
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
[](https://github.com/iloveitaly/docker-image-cleanup/releases)
|
|
2
|
+
[](https://pepy.tech/project/docker-image-cleanup)
|
|
3
|
+

|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
1
6
|
# Clean Up Images from Self-Hosted Docker Registries
|
|
2
7
|
|
|
3
8
|
If you're running a self-hosted Docker registry (using tools like [unregistry](https://github.com/psviderski/unregistry)), you'll want to clean up old images to reclaim disk space. This tool connects to your local Docker daemon and removes old images from specified repositories while keeping the ones you actually need.
|
|
@@ -107,3 +112,7 @@ The cleanup process is straightforward:
|
|
|
107
112
|
Images with multiple tags are handled intelligently. If all tags on an image would be removed, the entire image is deleted. If only some tags would be removed, those tags are untagged but the image remains.
|
|
108
113
|
|
|
109
114
|
# [MIT License](LICENSE.md)
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
*This project was created from [iloveitaly/python-package-template](https://github.com/iloveitaly/python-package-template)*
|
|
@@ -12,6 +12,8 @@ from pydantic import BaseModel, Field
|
|
|
12
12
|
from structlog_config import configure_logger
|
|
13
13
|
from whenever import Instant
|
|
14
14
|
|
|
15
|
+
from .version import __version__ as __version__
|
|
16
|
+
|
|
15
17
|
log = configure_logger()
|
|
16
18
|
|
|
17
19
|
|
|
@@ -48,9 +50,25 @@ def parse_docker_image(
|
|
|
48
50
|
used_image_ids: set[
|
|
49
51
|
str
|
|
50
52
|
], # Use Any due to pyright resolution issues with docker library types
|
|
51
|
-
) -> ImageInfo:
|
|
52
|
-
"""Parses docker.models.images.Image into our ImageInfo model.
|
|
53
|
-
|
|
53
|
+
) -> ImageInfo | None:
|
|
54
|
+
"""Parses docker.models.images.Image into our ImageInfo model.
|
|
55
|
+
|
|
56
|
+
Returns None if the creation date cannot be determined.
|
|
57
|
+
"""
|
|
58
|
+
created_str = image_data.attrs.get("Created")
|
|
59
|
+
if not created_str:
|
|
60
|
+
metadata = image_data.attrs.get("Metadata") or {}
|
|
61
|
+
created_str = metadata.get("LastTagTime")
|
|
62
|
+
|
|
63
|
+
if not created_str or created_str == "0001-01-01T00:00:00Z":
|
|
64
|
+
log.warning(
|
|
65
|
+
"could not determine image age, skipping",
|
|
66
|
+
image_id=image_data.id,
|
|
67
|
+
tags=image_data.tags,
|
|
68
|
+
)
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
created_instant = Instant.parse_iso(created_str)
|
|
54
72
|
image_id = image_data.id
|
|
55
73
|
|
|
56
74
|
return ImageInfo(
|
|
@@ -71,7 +89,11 @@ def get_images_to_process(
|
|
|
71
89
|
log.info("no images found for repository", repo=repo_name)
|
|
72
90
|
return []
|
|
73
91
|
|
|
74
|
-
parsed_images = [
|
|
92
|
+
parsed_images = [
|
|
93
|
+
parsed
|
|
94
|
+
for img in images
|
|
95
|
+
if (parsed := parse_docker_image(img, used_image_ids)) is not None
|
|
96
|
+
]
|
|
75
97
|
return sorted(parsed_images, key=lambda img: img.created, reverse=True)
|
|
76
98
|
|
|
77
99
|
|
|
@@ -91,7 +113,7 @@ def determine_cleanup_actions(
|
|
|
91
113
|
if img.created >= min_age_threshold:
|
|
92
114
|
keep_tags.update(img.tags)
|
|
93
115
|
|
|
94
|
-
|
|
116
|
+
images_to_delete: dict[str, int] = {}
|
|
95
117
|
tags_to_remove: list[str] = []
|
|
96
118
|
|
|
97
119
|
images_by_id: dict[str, list[ImageInfo]] = defaultdict(list)
|
|
@@ -120,7 +142,7 @@ def determine_cleanup_actions(
|
|
|
120
142
|
if set(tags_for_this_image_to_remove) == current_image_repo_tags:
|
|
121
143
|
img_info = img_list[0]
|
|
122
144
|
if not img_info.is_in_use:
|
|
123
|
-
|
|
145
|
+
images_to_delete[img_id] = img_info.size
|
|
124
146
|
total_size_saved += img_info.size
|
|
125
147
|
else:
|
|
126
148
|
log.warning(
|
|
@@ -130,7 +152,7 @@ def determine_cleanup_actions(
|
|
|
130
152
|
)
|
|
131
153
|
|
|
132
154
|
return {
|
|
133
|
-
"images_to_delete":
|
|
155
|
+
"images_to_delete": images_to_delete,
|
|
134
156
|
"tags_to_remove": list(set(tags_to_remove)),
|
|
135
157
|
"total_size_saved": total_size_saved,
|
|
136
158
|
}
|
|
@@ -138,7 +160,7 @@ def determine_cleanup_actions(
|
|
|
138
160
|
|
|
139
161
|
def execute_cleanup(
|
|
140
162
|
client: docker.DockerClient,
|
|
141
|
-
images_to_delete:
|
|
163
|
+
images_to_delete: dict[str, int],
|
|
142
164
|
tags_to_remove: list[str],
|
|
143
165
|
total_size_saved: int,
|
|
144
166
|
dry_run: bool,
|
|
@@ -150,7 +172,7 @@ def execute_cleanup(
|
|
|
150
172
|
if tags_to_remove:
|
|
151
173
|
log.info("would remove tags", tags=tags_to_remove)
|
|
152
174
|
if images_to_delete:
|
|
153
|
-
log.info("would remove images", image_ids=images_to_delete)
|
|
175
|
+
log.info("would remove images", image_ids=list(images_to_delete.keys()))
|
|
154
176
|
|
|
155
177
|
log.info(
|
|
156
178
|
"total space that would be saved",
|
|
@@ -162,19 +184,25 @@ def execute_cleanup(
|
|
|
162
184
|
for tag in tags_to_remove:
|
|
163
185
|
try:
|
|
164
186
|
client.images.remove(tag, force=False)
|
|
165
|
-
log.info("
|
|
187
|
+
log.info("removed tag", tag=tag)
|
|
166
188
|
except docker.errors.APIError as e:
|
|
167
|
-
log.warning("skipped
|
|
189
|
+
log.warning("skipped tag removal", tag=tag, reason=str(e))
|
|
168
190
|
|
|
169
|
-
|
|
191
|
+
# We explicitly remove the Image ID here even after untagging.
|
|
192
|
+
# This is primarily to catch and clean up "dangling" images (images with no tags)
|
|
193
|
+
# that might have been orphaned but wouldn't be caught by the tag loop.
|
|
194
|
+
for img_id, size in images_to_delete.items():
|
|
170
195
|
try:
|
|
171
|
-
img = client.images.get(img_id)
|
|
172
|
-
size = img.attrs.get("Size", 0)
|
|
173
196
|
client.images.remove(img_id, force=False)
|
|
174
197
|
log.info("removed image", image_id=img_id, size=format_size(size))
|
|
175
198
|
executed_size_saved += size
|
|
176
199
|
except docker.errors.ImageNotFound:
|
|
177
|
-
|
|
200
|
+
# If the image is not found here, it means Docker automatically deleted
|
|
201
|
+
# the underlying image data when we removed its last tag in the loop above.
|
|
202
|
+
log.info(
|
|
203
|
+
"image removed via untagging", image_id=img_id, size=format_size(size)
|
|
204
|
+
)
|
|
205
|
+
executed_size_saved += size
|
|
178
206
|
except docker.errors.APIError as e:
|
|
179
207
|
log.warning("skipped image removal", image_id=img_id, reason=str(e))
|
|
180
208
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Version handling for docker-image-cleanup."""
|
|
2
|
+
|
|
3
|
+
import importlib.metadata
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def is_local_source_checkout() -> bool:
|
|
8
|
+
"""Check if the code is running from a local source checkout."""
|
|
9
|
+
package_dir = Path(__file__).resolve().parent
|
|
10
|
+
# Since this is a flat layout (module-root = ""), repo root is the parent of the package dir
|
|
11
|
+
repo_root = package_dir.parent
|
|
12
|
+
|
|
13
|
+
return (repo_root / ".git").exists() and (repo_root / "pyproject.toml").exists()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_version() -> str:
|
|
17
|
+
"""Get the version string, appending .dev if running from source."""
|
|
18
|
+
try:
|
|
19
|
+
# Try to get the version of the installed package
|
|
20
|
+
version = importlib.metadata.version("docker-image-cleanup")
|
|
21
|
+
except importlib.metadata.PackageNotFoundError:
|
|
22
|
+
# Fallback for local development if not installed
|
|
23
|
+
version = "0.1.0"
|
|
24
|
+
|
|
25
|
+
if not is_local_source_checkout():
|
|
26
|
+
return version
|
|
27
|
+
|
|
28
|
+
if version.endswith(".dev"):
|
|
29
|
+
return version
|
|
30
|
+
|
|
31
|
+
return f"{version}.dev"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
__version__ = get_version()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "docker-image-cleanup"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.4"
|
|
4
4
|
description = "Clean up old Docker images from local storage with retention controls"
|
|
5
5
|
keywords = ["docker", "cleanup", "images", "disk-space"]
|
|
6
6
|
readme = "README.md"
|
|
@@ -15,12 +15,14 @@ dependencies = [
|
|
|
15
15
|
authors = [{ name = "Michael Bianco", email = "mike@mikebian.co" }]
|
|
16
16
|
urls = { "Repository" = "https://github.com/iloveitaly/docker-image-cleanup" }
|
|
17
17
|
|
|
18
|
+
|
|
18
19
|
# additional packaging information: https://packaging.python.org/en/latest/specifications/core-metadata/#license
|
|
19
20
|
[project.scripts]
|
|
20
21
|
docker-image-cleanup = "docker_image_cleanup:main"
|
|
21
22
|
|
|
23
|
+
|
|
22
24
|
[build-system]
|
|
23
|
-
requires = ["uv_build>=0.
|
|
25
|
+
requires = ["uv_build>=0.11.0,<0.12"]
|
|
24
26
|
build-backend = "uv_build"
|
|
25
27
|
|
|
26
28
|
[tool.uv.build-backend]
|
|
@@ -28,10 +30,24 @@ build-backend = "uv_build"
|
|
|
28
30
|
module-root = ""
|
|
29
31
|
|
|
30
32
|
[dependency-groups]
|
|
31
|
-
dev = [
|
|
33
|
+
dev = [
|
|
34
|
+
"pytest>=8.3.4",
|
|
35
|
+
"pyright[nodejs]>=1.1.408",
|
|
36
|
+
"ruff>=0.8.0",
|
|
37
|
+
"coverage>=7.13.4",
|
|
38
|
+
"pytest-cov>=7.0.0",
|
|
39
|
+
"covdefaults>=2.3.0",
|
|
40
|
+
]
|
|
32
41
|
|
|
33
42
|
[tool.pyright]
|
|
34
43
|
exclude = ["examples/", "playground/", "tmp/", ".venv/", "tests/"]
|
|
35
44
|
|
|
36
|
-
[tool.
|
|
37
|
-
|
|
45
|
+
[tool.pytest.ini_options]
|
|
46
|
+
addopts = "--cov --cov-report=term-missing --cov-report=html:tmp/htmlcov"
|
|
47
|
+
|
|
48
|
+
[tool.coverage.run]
|
|
49
|
+
plugins = ["covdefaults"]
|
|
50
|
+
source = ["docker_image_cleanup"]
|
|
51
|
+
|
|
52
|
+
[tool.coverage.report]
|
|
53
|
+
fail_under = 50
|