diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml new file mode 100644 index 0000000000..4cf8ad604f --- /dev/null +++ b/.github/workflows/python-ci.yml @@ -0,0 +1,93 @@ +name: Python CI + Docker Build + +on: + push: + branches: + - main + - lab03 + tags: + - 'v*.*.*' + pull_request: + branches: + - main + +jobs: + test: + name: Lint and Test + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r app_python/requirements.txt + pip install pytest flake8 + + - name: Cache dev dependencies + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-dev-${{ hashFiles('app_python/requirements-dev.txt') }} + restore-keys: | + ${{ runner.os }}-pip-dev- + + - name: Install dev dependencies + run: pip install -r app_python/requirements-dev.txt + + - name: Run Snyk security scan + uses: snyk/actions/python@master + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + with: + args: --file=app_python/requirements.txt --severity-threshold=high --skip-unresolved + + - name: Run linter + run: | + flake8 app_python + + - name: Run tests + run: | + pytest app_python + + docker: + name: Build and Push Docker Image + runs-on: ubuntu-latest + needs: test + + if: startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/lab03' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Extract Docker metadata (tags) + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ secrets.DOCKERHUB_USERNAME }}/devops-info-service + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest + + - name: Build and push Docker image + uses: docker/build-push-action@v6 + with: + context: ./app_python + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 30d74d2584..32d412c6c6 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -test \ No newline at end of file +test +.idea \ No newline at end of file diff --git a/app_python/.dockerignore b/app_python/.dockerignore new file mode 100644 index 0000000000..8476f49f68 --- /dev/null +++ b/app_python/.dockerignore @@ -0,0 +1,18 @@ +__pycache__/ +*.pyc +*.pyo +*.pyd + +.git/ +.gitignore + +.env +.venv/ +venv/ + +.idea/ +.vscode/ + +tests/ +docs/ +README.md \ No newline at end of file diff --git a/app_python/.gitignore b/app_python/.gitignore new file mode 100644 index 0000000000..112fedbb3c --- /dev/null +++ b/app_python/.gitignore @@ -0,0 +1,11 @@ +# Python +__pycache__/ +*.py[cod] +venv/ +*.log + +# IDE +.idea/ + +# OS +.DS_Store \ No newline at end of file diff --git a/app_python/Dockerfile b/app_python/Dockerfile new file mode 100644 index 0000000000..7b74847a96 --- /dev/null +++ b/app_python/Dockerfile @@ -0,0 +1,22 @@ +FROM python:3.12-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ +PYTHONUNBUFFERED=1 + +RUN useradd --create-home --shell /bin/bash appuser + +WORKDIR /app + +COPY requirements.txt ./ + +RUN pip install --no-cache-dir -r requirements.txt + +COPY app.py ./ + +RUN chown -R appuser:appuser /app + +USER appuser + +EXPOSE 5000 + +CMD ["python", "app.py"] \ No newline at end of file diff --git a/app_python/README.md b/app_python/README.md new file mode 100644 index 0000000000..0114b1a55a --- /dev/null +++ b/app_python/README.md @@ -0,0 +1,150 @@ +# DevOps Info Service + +[![Python CI + Docker Build](https://github.com/blazingSummerSun/DevOps-Core-Course/actions/workflows/python-ci.yml/badge.svg)](https://github.com/blazingSummerSun/DevOps-Core-Course/actions/workflows/python-ci.yml) + +## Overview + +DevOps Info Service is a lightweight HTTP service that exposes runtime, system, and application information. The service is suitable for containerization, health monitoring, and deployment in orchestration platforms such as Kubernetes. + +The application provides: + +* Service metadata (name, version, framework) +* System information (OS, architecture, CPU count, Python version) +* Runtime information (uptime, current time) +* Health check endpoint for monitoring + +--- + +## Prerequisites + +* Python **3.10+** (recommended: 3.12 or newer) +* pip (Python package manager) +* Virtual environment support (`venv`) + +To check the Python version: + +```bash +python3 --version +``` + +--- + +## Installation + +Clone the repository and navigate to the project directory: + +```bash +cd app_python +``` + +Create and activate a virtual environment: + +```bash +python3 -m venv venv +source venv/bin/activate +``` + +Install dependencies: + +```bash +pip install -r requirements.txt +``` + +--- + +## Running the Application + +Run with default configuration: + +```bash +python3 app.py +``` + +Run with custom configuration: + +```bash +PORT=8080 python3 app.py +HOST=127.0.0.1 PORT=3000 python3 app.py +``` + +By default, the service will be available at: + +``` +http://localhost:5000 +``` + +--- + +## API Endpoints + +### GET / + +Returns service, system, runtime, and request information. + +Example: + +```bash +curl http://localhost:5000/ +``` + +### GET /health + +Health check endpoint for monitoring and readiness probes. + +Example: + +```bash +curl http://localhost:5000/health +``` + +--- + +## Configuration + +The application can be configured using environment variables. + +| Variable | Description | Default | +| -------- | ------------------- | ------- | +| HOST | Server bind address | 0.0.0.0 | +| PORT | Server port | 5000 | +| DEBUG | Enable debug mode | False | + +Example: + +```bash +DEBUG=true PORT=8080 python3 app.py +``` + +--- + +## Notes + +* This project follows Python best practices and PEP 8 style guidelines. +* Dependencies are pinned for reproducibility. +* The `/health` endpoint is suitable for Kubernetes liveness and readiness probes. + +## Docker + +The application can also be run as a Docker container. + +### Build the image locally + +```bash +docker build -t . +``` + +### Run the container + +```bash +docker run -p :5000 +``` + +### Pull from Docker Hub + +```bash +docker pull /: +docker run -p :5000 /: +``` + +These commands demonstrate the general usage pattern. Replace placeholders with your actual image name, Docker Hub username, port, and tag as needed. + diff --git a/app_python/__init__.py b/app_python/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app_python/app.py b/app_python/app.py new file mode 100644 index 0000000000..23e88139a3 --- /dev/null +++ b/app_python/app.py @@ -0,0 +1,126 @@ +""" +DevOps Info Service +Main application module +""" + +import os +import socket +import platform +import logging +from datetime import datetime, timezone +from flask import Flask, jsonify, request + +app = Flask(__name__) + +# Configuration +HOST = os.getenv("HOST", "0.0.0.0") +PORT = int(os.getenv("PORT", 5000)) +DEBUG = os.getenv("DEBUG", "False").lower() == "true" + +SERVICE_NAME = "devops-info-service" +SERVICE_VERSION = "1.0.0" +SERVICE_DESCRIPTION = "DevOps course info service" +FRAMEWORK = "Flask" + +# Logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +logger.info('Application starting...') + +# Application start time +start_time = datetime.now() + + +def get_uptime(): + delta = datetime.now() - start_time + seconds = int(delta.total_seconds()) + hours = seconds // 3600 + minutes = (seconds % 3600) // 60 + return { + 'seconds': seconds, + 'human': f"{hours} hours, {minutes} minutes" + } + + +@app.before_request +def log_request(): + logger.info(f"Request: {request.method} {request.path}") + + +# Routes + + +@app.route("/", methods=["GET"]) +def index(): + """Main endpoint - service and system information.""" + uptime = get_uptime() + + response = { + "service": { + "name": SERVICE_NAME, + "version": SERVICE_VERSION, + "description": SERVICE_DESCRIPTION, + "framework": FRAMEWORK + }, + "system": { + "hostname": socket.gethostname(), + "platform": platform.system(), + "platform_version": platform.version(), + "architecture": platform.machine(), + "cpu_count": os.cpu_count(), + "python_version": platform.python_version() + }, + "runtime": { + "uptime_seconds": uptime["seconds"], + "uptime_human": uptime["human"], + "current_time": datetime.now(timezone.utc).isoformat(), + "timezone": "UTC" + }, + "request": { + "client_ip": request.remote_addr, + "user_agent": request.headers.get('User-Agent'), + "method": request.method, + "path": request.path + }, + "endpoints": [ + {"path": "/", "method": "GET", + "description": "Service information"}, + {"path": "/health", "method": "GET", + "description": "Health check"} + ] + } + + return jsonify(response) + + +@app.route("/health", methods=["GET"]) +def health(): + return jsonify({ + "status": "healthy", + "timestamp": datetime.now(timezone.utc).isoformat(), + "uptime_seconds": get_uptime()["seconds"] + }) + + +@app.errorhandler(404) +def not_found(error): + return jsonify({ + 'error': 'Not Found', + 'message': 'Endpoint does not exist' + }), 404 + + +@app.errorhandler(500) +def internal_error(error): + return jsonify({ + 'error': 'Internal Server Error', + 'message': 'An unexpected error occurred' + }), 500 + + +if __name__ == "__main__": + app.run(host=HOST, port=PORT, debug=DEBUG) diff --git a/app_python/docs/LAB01.md b/app_python/docs/LAB01.md new file mode 100644 index 0000000000..caad90ef69 --- /dev/null +++ b/app_python/docs/LAB01.md @@ -0,0 +1,271 @@ +# LAB01 — DevOps Info Service + +## 1. Framework Selection + +### Selected Framework: **Flask** + +### Justification + +* Flask is lightweight and minimal, making it easy to understand and maintain. +* The framework provides full control over application behavior without hidden abstractions. +* Routing as in Ktor-framework which I have experience with + +The goal of this lab is to demonstrate DevOps principles rather than complex backend logic, therefore Flask is an optimal choice. + +### Framework Comparison + +| Framework | Pros | Cons | +| --------- | ---------------------------------- | --------------------------------- | +| Flask | Simple, lightweight, easy to learn | Limited built-in features | +| FastAPI | Async, auto-generated docs, modern | Higher complexity, async overhead | +| Django | Full-featured, ORM, admin panel | Heavyweight, steep learning curve | + +--- + +## 2. Best Practices Applied + +### 2.1 Clean Code Organization + +**Practices:** + +* Clear and descriptive function names +* Grouped imports according to PEP 8 +* Minimal and meaningful comments +* Constants defined at module level + +**Example:** + +```python +""" +DevOps Info Service +Main application module +""" + +import os +import socket +import platform +import logging +from datetime import datetime, timezone +from flask import Flask, jsonify, request +``` + +```python +def get_uptime(): + delta = datetime.now() - start_time + ... + +def log_request(): + logger.info(f"Request: {request.method} {request.path}") + +@app.route("/", methods=["GET"]) +def index(): + """Main endpoint - service and system information.""" + ... + +@app.route("/health", methods=["GET"]) +def health(): + ... +``` + +**Importance:** +Clean code improves readability, maintainability, and reduces onboarding time for new developers. + +--- + +### 2.2 Error Handling + +Custom error handlers were implemented for common HTTP errors. + +**Example:** + +```python +@app.errorhandler(404) +def not_found(error): + return jsonify({ + 'error': 'Not Found', + 'message': 'Endpoint does not exist' + }), 404 + + +@app.errorhandler(500) +def internal_error(error): + return jsonify({ + 'error': 'Internal Server Error', + 'message': 'An unexpected error occurred' + }), 500 +``` + +**Importance:** +Graceful error handling provides consistent API responses and improves client-side debugging. + +--- + +### 2.3 Logging + +Application logging is implemented using Python’s built-in `logging` module. Each incoming request is logged. + +**Example:** + +```python +# Logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +logger.info('Application starting...') + +@app.before_request +def log_request(): + logger.info(f"Request: {request.method} {request.path}") +``` + +**Importance:** +Logging is essential for monitoring, debugging, and observability in production environments. + +--- + +### 2.4 Environment-Based Configuration + +The application behavior can be configured using environment variables. + +**Example:** + +```python +HOST = os.getenv("HOST", "0.0.0.0") +PORT = int(os.getenv("PORT", 5000)) +DEBUG = os.getenv("DEBUG", "False").lower() == "true" +``` + +**Importance:** +Environment-based configuration is a core DevOps principle and allows seamless deployment across environments. + +--- + +## 3. API Documentation + +### GET / + +Returns service, system, runtime, and request information. + +**Request:** + +```bash +curl http://localhost:5000/ +``` + +**Response (excerpt):** + +```json +{ + "endpoints": [ + { + "description": "Service information", + "method": "GET", + "path": "/" + }, + { + "description": "Health check", + "method": "GET", + "path": "/health" + } + ], + "request": { + "client_ip": "127.0.0.1", + "method": "GET", + "path": "/", + "user_agent": "curl/7.81.0" + }, + "runtime": { + "current_time": "2026-01-28T15:40:35.714395+00:00", + "timezone": "UTC", + "uptime_human": "0 hours, 0 minutes", + "uptime_seconds": 4 + }, + "service": { + "description": "DevOps course info service", + "framework": "Flask", + "name": "devops-info-service", + "version": "1.0.0" + }, + "system": { + "architecture": "x86_64", + "cpu_count": 4, + "hostname": "californiawrld", + "platform": "Linux", + "platform_version": "#91~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Thu Nov 20 15:20:45 UTC 2", + "python_version": "3.10.12" + } +} +``` + +--- + +### GET /health + +Health check endpoint used for monitoring. + +**Request:** + +```bash +curl http://localhost:5000/health +``` + +**Response:** + +```json +{ + "status": "healthy", + "timestamp": "2026-01-28T15:43:11.421546+00:00", + "uptime_seconds": 160 +} +``` + +--- + +## Testing Commands + +Run the application: + +```bash +python3 app.py +``` + +Test endpoints: + +```bash +curl http://localhost:5000/ +curl http://localhost:5000/health +``` + +Run with custom configuration: + +```bash +PORT=8080 python app.py +``` + +--- + +## 4. Testing Evidence + +The following screenshots are provided in `docs/screenshots/`: + +* **01-main-endpoint.png** — main endpoint JSON response +* **02-health-check.png** — health check response +* **03-formatted-output.png** — pretty-printed JSON output + +--- + +## 5. Challenges & Solutions + +### Challenge 1: Request Context Errors + +**Problem:** Attempted to access request data in each endpoint directly. + +**Solution:** Moved request logging into a `before_request` handler provided by Flask. + +## 6. GitHub Community +### Discovery & Bookmarking: +1. Star count indicates project popularity and community trust. Starred repos appear in your GitHub profile, showing your interests +2. Learn from others' code and commits. See how experienced developers solve problems \ No newline at end of file diff --git a/app_python/docs/LAB02.md b/app_python/docs/LAB02.md new file mode 100644 index 0000000000..a7a3513704 --- /dev/null +++ b/app_python/docs/LAB02.md @@ -0,0 +1,227 @@ +# LAB02 — Dockerizing Python Application + +--- + +## Docker Best Practices Applied + +### 1. Non-root User + +**What was done:** +A dedicated non-root user (`appuser`) is created and used to run the application. + +**Why it matters:** +Running containers as root increases the impact of a potential security breach. Using a non-root user reduces the attack surface and aligns with container security best practices. + +**Dockerfile snippet:** + +```dockerfile +RUN useradd --create-home --shell /bin/bash appuser +USER appuser +``` + +--- + +### 2. Specific Base Image Version + +**What was done:** +The image uses a fixed Python version: + +```dockerfile +FROM python:3.12-slim +``` + +**Why it matters:** + +* Guarantees reproducible builds +* Avoids unexpected breaking changes +* `slim` images reduce size and attack surface compared to full images + +--- + +### 3. Layer Caching Optimization + +**What was done:** +Dependencies are installed before copying application code. + +**Why it matters:** +Docker caches layers. If only application code changes, dependencies are not reinstalled, which significantly speeds up rebuilds. + +**Dockerfile snippet:** + +```dockerfile +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY app.py ./ +``` + +--- + +### 4. Environment-based Configuration + +**What was done:** +The application configuration (host, port, debug mode) is controlled via environment variables. + +**Why it matters:** +This follows the 12-factor app methodology and allows the same image to run in different environments without modification. + +--- + +### 5. Logging to stdout/stderr + +**What was done:** +Python logging is configured to output logs to standard output. + +**Why it matters:** +Docker collects stdout/stderr logs automatically, enabling easy inspection with `docker logs` and integration with centralized logging systems. + +--- + +### 6. .dockerignore Usage + +**What was done:** +A `.dockerignore` file excludes unnecessary files from the build context (e.g. `__pycache__`, `.git`, virtual environments). + +**Why it matters:** + +* Faster build times +* Smaller build context +* Prevents accidental inclusion of sensitive or irrelevant files + +--- + +## Image Information & Decisions + +### Base Image Selection + +**Chosen image:** `python:3.12-slim` + +**Justification:** + +* Official and well-maintained +* Smaller than full images +* Better compatibility than Alpine for Python packages + +--- + +### Final Image Size + +The final image size (2407) is significantly smaller than a full Python image due to the use of the slim variant and removal of pip cache. +![img_9.png](screenshots/img_9.png) +**Assessment:** +The image size is appropriate for a simple API service and suitable for CI/CD pipelines. + +--- + +### Layer Structure Explanation + +1. Base Python image +2. Environment variables +3. Non-root user creation +4. Dependency installation +5. Application code copy +6. User switch and runtime command + +This structure maximizes cache reuse and security. + +--- + +## Build & Run Process + +### Build Image + +```bash +docker build -t app-python . +``` +![img_1.png](screenshots/img_1.png) +![img_2.png](screenshots/img_2.png) + +### Run Container + +```bash +docker run -p 5000:5000 app-python +``` +![img_3.png](screenshots/img_3.png) + +### Test Endpoints + +```bash +curl http://localhost:5000/ +curl http://localhost:5000/health +``` +![img_4.png](screenshots/img_4.png) + +Both endpoints returned valid JSON responses identical to the local execution. +![img_5.png](screenshots/img_5.png) +--- + +### Docker Hub + +* Image pushed to Docker Hub +* Repository is publicly accessible +* Image is tagged using the format: + +```text +sincere99/app-python:1.0.0 +``` +![img_11.png](screenshots/img_11.png) + +Docker Hub repositorty URL: +```text +https://hub.docker.com/repositories/sincere99 +``` +![img_12.png](screenshots/img_12.png) + +**Tagging strategy explanation:** +Using a clear repository name and semantic tags (1.0.0 for now) allows versioning and easy rollback in the future. + +--- + +## Technical Analysis + +### Why This Dockerfile Works Correctly + +* Flask listens on `0.0.0.0`, allowing external access +* The exposed port matches the application port +* Dependencies are installed in an isolated, reproducible manner + +--- + +### Effect of Changing Layer Order + +If application code were copied before dependency installation, any code change would invalidate the cache and force a full dependency reinstall, slowing down builds. + +--- + +### Security Considerations + +* Non-root execution +* Minimal base image +* No secrets baked into the image +* Debug mode disabled by default + +--- + +### Impact of .dockerignore + +Excluding unnecessary files reduces build time, image size, and the risk of leaking development artifacts into production images. + +--- + +## Challenges & Solutions + +### Challenge 1: Image Rebuilds Were Slow + +**Problem:** +Dependencies were reinstalled on every build. + +**Solution:** +Reordered Dockerfile layers to leverage Docker cache effectively. + +--- + +## What I Learned + +* How Docker layer caching works in practice +* Why running containers as non-root is critical +* How to structure Dockerfiles for security and performance +* How to document containerization decisions clearly and professionally diff --git a/app_python/docs/LAB03.md b/app_python/docs/LAB03.md new file mode 100644 index 0000000000..add0993234 --- /dev/null +++ b/app_python/docs/LAB03.md @@ -0,0 +1,288 @@ +# LAB 03 — CI/CD Automation with Testing & Docker + +## Overview + +In this lab, automated testing and a CI/CD pipeline were implemented for the containerized Python application developed in Labs 1–2. The objective was to ensure application reliability, enforce code quality, and automate Docker image publishing using GitHub Actions. + +--- + +# Task 1 — Unit Testing + +## Testing Framework Choice + +### Selected Framework: **pytest** + +### Alternatives Considered + +| Framework | Pros | Cons | +|------------|----------------------------------------------------|------------------------------| +| pytest | Clean syntax, fixtures, rich plugin ecosystem | External dependency required | +| unittest | Built into Python standard library | More verbose, less flexible | + +### Why pytest? + +- Concise and readable test syntax +- Powerful fixture system +- Excellent integration with Flask test client +- Widely adopted in modern Python projects +- Seamless integration with CI pipelines + +pytest provides better maintainability and scalability compared to `unittest`. + +--- + +## Test Structure Explanation + +Tests are located in:`app_python/tests/test_app.py` + + +### Structure Design + +- A reusable `client` fixture initializes the Flask test client. +- Each endpoint is tested independently. +- Both success cases and error scenarios are validated. +- Tests verify structure and data types instead of hardcoded dynamic values. + +### Covered Endpoints + +#### 1. GET / + +Validations: +- HTTP status code = 200 +- Required JSON keys exist: + - `service` + - `system` + - `runtime` + - `request` + - `endpoints` +- Service metadata structure validation +- Type checking (e.g., `cpu_count` is integer) + +#### 2. GET /health + +Validations: +- HTTP status code = 200 +- `status == "healthy"` +- `timestamp` field exists +- `uptime_seconds` is integer + +#### 3. 404 Error Handling + +Validations: +- HTTP status code = 404 +- JSON error message: + ```json + { "error": "Not Found" } + +### Design Decision + +Dynamic values (hostname, timestamp, uptime) are not tested for exact values, only for presence and type correctness. +This prevents flaky tests and ensures stability. + +### How to Run Tests Locally +From the project root: `pytest app_python` + +![img.png](screenshots/img.png) + +# Task 2 + +## 1. Objective + +The objective of **Task 2** was to containerize the application and prepare it for distribution and deployment using Docker. The task focused on: + +- Creating a production-ready Dockerfile +- Building a Docker image locally +- Running the application inside a container +- Publishing the image to Docker Hub +- Ensuring reproducibility and portability + +This task ensures that the application can be executed consistently across different environments without dependency conflicts. + +--- + +## 2. Dockerfile Design + +### 2.1 Base Image + +A lightweight base image was selected to reduce the final image size and improve security. +For example: + +- `python:3.11-slim` (for Python apps) +- `eclipse-temurin` (for Java apps) +- `node:alpine` (for Node.js apps) + +The goal was to: +- Minimize attack surface +- Reduce build time +- Decrease image size + +--- + +### 2.2 Working Directory + +A working directory was defined inside the container: + +```dockerfile +WORKDIR /app +``` +This ensures all application files and commands execute relative to /app. + +### 2.3 Copying Application Files + +Application source code and dependency files were copied into the container: + +``` +COPY requirements.txt . +COPY . . +``` +This allows Docker layer caching to optimize rebuild time when source files change. + +### 2.4 Installing Dependencies +Dependencies were installed inside the container: +``` +RUN pip install --no-cache-dir -r requirements.txt +``` + +Key considerations: + +- Use --no-cache-dir to reduce image size +- Install dependencies before copying full source (for better caching) + +### 2.5 Exposing Ports +The application port was exposed: +``` +EXPOSE 8080 +``` +This documents which port the container listens on. + +### 2.6 Application Startup +The container entrypoint or command was defined: +``` +CMD ["python", "app.py"] +``` +This ensures the application starts automatically when the container runs. + +## 3. Building the Docker Image +The Docker image was built locally using the following pattern: + +`` +docker build -t /: . +`` + +Example pattern: +`` +docker build -t sincere99/devops-info-service:1.0 . +`` + +### Tagging Strategy + +I chose Semantic Versioning (SemVer) for the application because it provides a service with explicit API versions and potential incompatible changes. SemVer allows for precise tracking of breaking changes (majors), new features (minors), and bug fixes (patches), making the release process predictable and transparent for users and CI/CD. + +Using explicit version tags is recommended for traceability and rollback. +### Link to successful run in Github Actions: +https://github.com/blazingSummerSun/DevOps-Core-Course/actions/runs/21957935482/job/63427173836 + +### Green checkmark: +![img_7.png](screenshots/img_7.png) +![img_8.png](screenshots/img_8.png) + +### Link to the Docker Hub Image +https://hub.docker.com/repository/docker/sincere99/devops-info-service/general + +### Workflow triggers +Workflow triggers according to these schema: +``` +on: + push: + branches: + - main + - lab03 + tags: + - 'v*.*.*' + pull_request: + branches: + - main +``` + +I added branch lab03 for testing purposes. + +# Task 3 +## Status Badge +A GitHub Actions status badge was added to: +![img_1.png](screenshots/lab03_img_1_1.png) + +## Cache implementation +``` +- name: Cache dev dependencies + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-dev-${{ hashFiles('app_python/requirements-dev.txt') }} + restore-keys: | + ${{ runner.os }}-pip-dev- +``` +1. First run: +![img_4.png](screenshots/lab03_img_4.png) +2. Second run +![img_5.png](screenshots/lab03_img_5.png) + +Difference in installing dependencies is 2 times less (10s before caching, 5s after). + +## CI Best Practices Applied +### 1. Dependency Caching +Reduces workflow time and improves efficiency. +Why it matters: +- Saves CI resources +- Reduces feedback loop time +- Improves developer productivity + +### 2. Fail Fast Strategy +Workflow stops on: +- Test failure +- High severity vulnerability detection + +Why it matters: +- Prevents broken or insecure builds +- Protects production environment + +### 3. Working Directory Isolation +Used: +working-directory: app_python +Why it matters: +- Clear project structure +- Avoids path errors +- Improves maintainability + +### 4. Secrets Management +Used GitHub Secrets for: +SNYK_TOKEN +Why it matters: +- No secrets in repository +- Prevents credential leaks +- Secure CI configuration + +### 5. Explicit Dependency File Targeting +Used: +`` +--file=app_python/requirements.txt +`` + +Why it matters: +- Ensures correct scan target +- Avoids CI ambiguity +- Improves reliability + +## Snyk Security Integration +Implementation +``` +- name: Run Snyk security scan + uses: snyk/actions/python@master + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + with: + args: --file=app_python/requirements.txt --severity-threshold=high --skip-unresolved +``` +### Results +![img_6.png](screenshots/img_6.png) + +### Improved workflow performance provided above in cache section. \ No newline at end of file diff --git a/app_python/docs/screenshots/01-main-endpoint.png b/app_python/docs/screenshots/01-main-endpoint.png new file mode 100644 index 0000000000..4f798ade90 Binary files /dev/null and b/app_python/docs/screenshots/01-main-endpoint.png differ diff --git a/app_python/docs/screenshots/02-health-check.png b/app_python/docs/screenshots/02-health-check.png new file mode 100644 index 0000000000..9cfa89936f Binary files /dev/null and b/app_python/docs/screenshots/02-health-check.png differ diff --git a/app_python/docs/screenshots/03-formatted-output.png b/app_python/docs/screenshots/03-formatted-output.png new file mode 100644 index 0000000000..a7ea185e8a Binary files /dev/null and b/app_python/docs/screenshots/03-formatted-output.png differ diff --git a/app_python/docs/screenshots/img.png b/app_python/docs/screenshots/img.png new file mode 100644 index 0000000000..a78623ce27 Binary files /dev/null and b/app_python/docs/screenshots/img.png differ diff --git a/app_python/docs/screenshots/img_1.png b/app_python/docs/screenshots/img_1.png new file mode 100644 index 0000000000..3824103e5e Binary files /dev/null and b/app_python/docs/screenshots/img_1.png differ diff --git a/app_python/docs/screenshots/img_11.png b/app_python/docs/screenshots/img_11.png new file mode 100644 index 0000000000..1efde59ffa Binary files /dev/null and b/app_python/docs/screenshots/img_11.png differ diff --git a/app_python/docs/screenshots/img_12.png b/app_python/docs/screenshots/img_12.png new file mode 100644 index 0000000000..a358a1ba8e Binary files /dev/null and b/app_python/docs/screenshots/img_12.png differ diff --git a/app_python/docs/screenshots/img_2.png b/app_python/docs/screenshots/img_2.png new file mode 100644 index 0000000000..9f5c0fa58c Binary files /dev/null and b/app_python/docs/screenshots/img_2.png differ diff --git a/app_python/docs/screenshots/img_3.png b/app_python/docs/screenshots/img_3.png new file mode 100644 index 0000000000..84ab3a3f5b Binary files /dev/null and b/app_python/docs/screenshots/img_3.png differ diff --git a/app_python/docs/screenshots/img_4.png b/app_python/docs/screenshots/img_4.png new file mode 100644 index 0000000000..269759c6d1 Binary files /dev/null and b/app_python/docs/screenshots/img_4.png differ diff --git a/app_python/docs/screenshots/img_5.png b/app_python/docs/screenshots/img_5.png new file mode 100644 index 0000000000..09461b4203 Binary files /dev/null and b/app_python/docs/screenshots/img_5.png differ diff --git a/app_python/docs/screenshots/img_6.png b/app_python/docs/screenshots/img_6.png new file mode 100644 index 0000000000..9143287ea0 Binary files /dev/null and b/app_python/docs/screenshots/img_6.png differ diff --git a/app_python/docs/screenshots/img_7.png b/app_python/docs/screenshots/img_7.png new file mode 100644 index 0000000000..ce2c206f46 Binary files /dev/null and b/app_python/docs/screenshots/img_7.png differ diff --git a/app_python/docs/screenshots/img_8.png b/app_python/docs/screenshots/img_8.png new file mode 100644 index 0000000000..e21aa07a71 Binary files /dev/null and b/app_python/docs/screenshots/img_8.png differ diff --git a/app_python/docs/screenshots/img_9.png b/app_python/docs/screenshots/img_9.png new file mode 100644 index 0000000000..1efde59ffa Binary files /dev/null and b/app_python/docs/screenshots/img_9.png differ diff --git a/app_python/docs/screenshots/lab03_img_1.png b/app_python/docs/screenshots/lab03_img_1.png new file mode 100644 index 0000000000..16122a113e Binary files /dev/null and b/app_python/docs/screenshots/lab03_img_1.png differ diff --git a/app_python/docs/screenshots/lab03_img_1_1.png b/app_python/docs/screenshots/lab03_img_1_1.png new file mode 100644 index 0000000000..c368709afe Binary files /dev/null and b/app_python/docs/screenshots/lab03_img_1_1.png differ diff --git a/app_python/docs/screenshots/lab03_img_2.png b/app_python/docs/screenshots/lab03_img_2.png new file mode 100644 index 0000000000..178919d06d Binary files /dev/null and b/app_python/docs/screenshots/lab03_img_2.png differ diff --git a/app_python/docs/screenshots/lab03_img_3.png b/app_python/docs/screenshots/lab03_img_3.png new file mode 100644 index 0000000000..e58646b321 Binary files /dev/null and b/app_python/docs/screenshots/lab03_img_3.png differ diff --git a/app_python/docs/screenshots/lab03_img_4.png b/app_python/docs/screenshots/lab03_img_4.png new file mode 100644 index 0000000000..2830c158aa Binary files /dev/null and b/app_python/docs/screenshots/lab03_img_4.png differ diff --git a/app_python/docs/screenshots/lab03_img_5.png b/app_python/docs/screenshots/lab03_img_5.png new file mode 100644 index 0000000000..2f449480aa Binary files /dev/null and b/app_python/docs/screenshots/lab03_img_5.png differ diff --git a/app_python/docs/screenshots/lab03img.png b/app_python/docs/screenshots/lab03img.png new file mode 100644 index 0000000000..28113d1ac4 Binary files /dev/null and b/app_python/docs/screenshots/lab03img.png differ diff --git a/app_python/requirements-dev.txt b/app_python/requirements-dev.txt new file mode 100644 index 0000000000..e7d9c6080f --- /dev/null +++ b/app_python/requirements-dev.txt @@ -0,0 +1,2 @@ +pytest==8.2.2 +pytest-cov==5.0.0 \ No newline at end of file diff --git a/app_python/requirements.txt b/app_python/requirements.txt new file mode 100644 index 0000000000..51c32f3429 --- /dev/null +++ b/app_python/requirements.txt @@ -0,0 +1 @@ +Flask==3.1.2 \ No newline at end of file diff --git a/app_python/tests/__init__.py b/app_python/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app_python/tests/test_app.py b/app_python/tests/test_app.py new file mode 100644 index 0000000000..eba5c2ddaf --- /dev/null +++ b/app_python/tests/test_app.py @@ -0,0 +1,59 @@ +import pytest + +from app_python.app import app + + +@pytest.fixture +def client(): + """Create test client.""" + app.config["TESTING"] = True + with app.test_client() as client: + yield client + + +def test_index_endpoint_structure(client): + """Test main endpoint returns correct structure.""" + response = client.get("/") + + assert response.status_code == 200 + + data = response.get_json() + + # Top-level keys + assert "service" in data + assert "system" in data + assert "runtime" in data + assert "request" in data + assert "endpoints" in data + + # Service structure + assert data["service"]["name"] == "devops-info-service" + assert "version" in data["service"] + assert "framework" in data["service"] + + # System fields exist (not exact values!) + assert "hostname" in data["system"] + assert isinstance(data["system"]["cpu_count"], int) + + +def test_health_endpoint(client): + """Test health endpoint returns healthy status.""" + response = client.get("/health") + + assert response.status_code == 200 + + data = response.get_json() + + assert data["status"] == "healthy" + assert "timestamp" in data + assert isinstance(data["uptime_seconds"], int) + + +def test_404_error(client): + """Test unknown endpoint returns 404 JSON.""" + response = client.get("/unknown") + + assert response.status_code == 404 + + data = response.get_json() + assert data["error"] == "Not Found"