Compare commits

..

10 Commits

Author SHA1 Message Date
Kevin Veen-Birkenbach
d76e064556 Optimized Mirrors
Some checks failed
CI (unit tests, stable tag) / test (push) Has been cancelled
2025-12-28 11:38:20 +01:00
Kevin Veen-Birkenbach
5b31214e34 Ignored *.pyc files 2025-12-28 11:35:43 +01:00
Kevin Veen-Birkenbach
04850a8f20 test: add unit tests and CI workflow for dockreap
- Add unittest-based unit tests for core volume logic
- Add Makefile with isolated venv-based test runner
- Add GitHub Actions CI workflow
- Automatically mark SemVer-tagged commits as stable

https://chatgpt.com/share/695107c4-f320-800f-b4ce-da953de9bb86
2025-12-28 11:34:35 +01:00
Kevin Veen-Birkenbach
33cac347ea Improved Changelog message and added ignore files 2025-12-28 11:30:09 +01:00
Kevin Veen-Birkenbach
57087c1bd7 Release version 1.0.0 2025-12-28 11:28:27 +01:00
Kevin Veen-Birkenbach
092965d1ca feat: package dockreap as proper Python CLI and update README
- Add pyproject.toml for setuptools-based packaging
- Rename entrypoint to dockreap.__main__ and expose console script
- Update README with pip/pipx installation instructions
- Remove obsolete package-manager references
- Add MIRRORS file including PyPI endpoint
- Align documentation with actual volume usage detection logic
2025-12-28 11:27:37 +01:00
Kevin Veen-Birkenbach
2f722824e6 Added funding 2025-12-28 11:10:36 +01:00
Kevin Veen-Birkenbach
8f8ec75aee Solved symlink bug 2025-04-07 15:07:58 +02:00
Kevin Veen-Birkenbach
76cfa5312d Optimized How to Use 2025-04-07 14:56:04 +02:00
Kevin Veen-Birkenbach
39f8ccb376 Update README.md 2025-04-07 14:47:10 +02:00
14 changed files with 291 additions and 10 deletions

7
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,7 @@
github: kevinveenbirkenbach
patreon: kevinveenbirkenbach
buy_me_a_coffee: kevinveenbirkenbach
custom: https://s.veen.world/paypaldonate

43
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,43 @@
name: CI (unit tests, stable tag)
on:
push:
branches: ["**"]
tags: ["v*.*.*"]
pull_request:
permissions:
contents: write
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # needed to create/move tags reliably
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install package (editable)
run: |
python -m pip install --upgrade pip
pip install -e .
- name: Run unit tests
run: make test
- name: Mark commit as stable (move stable tag)
if: startsWith(github.ref, 'refs/tags/v')
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
# Move/create 'stable' tag to the same commit as the pushed SemVer tag
git tag -f stable "${GITHUB_SHA}"
git push -f origin stable

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
*.egg-info
dist/
__pycache__
*.pyc

4
CHANGELOG.md Normal file
View File

@@ -0,0 +1,4 @@
## [1.0.0] - 2025-12-28
* Official Release 🥳

4
MIRRORS Normal file
View File

@@ -0,0 +1,4 @@
git@github.com:kevinveenbirkenbach/dockreap.git
ssh://git@code.infinito.nexus:2201/kevinveenbirkenbach/dockreap.git
ssh://git@git.veen.world:2201/kevinveenbirkenbach/dockreap.git
https://pypi.org/project/dockreap/

21
Makefile Normal file
View File

@@ -0,0 +1,21 @@
.ONESHELL:
SHELL := /bin/bash
.SHELLFLAGS := -euo pipefail -c
VENV ?= .venv
PY := $(VENV)/bin/python
PIP := $(VENV)/bin/pip
.PHONY: venv test clean
venv:
test -x "$(PY)" || python3 -m venv "$(VENV)"
"$(PIP)" install -U pip
"$(PIP)" install -e .
test: venv
"$(PY)" -m unittest discover -s tests/unit -p "test_*.py" -v
clean:
rm -rf "$(VENV)" .pytest_cache .coverage
find . -type d -name "__pycache__" -print0 | xargs -0r rm -rf

