Skip to content

Commit aebfad0

Browse files
authored
Merge pull request #3 from flowelx/lab03
Lab03
2 parents 91fdcf4 + 44c49ea commit aebfad0

9 files changed

Lines changed: 291 additions & 6 deletions

File tree

.github/workflows/python-ci.yml

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
name: Python CI/CD
2+
3+
on:
4+
push:
5+
branches: [ main, lab03 ]
6+
paths:
7+
- 'app_python/**'
8+
- '.github/workflows/python-ci.yml'
9+
pull_request:
10+
branches: [ main ]
11+
paths:
12+
- 'app_python/**'
13+
- '.github/workflows/python-ci.yml'
14+
15+
jobs:
16+
test:
17+
runs-on: ubuntu-latest
18+
19+
steps:
20+
- name: Checkout code
21+
uses: actions/checkout@v4
22+
23+
- name: Set up Python
24+
uses: actions/setup-python@v5
25+
with:
26+
python-version: '3.12'
27+
cache: 'pip'
28+
cache-dependency-path: 'app_python/requirements.txt'
29+
30+
- name: Install dependencies
31+
run: |
32+
cd app_python
33+
pip install -r requirements.txt
34+
35+
- name: Run linter
36+
run: |
37+
cd app_python
38+
flake8 app.py
39+
40+
- name: Run tests
41+
run: |
42+
cd app_python
43+
pytest tests/test_app.py
44+
45+
- name: Security scan with pip-audit
46+
run: |
47+
cd app_python
48+
pip install pip-audit
49+
pip-audit -r requirements.txt || echo "Security scan completed"
50+
51+
build:
52+
runs-on: ubuntu-latest
53+
needs: test
54+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
55+
56+
steps:
57+
- name: Checkout code
58+
uses: actions/checkout@v4
59+
60+
- name: Login to Docker Hub
61+
uses: docker/login-action@v3
62+
with:
63+
username: ${{ secrets.DOCKER_USERNAME }}
64+
password: ${{ secrets.DOCKER_TOKEN }}
65+
66+
- name: Build and push Docker image
67+
uses: docker/build-push-action@v5
68+
with:
69+
context: ./app_python
70+
push: true
71+
tags: |
72+
${{ secrets.DOCKER_USERNAME }}/fastapi-lab-app:latest
73+
${{ secrets.DOCKER_USERNAME }}/fastapi-lab-app:$(date +%Y.%m.%d)

app_python/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ __pycache__/
33
*.py[cod]
44
venv/
55
*.log
6+
.pytest_cache
67

78
# IDE
89
.vscode/

app_python/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# DevOps Info Service (Python / FastAPI)
22

