diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml new file mode 100644 index 0000000000..2b9f413041 --- /dev/null +++ b/.github/workflows/python-ci.yml @@ -0,0 +1,62 @@ +name: Python CI/CD + +on: [push, pull_request] + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + runs-on: ubuntu-latest + defaults: + run: + working-directory: labs/app_python + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + cache: "pip" + cache-dependency-path: labs/app_python/requirements.txt + + - name: Install dependencies + run: | + pip install -r requirements.txt + pip install -r requirements-dev.txt + + - name: Run linter + run: flake8 . + + - name: Run tests with coverage + run: pytest --cov=app --cov-report=xml --cov-fail-under=70 + + docker: + needs: test + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Generate version + run: echo "VERSION=$(date +'%Y.%m').${{ github.run_number }}" >> $GITHUB_ENV + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and Push Docker Image + uses: docker/build-push-action@v6 + with: + context: ./labs/app_python + push: true + tags: | + ${{ secrets.DOCKERHUB_USERNAME }}/devops-info-service:${{ env.VERSION }} + ${{ secrets.DOCKERHUB_USERNAME }}/devops-info-service:latest \ No newline at end of file diff --git a/labs/app_python/README.md b/labs/app_python/README.md index 1d5f66dfd4..892c45ae14 100644 --- a/labs/app_python/README.md +++ b/labs/app_python/README.md @@ -1,133 +1,25 @@ +````markdown # DevOps Info Service -## Overview -DevOps Info Service is a web application that provides detailed information about the service itself and its runtime environment. This service will be developed throughout the DevOps course into a comprehensive monitoring tool. +![CI](https://github.com/qobz1e/DevOps-Core-Course/actions/workflows/python-ci.yml/badge.svg) -## Prerequisites -- Python 3.11 or higher -- pip package manager +## Run locally -## Installation - -1. Create and activate virtual environment: -```bash -python -m venv venv -source venv/bin/activate -``` - -2. Install dependencies -```bash -pip install -r requirements.txt -``` - -## Running the Application ```bash -# Basic startup python app.py +```` -# With custom configuration -PORT=8080 python app.py - -# With all environment variables -HOST=127.0.0.1 PORT=3000 DEBUG=True python app.py -``` - -## API Endpoints - -### GET `/` -Returns comprehensive information about the service, system, and current request. - -**Example response:** -```json -{ - "service": { - "name": "devops-info-service", - "version": "1.0.0", - "description": "DevOps course info service", - "framework": "Flask" - }, - "system": { - "hostname": "my-pc", - "platform": "Linux", - "cpu_count": 8, - "python_version": "3.11.5" - }, - "runtime": { - "uptime_seconds": 120, - "uptime_human": "2 minutes", - "current_time": "2024-01-27T10:30:00.000Z" - } -} -``` - -### GET `/health` -Returns the health status of the service. Used for monitoring and Kubernetes probes. - -**Example response:** -```json -{ - "status": "healthy", - "timestamp": "2024-01-27T10:30:00.000Z", - "uptime_seconds": 120 -} -``` - -## Configuration - -The application can be configured using environment variables: +## Run tests -| Variable | Description | Default Value | -|----------|-------------|---------------| -| `HOST` | Host to bind the server to | `0.0.0.0` | -| `PORT` | Port to run the server on | `5000` | -| `DEBUG` | Enable Flask debug mode | `False` | - - -## 🐳 Docker - -### Building the Image -To build the Docker image locally, navigate to the application directory and use: -```bash -docker build -t qobz1e/devops-info-service:lab2 . -``` - -The Dockerfile follows security best practices including non-root user execution and optimized layer caching. - -### Running the Container -Run the container with port mapping to access the service: -```bash -docker run -d -p 5000:5000 --name devops-app qobz1e/devops-info-service:lab2 -``` - -### Pulling from Docker Hub -Pull the pre-built image from Docker Hub: ```bash -docker pull qobz1e/devops-info-service:lab2 -``` - -### Quick Commands -```bash -# Build -docker build -t qobz1e/devops-info-service:lab2 . - -# Run -docker run -d -p 5000:5000 --name devops-app qobz1e/devops-info-service:lab2 - -# Test -curl http://localhost:5000/ -curl http://localhost:5000/health +cd app_python +pip install -r requirements.txt +pip install -r requirements-dev.txt +pytest -v ``` -### Configuration -Set environment variables when running: -- `HOST`: Binding address (default: 0.0.0.0) -- `PORT`: Application port (default: 5000) -- `DEBUG`: Debug mode (default: False) +## Run with coverage -Example: ```bash -docker run -d -p 8080:5000 -e PORT=5000 -e DEBUG=True qobz1e/devops-info-service:lab2 -``` - -### Health Check -The container includes a health endpoint at `/health` for monitoring and orchestration systems. +pytest --cov=app --cov-report=term +``` \ No newline at end of file diff --git a/labs/app_python/requirements-dev.txt b/labs/app_python/requirements-dev.txt new file mode 100644 index 0000000000..cccf9f7a3e --- /dev/null +++ b/labs/app_python/requirements-dev.txt @@ -0,0 +1,3 @@ +pytest==8.2.0 +pytest-cov==5.0.0 +flake8==7.0.0 \ No newline at end of file diff --git a/labs/app_python/tests/conftest.py b/labs/app_python/tests/conftest.py new file mode 100644 index 0000000000..06a978b9a6 --- /dev/null +++ b/labs/app_python/tests/conftest.py @@ -0,0 +1,9 @@ +import pytest +from app import app + + +@pytest.fixture +def client(): + app.config['TESTING'] = True + with app.test_client() as client: + yield client diff --git a/labs/app_python/tests/test_docker.py b/labs/app_python/tests/test_docker.py new file mode 100644 index 0000000000..5de2d1019e --- /dev/null +++ b/labs/app_python/tests/test_docker.py @@ -0,0 +1,9 @@ +def test_docker_endpoint(client): + response = client.get("/docker") + assert response.status_code == 200 + + data = response.get_json() + + assert "is_docker" in data + assert "container_id" in data + assert "message" in data diff --git a/labs/app_python/tests/test_errors.py b/labs/app_python/tests/test_errors.py new file mode 100644 index 0000000000..84bc2eefa3 --- /dev/null +++ b/labs/app_python/tests/test_errors.py @@ -0,0 +1,8 @@ +def test_404_handler(client): + response = client.get("/unknown-endpoint") + assert response.status_code == 404 + + data = response.get_json() + + assert data["error"] == "Not Found" + assert "available_endpoints" in data diff --git a/labs/app_python/tests/test_health.py b/labs/app_python/tests/test_health.py new file mode 100644 index 0000000000..697a567d81 --- /dev/null +++ b/labs/app_python/tests/test_health.py @@ -0,0 +1,13 @@ +def test_health_status_code(client): + response = client.get("/health") + assert response.status_code == 200 + + +def test_health_structure(client): + response = client.get("/health") + data = response.get_json() + + assert data["status"] == "healthy" + assert "timestamp" in data + assert "uptime_seconds" in data + assert "environment" in data diff --git a/labs/app_python/tests/test_main.py b/labs/app_python/tests/test_main.py new file mode 100644 index 0000000000..7d315cc68e --- /dev/null +++ b/labs/app_python/tests/test_main.py @@ -0,0 +1,17 @@ +def test_main_endpoint_status(client): + response = client.get("/") + assert response.status_code == 200 + + +def test_main_endpoint_structure(client): + response = client.get("/") + data = response.get_json() + + assert "service" in data + assert "system" in data + assert "runtime" in data + assert "request" in data + assert "endpoints" in data + + assert data["service"]["name"] == "devops-info-service" + assert data["service"]["framework"] == "Flask"