docker-image-cleanup 0.1.3__tar.gz → 0.2.0__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.3 → docker_image_cleanup-0.2.0}/PKG-INFO +13 -2
- {docker_image_cleanup-0.1.3 → docker_image_cleanup-0.2.0}/README.md +12 -1
- {docker_image_cleanup-0.1.3 → docker_image_cleanup-0.2.0}/docker_image_cleanup/__init__.py +27 -13
- docker_image_cleanup-0.2.0/pyproject.toml +65 -0
- docker_image_cleanup-0.1.3/pyproject.toml → docker_image_cleanup-0.2.0/pyproject.toml.orig +2 -2
- {docker_image_cleanup-0.1.3 → docker_image_cleanup-0.2.0}/docker_image_cleanup/version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: docker-image-cleanup
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
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
|
|
@@ -89,6 +89,18 @@ docker run -d \
|
|
|
89
89
|
ghcr.io/iloveitaly/docker-image-cleanup:latest
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
+
Here's an example script to get the latest image and restart any existing docker image:
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
docker pull ghcr.io/iloveitaly/docker-image-cleanup:latest
|
|
96
|
+
docker stop docker-image-cleanup && docker rm docker-image-cleanup
|
|
97
|
+
docker run -d \
|
|
98
|
+
--name docker-image-cleanup \
|
|
99
|
+
-v /var/run/docker.sock:/var/run/docker.sock \
|
|
100
|
+
-e TARGET_REPOS="repo.com/one repo.com/two" \
|
|
101
|
+
ghcr.io/iloveitaly/docker-image-cleanup:latest
|
|
102
|
+
```
|
|
103
|
+
|
|
92
104
|
### Docker Compose
|
|
93
105
|
|
|
94
106
|
```yaml
|
|
@@ -132,4 +144,3 @@ Images with multiple tags are handled intelligently. If all tags on an image wou
|
|
|
132
144
|
---
|
|
133
145
|
|
|
134
146
|
*This project was created from [iloveitaly/python-package-template](https://github.com/iloveitaly/python-package-template)*
|
|
135
|
-
> theirs
|
|
@@ -73,6 +73,18 @@ docker run -d \
|
|
|
73
73
|
ghcr.io/iloveitaly/docker-image-cleanup:latest
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
+
Here's an example script to get the latest image and restart any existing docker image:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
docker pull ghcr.io/iloveitaly/docker-image-cleanup:latest
|
|
80
|
+
docker stop docker-image-cleanup && docker rm docker-image-cleanup
|
|
81
|
+
docker run -d \
|
|
82
|
+
--name docker-image-cleanup \
|
|
83
|
+
-v /var/run/docker.sock:/var/run/docker.sock \
|
|
84
|
+
-e TARGET_REPOS="repo.com/one repo.com/two" \
|
|
85
|
+
ghcr.io/iloveitaly/docker-image-cleanup:latest
|
|
86
|
+
```
|
|
87
|
+
|
|
76
88
|
### Docker Compose
|
|
77
89
|
|
|
78
90
|
```yaml
|
|
@@ -116,4 +128,3 @@ Images with multiple tags are handled intelligently. If all tags on an image wou
|
|
|
116
128
|
---
|
|
117
129
|
|
|
118
130
|
*This project was created from [iloveitaly/python-package-template](https://github.com/iloveitaly/python-package-template)*
|
|
119
|
-
> theirs
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Cleans up old Docker images from local storage with retention controls.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
import sys
|
|
5
6
|
from collections import defaultdict
|
|
6
7
|
from typing import Any
|
|
7
8
|
|
|
@@ -113,7 +114,7 @@ def determine_cleanup_actions(
|
|
|
113
114
|
if img.created >= min_age_threshold:
|
|
114
115
|
keep_tags.update(img.tags)
|
|
115
116
|
|
|
116
|
-
|
|
117
|
+
images_to_delete: dict[str, int] = {}
|
|
117
118
|
tags_to_remove: list[str] = []
|
|
118
119
|
|
|
119
120
|
images_by_id: dict[str, list[ImageInfo]] = defaultdict(list)
|
|
@@ -142,7 +143,7 @@ def determine_cleanup_actions(
|
|
|
142
143
|
if set(tags_for_this_image_to_remove) == current_image_repo_tags:
|
|
143
144
|
img_info = img_list[0]
|
|
144
145
|
if not img_info.is_in_use:
|
|
145
|
-
|
|
146
|
+
images_to_delete[img_id] = img_info.size
|
|
146
147
|
total_size_saved += img_info.size
|
|
147
148
|
else:
|
|
148
149
|
log.warning(
|
|
@@ -152,7 +153,7 @@ def determine_cleanup_actions(
|
|
|
152
153
|
)
|
|
153
154
|
|
|
154
155
|
return {
|
|
155
|
-
"images_to_delete":
|
|
156
|
+
"images_to_delete": images_to_delete,
|
|
156
157
|
"tags_to_remove": list(set(tags_to_remove)),
|
|
157
158
|
"total_size_saved": total_size_saved,
|
|
158
159
|
}
|
|
@@ -160,7 +161,7 @@ def determine_cleanup_actions(
|
|
|
160
161
|
|
|
161
162
|
def execute_cleanup(
|
|
162
163
|
client: docker.DockerClient,
|
|
163
|
-
images_to_delete:
|
|
164
|
+
images_to_delete: dict[str, int],
|
|
164
165
|
tags_to_remove: list[str],
|
|
165
166
|
total_size_saved: int,
|
|
166
167
|
dry_run: bool,
|
|
@@ -172,7 +173,7 @@ def execute_cleanup(
|
|
|
172
173
|
if tags_to_remove:
|
|
173
174
|
log.info("would remove tags", tags=tags_to_remove)
|
|
174
175
|
if images_to_delete:
|
|
175
|
-
log.info("would remove images", image_ids=images_to_delete)
|
|
176
|
+
log.info("would remove images", image_ids=list(images_to_delete.keys()))
|
|
176
177
|
|
|
177
178
|
log.info(
|
|
178
179
|
"total space that would be saved",
|
|
@@ -184,19 +185,25 @@ def execute_cleanup(
|
|
|
184
185
|
for tag in tags_to_remove:
|
|
185
186
|
try:
|
|
186
187
|
client.images.remove(tag, force=False)
|
|
187
|
-
log.info("
|
|
188
|
+
log.info("removed tag", tag=tag)
|
|
188
189
|
except docker.errors.APIError as e:
|
|
189
|
-
log.warning("skipped
|
|
190
|
+
log.warning("skipped tag removal", tag=tag, reason=str(e))
|
|
190
191
|
|
|
191
|
-
|
|
192
|
+
# We explicitly remove the Image ID here even after untagging.
|
|
193
|
+
# This is primarily to catch and clean up "dangling" images (images with no tags)
|
|
194
|
+
# that might have been orphaned but wouldn't be caught by the tag loop.
|
|
195
|
+
for img_id, size in images_to_delete.items():
|
|
192
196
|
try:
|
|
193
|
-
img = client.images.get(img_id)
|
|
194
|
-
size = img.attrs.get("Size", 0)
|
|
195
197
|
client.images.remove(img_id, force=False)
|
|
196
198
|
log.info("removed image", image_id=img_id, size=format_size(size))
|
|
197
199
|
executed_size_saved += size
|
|
198
200
|
except docker.errors.ImageNotFound:
|
|
199
|
-
|
|
201
|
+
# If the image is not found here, it means Docker automatically deleted
|
|
202
|
+
# the underlying image data when we removed its last tag in the loop above.
|
|
203
|
+
log.info(
|
|
204
|
+
"image removed via untagging", image_id=img_id, size=format_size(size)
|
|
205
|
+
)
|
|
206
|
+
executed_size_saved += size
|
|
200
207
|
except docker.errors.APIError as e:
|
|
201
208
|
log.warning("skipped image removal", image_id=img_id, reason=str(e))
|
|
202
209
|
|
|
@@ -264,6 +271,13 @@ def main(
|
|
|
264
271
|
dry_run=dry_run,
|
|
265
272
|
)
|
|
266
273
|
|
|
274
|
+
log.info(
|
|
275
|
+
"cleanup configuration",
|
|
276
|
+
num_recent=config.num_recent,
|
|
277
|
+
min_age_days=config.min_age_days,
|
|
278
|
+
dry_run=config.dry_run,
|
|
279
|
+
)
|
|
280
|
+
|
|
267
281
|
grand_total_saved = 0
|
|
268
282
|
for repo_name in image_repos:
|
|
269
283
|
log.info("processing repository", repo=repo_name)
|
|
@@ -298,10 +312,10 @@ def main(
|
|
|
298
312
|
|
|
299
313
|
except docker.errors.DockerException as e:
|
|
300
314
|
log.error("failed to connect to Docker daemon", reason=str(e))
|
|
301
|
-
exit(1)
|
|
315
|
+
sys.exit(1)
|
|
302
316
|
except Exception:
|
|
303
317
|
log.exception("an unexpected error occurred")
|
|
304
|
-
exit(1)
|
|
318
|
+
sys.exit(1)
|
|
305
319
|
|
|
306
320
|
|
|
307
321
|
if __name__ == "__main__":
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "docker-image-cleanup"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "Clean up old Docker images from local storage with retention controls"
|
|
5
|
+
keywords = [
|
|
6
|
+
"docker",
|
|
7
|
+
"cleanup",
|
|
8
|
+
"images",
|
|
9
|
+
"disk-space",
|
|
10
|
+
]
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
requires-python = ">=3.11"
|
|
13
|
+
dependencies = [
|
|
14
|
+
"click>=8.3.0",
|
|
15
|
+
"docker>=7.1.0",
|
|
16
|
+
"pydantic>=2.12.5",
|
|
17
|
+
"structlog-config>=0.6.0",
|
|
18
|
+
"whenever>=0.9.3",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[[project.authors]]
|
|
22
|
+
name = "Michael Bianco"
|
|
23
|
+
email = "mike@mikebian.co"
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Repository = "https://github.com/iloveitaly/docker-image-cleanup"
|
|
27
|
+
|
|
28
|
+
[project.scripts]
|
|
29
|
+
docker-image-cleanup = "docker_image_cleanup:main"
|
|
30
|
+
|
|
31
|
+
[build-system]
|
|
32
|
+
requires = ["uv_build>=0.11.0,<0.13"]
|
|
33
|
+
build-backend = "uv_build"
|
|
34
|
+
|
|
35
|
+
[tool.uv.build-backend]
|
|
36
|
+
module-root = ""
|
|
37
|
+
|
|
38
|
+
[tool.pyright]
|
|
39
|
+
exclude = [
|
|
40
|
+
"examples/",
|
|
41
|
+
"playground/",
|
|
42
|
+
"tmp/",
|
|
43
|
+
".venv/",
|
|
44
|
+
"tests/",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[tool.pytest.ini_options]
|
|
48
|
+
addopts = "--cov --cov-report=term-missing --cov-report=html:tmp/htmlcov"
|
|
49
|
+
|
|
50
|
+
[tool.coverage.run]
|
|
51
|
+
plugins = ["covdefaults"]
|
|
52
|
+
source = ["docker_image_cleanup"]
|
|
53
|
+
|
|
54
|
+
[tool.coverage.report]
|
|
55
|
+
fail_under = 50
|
|
56
|
+
|
|
57
|
+
[dependency-groups]
|
|
58
|
+
dev = [
|
|
59
|
+
"pytest>=8.3.4",
|
|
60
|
+
"pyright[nodejs]>=1.1.408",
|
|
61
|
+
"ruff>=0.8.0",
|
|
62
|
+
"coverage>=7.13.4",
|
|
63
|
+
"pytest-cov>=7.0.0",
|
|
64
|
+
"covdefaults>=2.3.0",
|
|
65
|
+
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "docker-image-cleanup"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
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"
|
|
@@ -22,7 +22,7 @@ docker-image-cleanup = "docker_image_cleanup:main"
|
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
[build-system]
|
|
25
|
-
requires = ["uv_build>=0.11.0,<0.
|
|
25
|
+
requires = ["uv_build>=0.11.0,<0.13"]
|
|
26
26
|
build-backend = "uv_build"
|
|
27
27
|
|
|
28
28
|
[tool.uv.build-backend]
|
|
File without changes
|