View File

@@ -1,2 +1,94 @@
# docker-volume-cleaner
A Python tool to safely detect and remove unused anonymous Docker volumes. Supports whitelisting, symlink cleanup, and optional confirmation prompts. Ideal for keeping your Docker environment clean and efficient.
# 🧹 Docker Volume Cleaner (dockreap)
[![GitHub Sponsors](https://img.shields.io/badge/Sponsor-GitHub%20Sponsors-blue?logo=github)](https://github.com/sponsors/kevinveenbirkenbach)
[![Patreon](https://img.shields.io/badge/Support-Patreon-orange?logo=patreon)](https://www.patreon.com/c/kevinveenbirkenbach)
[![Buy Me a Coffee](https://img.shields.io/badge/Buy%20me%20a%20Coffee-Funding-yellow?logo=buymeacoffee)](https://buymeacoffee.com/kevinveenbirkenbach)
[![PayPal](https://img.shields.io/badge/Donate-PayPal-blue?logo=paypal)](https://s.veen.world/paypaldonate)
**dockreap** is a lightweight Python CLI tool that helps you identify and remove unused **anonymous Docker volumes** — including symlinks and their targets 🗑️
Keep your Docker environment tidy, automated, and efficient 🚀
---
## ⚙️ Features
- Detects anonymous Docker volumes (64-character hash names)
- Skips whitelisted volumes
- Skips bootstrap mounts (`/var/www/bootstrap`)
- Cleans up symlinks **and** their target directories
- Optional confirmation prompt via `--no-confirmation`
- Pure Python — **no external dependencies**
---
## 📦 Installation
### Install from PyPI (recommended)
```bash
pip install dockreap
```
or with an isolated environment:
```bash
pipx install dockreap
```
### Install from source (development)
```bash
git clone https://github.com/kevinveenbirkenbach/dockreap.git
cd dockreap
pip install .
```
---
## 🧪 Usage
```bash
# Basic usage (with confirmation prompt)
dockreap
# Skip confirmation
dockreap --no-confirmation
# Skip specific volumes by adding them to the whitelist
dockreap "volumeid1 volumeid2"
# Skip confirmation + whitelist
dockreap "volumeid1 volumeid2" --no-confirmation
```
📝 Notes:
* Only volumes with **64-character hash names** (anonymous volumes) are considered.
* Volumes mounted at `/var/www/bootstrap` are automatically excluded.
* If a volumes `_data` directory is a **symlink**, both the symlink **and its target directory** are removed.
* Volumes referenced by **any container (running or stopped)** are not deleted.
---
## 🔐 Requirements
* Python ≥ 3.9
* Docker CLI available and configured
* Sufficient permissions to remove Docker volumes
(usually requires `root` or membership in the `docker` group)
---
## 📜 License
This project is licensed under the **MIT License**.
---
## 👤 Author
**Kevin Veen-Birkenbach**
🌍 [https://www.veen.world/](https://www.veen.world/)
```

36
pyproject.toml Normal file
View File

@@ -0,0 +1,36 @@
[build-system]
requires = ["setuptools>=69", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "dockreap"
version = "1.0.0"
description = "Remove unused anonymous Docker volumes (with symlink cleanup)."
readme = "README.md"
requires-python = ">=3.9"
license = { text = "MIT" }
authors = [{ name = "Kevin Veen-Birkenbach" }]
keywords = ["docker", "volumes", "cleanup", "devops"]
classifiers = [
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Environment :: Console",
"Operating System :: POSIX :: Linux",
"Topic :: System :: Systems Administration",
]
dependencies = []
[project.urls]
Homepage = "https://www.veen.world/"
Repository = "https://github.com/kevinveenbirkenbach/dockreap"
[project.scripts]
dockreap = "dockreap.__main__:main"
[tool.setuptools]
package-dir = { "" = "src" }
[tool.setuptools.packages.find]
where = ["src"]

0
src/dockreap/__init__.py Normal file
View File

17
main.py → src/dockreap/__main__.py Normal file → Executable file
View File

@@ -4,7 +4,6 @@ import argparse
import subprocess
import re
import sys
import os
from pathlib import Path
import shutil
@@ -45,18 +44,20 @@ def is_volume_used(volume):
return bool(result.stdout.strip())
def cleanup_symlink(volume):
volume_path = VOLUME_BASE_PATH / volume
if volume_path.is_symlink():
target_path = volume_path.resolve()
print(f"Volume directory {volume_path} is a symlink to {target_path}.")
volume_dir = VOLUME_BASE_PATH / volume
data_path = volume_dir / "_data"
if data_path.is_symlink():
target_path = data_path.resolve()
print(f"_data of volume {volume} is a symlink to {target_path}.")
try:
print(f"Removing symlink: {volume_path}")
volume_path.unlink()
print(f"Removing symlink: {data_path}")
data_path.unlink()
if target_path.exists():
print(f"Removing symlink target directory: {target_path}")
shutil.rmtree(target_path)
except Exception as e:
print(f"Failed to clean up symlink or target for {volume}: {e}")
print(f"Failed to clean up _data symlink or its target for {volume}: {e}")
def delete_volume(volume):
cleanup_symlink(volume)

Binary file not shown.

Binary file not shown.

0
tests/unit/__init__.py Normal file
View File

View File

@@ -0,0 +1,69 @@
import unittest
from unittest.mock import patch, MagicMock
import dockreap.__main__ as dockreap_main
class TestDockreapMain(unittest.TestCase):
def test_get_anonymous_volumes_filters_64char_hex(self):
fake_stdout = "\n".join(
[
"short",
"nothex" * 10,
"a" * 64, # valid
"A" * 64, # invalid (uppercase not matched)
"g" * 64, # invalid (not hex)
"b" * 63, # invalid (too short)
"c" * 65, # invalid (too long)
"deadbeef" * 8, # valid 64-char hex
]
)
completed = MagicMock()
completed.stdout = fake_stdout
with patch.object(dockreap_main.subprocess, "run", return_value=completed) as run_mock:
vols = dockreap_main.get_anonymous_volumes()
# Ensure docker command was called correctly
run_mock.assert_called_with(
["docker", "volume", "ls", "--format", "{{.Name}}"],
stdout=dockreap_main.subprocess.PIPE,
text=True,
)
self.assertEqual(vols, ["a" * 64, "deadbeef" * 8])
def test_is_volume_used_true_when_docker_returns_container_ids(self):
completed = MagicMock()
completed.stdout = "abc123\n"
with patch.object(dockreap_main.subprocess, "run", return_value=completed):
self.assertTrue(dockreap_main.is_volume_used("x" * 64))
def test_is_volume_used_false_when_no_container_ids(self):
completed = MagicMock()
completed.stdout = "\n"
with patch.object(dockreap_main.subprocess, "run", return_value=completed):
self.assertFalse(dockreap_main.is_volume_used("x" * 64))
def test_cleanup_symlink_does_nothing_when_not_symlink(self):
# We don't want to touch the real filesystem, so we patch the Path object
fake_data_path = MagicMock()
fake_data_path.is_symlink.return_value = False
fake_volume_dir = MagicMock()
fake_volume_dir.__truediv__.side_effect = lambda x: fake_data_path if x == "_data" else MagicMock()
with patch.object(dockreap_main, "VOLUME_BASE_PATH") as base_path_mock:
base_path_mock.__truediv__.return_value = fake_volume_dir
# Should not raise; should not try to unlink/rmtree
dockreap_main.cleanup_symlink("a" * 64)
fake_data_path.unlink.assert_not_called()
if __name__ == "__main__":
unittest.main()