Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions .github/workflows/python-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
name: Python CI and Docker Release

on:
push:
branches:
- main
- lab3
tags:
- "v*.*.*"
pull_request:
branches:
- main
workflow_dispatch:

permissions:
contents: read

concurrency:
group: python-ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
name: Lint and Test
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: true
matrix:
python-version: ["3.11", "3.12"]
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Python
id: setup-python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip
cache-dependency-path: app_python/requirements.txt

- name: Install dependencies
id: deps
run: |
START=$(date +%s)
python -m pip install --upgrade pip
pip install -r app_python/requirements.txt
pip install ruff
END=$(date +%s)
echo "install_seconds=$((END-START))" >> "$GITHUB_OUTPUT"

- name: Lint (ruff)
run: ruff check app_python

- name: Run unit tests
run: python -m unittest discover -s app_python/tests -v

- name: Dependency cache report
if: always()
run: |
echo "### Dependency install metrics (Python ${{ matrix.python-version }})" >> "$GITHUB_STEP_SUMMARY"
echo "- cache-hit: \`${{ steps.setup-python.outputs.cache-hit }}\`" >> "$GITHUB_STEP_SUMMARY"
echo "- install-seconds: \`${{ steps.deps.outputs.install_seconds }}\`" >> "$GITHUB_STEP_SUMMARY"

security:
name: Snyk Dependency Scan
runs-on: ubuntu-latest
needs: test
timeout-minutes: 15
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: pip
cache-dependency-path: app_python/requirements.txt

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r app_python/requirements.txt

- name: Set up Snyk CLI
if: ${{ env.SNYK_TOKEN != '' }}
uses: snyk/actions/setup@master

- name: Run Snyk scan (high and critical)
if: ${{ env.SNYK_TOKEN != '' }}
continue-on-error: true
env:
SNYK_TOKEN: ${{ env.SNYK_TOKEN }}
run: snyk test \
--org=sofiakulagina \
--file=app_python/requirements.txt \
--severity-threshold=high


- name: Snyk token reminder
if: ${{ env.SNYK_TOKEN == '' }}
run: echo "SNYK_TOKEN secret is not configured; Snyk scan skipped."

docker:
name: Build and Push Docker Image
runs-on: ubuntu-latest
needs: [test, security]
if: startsWith(github.ref, 'refs/tags/v')
timeout-minutes: 20
env:
IMAGE_NAME: ${{ secrets.DOCKERHUB_USERNAME }}/devops-info-service
steps:
- name: Checkout
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: Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.IMAGE_NAME }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest

- name: Build and push
uses: docker/build-push-action@v6
with:
context: app_python
file: app_python/Dockerfile
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Master **production-grade DevOps practices** through hands-on labs. Build, conta
| 16 | 16 | Cluster Monitoring | Kube-Prometheus, Init Containers |
| — | **Exam Alternative Labs** | | |
| 17 | 17 | Edge Deployment | Fly.io, Global Distribution |
| 18 | 18 | Decentralized Storage | 4EVERLAND, IPFS, Web3 |
| 18 | 18 | Reproducible Builds | Nix, Deterministic Builds, Flakes |

---

Expand All @@ -61,7 +61,7 @@ Don't want to take the exam? Complete **both** bonus labs:
| Lab | Topic | Points |
|-----|-------|--------|
| **Lab 17** | Fly.io Edge Deployment | 20 pts |
| **Lab 18** | 4EVERLAND & IPFS | 20 pts |
| **Lab 18** | Reproducible Builds with Nix | 20 pts |

**Requirements:**
- Complete both labs (17 + 18 = 40 pts, replaces exam)
Expand Down Expand Up @@ -142,7 +142,7 @@ Each lab is worth **10 points** (main tasks) + **2.5 points** (bonus).
- StatefulSets, Monitoring

**Exam Alternative (Labs 17-18)**
- Fly.io, 4EVERLAND/IPFS
- Fly.io, Nix Reproducible Builds

</details>

Expand Down
193 changes: 191 additions & 2 deletions app_python/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# DevOps Info Service (Lab 1)

