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
86 changes: 86 additions & 0 deletions .github/workflows/python-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
name: python-ci

on:
push:
branches:
- master
paths:
- "lab_solutions/lab1/app_python/**"
- ".github/workflows/python-ci.yml"
pull_request:
paths:
- "lab_solutions/lab1/app_python/**"
- ".github/workflows/python-ci.yml"

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

permissions:
contents: read

env:
APP_DIR: lab_solutions/lab1/app_python
IMAGE_NAME: ${{ secrets.DOCKERHUB_USERNAME }}/devops-info-service

jobs:
test:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v4

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

- name: Install dependencies
working-directory: ${{ env.APP_DIR }}
run: pip install -r requirements.txt -r requirements-dev.txt

- name: Lint
working-directory: ${{ env.APP_DIR }}
run: ruff check .

- name: Run tests with coverage
working-directory: ${{ env.APP_DIR }}
run: pytest --cov=app --cov-report=term --cov-report=xml

- name: Snyk scan
uses: snyk/actions/python@0.4.0
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --file=lab_solutions/lab1/app_python/requirements.txt --severity-threshold=high --skip-unresolved

docker:
needs: test
runs-on: ubuntu-latest
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master')
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: Set version (CalVer)
run: echo "VERSION=$(date +%Y.%m.%d)" >> $GITHUB_ENV

- name: Build and push image
uses: docker/build-push-action@v6
with:
context: ${{ env.APP_DIR }}
file: ${{ env.APP_DIR }}/Dockerfile
push: true
tags: |
${{ env.IMAGE_NAME }}:${{ env.VERSION }}
${{ env.IMAGE_NAME }}:latest
43 changes: 43 additions & 0 deletions lab_solutions/lab1/app_python/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Python
__pycache__/
*.py[cod]
*.pyo
*.pyd
.Python
*.so
*.egg
*.egg-info/
dist/
build/

# Virtual environments
venv/
.venv/
env/
ENV/

# IDE
.vscode/
.idea/
*.swp
*.swo
*~

# OS
.DS_Store
Thumbs.db

# Git
.git/
.gitignore

# Logs
*.log

# Tests
tests/
.pytest_cache/

# Documentation
docs/
README.md
12 changes: 12 additions & 0 deletions lab_solutions/lab1/app_python/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Python
__pycache__/
*.py[cod]
venv/
*.log

# IDE
.vscode/
.idea/

# OS
.DS_Store
22 changes: 22 additions & 0 deletions lab_solutions/lab1/app_python/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
FROM python:3.13-slim

# restricted user
RUN useradd --create-home --shell /bin/bash appuser

WORKDIR /app

COPY requirements.txt .

# pip installation requires root
RUN pip install --no-cache-dir -r requirements.txt

COPY app.py .

# adduser now owns app
RUN chown -R appuser:appuser /app

USER appuser

EXPOSE 5000

CMD ["python", "app.py"]
66 changes: 66 additions & 0 deletions lab_solutions/lab1/app_python/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# DevOps Info Service (FastAPI)

[![python-ci](https://github.com/<username>/<repo>/actions/workflows/python-ci.yml/badge.svg)](https://github.com/<username>/<repo>/actions/workflows/python-ci.yml)

## Overview
A lightweight web service that reports system and runtime information for the DevOps course labs.

## Prerequisites
- Python 3.11+
- FastAPI - perfect fit for small WEB application here. It has built-in OpenAPI docs and provides modern async interface.

## Installation
```bash
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
```

## Running the Application
```bash
python app.py
# Or with custom config
PORT=8080 python app.py
```

## Testing
```bash
pip install -r requirements.txt -r requirements-dev.txt
pytest
```

## Linting
```bash
ruff check .
```

## API Endpoints
- `GET /` - Service and system information
- `GET /health` - Health check

## Configuration
| Variable | Default | Description |
| --- | --- | --- |
| HOST | 0.0.0.0 | Bind address |
| PORT | 5000 | HTTP port |
| DEBUG | False | Enable auto-reload and debug logging |

## Docker

### Building the Image
```bash
docker build -t chupapupa/devops-info-service:latest .
```

### Running the Container
```bash
docker run -p 5000:5000 chupapupa/devops-info-service:latest
# With custom port
docker run -p 8080:5000 -e PORT=5000 chupapupa/devops-info-service:latest
```

### Pulling from Docker Hub
```bash
docker pull chupapupa/devops-info-service:latest
docker run -p 5000:5000 chupapupa/devops-info-service:latest
```
109 changes: 109 additions & 0 deletions lab_solutions/lab1/app_python/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import os
import logging
import platform
import socket
from datetime import datetime, timezone
from fastapi import FastAPI, Request


HOST = os.getenv("HOST", "0.0.0.0")
PORT = int(os.getenv("PORT", "5000"))
DEBUG = os.getenv("DEBUG", "False").lower() == "true"


logging.basicConfig(
level=logging.DEBUG if DEBUG else logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)


logger = logging.getLogger(__name__)


START_TIME = datetime.now(timezone.utc)


app = FastAPI(title="DevOps Info Service", version="1.0.0")


def get_uptime():
delta = datetime.now(timezone.utc) - START_TIME
seconds = int(delta.total_seconds())
hours = seconds // 3600
minutes = (seconds % 3600) // 60
return {"seconds": seconds, "human": f"{hours} hours, {minutes} minutes"}


def get_system_info():
return {
"hostname": socket.gethostname(),
"platform": platform.system(),
"platform_version": platform.platform(),
"architecture": platform.machine(),
"cpu_count": os.cpu_count() or 0,
"python_version": platform.python_version(),
}


def get_request_info(request: Request):
client_ip = request.client.host if request.client else "unknown"
user_agent = request.headers.get("user-agent", "unknown")
return {
"client_ip": client_ip,
"user_agent": user_agent,
"method": request.method,
"path": request.url.path,
}


def get_runtime_info():
now = datetime.now(timezone.utc)
return {
"uptime_seconds": get_uptime()["seconds"],
"uptime_human": get_uptime()["human"],
"current_time": now.isoformat().replace("+00:00", "Z"),
"timezone": now.tzname() or "UTC",
}


@app.get("/")
async def index(request: Request):
logger.info("Request: %s %s", request.method, request.url.path)
return {
"service": {
"name": "devops-info-service",
"version": "1.0.0",
"description": "DevOps course info service",
"framework": "FastAPI",
},
"system": get_system_info(),
"runtime": get_runtime_info(),
"request": get_request_info(request),
"endpoints": [
{"path": "/", "method": "GET", "description": "Service information"},
{"path": "/health", "method": "GET", "description": "Health check"},
],
}


@app.get("/health")
async def health():
now = datetime.now(timezone.utc)
return {
"status": "healthy",
"timestamp": now.isoformat().replace("+00:00", "Z"),
"uptime_seconds": get_uptime()["seconds"],
}


if __name__ == "__main__":
import uvicorn

logger.info("Starting application on %s:%s", HOST, PORT)
uvicorn.run(
"app:app",
host=HOST,
port=PORT,
reload=DEBUG,
log_level="debug" if DEBUG else "info",
)
11 changes: 11 additions & 0 deletions lab_solutions/lab1/app_python/docs/LAB01.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# LAB01 - FastAPI Selection

## Framework Selection
**FastAPI** has been chosen for this lab because it provides automatic documentation, excellent performance, and modern API while staying lightweight for a simple service.

### Quick Comparison
| Framework | Pros | Cons |
| --- | --- | --- |
| FastAPI | Fast, async-ready, built-in docs, type hints | Slightly more setup than Flask |
| Flask | Minimal, easy to learn | No built-in docs, fewer batteries |
| Django | Full-featured, ORM included | Heavy for a small API |
Loading