3+
[![Python CI/CD](https://github.com/flowelx/DevOps-Core-Course/actions/workflows/python-ci.yml/badge.svg)](https://github.com/flowelx/DevOps-Core-Course/actions/workflows/python-ci.yml)
4+
35
## Overview
46

57
This FastAPI application delivers runtime and system data through HTTP endpoints. Built as a modular platform for DevOps education, it enables practical exploration of containerization, CI/CD pipelines, monitoring solutions, and infrastructure automation concepts.

app_python/app.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,14 @@ def format_datetime_iso(dt: datetime):
5050
async def get_service_info(request: Request):
5151
"""
5252
Root endpoint returning comprehensive service and system information.
53-
53+
5454
Returns:
55-
dict: JSON containing service, system, runtime, and request information.
55+
dict: JSON with service, system, runtime, and request information.
5656
"""
5757
logger.info(
5858
f"GET / from {request.client.host if request.client else 'unknown'}"
5959
)
60-
60+
6161
service_info = {
6262
'name': 'devops-info-request',
6363
'version': '1.0.0',
@@ -110,7 +110,7 @@ async def get_service_info(request: Request):
110110
async def health_check(request: Request):
111111
"""
112112
Health check endpoint for service monitoring.
113-
113+
114114
Returns:
115115
dict: Service health status with timestamp and uptime.
116116
"""
@@ -157,4 +157,4 @@ async def internal_error_handler(request: Request, exc: Exception):
157157
host=HOST,
158158
port=PORT,
159159
reload=DEBUG
160-
)
160+
)

app_python/docs/LAB03.md

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
# Lab 3 — Continuous Integration (CI/CD)
2+
3+
## 1. Unit Testing
4+
5+
### Framework chosen
6+
7+
I chose `pytest` because of using plain `assert` statement instead of complex assertion methods. The framework has clear output with `-v` flag showing exactly what passed/failed. `pytest` is well-documented with many tutorials and examples.
8+
9+
### Test Structure
10+
11+
**Test Coverage:**
12+
13+
1. `test_root_endpoint()` - Tests `GET /` endpoint
14+
15+
2. `test_health_endpoint()` - Tests `GET /health` endpoint
16+
17+
3. `test_404_error` - Tests error handling
18+
19+
Each test is independent. Tests use FastAPI's `TestClient` (no live server needed).
20+
21+
### How to Run Tests Locally
22+
23+
```bash
24+
cd app_python
25+
pip install -r requirements.txt
26+
pytest tests/test_app.py -v
27+
```
28+
29+
### Terminal Output Showing All Tests Passing
30+
31+
```bash
32+
=================================================================== test session starts ====================================================================
33+
platform linux -- Python 3.14.2, pytest-8.0.0, pluggy-1.6.0
34+
rootdir: /home/flowelx/DevOps-Core-Course/app_python
35+
plugins: anyio-4.12.1
36+
collected 3 items
37+
38+
tests/test_app.py ... [100%]
39+
40+
===================================================================== warnings summary =====================================================================
41+
venv/lib/python3.14/site-packages/starlette/_utils.py:40
42+
venv/lib/python3.14/site-packages/starlette/_utils.py:40
43+
venv/lib/python3.14/site-packages/starlette/_utils.py:40
44+
venv/lib/python3.14/site-packages/starlette/_utils.py:40
45+
venv/lib/python3.14/site-packages/starlette/_utils.py:40
46+
venv/lib/python3.14/site-packages/starlette/_utils.py:40
47+
venv/lib/python3.14/site-packages/starlette/_utils.py:40
48+
venv/lib/python3.14/site-packages/starlette/_utils.py:40
49+
tests/test_app.py::test_404_error
50+
/home/flowelx/DevOps-Core-Course/app_python/venv/lib/python3.14/site-packages/starlette/_utils.py:40: DeprecationWarning: 'asyncio.iscoroutinefunction' is deprecated and slated for removal in Python 3.16; use inspect.iscoroutinefunction() instead
51+
return asyncio.iscoroutinefunction(obj) or (callable(obj) and asyncio.iscoroutinefunction(obj.__call__))
52+
53+
venv/lib/python3.14/site-packages/fastapi/routing.py:233
54+
venv/lib/python3.14/site-packages/fastapi/routing.py:233
55+
/home/flowelx/DevOps-Core-Course/app_python/venv/lib/python3.14/site-packages/fastapi/routing.py:233: DeprecationWarning: 'asyncio.iscoroutinefunction' is deprecated and slated for removal in Python 3.16; use inspect.iscoroutinefunction() instead
56+
is_coroutine = asyncio.iscoroutinefunction(dependant.call)
57+
58+
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
59+
============================================================== 3 passed, 11 warnings in 0.27s ==============================================================
60+
```
61+
62+
## 2. GitHub Actions CI Workflow
63+
64+
### Workflow Trigger Strategy
65+
66+
**Configuration:**
67+
68+
```yaml
69+
on:
70+
push:
71+
branches: [ main, lab03 ]
72+
pull_request:
73+
branches: [ main ]
74+
```
75+
76+
CI runs on feature branch and main. It saves GitHub Actions minutes, focused on importnant branches.
77+
Docker build only runs on push to `main`. This prevents unnecessary Docker builds for every commit.
78+
79+
### Marketplace Actions Chosen
80+
81+
1. `actions/checkout@v4` - Official GitHub action, reliable, well-maintained
82+
83+
2. `actions/setup-python@v5` - Handles multiple Python versions, caching built-in
84+
85+
3. `docker/login-action@v3` - Secure token-based login, handles credentials properly
86+
87+
4. `docker/build-push-action@v5` - Single action for both operations, supports caching
88+
89+
### Docker Tagging Strategy
90+
91+
**Strategy:** Calendar Versioning
92+
93+
**Format:** `YYYY.NN.DD`
94+
95+
It is convinient for frequent updates. There is no need to track breaking changes.
96+
97+
### Successful Workflow Run
98+
99+
**Link to Workflow Run:** https://github.com/flowelx/DevOps-Core-Course/actions/runs/21786077651/job/62857660802
100+
101+
**Screenshot of Green Checkmark:**
102+
103+
![successfull ci](screenshots/successful-ci.jpg)
104+
105+
## CI Best Practices & Security
106+
107+
### Status Badge in README
108+
109+
![status badge](screenshots/status-badge.jpg)
110+
111+
### Caching Implementation
112+
113+
**Python Package Caching:**
114+
115+
```yaml
116+
- uses: actions/setup-python@v5
117+
with:
118+
cache: 'pip'
119+
cache-dependency-path: 'app_python/requirements.txt'
120+
```
121+
122+
### CI Best Practices Applied
123+
124+
1. Path-based Triggers
125+
126+
```yaml
127+
paths:
128+
- 'app_python/**'
129+
- '.github/workflows/python-ci.yml'
130+
```
131+
132+
2. **Job Dependencies**
133+
134+
```yaml
135+
build:
136+
needs: test
137+
```
138+
139+
3. **Conditional Execution**
140+
141+
```yaml
142+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
143+
```
144+
145+
4. **Security Scanning**
146+
147+
```yaml
148+
- name: Security scan with pip-audit
149+
run: |
150+
cd app_python
151+
pip install pip-audit
152+
pip-audit -r requirements.txt || echo "Security scan completed"
153+
```
154+
155+
5. **Linting**
156+
157+
```yaml
158+
- name: Run linter
159+
run: |
160+
cd app_python
161+
flake8 app.py
162+
```
163+
164+
6. **Test Reporting**
165+
166+
```yaml
167+
pytest tests/test_app.py
168+
```
169+
170+
### Security Scanning Results
171+
172+
**Tool Used:** `pip-audit`
173+
174+
I couldn't use Snyk because the site did not open with or without vpn. So I applied `pip-audit`.
175+
176+
**Scan Results:**
177+
178+
```
179+
Found 2 known vulnerabilities in 1 package
180+
Name Version ID Fix Versions
181+
--------- ------- -------------- ------------
182+
starlette 0.38.6 CVE-2024-47874 0.40.0
183+
starlette 0.38.6 CVE-2025-54121 0.47.2
184+
```
14.5 KB
Loading
30.8 KB
Loading

app_python/requirements.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
fastapi==0.115.0
2-
uvicorn[standard]==0.32.0
2+
uvicorn[standard]==0.32.0
3+
pytest==8.0.0
4+
httpx
5+
flake8

app_python/tests/test_app.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from fastapi.testclient import TestClient
2+
from app import app
3+
4+
client = TestClient(app)
5+
6+
def test_root_endpoint():
7+
response = client.get("/")
8+
assert response.status_code == 200
9+
10+
data = response.json()
11+
assert "service" in data
12+
assert data["service"]["name"] == "devops-info-request"
13+
14+
def test_health_endpoint():
15+
response = client.get("/health")
16+
assert response.status_code == 200
17+
data = response.json()
18+
assert data["status"] == "healthy"
19+
20+
def test_404_error():
21+
response = client.get("/not-exists")
22+
assert response.status_code == 404

0 commit comments

Comments
 (0)