[![Python CI and Docker Release](https://github.com/sofiakulagina/DevOps-Core-Course/actions/workflows/python-ci.yml/badge.svg?branch=main)](https://github.com/sofiakulagina/DevOps-Core-Course/actions/workflows/python-ci.yml)

## Overview

This project implements a simple **DevOps info service** written in Python using **Flask**. The service exposes HTTP endpoints that return detailed information about the application, the underlying system, and its runtime health. It is the base for later labs (Docker, CI/CD, monitoring, persistence, etc.).
Expand All @@ -14,9 +16,9 @@ This project implements a simple **DevOps info service** written in Python using

```bash
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
source venv/bin/activate
pip install -r requirements.txt
cp .env_example .env # create local env file from example
cp .env_example .env
```

## Running the Application
Expand Down Expand Up @@ -54,6 +56,57 @@ Configuration is done via environment variables:

All configuration is read in `app.py` at startup, so restart the application after changing environment variables.

## Unit Testing

### Framework Choice

For this lab, the project uses Python `unittest`.

Short comparison:
- `pytest`: concise syntax and rich plugin ecosystem, but adds an external dependency.
- `unittest`: part of the Python standard library, no additional package required.

Why `unittest` was chosen:
- Works out of the box in minimal lab environments.
- Keeps dependencies small and predictable.
- Supports fixtures (`setUpClass`) and mocking (`unittest.mock`) needed for endpoint testing.

### Test Structure

Tests are located in `tests/test_app.py` and cover:
- `GET /` success response:
- expected top-level JSON fields,
- required nested fields and data types,
- request metadata (client IP and user-agent handling).
- `GET /health` success response:
- status, timestamp, uptime checks.
- Error responses:
- `404` JSON error for unknown route,
- simulated internal failures for `/` and `/health` returning JSON `500`.

### Run Tests Locally

```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
python -m unittest discover -s tests -v
```

Optional coverage (standard library):

```bash
python -m trace --count --summary -m unittest discover -s tests -v
```

### Example Passing Output

```text
Ran 6 tests in 0.018s

OK
```

## Docker

How to use the containerized application (patterns):
Expand All @@ -67,3 +120,139 @@ Notes:
- The container exposes port `5002` by default (see `app.py`).
- The image runs as a non-root user for improved security.

## CI Workflow (GitHub Actions)

### Workflow Overview

Workflow file: `.github/workflows/python-ci.yml`

It runs on:
- `push` to `main` and `lab3`, and `pull_request` into `main` for lint + tests.
- `push` of SemVer git tags (`vX.Y.Z`) for Docker build and push.
- manual run via `workflow_dispatch`.

### Versioning Strategy

Chosen strategy: **Semantic Versioning (SemVer)**.

Why SemVer:
- Clear signal for breaking vs backward-compatible changes.
- Common convention for releases and container tags.

Docker tags produced on `vX.Y.Z`:
- `X.Y.Z` (full version)
- `X.Y` (rolling minor)
- `latest`

Example:
- `username/devops-info-service:1.2.3`
- `username/devops-info-service:1.2`
- `username/devops-info-service:latest`

### Secrets Required

Add these GitHub repository secrets:
- `DOCKERHUB_USERNAME`
- `DOCKERHUB_TOKEN` (Docker Hub access token)

### Release Flow

```bash
git tag v1.0.0
git push origin v1.0.0
```

The Docker job runs only on SemVer tags and pushes images with the tags above.

## CI Best Practices and Security (Task 3)

### Status Badge

The README includes a GitHub Actions badge for `.github/workflows/python-ci.yml` showing pass/fail status for `main`.

Badge and workflow link:
- Badge: `https://github.com/sofiakulagina/DevOps-Core-Course/actions/workflows/python-ci.yml/badge.svg?branch=main`
- Workflow runs: `https://github.com/sofiakulagina/DevOps-Core-Course/actions/workflows/python-ci.yml`

### Dependency Caching

Implemented in workflow via `actions/setup-python@v5`:
- `cache: pip`
- `cache-dependency-path: app_python/requirements.txt`

The workflow also writes install metrics into the Job Summary for each Python version:
- `cache-hit` (`true` or `false`)
- `install-seconds` (dependency installation time)

Measured baseline from workflow summary:
- Python 3.11: `cache-hit=false`, `install-seconds=5`
- Python 3.12: `cache-hit=false`, `install-seconds=3`

How speed improvement is measured:
1. Run workflow once after dependency change (cache miss baseline).
2. Run workflow again without changing `app_python/requirements.txt` (expected cache hit).
3. Compare `install-seconds` from Job Summary:
`improvement_percent = ((miss_seconds - hit_seconds) / miss_seconds) * 100`

Current status:
- Baseline (miss) is recorded.
- Next run is needed to capture hit values and final percentage.

Metrics screenshot:
- Link: `docs/screenshots/metrics_lab3.png`

![Dependency metrics screenshot](docs/screenshots/metrics_lab3.png)

### Snyk Security Scanning

Integrated with `snyk/actions/setup@master` and `snyk test` CLI command in a dedicated `security` job.

Configuration:
- Secret required: `SNYK_TOKEN`
- Scan target: `app_python/requirements.txt`
- Threshold: `high` (`--severity-threshold=high`)
- Mode: non-blocking (`continue-on-error: true`) to keep visibility without blocking delivery during lab work.

If `SNYK_TOKEN` is missing, workflow prints a clear skip message.

Security results documentation:
- Latest scan status: `Succeeded`
- Scan output: `Tested 7 dependencies for known issues, no vulnerable paths found.`
- Vulnerability count: `0` (for threshold `high`)
- Vulnerability handling policy: upgrade direct dependencies first; if no fix exists, track risk in lab notes and keep non-blocking scan mode.

Snyk screenshot:
- Link: `docs/screenshots/snyk_lab3.png`

![Snyk scan screenshot](docs/screenshots/snyk_lab3.png)

How to get `SNYK_TOKEN`:
1. Open `https://app.snyk.io`
2. Go to `Account Settings` -> `API Token`
3. Copy token and add GitHub secret:
`Repository Settings` -> `Secrets and variables` -> `Actions` -> `New repository secret`
4. Secret name must be `SNYK_TOKEN`

### Additional CI Best Practices Applied

Implemented practices:
- **Concurrency control:** cancels outdated runs for same ref (`cancel-in-progress: true`).
- **Least-privilege permissions:** workflow-level `permissions: contents: read`.
- **Matrix testing:** tests run on Python `3.11` and `3.12`.
- **Fail-fast matrix:** stops quickly when one matrix leg fails.
- **Job dependencies:** Docker job requires successful `test` and `security` jobs.
- **Docker layer cache:** `cache-from/cache-to type=gha` for faster image builds.
- **Manual trigger:** `workflow_dispatch` for controlled reruns.
- **Timeouts:** explicit `timeout-minutes` per job to avoid stuck pipelines.

### Docker Build Evidence

From `Build and Push Docker Image` summary:
- Build status: `completed`
- Build duration: `17s`
- Docker build cache usage in that run: `0%`

Final CI/CD execution screenshot:
- Link: `docs/screenshots/artifacts_lab3.png`

![Final CI/CD screenshot](docs/screenshots/artifacts_lab3.png)
Binary file added app_python/docs/screenshots/artifacts_lab3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app_python/docs/screenshots/metrics_lab3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app_python/docs/screenshots/snyk_lab3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading