From 4d6d2f8f3f11a7df2c5c909f7dc74fb5576022ab Mon Sep 17 00:00:00 2001 From: Bulat Gazizov Date: Wed, 11 Feb 2026 00:09:14 +0300 Subject: [PATCH 1/4] lab03 submission --- .github/workflows/python-ci.yml | 78 ++++++++++++++++++++++++++ .gitignore | 3 +- README.md | 2 + app_python/app.py | 2 - app_python/requirements.txt | 8 +++ app_python/tests/test_main.py | 99 +++++++++++++++++++++++++++++++++ 6 files changed, 189 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/python-ci.yml create mode 100644 app_python/tests/test_main.py diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml new file mode 100644 index 0000000000..eff2fe1edf --- /dev/null +++ b/.github/workflows/python-ci.yml @@ -0,0 +1,78 @@ +name: Python CI + +on: [push] + +jobs: + build: + + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./app_python + + steps: + - uses: actions/checkout@v5 + - name: Set up Python + uses: actions/setup-python@v5 + with: + # cache: 'pip' + python-version: '3.12' + architecture: 'x64' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: Lint flake8 + run: | + flake8 app.py tests --count --max-complexity=15 --max-line-length=127 --statistics + - name: Testing with pytest + run: | + pytest tests/ --doctest-modules --junitxml=junit/test-results.xml + + push_to_docker: + + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./app_python + + steps: + - name: Check out the repo + uses: actions/checkout@v5 + + - name: Log in to Docker Hub + uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: bulatgazizov/python_app + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }} + + - name: Build and push Docker image + id: push + uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671 + with: + context: ./app_python + file: ./app_python/Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + security: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: Run Snyk to check for vulnerabilities + uses: snyk/actions/python@master + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} diff --git a/.gitignore b/.gitignore index 30d74d2584..5a92b74c66 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -test \ No newline at end of file +test +.secrets diff --git a/README.md b/README.md index 371d51f456..9915cb0adb 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # DevOps Engineering: Core Practices +![Workflow](https://github.com/BulatGazizov-dev/DevOps-Core-Course/actions/workflows/python-ci.yml/badge.svg) + [![Labs](https://img.shields.io/badge/Labs-18-blue)](#labs) [![Exam](https://img.shields.io/badge/Exam-Optional-green)](#exam-alternative) [![Duration](https://img.shields.io/badge/Duration-18%20Weeks-lightgrey)](#course-roadmap) diff --git a/app_python/app.py b/app_python/app.py index 8d6eb47e22..532bd6786a 100644 --- a/app_python/app.py +++ b/app_python/app.py @@ -1,5 +1,3 @@ -from typing import Union - from fastapi import FastAPI, Request from fastapi.responses import JSONResponse import os diff --git a/app_python/requirements.txt b/app_python/requirements.txt index d8e68f2cd9..ff8283619c 100644 --- a/app_python/requirements.txt +++ b/app_python/requirements.txt @@ -40,3 +40,11 @@ uvicorn==0.40.0 uvloop==0.22.1 watchfiles==1.1.1 websockets==16.0 +iniconfig==2.3.0 +pluggy==1.6.0 +pytest==9.0.2 +jsonschema==4.23.0 +flake8==7.3.0 +mccabe==0.7.0 +pycodestyle==2.14.0 +pyflakes==3.4.0 \ No newline at end of file diff --git a/app_python/tests/test_main.py b/app_python/tests/test_main.py new file mode 100644 index 0000000000..f1f3071658 --- /dev/null +++ b/app_python/tests/test_main.py @@ -0,0 +1,99 @@ +import time +from fastapi.testclient import TestClient +from jsonschema import validate +from app import app + +client = TestClient(app) + + +def test_read_health(): + response = client.get("/health") + schema = { + "type": "object", + "properties": { + "status": {"type": "string"}, + "timestamp": {"type": "string"}, + "uptime_seconds": {"type": "integer"}, + }, + "required": ["status", "timestamp", "uptime_seconds"] + } + assert response.status_code == 200 + + validate(instance=response.json(), schema=schema) + + +def test_read_root(): + response = client.get("/") + + schema = { + "type": "object", + "properties": { + "service": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "version": {"type": "string"}, + "description": {"type": "string"}, + "framework": {"type": "string"} + }, + "required": ["name", "version", "description", "framework"] + }, + "system": { + "type": "object", + "properties": { + "hostname": {"type": "string"}, + "platform": {"type": "string"}, + "platform_version": {"type": "string"}, + "architecture": {"type": "string"}, + "cpu_count": {"type": "integer"}, + "python_version": {"type": "string"} + }, + "required": [ + "hostname", "platform", "platform_version", + "architecture", "cpu_count", "python_version" + ] + }, + "runtime": { + "type": "object", + "properties": { + "uptime_seconds": {"type": "integer"}, + "uptime_human": {"type": "string"}, + "current_time": {"type": "string"}, + "timezone": {"type": "string"} + }, + "required": [ + "uptime_seconds", "uptime_human", + "current_time", "timezone" + ] + }, + "request": { + "type": "object", + "properties": { + "client_ip": {"type": "string"}, + "user_agent": {"type": "string"}, + "method": {"type": "string"}, + "path": {"type": "string"} + }, + "required": ["client_ip", "user_agent", "method", "path"] + }, + "endpoints": {"type": "array"} + }, + "required": ["service", "system", "runtime", "request", "endpoints"] + } + + assert response.status_code == 200 + + validate(instance=response.json(), schema=schema) + + +def test_404_handler(): + response = client.get("/this-route-does-not-exist") + assert response.status_code == 404 + assert response.json()["error"] == "Not Found" + + +def test_uptime_increments(): + res1 = client.get("/health").json()["uptime_seconds"] + time.sleep(1) + res2 = client.get("/health").json()["uptime_seconds"] + assert res2 > res1 From b1f25a90acf34bf1ed9fd4b832281a464af42f63 Mon Sep 17 00:00:00 2001 From: Bulat Gazizov Date: Wed, 11 Feb 2026 00:48:45 +0300 Subject: [PATCH 2/4] fix snyk workflow --- .github/workflows/python-ci.yml | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index eff2fe1edf..ed812866f0 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -30,8 +30,9 @@ jobs: pytest tests/ --doctest-modules --junitxml=junit/test-results.xml push_to_docker: - + needs: build runs-on: ubuntu-latest + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') defaults: run: working-directory: ./app_python @@ -71,8 +72,19 @@ jobs: security: runs-on: ubuntu-latest steps: - - uses: actions/checkout@master - - name: Run Snyk to check for vulnerabilities - uses: snyk/actions/python@master - env: - SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + - name: Checkout code + uses: actions/checkout@v5 + - name: Install Snyk CLI + run: | + curl https://downloads.snyk.io/cli/stable/snyk-linux -o snyk-linux + curl https://downloads.snyk.io/cli/stable/snyk-linux.sha256 -o snyk.sha256 + sha256sum -c snyk.sha256 + chmod +x snyk-linux + sudo mv snyk-linux /usr/local/bin/snyk + - name: Run Snyk to test project dependencies + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + run: | + cd app_python + pip install -r requirements.txt --break-system-packages + snyk test \ No newline at end of file From 3278c0b2fd76bf5b51cb6ee60b3306166a458832 Mon Sep 17 00:00:00 2001 From: Bulat Gazizov Date: Wed, 11 Feb 2026 01:01:12 +0300 Subject: [PATCH 3/4] add caching --- .github/workflows/python-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index ed812866f0..035dd65d07 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -15,7 +15,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - # cache: 'pip' + cache: 'pip' python-version: '3.12' architecture: 'x64' - name: Install dependencies From cc0dca5abd01456908b2130ff1b6909254df9624 Mon Sep 17 00:00:00 2001 From: Bulat Gazizov Date: Wed, 11 Feb 2026 01:09:16 +0300 Subject: [PATCH 4/4] add report --- app_python/docs/LAB03.md | 76 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 app_python/docs/LAB03.md diff --git a/app_python/docs/LAB03.md b/app_python/docs/LAB03.md new file mode 100644 index 0000000000..9505d19ba0 --- /dev/null +++ b/app_python/docs/LAB03.md @@ -0,0 +1,76 @@ +# Lab 03 + +## 1. Overview + +### Testing Framework + +* **Framework Chosen:** pytest +* **Rationale:** pytest was chosen for its simple syntax, powerful fixtures, and native integration with many CI tools. +* **Coverage:** My tests cover the following: +* `GET /`: Validates JSON structure, status code 200, and required fields. +* `GET /health`: Verifies the health check response and uptime increments +* `GET /unknown`: Verifies 404 response + + +### CI Workflow Configuration + +* **Trigger Strategy:** The workflow is triggered on: +* `push` to the `main` branch. + + +* **Versioning Strategy:** SemVer +* **Rationale:** SemVer was chosen because it clearly communicates breaking changes to users of the Docker image. + +--- + +## 2. Workflow Evidence + +| Requirement | Evidence/Link | +| --- | --- | +| **Successful Workflow Run** | [Link to GitHub Actions Run](https://github.com/BulatGazizov-dev/DevOps-Core-Course/actions/runs/21883844379) | +| **Docker Hub Image** | [Link to Docker Hub Repository](https://hub.docker.com/r/bulatgazizov/python_app) | + +### Local Terminal Output + +```text +================================================================== test session starts =================================================================== +platform linux -- Python 3.13.9, pytest-9.0.2, pluggy-1.6.0 +rootdir: /home/bulatgazizov/Projects/DevOps-Core-Course/app_python +plugins: anyio-4.9.0 +collected 4 items + +tests/test_main.py .... [100%] + +=================================================================== 4 passed in 1.62s ==================================================================== +``` + +--- + +## 3. Best Practices Implemented + +* **Linting:** Integrated `flake8` to ensure code style consistency. +* **Caching:** Implemented `actions/setup-python` caching. +* **Snyk Security:** Integrated Snyk vulnerability scanning. >> Tested 55 dependencies for known issues, no vulnerable paths found. + +* **Docker Metadata:** Used `docker/metadata-action` for automated, multi-tag management. + +--- + +## 4. Key Decisions + +**Versioning Strategy:** +I chose SemVer because it clearly says whether an update contains bug fixes (patch), new features (minor), or breaking changes (major). + +**Docker Tags:** +My CI generates the following tags: + +* `latest`: Always points to the most recent build +* `vX.Y.Z`: Specific semantic versions based on Git tags. + +**Workflow Triggers:** +The workflow triggers on pushes to main or any other branch. This ensures all code is linted, tested, and scanned for vulnerabilities before integration. + +**Test coverage:** +* `GET /`: Validates JSON structure, status code 200, and required fields. +* `GET /health`: Verifies the health check response and uptime increments +* `GET /unknown`: Verifies 404 response