diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..eadf039 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,144 @@ +name: Python Poetry Application + +on: + push: + branches: ["main"] + tags: + - "v*" + pull_request: + branches: ["main"] + +permissions: + contents: read + pull-requests: write # Needed for commenting on PRs + +env: + PYTHON_VERSION: "3.13" + POETRY_VERSION: "2.1.2" + +jobs: + test: + name: Run Tests + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Cache Poetry + uses: actions/cache@v4 + with: + path: ~/.local + key: poetry-${{ github.runner.os }}-${{ env.POETRY_VERSION }} + + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + version: ${{ env.POETRY_VERSION }} + virtualenvs-create: true + virtualenvs-in-project: true + + - name: Cache Dependencies + id: cache-deps + uses: actions/cache@v4 + with: + path: .venv + key: deps-${{ github.runner.os }}-${{ hashFiles('**/poetry.lock') }} + + - name: Install Dependencies + run: poetry install --no-interaction + if: steps.cache-deps.outputs.cache-hit != 'true' + + - name: Run Pre-commit Checks + run: poetry run pre-commit run --all-files + + - name: Run Tests with Coverage + run: poetry run pytest --cov=docker_volume_analyzer --cov-report=xml --cov-report=term + + - name: Comment Coverage on PR + if: ${{ github.event_name == 'pull_request' }} + uses: MishaKav/pytest-coverage-comment@main + with: + pytest-xml-coverage-path: ./coverage.xml + + - name: Upload Coverage to Codecov + if: github.ref == 'refs/heads/main' + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + + release: + name: Build & Release + runs-on: ubuntu-latest + needs: test + if: startsWith(github.ref, 'refs/tags/v') + + steps: + - uses: actions/checkout@v4 + + - name: Extract version from Git tag and set in pyproject.toml + run: | + VERSION=${GITHUB_REF#refs/tags/v} + echo "VERSION=$VERSION" >> $GITHUB_ENV + poetry version $VERSION + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Cache Poetry + uses: actions/cache@v4 + with: + path: ~/.local + key: poetry-${{ github.runner.os }}-${{ env.POETRY_VERSION }} + + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + version: ${{ env.POETRY_VERSION }} + virtualenvs-create: true + virtualenvs-in-project: true + + - name: Cache Dependencies + id: cache-deps + uses: actions/cache@v4 + with: + path: .venv + key: deps-${{ github.runner.os }}-${{ hashFiles('**/poetry.lock') }} + + - name: Install Dependencies + run: poetry install --no-dev --no-interaction + if: steps.cache-deps.outputs.cache-hit != 'true' + + - name: Build Distribution + run: poetry build + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ env.VERSION }} + files: | + dist/*.whl + dist/*.tar.gz + + - name: Docker Login + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build Docker Image + run: docker build \ + --build-arg APP_VERSION=${{ env.VERSION }} \ + -t glefer/docker-volumes-analyzer:${{ env.VERSION }} \ + -t glefer/docker-volumes-analyzer:latest . + + - name: Push Docker Image + run: | + docker push glefer/docker-volumes-analyzer:${{ env.VERSION }} + docker push glefer/docker-volumes-analyzer:latest \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml deleted file mode 100644 index 4b3269d..0000000 --- a/.github/workflows/tests.yml +++ /dev/null @@ -1,70 +0,0 @@ -name: Python poetry application - -on: - push: - branches: ["main"] - pull_request: - branches: ["main"] - -permissions: - contents: read - pull-requests: write # Needed for commenting on PRs - -jobs: - build: - name: Tests - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Set up Python 3.13 - uses: actions/setup-python@v5 - with: - python-version: "3.13" - - - name: cache poetry install - uses: actions/cache@v4 - with: - path: ~/.local - key: poetry-2.1.2-0 - - - uses: snok/install-poetry@v1 - with: - version: 2.1.2 - virtualenvs-create: true - virtualenvs-in-project: true - - - name: cache deps - id: cache-deps - uses: actions/cache@v4 - with: - path: .venv - key: pydeps-${{ hashFiles('**/poetry.lock') }} - - - name: Install dependencies using poetry - run: poetry install --no-interaction --no-root - if: steps.cache-deps.outputs.cache-hit != 'true' - - - name: Install project - run: poetry install --no-interaction - - - name: Analysing the code with black, flake8, isort, etc... - run: | - poetry run pre-commit run --all-files - - - name: Test with coverage - run: | - poetry run pytest --cov=docker_volume_analyzer --cov-report=xml --cov-report=term - - - name: Pytest coverage comment - if: ${{ github.event_name == 'pull_request' }} - uses: MishaKav/pytest-coverage-comment@main - with: - pytest-xml-coverage-path: ./coverage.xml - - - name: Upload coverage reports to Codecov - if: github.ref == 'refs/heads/main' - uses: codecov/codecov-action@v5 - with: - token: ${{ secrets.CODECOV_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index c7ec32e..59f4488 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ __pycache__ htmlcov/ .coverage -.vscode/ \ No newline at end of file +.vscode/ +dist/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..84a17c8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +FROM python:3.13-slim AS builder + +ENV PYTHONUNBUFFERED=1 \ + POETRY_VERSION=2.1.2 + +RUN pip install "poetry==$POETRY_VERSION" + +WORKDIR /build + +COPY pyproject.toml poetry.lock ./ +RUN poetry install --only main --no-interaction --no-ansi --no-root +COPY . . + +RUN poetry build + +FROM python:3.13-slim AS runtime + +WORKDIR /app +ENV PYTHONUNBUFFERED=1 + +ARG APP_VERSION='undefined' +ENV APP_VERSION=${APP_VERSION} + +COPY --from=builder /build/dist/*.whl ./dist/ + +RUN pip install ./dist/*.whl + +CMD ["start"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8aa2645 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) [year] [fullname] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index c869158..50b6dd8 100644 --- a/README.md +++ b/README.md @@ -14,3 +14,119 @@ This project aims to make Docker volume management more intuitive and user-frien [![Build Status](https://github.com/glefer/docker-volumes-analyzer/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/glefer/docker-volumes-analyzer/actions) [![codecov](https://codecov.io/gh/glefer/docker-volumes-analyzer/branch/main/graph/badge.svg?token=JRjmc0emjT)](https://codecov.io/gh/glefer/docker-volumes-analyzer) ![Python](https://img.shields.io/badge/python-3.13-blue) + + +## Installation + +### Prérequis + +- **Python** `>=3.13,<4.0.0` +- **Poetry** `>=2.1.2` installé globalement ([lien d’installation](https://python-poetry.org/docs/#installation)) +- Docker en local (si tu veux analyser des volumes) + +--- + +### 1. Cloner le projet + +```bash +git clone https://github.com/glefer/docker-volumes-analyzer.git +cd docker-volumes-analyzer +``` + +### 2. Installer les dépendances + +```bash +poetry install +``` + +### 3. Lancer l'application + +```bash +poetry run start +``` + +> ⚠️ L'application utilise le socket Docker à l'emplacement standard : `/var/run/docker.sock` + +--- + +## 🐳 Utilisation via Docker + +Pas envie d’installer Python ? Utilise simplement l’image Docker : + +```bash +docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -ti glefer/docker-volumes-analyzer:latest +``` + +### Utiliser une version spécifique + +```bash +docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -ti glefer/docker-volumes-analyzer:0.1.0 +``` + +--- + +## Lancer les tests + +```bash +poetry run pytest +``` + +Avec couverture : + +```bash +poetry run pytest --cov=docker_volume_analyzer +``` + +--- + +## 🛠 Développement + +Lance un shell virtuel : + +```bash +poetry shell +``` + +Formatage et vérifications : + +```bash +poetry run pre-commit run --all-files +``` + +--- + +## 🔧 Structure du projet + +``` +. +├── src/ +│ └── docker_volume_analyzer/ +│ └── main.py # Point d’entrée +├── tests/ +├── README.md +├── pyproject.toml +└── poetry.lock +``` + +## Contributing + +Contributions are welcome! If you'd like to contribute, please follow these steps: + +1. Fork the repository. +2. Create a new branch (`git checkout -b feature/my-feature`). +3. Commit your changes (`git commit -m 'Add some feature'`). +4. Push to the branch (`git push origin feature/my-feature`). +5. Open a pull request. + +For major changes, please open an issue first to discuss what you would like to change. + +--- + +## 📝 Licence + +Ce projet est sous licence **MIT** — voir le fichier [LICENSE](./LICENSE). + +--- + +## 👨‍💻 Auteur +[github.com/glefer](https://github.com/glefer) \ No newline at end of file diff --git a/src/docker_volume_analyzer/docker_client.py b/src/docker_volume_analyzer/docker_client.py index 97078a0..58efbf3 100644 --- a/src/docker_volume_analyzer/docker_client.py +++ b/src/docker_volume_analyzer/docker_client.py @@ -2,10 +2,15 @@ import docker +from docker_volume_analyzer.errors import DockerNotAvailableError + class DockerClient: def __init__(self): - self.client = docker.from_env() + try: + self.client = docker.from_env() + except docker.errors.DockerException as e: + raise DockerNotAvailableError from e def list_volumes(self) -> List[docker.models.volumes.Volume]: """ @@ -17,7 +22,7 @@ def list_containers(self) -> List[docker.models.containers.Container]: """ Returns all running Docker container objects. """ - return self.client.containers.list() + return self.client.containers.list(all=True) def _run_in_container( self, diff --git a/src/docker_volume_analyzer/errors.py b/src/docker_volume_analyzer/errors.py new file mode 100644 index 0000000..12a8c9f --- /dev/null +++ b/src/docker_volume_analyzer/errors.py @@ -0,0 +1,9 @@ +class DockerNotAvailableError(Exception): + """Raised when Docker is not available on the system.""" + + def __init__( + self, + message="Docker is not available. " + "Please ensure Docker is installed and running.", + ): + super().__init__(message) diff --git a/src/docker_volume_analyzer/tui.py b/src/docker_volume_analyzer/tui.py index 8b0e2bc..f00ab63 100644 --- a/src/docker_volume_analyzer/tui.py +++ b/src/docker_volume_analyzer/tui.py @@ -1,3 +1,5 @@ +import os + from textual.app import App, ComposeResult from textual.containers import Container, Horizontal, Vertical from textual.screen import ModalScreen @@ -21,7 +23,12 @@ class DockerTUI(App): def __init__(self): super().__init__() - self.title = "Docker Volume Analyzer" + app_version = os.getenv("APP_VERSION", None) + self.title = ( + f"Docker Volume Analyzer (v{app_version})" + if app_version + else "Docker Volume Analyzer" + ) self.manager = VolumeManager() self.volumes = None diff --git a/tests/test_docker_client.py b/tests/test_docker_client.py index fa64dd7..4f77aed 100644 --- a/tests/test_docker_client.py +++ b/tests/test_docker_client.py @@ -1,8 +1,10 @@ -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch import docker +import pytest from docker_volume_analyzer.docker_client import DockerClient +from docker_volume_analyzer.errors import DockerNotAvailableError def test_list_volumes(): @@ -100,3 +102,11 @@ def test_get_volume_size(): size = docker_client.get_volume_size("volume_name") assert size == "10M" + + +def test_docker_not_available_error(): + # Mock docker.from_env to raise DockerException + with patch("docker.from_env", side_effect=docker.errors.DockerException): + # Assert that DockerNotAvailableError is raised + with pytest.raises(DockerNotAvailableError): + DockerClient() diff --git a/tests/test_errors.py b/tests/test_errors.py new file mode 100644 index 0000000..be731b2 --- /dev/null +++ b/tests/test_errors.py @@ -0,0 +1,26 @@ +import pytest + +from docker_volume_analyzer.errors import DockerNotAvailableError + +EXCEPTIONS = [ + ( + DockerNotAvailableError, + "Docker is not available. " + "Please ensure Docker is installed and running.", + ), +] + + +@pytest.mark.parametrize("exception_class, default_message", EXCEPTIONS) +def test_custom_exceptions_default_message(exception_class, default_message): + with pytest.raises(exception_class) as exc_info: + raise exception_class() + assert str(exc_info.value) == default_message + + +@pytest.mark.parametrize("exception_class, _", EXCEPTIONS) +def test_custom_exceptions_custom_message(exception_class, _): + msg = f"Custom message for {exception_class.__name__}" + with pytest.raises(exception_class) as exc_info: + raise exception_class(msg) + assert str(exc_info.value) == msg