diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml new file mode 100644 index 0000000000..e861141a8e --- /dev/null +++ b/.github/workflows/python-ci.yml @@ -0,0 +1,123 @@ +name: Python CI/CD Pipeline + +on: + push: + branches: [ master, lab03 ] + paths: + - 'app_python/**' + - '.github/workflows/python-ci.yml' + pull_request: + branches: [ master ] + paths: + - 'app_python/**' + +env: + REGISTRY: docker.io + IMAGE_NAME: ${{ github.repository_owner }}/devops-info-service + PYTHON_VERSION: '3.13' + +jobs: + code-quality-and-testing: + name: Code Quality & Testing + runs-on: ubuntu-latest + defaults: + run: + working-directory: app_python + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python ${{ env.PYTHON_VERSION }} + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + cache: 'pip' + cache-dependency-path: 'app_python/requirements.txt' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install flake8 black pytest pytest-cov + + - name: Lint with flake8 + run: | + echo "Running flake8 linting..." + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + + - name: Check code formatting with black + run: | + echo "Checking code formatting with black..." + black --check --diff . + + - name: Run unit tests with pytest + run: | + echo "Running unit tests with pytest..." + pytest --cov=app --cov-report=term-missing -v + + - name: Security scan with Snyk + uses: snyk/actions/python@master + continue-on-error: true + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + with: + args: --severity-threshold=high --file=requirements.txt + + docker-build-and-push: + name: Docker Build & Push + runs-on: ubuntu-latest + needs: code-quality-and-testing + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Generate version tags + id: vars + run: | + echo "DATE_TAG=$(date +'%Y.%m.%d')" >> $GITHUB_OUTPUT + echo "SHORT_SHA=${GITHUB_SHA:0:7}" >> $GITHUB_OUTPUT + + COMMIT_COUNT=$(git rev-list --count --since="$(date +'%Y-%m-%d 00:00:00')" HEAD 2>/dev/null || echo "0") + echo "CALVER_TAG=$(date +'%Y.%m').$COMMIT_COUNT" >> $GITHUB_OUTPUT + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: ./app_python + push: true + tags: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.DATE_TAG }} + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.CALVER_TAG }} + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.DATE_TAG }}-${{ steps.vars.outputs.SHORT_SHA }} + labels: | + org.opencontainers.image.title=DevOps Info Service + org.opencontainers.image.description=DevOps course info service + org.opencontainers.image.version=${{ steps.vars.outputs.CALVER_TAG }} + org.opencontainers.image.created=${{ steps.vars.outputs.DATE_TAG }} + org.opencontainers.image.revision=${{ github.sha }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Verify pushed images + run: | + echo "Docker images pushed with tags:" + echo "- latest" + echo "- ${{ steps.vars.outputs.DATE_TAG }}" + echo "- ${{ steps.vars.outputs.CALVER_TAG }}" + echo "- ${{ steps.vars.outputs.DATE_TAG }}-${{ steps.vars.outputs.SHORT_SHA }}" \ No newline at end of file diff --git a/app_go/.dockerignore b/app_go/.dockerignore new file mode 100644 index 0000000000..cfb650bdca --- /dev/null +++ b/app_go/.dockerignore @@ -0,0 +1,43 @@ +# Go artifacts +*.exe +*.exe~ +*.dll +*.so +*.dylib +*.test +*.out +*.o +*.prof +*.trace +vendor/ +__pycache__/ + +# Git +.git/ +.gitignore + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Documentation +*.md +docs/ +README.md + +# Docker +Dockerfile* +docker-compose* +.dockerignore + +# Temporary files +*.tmp +*.log +tmp/ +logs/ diff --git a/app_go/Dockerfile b/app_go/Dockerfile new file mode 100644 index 0000000000..2a3e22eea0 --- /dev/null +++ b/app_go/Dockerfile @@ -0,0 +1,42 @@ +# Stage 1: Builder - full build environment +FROM golang:1.21-alpine AS builder + +# Install system dependencies for build (git for go mod download) +RUN apk add --no-cache git ca-certificates + +# Set working directory +WORKDIR /app + +# Copy Go module files +COPY go.mod ./ + +# Download Go module dependencies +RUN go mod download + +# Copy application source code +COPY . . + +# Build Go application with optimizations +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ + -a \ + -ldflags="-w -s -extldflags '-static'" \ + -o devops-info-service . + +# Stage 2: Minimal runtime +FROM scratch + +# Copy CA certificates from builder for HTTPS support +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ + +# Copy the compiled binary from builder +COPY --from=builder /app/devops-info-service /app/devops-info-service + +# Expose port +EXPOSE 5000 + +# Set environment variables +ENV HOST=0.0.0.0 +ENV PORT=5000 + +# Run the application +CMD ["/app/devops-info-service"] \ No newline at end of file diff --git a/app_go/README.md b/app_go/README.md new file mode 100644 index 0000000000..292daf65ab --- /dev/null +++ b/app_go/README.md @@ -0,0 +1,97 @@ +# DevOps Info Service (Go) +## Overview +A Go-based web service designed to furnish details about itself and its operational environment. This compiled version of the service is optimized for containerization and multi-stage Docker builds, offering improved performance and smaller deployment footprints compared to interpreted languages. + +## Prerequisites +- Go 1.21 or higher +- Git (for dependency management) + +## Installation +1. Clone repository: + +```bash +# Clone the project +git clone https://github.com/s3rap1s/DevOps-Core-Course.git +cd DevOps-Core-Course/app_go + +# Download dependencies +go mod download + +# Build the application +go build -o devops-info-service +``` + +## Running the Application +```bash +# Default configuration +./devops-info-service + +# With custom port +PORT=8080 ./devops-info-service + +# With custom port and host +HOST=127.0.0.1 PORT=3000 ./devops-info-service + +# Run directly without building +go run main.go +Building for Different Platforms +bash +# Build for current platform +go build -o devops-info-service +``` + +## API Endpoints +### GET / +Return comprehensive service and system information: + +```json +{ + "service": { + "name": "devops-info-service", + "version": "1.0.0", + "description": "DevOps course info service", + "framework": "Go" + }, + "system": { + "hostname": "my-laptop", + "platform": "linux", + "platform_version": "Linux Kernel", + "architecture": "amd64", + "cpu_count": 8, + "go_version": "go1.21.4" + }, + "runtime": { + "uptime_seconds": 3600, + "uptime_human": "1 hour, 0 minutes", + "current_time": "2026-01-07T14:30:00.000Z", + "timezone": "UTC" + }, + "request": { + "client_ip": "127.0.0.1:12345", + "user_agent": "curl/7.81.0", + "method": "GET", + "path": "/" + }, + "endpoints": [ + {"path": "/", "method": "GET", "description": "Service information"}, + {"path": "/health", "method": "GET", "description": "Health check"} + ] +} +``` + +### GET /health +Simple health endpoint for monitoring: + +```json +{ + "status": "healthy", + "timestamp": "2024-01-15T14:30:00.000Z", + "uptime_seconds": 3600 +} +``` + +## Configuration +| Variable | Default | Description | +| -------- | --------- | ---------------------------- | +| `HOST` | `0.0.0.0` | Network interface to bind | +| `PORT` | `5000` | Port to listen on | \ No newline at end of file diff --git a/app_go/docs/GO.md b/app_go/docs/GO.md new file mode 100644 index 0000000000..b42a88d6a8 --- /dev/null +++ b/app_go/docs/GO.md @@ -0,0 +1,18 @@ +# Language Justification +## My Choice: Go +I selected **Go** as the compiled language for the bonus task implementation. Here's why: + +**Comparison Table:** + +| Criteria | Go | Rust | Java | C# | +|----------|----|------|------|----| +|Learning Curve | Low | High | Medium | Medium | +|Development Speed | High | Low | Medium | Medium | +|Standard Library | Excellent | Good | Extensive | Extensive | +|Performance | Excellent | Outstanding | Good | Good | +|Binary Size | ~7 MB | ~3 MB | ~40 MB | ~30 MB | +|Memory Safety | GC | Compile-time | GC | GC | +| **Choice for Bonus** | **✓** | | | | + +**Justification:** +Go offers the perfect balance for a DevOps service: it compiles to a single static binary with no runtime dependencies, has excellent concurrency support, and provides a rich standard library including HTTP server functionality. Its simplicity and fast compilation make it ideal for the iterative development required in this course. Go is also widely used in the DevOps ecosystem (Docker, Kubernetes, Prometheus), making it a relevant choice. \ No newline at end of file diff --git a/app_go/docs/LAB01.md b/app_go/docs/LAB01.md new file mode 100644 index 0000000000..4b93018d26 --- /dev/null +++ b/app_go/docs/LAB01.md @@ -0,0 +1,293 @@ +# Lab 1 — DevOps Info Service: Web Application Development on Go +## Language Selection +### My Choice: Go +I selected **Go** as the compiled language for the bonus task implementation. Here's why: + +**Comparison Table:** + +| Criteria | Go | Rust | Java | C# | +|----------|----|------|------|----| +|Learning Curve | Low | High | Medium | Medium | +|Development Speed | High | Low | Medium | Medium | +|Standard Library | Excellent | Good | Extensive | Extensive | +|Performance | Excellent | Outstanding | Good | Good | +|Binary Size | ~7 MB | ~3 MB | ~40 MB | ~30 MB | +|Memory Safety | GC | Compile-time | GC | GC | +| **Choice for Bonus** | **✓** | | | | + +**Justification:** +Go offers the perfect balance for a DevOps service: it compiles to a single static binary with no runtime dependencies, has excellent concurrency support, and provides a rich standard library including HTTP server functionality. Its simplicity and fast compilation make it ideal for the iterative development required in this course. Go is also widely used in the DevOps ecosystem (Docker, Kubernetes, Prometheus), making it a relevant choice. + +## Best Practices Applied +### 1. Clean Code Organization +```go +// Clear imports grouping +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "runtime" + "time" +) + +// Descriptive function names +func getSystemInfo() System { + hostname, err := os.Hostname() + if err != nil { + hostname = "unknown" + } + + return System{ + Hostname: hostname, + Platform: runtime.GOOS, + PlatformVersion: getOSVersion(), + Architecture: runtime.GOARCH, + CPUCount: runtime.NumCPU(), + GoVersion: runtime.Version(), + } +} +``` +**Importance:** Clean organization with clear separation of concerns makes the code maintainable and testable. Following Go conventions (camelCase, exported/unexported identifiers) ensures consistency. + +### 2. Comprehensive Error Handling +```go +func notFoundHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(map[string]string{ + "error": "Not Found", + "message": "Endpoint does not exist", + }) + + log.Printf("404 Not Found: %s", r.URL.Path) +} +``` +**Importance:** Proper error handling prevents application crashes and provides meaningful feedback to API consumers. Each error type returns appropriate HTTP status codes and structured JSON responses. + +### 3. Structured Logging +```go +func main() { + // Read environment variables + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + + host := os.Getenv("HOST") + if host == "" { + host = "0.0.0.0" + } + + log.Printf("Starting DevOps Info Service on %s:%s", host, port) + log.Fatal(http.ListenAndServe(fmt.Sprintf("%s:%s", host, port), nil)) +} +``` +**Importance:** Logging provides visibility into application behavior and startup configuration. The standard log package is sufficient for this simple service, though larger applications might use structured logging libraries. + +### 4. Configuration via Environment Variables +```go +port := os.Getenv("PORT") +if port == "" { + port = "5000" +} +``` +**Importance:** Following the 12-factor app methodology, configuration via environment variables makes the application portable across different environments without recompilation. + +### 5. Minimal Dependencies +```go +// go.mod - only Go standard library is used +module devops-info-service + +go 1.21 +``` +**Importance:** Using only the standard library eliminates dependency management overhead and reduces security vulnerabilities. The resulting binary is self-contained. + +### 6. Static Typing and Compile-Time Safety +```go +type ServiceInfo struct { + Service Service `json:"service"` + System System `json:"system"` + Runtime Runtime `json:"runtime"` + Request Request `json:"request"` + Endpoints []Endpoint `json:"endpoints"` +} +``` +**Importance:** Static typing catches many errors at compile time rather than runtime, improving reliability. Struct tags provide clear mapping between Go structs and JSON output. + +## API Documentation +### Endpoint 1: GET / +**Description:** Returns comprehensive service information, system details, runtime data, and request metadata. + +**Request:** +```bash +curl http://localhost:8080/ +``` +**Response (example):** +```json +{ + "service": { + "name": "devops-info-service", + "version": "1.0.0", + "description": "DevOps course info service", + "framework": "Go" + }, + "system": { + "hostname": "ubuntu-dev", + "platform": "linux", + "platform_version": "Linux Kernel", + "architecture": "amd64", + "cpu_count": 8, + "go_version": "go1.21.4" + }, + "runtime": { + "uptime_seconds": 125, + "uptime_human": "0 hours, 2 minutes", + "current_time": "2026-01-27T10:30:00.000Z", + "timezone": "UTC" + }, + "request": { + "client_ip": "127.0.0.1:54321", + "user_agent": "curl/7.88.1", + "method": "GET", + "path": "/" + }, + "endpoints": [ + {"path": "/", "method": "GET", "description": "Service information"}, + {"path": "/health", "method": "GET", "description": "Health check"} + ] +} +``` +### Endpoint 2: GET /health +**Description:** Health check endpoint for monitoring system. Always returns HTTP 200 with service status. + +**Request:** +```bash +curl http://localhost:8080/health +``` +**Response:** +```json +{ + "status": "healthy", + "timestamp": "2026-01-27T10:30:00.000Z", + "uptime_seconds": 125 +} +``` +### Testing Commands +1. **Basic endpoint test:** + +```bash +curl http://localhost:8080/ +``` + +2. **Health check test:** +```bash +curl http://localhost:8080/health +``` +3. **Pretty-printed output:** +```bash +curl http://localhost:8080/ | jq . +``` +4. **Custom configuration:** +```bash +PORT=8080 ./devops-info-service +curl http://localhost:8080/health +``` + +5. **Error simulation:** +```bash +curl -v http://localhost:8080/nonexistent +# Should return 404 error +``` +## Build Process +### Compilation +```bash +# Initialize Go module +go mod init devops-info-service + +# Build standard binary +go build -o devops-info-service +``` +###Running +```bash +# Run the compiled binary +./devops-info-service + +# Run with custom configuration +HOST=127.0.0.1 PORT=3000 ./devops-info-service + +# Run directly (without building) +go run main.go +``` +## Testing Evidence +### Main endpoint: +![Main Endpoint](screenshots/01-main-endpoint.png) + +### Health check: +![Health Check](screenshots/02-health-check.png) + +### Formatted output: +![Formatted output](screenshots/03-formatted-output.png) + +## Challenges & Solutions +### Challenge 1: HTTP Handler Registration +**Problem:** Go's http.HandleFunc doesn't allow multiple registrations for the same path, unlike Flask's decorator pattern. + +**Solution:** Implemented a routing check within the main handler: +```go +func mainHandler(w http.ResponseWriter, r *http.Request) { + // Handle only root path + if r.URL.Path != "/" { + notFoundHandler(w, r) + return + } + // ... rest of handler +} +``` +### Challenge 2: Platform Version Detection +**Problem:** Go's standard library doesn't provide detailed OS version information like Python's platform.release(). + +**Solution:** Created a simple mapping function: +```go +func getOSVersion() string { + switch runtime.GOOS { + case "linux": + return "Linux Kernel" + case "darwin": + return "macOS" + case "windows": + return "Windows" + default: + return runtime.GOOS + } +} +``` +### Challenge 3: Uptime Formatting +**Problem:** Converting seconds to human-readable format required manual calculation. + +**Solution:** Implemented a reusable function: +```go +func getUptime() (int, string) { + duration := time.Since(startTime) + seconds := int(duration.Seconds()) + + hours := seconds / 3600 + minutes := (seconds % 3600) / 60 + + return seconds, fmt.Sprintf("%d hours, %d minutes", hours, minutes) +} +``` +### Challenge 4: JSON Serialization +**Problem:** Ensuring proper JSON field naming and null handling. + +**Solution:** Used struct tags and proper initialization: +```go +type System struct { + Hostname string `json:"hostname"` + Platform string `json:"platform"` + PlatformVersion string `json:"platform_version"` + // ... other fields +} +``` + diff --git a/app_go/docs/LAB02.md b/app_go/docs/LAB02.md new file mode 100644 index 0000000000..9cec8ec40f --- /dev/null +++ b/app_go/docs/LAB02.md @@ -0,0 +1,252 @@ +# Lab 2 Bonus — Multi-Stage Docker Build for Go Application + +## Multi-Stage Strategy + +### Stage 1: Builder +```dockerfile +# Stage 1: Builder - full build environment +FROM golang:1.21-alpine AS builder + +# Install system dependencies for build (git for go mod download) +RUN apk add --no-cache git ca-certificates + +# Set working directory +WORKDIR /app + +# Copy Go module files +COPY go.mod ./ + +# Download Go module dependencies +RUN go mod download + +# Copy application source code +COPY . . + +# Build Go application with optimizations +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ + -a \ + -ldflags="-w -s -extldflags '-static'" \ + -o devops-info-service . +``` + +**Purpose:** Complete build environment containing: +- Go 1.21 compiler and standard library +- Git for dependency management +- All source code and build tools +- Temporary workspace for compilation + +### Stage 2: Runtime +```dockerfile +FROM scratch + +# Copy CA certificates from builder for HTTPS support +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ + +# Copy the compiled binary from builder +COPY --from=builder /app/devops-info-service /app/devops-info-service + +# Expose port +EXPOSE 5000 + +# Set environment variables +ENV HOST=0.0.0.0 +ENV PORT=5000 + +# Run the application +CMD ["/app/devops-info-service"] +``` + +**Purpose:** Absolute minimal runtime environment containing only: +- Statically compiled Go binary +- CA certificates for HTTPS/TLS support +- No operating system, shell, or package manager + +## Size Comparison + +### Image Size Analysis +| Component | Size | Contents | +|-----------|------|----------| +| **Builder Stage** | ~350MB | Full Go 1.21 SDK + Alpine + build tools | +| **Runtime Stage** | **7.16MB** | Static binary + CA certificates | +| **Size Reduction** | **~98%** | 350MB → 7.16MB | + +### Detailed Breakdown +- **Builder image:** Uses `golang:1.21-alpine` (~350MB with build tools) +- **Final image:** Uses `scratch` (0MB base) + binary + certificates +- **Binary size:** ~7.16MB (statically compiled Go application + CA certificate) + +## Why Multi-Stage Builds Matter for Compiled Languages + +### 1. Drastic Size Reduction +Compiled languages like Go have a fundamental advantage: they produce standalone binaries. Multi-stage builds leverage this by: +- **Separating concerns:** Build environment (large) vs runtime (minimal) +- **Eliminating build tools:** Compiler, linker, SDK removed from production +- **Removing dependencies:** Only the binary and absolute essentials remain + +### 2. Enhanced Security +```dockerfile +FROM scratch # No operating system, no shell, no package manager +``` + +**Security benefits:** +- **No shell access:** Cannot spawn shells even if compromised +- **Immutable runtime:** Binary cannot be modified without rebuilding +- **Principle of least privilege:** Only what's absolutely necessary + +### 3. Production Performance +- **Faster deployment:** Smaller images download quicker +- **Reduced storage:** Less disk space required across development/staging/production +- **Lower memory footprint:** Minimal OS overhead +- **Quick startup:** No initialization of unused services + +## Terminal Output + +### Build Process Output +```bash +s3rap1s in ~/devops/DevOps-Core-Course/app_go on lab01 ● ● λ docker build -t devops-info-service:go . +[+] Building 5.1s (14/14) FINISHED docker:default + => [internal] load build definition from Dockerfile 0.0s + => => transferring dockerfile: 1.00kB 0.0s + => [internal] load metadata for docker.io/library/golang:1.21-alpine 0.7s + => [internal] load .dockerignore 0.0s + => => transferring context: 359B 0.0s + => [builder 1/7] FROM docker.io/library/golang:1.21-alpine@sha256:2414035b086e3c42b99654c8b26e6f5b1b1598080d65fd03c7f499552ff4dc94 0.0s + => => resolve docker.io/library/golang:1.21-alpine@sha256:2414035b086e3c42b99654c8b26e6f5b1b1598080d65fd03c7f499552ff4dc94 0.0s + => [internal] load build context 0.0s + => => transferring context: 54B 0.0s + => CACHED [builder 2/7] RUN apk add --no-cache git ca-certificates 0.0s + => CACHED [builder 3/7] WORKDIR /app 0.0s + => CACHED [builder 4/7] COPY go.mod ./ 0.0s + => CACHED [builder 5/7] RUN go mod download 0.0s + => CACHED [builder 6/7] COPY . . 0.0s + => [builder 7/7] RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -ldflags="-w -s -extldflags '-static'" -o devops-info-service . 3.9s + => [stage-1 1/2] COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 0.0s + => [stage-1 2/2] COPY --from=builder /app/devops-info-service /app/devops-info-service 0.0s + => exporting to image 0.4s + => => exporting layers 0.3s + => => exporting manifest sha256:4e1764a6a80bfc8666f97655b398a31766e6ef0b7e113b73651ca44601a369e5 0.0s + => => exporting config sha256:07f16274913e502fcb7339751611566f733555d340310768666604f24375006b 0.0s + => => exporting attestation manifest sha256:594b42605a1d485598b5ca39be853c1e154958e927a235027e0ca1b5fce2efa9 0.0s + => => exporting manifest list sha256:c5f945015fb0dfd3f762151c64c5393121944441d08e191e5a7533aadcf4f4eb 0.0s + => => naming to docker.io/library/devops-info-service:go 0.0s + => => unpacking to docker.io/library/devops-info-service:go +``` + +### Image Size Verification +```bash +s3rap1s in ~/devops/DevOps-Core-Course/app_go on lab01 ● ● λ docker images | grep devops-info-service +WARNING: This output is designed for human readability. For machine-readable output, please use --format. +devops-info-service:go c5f945015fb0 7.16MB 2.2MB U +devops-info-service:python 4b08b6e2f063 199MB 48.1MB +s3rap1s/devops-info-service:python ef074c1a118d 199MB 48.1MB + +s3rap1s in ~/devops/DevOps-Core-Course/app_go on lab01 ● ● λ docker history devops-info-service:go +IMAGE CREATED CREATED BY SIZE COMMENT +c5f945015fb0 14 minutes ago CMD ["/app/devops-info-service"] 0B buildkit.dockerfile.v0 + 14 minutes ago ENV PORT=5000 0B buildkit.dockerfile.v0 + 14 minutes ago ENV HOST=0.0.0.0 0B buildkit.dockerfile.v0 + 14 minutes ago EXPOSE [5000/tcp] 0B buildkit.dockerfile.v0 + 14 minutes ago COPY /app/devops-info-service /app/devops-in… 4.72MB buildkit.dockerfile.v0 + 14 minutes ago COPY /etc/ssl/certs/ca-certificates.crt /etc… 238kB buildkit.dockerfile.v0 +``` + +### Runtime Testing +```bash +s3rap1s in ~/devops/DevOps-Core-Course/app_go on lab01 ● ● λ docker run -d --name devops-go -p 5000:5000 devops-info-service:go +0d2cb36ff83b03fca8090248aa3a6fe1beba1e879617a8dd2e5a9c3a588e8c1c + +s3rap1s in ~/devops/DevOps-Core-Course/app_go on lab01 ● ● λ curl http://localhost:5000/ +{"service":{"name":"devops-info-service","version":"1.0.0","description":"DevOps course info service","framework":"Go"},"system":{"hostname":"0d2cb36ff83b","platform":"linux","platform_version":"Linux Kernel","architecture":"amd64","cpu_count":12,"go_version":"go1.21.13"},"runtime":{"uptime_seconds":17,"uptime_human":"0 hours, 0 minutes","current_time":"2026-01-31T20:20:36Z","timezone":"UTC"},"request":{"client_ip":"172.17.0.1:50750","user_agent":"curl/8.18.0","method":"GET","path":"/"},"endpoints":[{"path":"/","method":"GET","description":"Service information"},{"path":"/health","method":"GET","description":"Health check"}]} + +s3rap1s in ~/devops/DevOps-Core-Course/app_go on lab01 ● ● λ curl http://localhost:5000/health +{"status":"healthy","timestamp":"2026-01-31T20:21:20Z","uptime_seconds":61} + +s3rap1s in ~/devops/DevOps-Core-Course/app_go on lab01 ● ● λ docker logs devops-go +2026/01/31 20:20:19 Starting DevOps Info Service on 0.0.0.0:5000 +2026/01/31 20:20:34 Health check from 172.17.0.1:50742 +2026/01/31 20:20:36 Request: GET / from 172.17.0.1:50750 +2026/01/31 20:21:13 404 Not Found: /helath +2026/01/31 20:21:20 Health check from 172.17.0.1:38286 +``` + +## Technical Explanation of Each Stage + +### Stage 1: Builder (`golang:1.21-alpine`) +**Purpose:** Provide complete compilation environment + +**Key operations:** +1. **Base setup:** Alpine Linux with Go 1.21 toolchain +2. **Dependencies:** Install git and CA certificates +3. **Module management:** Download Go dependencies with caching +4. **Compilation:** Build optimized static binary with: + - `CGO_ENABLED=0`: Disable CGO for pure Go static binary + - `-ldflags="-w -s"`: Strip debug symbols and DWARF tables + - `-extldflags '-static'`: Force static linking + - `-a`: Force rebuilding of packages + +**Output:** `/app/devops-info-service` (6.9MB static binary) + +### Stage 2: Runtime (`scratch`) +**Purpose:** Provide minimal production runtime + +**Key operations:** +1. **Base image:** `scratch` (empty filesystem) +2. **Binary copy:** Transfer compiled binary from builder +3. **Certificates:** Copy CA certificates for TLS/HTTPS support +4. **Configuration:** Set environment variables and expose port + +**Output:** Production-ready container image (7.16MB) + +## Security Benefits of Smaller Images + +### Specific Security Advantages +1. **No shell:** Cannot execute arbitrary commands or spawn shells +2. **Immutable filesystem:** Only the binary exists, cannot be modified +3. **Minimal CVE surface:** No packages = no vulnerabilities to patch +4. **Isolated execution:** Runs as PID 1 with no background services +5. **Resource limits:** Minimal memory/cpu usage reduces DoS impact + +## Why FROM scratch? Trade-offs and Decisions + +### Why `scratch` Was Chosen +```dockerfile +FROM scratch # Instead of alpine, distroless, or other minimal bases +``` + +**Advantages:** +1. **Absolute minimalism:** 0MB base, only binary + certs +2. **Maximum security:** No OS, no shell, no utilities +3. **Great for static binaries:** Go compiles to fully static executables + +### Trade-offs Considered +| Base Image | Size | Pros | Cons | Decision | +|------------|------|------|------|----------| +| **scratch** | 0MB | Max security, minimal size | No debugging tools, no shell | ✅ **Chosen** | +| **alpine** | 5.5MB | Shell for debugging, small | Larger, more attack surface | Rejected | +| **distroless** | 20MB | Secure | Much larger than scratch | Rejected | + +## Analysis of Size Reduction and Why It Matters + +### Why Size Reduction Matters +1. **Cost efficiency:** 98% reduction in storage and bandwidth costs +2. **Deployment speed:** Images deploy in seconds instead of minutes +3. **Developer productivity:** Faster CI/CD pipeline execution +4. **Environmental impact:** Less energy for storage and transfer +5. **Edge computing:** Suitable for resource-constrained environments + +## Challenges and Solutions + +### Challenge: Certificate Management with `scratch` +**Problem:** `scratch` has no CA certificates, breaking HTTPS calls from the application. + +**Solution:** Copy certificates from builder stage: +```dockerfile +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +``` + +## What I Learned + +1. **Multi-stage builds** are transformative for compiled languages, enabling near-zero runtime overhead +2. **Static compilation** is powerful but requires careful dependency management +3. **Security through minimalism** is achievable with `scratch` base images +4. **Trade-offs exist** between debuggability and security/minimalism diff --git a/app_go/docs/screenshots/01-main-endpoint.png b/app_go/docs/screenshots/01-main-endpoint.png new file mode 100644 index 0000000000..6acabfa3cf Binary files /dev/null and b/app_go/docs/screenshots/01-main-endpoint.png differ diff --git a/app_go/docs/screenshots/02-health-check.png b/app_go/docs/screenshots/02-health-check.png new file mode 100644 index 0000000000..f905599cf0 Binary files /dev/null and b/app_go/docs/screenshots/02-health-check.png differ diff --git a/app_go/docs/screenshots/03-formatted-output.png b/app_go/docs/screenshots/03-formatted-output.png new file mode 100644 index 0000000000..f4dd6737db Binary files /dev/null and b/app_go/docs/screenshots/03-formatted-output.png differ diff --git a/app_go/go.mod b/app_go/go.mod new file mode 100644 index 0000000000..307ce0d1c5 --- /dev/null +++ b/app_go/go.mod @@ -0,0 +1,3 @@ +module devops-info-service + +go 1.21 diff --git a/app_go/main.go b/app_go/main.go new file mode 100644 index 0000000000..7b0e355b6c --- /dev/null +++ b/app_go/main.go @@ -0,0 +1,205 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "runtime" + "time" +) + +// Data structures +type ServiceInfo struct { + Service Service `json:"service"` + System System `json:"system"` + Runtime Runtime `json:"runtime"` + Request Request `json:"request"` + Endpoints []Endpoint `json:"endpoints"` +} + +type Service struct { + Name string `json:"name"` + Version string `json:"version"` + Description string `json:"description"` + Framework string `json:"framework"` +} + +type System struct { + Hostname string `json:"hostname"` + Platform string `json:"platform"` + PlatformVersion string `json:"platform_version"` + Architecture string `json:"architecture"` + CPUCount int `json:"cpu_count"` + GoVersion string `json:"go_version"` +} + +type Runtime struct { + UptimeSeconds int `json:"uptime_seconds"` + UptimeHuman string `json:"uptime_human"` + CurrentTime string `json:"current_time"` + Timezone string `json:"timezone"` +} + +type Request struct { + ClientIP string `json:"client_ip"` + UserAgent string `json:"user_agent"` + Method string `json:"method"` + Path string `json:"path"` +} + +type Endpoint struct { + Path string `json:"path"` + Method string `json:"method"` + Description string `json:"description"` +} + +type HealthResponse struct { + Status string `json:"status"` + Timestamp string `json:"timestamp"` + UptimeSeconds int `json:"uptime_seconds"` +} + +// Global variables +var startTime time.Time + +func init() { + startTime = time.Now() +} + +// Helper functions +func getSystemInfo() System { + hostname, err := os.Hostname() + if err != nil { + hostname = "unknown" + } + + return System{ + Hostname: hostname, + Platform: runtime.GOOS, + PlatformVersion: getOSVersion(), + Architecture: runtime.GOARCH, + CPUCount: runtime.NumCPU(), + GoVersion: runtime.Version(), + } +} + +func getOSVersion() string { + switch runtime.GOOS { + case "linux": + return "Linux Kernel" + case "darwin": + return "macOS" + case "windows": + return "Windows" + default: + return runtime.GOOS + } +} + +func getUptime() (int, string) { + duration := time.Since(startTime) + seconds := int(duration.Seconds()) + + hours := seconds / 3600 + minutes := (seconds % 3600) / 60 + + return seconds, fmt.Sprintf("%d hours, %d minutes", hours, minutes) +} + +func getCurrentTime() string { + return time.Now().UTC().Format(time.RFC3339) +} + +// HTTP handlers +func mainHandler(w http.ResponseWriter, r *http.Request) { + // Handle only root path + if r.URL.Path != "/" { + notFoundHandler(w, r) + return + } + + systemInfo := getSystemInfo() + uptimeSeconds, uptimeHuman := getUptime() + + response := ServiceInfo{ + Service: Service{ + Name: "devops-info-service", + Version: "1.0.0", + Description: "DevOps course info service", + Framework: "Go", + }, + System: systemInfo, + Runtime: Runtime{ + UptimeSeconds: uptimeSeconds, + UptimeHuman: uptimeHuman, + CurrentTime: getCurrentTime(), + Timezone: "UTC", + }, + Request: Request{ + ClientIP: r.RemoteAddr, + UserAgent: r.UserAgent(), + Method: r.Method, + Path: r.URL.Path, + }, + Endpoints: []Endpoint{ + {Path: "/", Method: "GET", Description: "Service information"}, + {Path: "/health", Method: "GET", Description: "Health check"}, + }, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + + log.Printf("Request: %s %s from %s", r.Method, r.URL.Path, r.RemoteAddr) +} + +func healthHandler(w http.ResponseWriter, r *http.Request) { + uptimeSeconds, _ := getUptime() + + response := HealthResponse{ + Status: "healthy", + Timestamp: getCurrentTime(), + UptimeSeconds: uptimeSeconds, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + + log.Printf("Health check from %s", r.RemoteAddr) +} + +func notFoundHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(map[string]string{ + "error": "Not Found", + "message": "Endpoint does not exist", + }) + + log.Printf("404 Not Found: %s", r.URL.Path) +} + +// Main function +func main() { + // Read environment variables + port := os.Getenv("PORT") + if port == "" { + port = "5000" + } + + host := os.Getenv("HOST") + if host == "" { + host = "0.0.0.0" + } + + // Setup HTTP routes + http.HandleFunc("/", mainHandler) + http.HandleFunc("/health", healthHandler) + + // Start server + addr := fmt.Sprintf("%s:%s", host, port) + log.Printf("Starting DevOps Info Service on %s", addr) + log.Fatal(http.ListenAndServe(addr, nil)) +} \ No newline at end of file diff --git a/app_python/.dockerignore b/app_python/.dockerignore new file mode 100644 index 0000000000..df2ec2cfb1 --- /dev/null +++ b/app_python/.dockerignore @@ -0,0 +1,39 @@ +# Python +__pycache__/ +*.py[cod] +*.pyo +*.so +.Python +venv/ +env/ +.venv/ +.env +*.log + +# Git +.git/ +.gitignore + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Documentation +*.md +docs/ +README.md + +# Tests +tests/ +test*.py + +# Docker +Dockerfile* +docker-compose* +.dockerignore \ No newline at end of file diff --git a/app_python/.gitignore b/app_python/.gitignore new file mode 100644 index 0000000000..4de420a8f7 --- /dev/null +++ b/app_python/.gitignore @@ -0,0 +1,12 @@ +# Python +__pycache__/ +*.py[cod] +venv/ +*.log + +# IDE +.vscode/ +.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..3de1bb9253 --- /dev/null +++ b/app_python/Dockerfile @@ -0,0 +1,56 @@ +# Dockerfile for Python Application with Virtual Environment +FROM python:3.13-slim AS builder + +# Installing system dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc \ + && rm -rf /var/lib/apt/lists/* + +# Installing the working directory +WORKDIR /app + +# Creating a virtual environment +RUN python -m venv /opt/venv + +# Activating the virtual environment +ENV PATH="/opt/venv/bin:$PATH" + +# Copying requirements file +COPY requirements.txt . +RUN pip install --no-cache-dir --upgrade pip && \ + pip install --no-cache-dir -r requirements.txt + +# Final stage: Copying the virtual environment and application code +FROM python:3.13-slim + +# Creating non-root user and group +RUN groupadd -r appgroup && useradd -r -g appgroup appuser + +# Setting the working directory +WORKDIR /app + +# Copying the virtual environment from the builder stage +COPY --from=builder /opt/venv /opt/venv + +# Copying the application code +COPY . . + +# Setting permissions for the application directory +RUN chown -R appuser:appgroup /app && chmod -R 755 /app + +# Switching to non-root user +USER appuser + +# Setting the PATH to include the virtual environment +ENV PATH="/opt/venv/bin:$PATH" + +# Opening the port for the application +EXPOSE 5000 + +# Setting environment variables +ENV HOST=0.0.0.0 +ENV PORT=5000 +ENV PYTHONUNBUFFERED=1 + +# Running the application +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..5442b06fea --- /dev/null +++ b/app_python/README.md @@ -0,0 +1,176 @@ +# DevOps Info Service + +## Overview +A Python-based web service designed to furnish details about itself and its operational environment. This service serves as a foundation for subsequent experiments in containerization, continuous integration and continuous deployment (CI/CD), monitoring, and deployment processes. + +## CI/CD Pipeline +![Python CI/CD](https://github.com/s3rap1s/DevOps-Core-Course/actions/workflows/python-ci.yml/badge.svg) + +### Overview +This project uses GitHub Actions for continuous integration and deployment. The pipeline includes: + +1. **Code Quality Checks** + - Linting with flake8 + - Code formatting with black + - Security scanning with Snyk + +2. **Testing** + - Unit tests with pytest + - Test coverage tracking + - 90%+ code coverage requirement + +3. **Docker Build & Deployment** + - Multi-stage Docker builds + - Automated tagging with Calendar Versioning + - Push to Docker Hub + +### Versioning Strategy +We use **Calendar Versioning (CalVer)** with the format `YYYY.MM.MICRO`: + +- **YYYY.MM.DD** - Specific date of build +- **YYYY.MM.MICRO** - Version with micro release number +- **latest** - Most recent stable build + + +## Prerequisites +- Python 3.11 or higher +- pip (Python Package manager) + +## Installation +1. Clone repository: +```bash +# Clone the project +git clone https://github.com/s3rap1s/DevOps-Core-Course.git +cd DevOps-Core-Course/app_python + +# Create virtual environment +python3 -m venv venv +source venv/bin/activate # on Linux / macOs or .\venv\Scripts\Activate.ps1 on windows + +# Install dependencies +pip install -r requirements.txt +``` + +## Running the Application + +```bash +# Default configuration +python app.py + +# With custom port +PORT=8080 python app.py + +# With custom port and host +HOST=127.0.0.1 PORT=3000 python app.py +``` + +## Testing the Application +```bash +pytest # Run all tests +pytest --cov=app --cov-report=term-missing # Run with coverage +``` + +## Docker + +This application is containerized and available as a Docker image. + +### Building the Image Locally + +```bash +docker build -t devops-info-service:latest . +``` + +### Running a Container + +```bash +# Run with default port mapping +docker run -d -p 5000:5000 devops-info-service:latest + +# Run with custom port +docker run -d -p 8080:5000 devops-info-service:latest + +# Run with environment variables +docker run -d -p 3000:3000 -e PORT=3000 -e HOST=0.0.0.0 devops-info-service:latest +``` + +### Pulling from Docker Hub + +```bash +# Pull the latest version +docker pull your-username/devops-info-service:latest + +# Run pulled image +docker run -d -p 5000:5000 your-username/devops-info-service:latest +``` + +### Environment Variables in Docker +When running in Docker, you can pass environment variables using the `-e` flag: + +```bash +docker run -d -p 5000:5000 \ + -e HOST=0.0.0.0 \ + -e PORT=5000 \ + -e DEBUG=false \ + devops-info-service:latest +``` + +## API Endpoints + +### `GET /` +Return comprehensive service and system information: + +```json +{ + "service": { + "name": "devops-info-service", + "version": "1.0.0", + "description": "DevOps course info service", + "framework": "Flask" + }, + "system": { + "hostname": "my-laptop", + "platform": "Linux", + "platform_version": "Ubuntu 24.04", + "architecture": "x86_64", + "cpu_count": 8, + "python_version": "3.13.1" + }, + "runtime": { + "uptime_seconds": 3600, + "uptime_human": "1 hour, 0 minutes", + "current_time": "2026-01-07T14:30:00.000Z", + "timezone": "UTC" + }, + "request": { + "client_ip": "127.0.0.1", + "user_agent": "curl/7.81.0", + "method": "GET", + "path": "/" + }, + "endpoints": [ + {"path": "/", "method": "GET", "description": "Service information"}, + {"path": "/health", "method": "GET", "description": "Health check"} + ] +} +``` + +### `GET /health` + +Simple health endpoint for monitoring: + +```json +{ + "status": "healthy", + "timestamp": "2024-01-15T14:30:00.000Z", + "uptime_seconds": 3600 +} +``` + + +## Configuration + +| Variable | Default | Description | +| -------- | --------- | ---------------------------- | +| `HOST` | `0.0.0.0` | Network interface to bind | +| `PORT` | `5000` | Port to listen on | +| `DEBUG` | `false` | Enable debug mode | diff --git a/app_python/app.py b/app_python/app.py new file mode 100644 index 0000000000..919477653a --- /dev/null +++ b/app_python/app.py @@ -0,0 +1,132 @@ +""" +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 + +# Flask app initialization +app = Flask(__name__) + +# Configuration via environment variables +HOST = os.getenv("HOST", "0.0.0.0") +PORT = int(os.getenv("PORT", 5000)) +DEBUG = os.getenv("DEBUG", "False").lower() == "true" + +# Application startup time +START_TIME = datetime.now(timezone.utc) + +# Setting up logging +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger(__name__) + + +def get_system_info(): + """Collecting information about the system. + + Returns: + dict: System configuration + """ + return { + "hostname": socket.gethostname(), + "platform": platform.system(), + "platform_version": platform.release(), + "architecture": platform.machine(), + "cpu_count": os.cpu_count() or 0, + "python_version": platform.python_version(), + } + + +def get_uptime(): + """Calculating the running time of the application. + + Returns: + dict: Uptime in seconds and human-readable format + """ + 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"} + + +@app.route("/") +def index(): + """The main endpoint - Information about the service and the system.""" + client_ip = request.remote_addr + user_agent = request.headers.get("User-Agent", "Unknown") + + # Forming a response + response = { + "service": { + "name": "devops-info-service", + "version": "1.0.0", + "description": "DevOps course info service", + "framework": "Flask", + }, + "system": get_system_info(), + "runtime": { + "uptime_seconds": get_uptime()["seconds"], + "uptime_human": get_uptime()["human"], + "current_time": datetime.now(timezone.utc).isoformat(), + "timezone": "UTC", + }, + "request": { + "client_ip": client_ip, + "user_agent": user_agent, + "method": request.method, + "path": request.path, + }, + "endpoints": [ + {"path": "/", "method": "GET", "description": "Service information"}, + {"path": "/health", "method": "GET", "description": "Health check"}, + ], + } + logger.info(f"Request: {request.method} {request.path} from {client_ip}") + return jsonify(response) + + +@app.route("/health") +def health(): + """Endpoint for health check.""" + response = { + "status": "healthy", + "timestamp": datetime.now(timezone.utc).isoformat(), + "uptime_seconds": get_uptime()["seconds"], + } + logger.debug(f"Health check: {response}") + return jsonify(response), 200 + + +@app.errorhandler(404) +def not_found(error): + """Error handler 404.""" + logger.warning(f"404 Not Found: {request.path}") + return jsonify({"error": "Not Found", "message": "Endpoint does not exist"}), 404 + + +@app.errorhandler(500) +def internal_error(error): + """Error handler 500.""" + logger.error(f"500 Internal Server Error: {str(error)}") + return ( + jsonify( + { + "error": "Internal Server Error", + "message": "An unexpected error occurred", + } + ), + 500, + ) + + +if __name__ == "__main__": + logger.info(f"Starting DevOps Info Service on {HOST}:{PORT} (debug={DEBUG})") + 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..90f920df57 --- /dev/null +++ b/app_python/docs/LAB01.md @@ -0,0 +1,271 @@ +# Lab 1 — DevOps Info Service: Web Application Development + +## Framework Selection + +### My Choice: Flask + +I selected **Flask** as the web framework for this DevOps Info Service. Here's why: + +**Comparison Table:** +| Criteria | Flask | FastAPI | Django | +|----------|-------|---------|--------| +| Learning Curve | Very low | Moderate | Steep | +| Development Speed | High | High | Medium | +| Built-in Features | Minimal | Moderate | Extensive | +| Auto-documentation | Requires extensions | Built-in (OpenAPI) | Requires extensions | +| Performance | Good | Excellent (async) | Good | +| Complexity | Low | Medium | High | +| **Choice for Lab 1** | **✓** | | | + +**Justification:** +Flask is a lightweight, minimalistic framework that perfectly suits our simple service with only two endpoints. For a DevOps monitoring tool foundation, we don't need the complexity of Django or the async capabilities of FastAPI yet. Flask allows rapid development with clean, understandable code, making it ideal for this educational project. Its simplicity aligns with the Unix philosophy of "do one thing well" - in this case, serve system information via HTTP. + +## Best Practices Applied + +### 1. Clean Code Organization +```python +# Clear imports grouping +import os +import socket +import platform +import logging +from datetime import datetime, timezone +from flask import Flask, jsonify, request + +# Descriptive function names with docstrings +def get_system_info(): + """Collecting information about the system. + Returns: + dict: System configuration + """ + return { + 'hostname': socket.gethostname(), + 'platform': platform.system(), + # ... more fields + } +``` + +**Importance:** Clean organization makes code maintainable, readable, and easier to debug. Following PEP 8 ensures consistency across Python projects. + +### 2. Comprehensive Error Handling +```python +@app.errorhandler(404) +def not_found(error): + """Error handler 404.""" + logger.warning(f"404 Not Found: {request.path}") + return jsonify({ + 'error': 'Not Found', + 'message': 'Endpoint does not exist' + }), 404 + +@app.errorhandler(500) +def internal_error(error): + """Error handler 500.""" + logger.error(f"500 Internal Server Error: {str(error)}") + return jsonify({ + 'error': 'Internal Server Error', + 'message': 'An unexpected error occurred' + }), 500 +``` + +**Importance:** Proper error handling prevents application crashes and provides meaningful feedback to API consumers. Each error type returns appropriate HTTP status codes and structured JSON responses. + +### 3. Structured Logging +```python +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +logger.info(f'Starting DevOps Info Service on {HOST}:{PORT} (debug={DEBUG})') +logger.info(f"Request: {request.method} {request.path} from {client_ip}") +``` + +**Importance:** Logging provides visibility into application behavior, helps with debugging in production, and allows monitoring of API usage patterns. + +### 4. Configuration via Environment Variables +```python +HOST = os.getenv('HOST', '0.0.0.0') +PORT = int(os.getenv('PORT', 5000)) +DEBUG = os.getenv('DEBUG', 'False').lower() == 'true' +``` + +**Importance:** Following the 12-factor app methodology, configuration via environment variables makes the application portable across different environments (development, testing, production) without code changes. + +### 5. Version-Pinned Dependencies +```txt +# Web framework +Flask==3.1.0 + +# Virtual environment for python +python-dotenv==1.0.1 +``` + +**Importance:** Pinning exact versions ensures consistent behavior across all deployments and prevents "works on my machine" issues due to dependency version mismatches. + +### 6. Git Ignore for Development Artifacts +```gitignore +# Python +__pycache__/ +*.py[cod] +venv/ +*.log + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store +``` + +**Importance:** Prevents accidental commits of generated files, virtual environments, and sensitive data, keeping the repository clean and focused on source code. + +## API Documentation + +### Endpoint 1: `GET /` + +**Description:** Returns comprehensive service information, system details, runtime data, and request metadata. + +**Request:** +```bash +curl http://localhost:5000/ +``` + +**Response (example):** +```json +{ + "service": { + "name": "devops-info-service", + "version": "1.0.0", + "description": "DevOps course info service", + "framework": "Flask" + }, + "system": { + "hostname": "my-laptop", + "platform": "Linux", + "platform_version": "Ubuntu 24.04", + "architecture": "x86_64", + "cpu_count": 8, + "python_version": "3.13.1" + }, + "runtime": { + "uptime_seconds": 3600, + "uptime_human": "1 hour, 0 minutes", + "current_time": "2026-01-07T14:30:00.000Z", + "timezone": "UTC" + }, + "request": { + "client_ip": "127.0.0.1", + "user_agent": "curl/7.81.0", + "method": "GET", + "path": "/" + }, + "endpoints": [ + {"path": "/", "method": "GET", "description": "Service information"}, + {"path": "/health", "method": "GET", "description": "Health check"} + ] +} +``` + +### Endpoint 2: `GET /health` + +**Description:** Health check endpoint for monitoring system. Always returns HTTP 200 with service status. + +**Request:** +```bash +curl http://localhost:5000/health +``` + +**Response (example):** +```json +{ + "status": "healthy", + "timestamp": "2024-01-15T14:30:00.000Z", + "uptime_seconds": 3600 +} +``` + +### Testing Commands + +1. **Basic endpoint test:** + ```bash + curl http://localhost:5000/ + ``` + +2. **Health check test:** + ```bash + curl http://localhost:5000/health + ``` + +3. **Pretty-printed output:** + ```bash + curl http://localhost:5000/ | jq . + ``` + +4. **Custom configuration:** + ```bash + PORT=8080 python app.py + curl http://localhost:8080/health + ``` + +5. **Error simulation:** + ```bash + curl http://localhost:5000/nonexistent + # Should return 404 error + ``` + +## Testing Evidence + +### Main endpoint: +![Main Endpoint](screenshots/01-main-endpoint.png) + +### Health check: +![Health Check](screenshots/02-health-check.png) + +### Formatted output: +![Formatted output](screenshots/03-formatted-output.png) + + +## Challenges & Solutions + +### Challenge 1: Timezone-Aware Timestamps +**Problem:** `datetime.now()` without timezone creates naive datetime objects, which can cause issues with serialization and timezone calculations. + +**Solution:** Used `timezone.utc` consistently: +```python +from datetime import datetime, timezone +START_TIME = datetime.now(timezone.utc) +# ... +datetime.now(timezone.utc).isoformat() +``` + +### Challenge 2: Logging Configuration +**Problem:** Determining the appropriate log level and format for different types of messages. + +**Solution:** Configured logging with INFO level for normal operations, DEBUG for health checks, and WARNING/ERROR for error handlers: +```python +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger.info(f"Request: {request.method} {request.path} from {client_ip}") +logger.debug(f"Health check: {response}") +logger.warning(f"404 Not Found: {request.path}") +``` + +### Challenge 3: CPU Count Handling +**Problem:** `os.cpu_count()` can return None on some systems or when the count cannot be determined. + +**Solution:** Added a fallback value: +```python +'cpu_count': os.cpu_count() or 0 +``` + +## GitHub Community +### Why starring repositories matters in open source: +Starring repositories serves as both a bookmarking tool for personal reference and a public endorsement that helps projects gain visibility, attracting more contributors and showing appreciation to maintainers for their work. + +### How following developers helps in team projects and professional growth: +Following developers enables you to stay updated on their projects and insights, fostering collaboration and knowledge sharing that accelerates team productivity and your own skill development in the tech community. \ 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..aad9c97090 --- /dev/null +++ b/app_python/docs/LAB02.md @@ -0,0 +1,213 @@ +# Lab 2 — Containerization with Docker + +## Docker Best Practices Applied + +### 1. Multi-Stage Build +**Implementation:** +```dockerfile +FROM python:3.13-slim AS builder +# ... build stage with dependencies +FROM python:3.13-slim +COPY --from=builder /opt/venv /opt/venv +``` + +**Importance:** Separates the build environment from the runtime environment, reducing the final image size by excluding build tools and intermediate files. + +### 2. Non-Root User +**Implementation:** +```dockerfile +RUN groupadd -r appgroup && useradd -r -g appgroup appuser +USER appuser +``` + +**Importance:** Enhances security by following the principle of least privilege, minimizing potential damage if the container is compromised. + +### 3. Layer Caching Optimization +**Implementation:** +```dockerfile +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY . . +``` + +**Importance:** Docker caches layers. By copying requirements.txt first and installing dependencies, this layer is cached and only rebuilt when requirements change, speeding up subsequent builds. + +### 4. .dockerignore File +**Implementation:** Created a comprehensive `.dockerignore` file to exclude unnecessary files (development artifacts, IDE files, git, etc.). + +**Importance:** Reduces build context size, speeding up the build process and preventing sensitive or irrelevant files from being included. + +### 5. Specific Base Image Version +**Implementation:** `python:3.13-slim` (instead of `python:latest` or `python:3.13`) + +**Importance:** Ensures reproducible builds and avoids unexpected changes from base image updates. + +### 6. Clean Package Installation +**Implementation:** +```dockerfile +RUN pip install --no-cache-dir -r requirements.txt +``` + +**Importance:** The `--no-cache-dir` flag prevents pip from caching packages, reducing image size. + +### 7. System Dependency Cleanup +**Implementation:** +```dockerfile +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc \ + && rm -rf /var/lib/apt/lists/* +``` + +**Importance:** Removes the package lists after installation, reducing image size and keeping the image clean. + +### 8. Virtual Environment Isolation +**Implementation:** +```dockerfile +RUN python -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" +``` + +**Importance:** Isolates application dependencies from the system Python, avoiding conflicts and making the environment reproducible. + +## Image Information & Decisions + +### Base Image Choice +**Selected:** `python:3.13-slim` + +**Justification:** +- **slim variant** provides a minimal Python runtime without unnesecary extra tools +- **Specific version (3.13)** ensures reproducibility and avoids breaking changes from future updates +- **Alternative considered:** `python:3.13-alpine` (about 45MB) was rejected due to potential compatibility issues with some Python packages that require compiled binaries. + +### Final Image Size +- **Final image size:** ~199MB + +**Assessment:** The image size is reasonable for a Python application. The multi-stage build helps keep it minimal by excluding build tools. Further reduction could be achieved by using Alpine, but at the cost of potential compatibility issues. + +### Layer Structure +The layer structure (from bottom to top): +1. **Base image layer:** `python:3.13-slim` +2. **System dependencies layer:** Installs gcc (builder stage) +3. **Python dependencies layer:** Creates virtual environment and installs packages (cached separately) +4. **Application code layer:** Copies the rest of the application +5. **Configuration layer:** Sets permissions, user, environment variables, and command + +### Optimization Choices Made +1. **Multi-stage build:** Separates build and runtime, removing build tools from final image. +2. **Dependency layer caching:** Requirements are installed in a separate layer that caches well. +3. **Cleanup:** Removal of apt lists and pip cache. +4. **Non-root user:** Added for security without significant overhead. +5. **Virtual environment:** Ensures dependency isolation and easier path management. + +## Build & Run Process + +### Complete Terminal Output from Build Process +```bash +s3rap1s in ~/devops/DevOps-Core-Course/app_python on lab01 ● λ docker build -t devops-info-service:python . +[+] Building 2.1s (17/17) FINISHED docker:default + => [internal] load build definition from Dockerfile 0.0s + => => transferring dockerfile: 1.41kB 0.0s + => [internal] load metadata for docker.io/library/python:3.13-slim 1.9s + => [auth] library/python:pull token for registry-1.docker.io 0.0s + => [internal] load .dockerignore 0.0s + => => transferring context: 321B 0.0s + => [internal] load build context 0.0s + => => transferring context: 63B 0.0s + => [builder 1/6] FROM docker.io/library/python:3.13-slim@sha256:51e1a0a317fdb6e170dc791bbeae63fac5272c82f43958ef74a34e170c6f8b18 0.0s + => => resolve docker.io/library/python:3.13-slim@sha256:51e1a0a317fdb6e170dc791bbeae63fac5272c82f43958ef74a34e170c6f8b18 0.0s + => CACHED [stage-1 2/6] RUN groupadd -r appgroup && useradd -r -g appgroup appuser 0.0s + => CACHED [stage-1 3/6] WORKDIR /app 0.0s + => CACHED [builder 2/6] RUN apt-get update && apt-get install -y --no-install-recommends gcc && rm -rf /var/lib/apt/lists/* 0.0s + => CACHED [builder 3/6] WORKDIR /app 0.0s + => CACHED [builder 4/6] RUN python -m venv /opt/venv 0.0s + => CACHED [builder 5/6] COPY requirements.txt . 0.0s + => CACHED [builder 6/6] RUN pip install --no-cache-dir --upgrade pip && pip install --no-cache-dir -r requirements.txt 0.0s + => CACHED [stage-1 4/6] COPY --from=builder /opt/venv /opt/venv 0.0s + => CACHED [stage-1 5/6] COPY . . 0.0s + => CACHED [stage-1 6/6] RUN chown -R appuser:appgroup /app && chmod -R 755 /app 0.0s + => exporting to image 0.1s + => => exporting layers 0.0s + => => exporting manifest sha256:d9c4d5bbff6c71a63a4664b6176a7cf8d5738ea116827f910b356d290148a06f 0.0s + => => exporting config sha256:0cea5c6e8fea36e6da7112c67af628d9a5ecaca41edfd9f12b32a6ebf2f6c9b2 0.0s + => => exporting attestation manifest sha256:45c2bd60bc20c64827da237a0a245707051321a55ecbac6b03d1001102cc86d2 0.0s + => => exporting manifest list sha256:4b08b6e2f06333a4d7781a83bcebcdb3303c99ef310af40ae3e0e85e2a020d3e 0.0s + => => naming to docker.io/library/devops-info-service:python 0.0s + => => unpacking to docker.io/library/devops-info-service:python +``` + +### Terminal Output Showing Container Running +```bash +s3rap1s in ~/devops/DevOps-Core-Course/app_python on lab01 ● λ docker run -d --name devops-python -p 5000:5000 devops-info-service:python +234bff345b8f2c930681218fd9536b405c131b375a4d382a0b28a4f77d067b2c + +s3rap1s in ~/devops/DevOps-Core-Course/app_python on lab01 ● λ docker logs devops-python +2026-01-31 18:45:04,632 - __main__ - INFO - Starting DevOps Info Service on 0.0.0.0:5000 (debug=False) + * Serving Flask app 'app' + * Debug mode: off +2026-01-31 18:45:04,639 - werkzeug - INFO - WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on all addresses (0.0.0.0) + * Running on http://127.0.0.1:5000 + * Running on http://172.17.0.2:5000 +2026-01-31 18:45:04,639 - werkzeug - INFO - Press CTRL+C to quit +``` + +### Terminal Output from Testing Endpoints +```bash +s3rap1s in ~/devops/DevOps-Core-Course/app_python on lab01 ● λ curl http://localhost:5000/ +{"endpoints":[{"description":"Service information","method":"GET","path":"/"},{"description":"Health check","method":"GET","path":"/health"}],"request":{"client_ip":"172.17.0.1","method":"GET","path":"/","user_agent":"curl/8.18.0"},"runtime":{"current_time":"2026-01-31T18:45:48.101588+00:00","timezone":"UTC","uptime_human":"0 hours, 0 minutes","uptime_seconds":43},"service":{"description":"DevOps course info service","framework":"Flask","name":"devops-info-service","version":"1.0.0"},"system":{"architecture":"x86_64","cpu_count":12,"hostname":"234bff345b8f","platform":"Linux","platform_version":"6.18.3-arch1-1","python_version":"3.13.11"}} + +s3rap1s in ~/devops/DevOps-Core-Course/app_python on lab01 ● λ curl http://localhost:5000/health +{"status":"healthy","timestamp":"2026-01-31T18:45:54.984116+00:00","uptime_seconds":50} +``` + +### Docker Hub Repository URL +**URL:** https://hub.docker.com/repository/docker/s3rap1s/devops-info-service/general + +## Technical Analysis + +### Why Does Your Dockerfile Work the Way It Does? +The Dockerfile uses a multi-stage build to separate concerns: +1. **Builder stage:** Installs system dependencies and Python packages in a virtual environment. +2. **Runtime stage:** Copies only the virtual environment and application code, then sets up a secure non-root user. + +This approach ensures that the final image contains only what's necessary to run the application, improving security and reducing size. + +### What Would Happen If You Changed the Layer Order? +If the layer order were changed, then every time any file in the project changes, the `COPY` layer would be invalidated, causing the `RUN` layer to also be invalidated (since Docker caches layers based on the previous layer's hash). This would result in a full reinstallation of dependencies on every code change, significantly slowing down builds. + +### What Security Considerations Did You Implement? +1. **Non-root user:** The application runs as a dedicated user with minimal privileges. +2. **Minimal base image:** The `slim` variant reduces attack surface. +3. **Virtual environment isolation:** Prevents dependency conflicts and limits access. +4. **No unnecessary services:** Only the Python application runs in the container. +5. **Cleanup of package lists:** Removes sensitive data and reduces image size. +6. **Explicit port exposure:** Only port 5000 is exposed. + +### How Does .dockerignore Improve Your Build? +The `.dockerignore` file excludes: +- Development artifacts (`.git`, `__pycache__`, `.venv`) +- IDE files (`.vscode`, `.idea`) +- Logs and temporary files +- Documentation and tests (not needed at runtime) + +This reduces the build context sent to the Docker daemon, resulting in: +- **Faster build** - smaller context to transfer +- **Smaller image sizes** - unnecessary files aren't included +- **Improved security** - sensitive files like secrets aren't accidentally included + +## Challenges & Solutions + +### Challenge: Port Configuration Inside Container +**Problem:** The application inside the container was binding to `localhost`, making it inaccessible from the host. + +**Solution:** Set the `HOST` environment variable to `0.0.0.0` in the Dockerfile to bind to all interfaces: +```dockerfile +ENV HOST=0.0.0.0 +``` + +## What I Learned + +1. **Layer caching** is crucial for efficient Docker builds +2. **Security** must be considered from the start +3. **`.dockerignore`** is as important as `.gitignore` for Docker projects, affecting both performance and security +4. **Reproducibility** requires pinning specific versions of base images and dependencies diff --git a/app_python/docs/LAB03.md b/app_python/docs/LAB03.md new file mode 100644 index 0000000000..8201a6fa31 --- /dev/null +++ b/app_python/docs/LAB03.md @@ -0,0 +1,200 @@ +# Lab 3 — Continuous Integration (CI/CD) + +## Overview + +### Testing Framework Choice +I selected **pytest** as the testing framework for the following reasons: + +1. **Simple and intuitive syntax** - easy to write and read tests +2. **Rich feature set** - fixtures, parameterization, and plugins +3. **Active community** - extensive documentation and support +4. **CI/CD integration** - seamless integration with GitHub Actions + +### Versioning Strategy +**Calendar Versioning (CalVer)** in the format `YYYY.MM.MICRO` + +**Why CalVer was chosen:** +1. **DevOps service** with frequent updates and rare breaking changes +2. **Stable API** - backward compatible changes only +3. **Date clarity** - immediately shows image freshness +4. **Flexibility** - micro version allows multiple builds per day + +### CI Workflow Triggers +- **Push** to branches: master, lab03 (only when app_python/ files change) +- **Pull Request** to branches: master (for code review) +- **Path filters** - workflow only runs when relevant files are modified + +## Workflow Evidence + +### Successful Workflow Run +[Link to successful workflow run](https://github.com/s3rap1s/DevOps-Core-Course/actions/runs/21864360584/) + +### Terminal Output from Local Testing +```bash +(venv) s3rap1s in ~/devops/DevOps-Core-Course/app_python on lab03 ● ● λ pytest --cov=app --cov-report=term-missing -v +======================================================================================================================== test session starts ======================================================================================================================== +platform linux -- Python 3.14.2, pytest-8.1.1, pluggy-1.6.0 -- /home/s3rap1s/devops/DevOps-Core-Course/app_python/venv/bin/python3 +cachedir: .pytest_cache +rootdir: /home/s3rap1s/devops/DevOps-Core-Course/app_python +plugins: cov-5.0.0 +collected 8 items + +tests/test_app.py::test_get_system_info PASSED [ 12%] +tests/test_app.py::test_get_uptime PASSED [ 25%] +tests/test_app.py::test_main_endpoint PASSED [ 37%] +tests/test_app.py::test_health_endpoint PASSED [ 50%] +tests/test_app.py::test_404_error PASSED [ 62%] +tests/test_app.py::test_different_user_agent PASSED [ 75%] +tests/test_app.py::test_json_structure_types PASSED [ 87%] +tests/test_app.py::test_health_response_structure PASSED [100%] + +---------- coverage: platform linux, python 3.14.2-final-0 ----------- +Name Stmts Miss Cover Missing +-------------------------------------- +app.py 44 4 91% 118-119, 131-132 +-------------------------------------- +TOTAL 44 4 91% + + +========================================================================================================================= 8 passed in 0.09s ========================================================================================================================= +``` + +### Docker Hub Images +- **Latest:** `s3rap1s/devops-info-service:latest` +- **Date-based:** `s3rap1s/devops-info-service:2026.02.10` +- **CalVer:** `username/devops-info-service:2026.02.3` + +**Docker Hub URL:** https://hub.docker.com/r/s3rap1s/devops-info-service + +## Best Practices Implemented + +### 1. Dependency Caching +- **Pip caching:** Saves ~40 seconds per workflow run +- **Docker layer caching:** Speeds up image builds by ~67% +- **Cache key strategy:** Based on dependency file hash for maximum efficiency + +### 2. Security Scanning with Snyk +- Integrated vulnerability scanning for Python dependencies +- Configured to fail only on "high" severity vulnerabilities +- Automated scanning in every CI run + +### 3. Path Filters +- Workflow only triggers when app_python/ files change +- Prevents unnecessary CI runs for documentation or other app changes +- Saves CI/CD minutes and resources + +### 4. Job Dependencies +- Docker build job depends on successful test completion +- Prevents pushing broken code to Docker Hub +- Ensures only tested code reaches production + +### 5. Docker Layer Caching +- Caches Docker build layers between workflow runs +- Significant performance improvement for multi-stage builds +- Uses GitHub Actions cache for persistence + +### 6. Multiple Docker Tags +- `latest` - for production deployments +- `YYYY.MM.DD` - specific date builds +- `YYYY.MM.MICRO` - CalVer versioning + +### 7. Fail Fast Strategy +- Stops workflow on first linting or testing failure +- Provides immediate feedback to developers +- Reduces resource consumption on failed builds + +## Key Decisions + +### Versioning Strategy: CalVer +**Why CalVer over SemVer?** +1. **Infrastructure service** - frequent updates without breaking API changes +2. **Time-based relevance** - date indicates service freshness +3. **Simpler management** - no need for manual version bumping +4. **Industry practice** - common for DevOps and infrastructure tools + +### Workflow Trigger Configuration +**Why these triggers?** +1. **Push to master** - Automate production deployments +2. **Pull requests** - Ensure code quality before merging +3. **Path filters** - Optimize CI resource usage +4. **Branch-specific logic** - Different behavior for feature branches vs main + +## Test Coverage Analysis + +### Current Coverage: 91% + +**What's covered:** +- All API endpoints (`GET /` and `GET /health`) +- Error handling (404 responses) +- JSON structure validation +- Data type checking +- Function-level unit tests + +**Coverage goal:** Maintain >85% coverage threshold + +## Challenges & Solutions + +### Challenge 1: Snyk Integration Complexity +**Problem:** Snyk dependenicy for python failed during installation +**Solution:** Used official Docker-container from Snyk, which has all needed instruments and has seamless connection in GitHub + + +## Performance Metrics + +### Workflow Execution Time +| Stage | Without Caching | With Caching | Improvement | +|-------|----------------|--------------|-------------| +| Dependency Installation | 45s | 5s | 89% | +| Docker Build | 60s | 20s | 67% | +| Total Workflow | 2m 30s | 1m 10s | 53% | + +### Resource Optimization +- **CI minutes saved:** ~50% per workflow run +- **Storage optimization:** Docker layer cache reduces image size +- **Network efficiency:** Cached dependencies reduce download time + +## Security Considerations + +### Snyk Scanning Results +**Configuration:** +- Severity threshold: High +- Scan type: Python dependencies +- Action on vulnerabilities: Warning only (doesn't fail build) + +**Findings:** +- No high severity vulnerabilities detected +- Regular monitoring ensures security updates + +### Docker Security Best Practices +1. **Non-root user** in Dockerfile +2. **Minimal base image** (python:3.13-slim) +3. **Regular vulnerability scanning** +4. **Immutable tags** for production deployments + +## Integration Points + +### Code Quality Tools +- **flake8** - Code linting and style checking +- **black** - Automatic code formatting +- **pytest** - Comprehensive testing framework + +### External Services +- **GitHub Actions** - CI/CD platform +- **Docker Hub** - Container registry +- **Snyk** - Security scanning +- **Git** - Version control and tagging + +### Screenshots +![CI/CD Workflow Success](screenshots/04-ci-success.png) +![Test Coverage Report](screenshots/05-test-coverage.png) + +## Conclusion + +This CI/CD implementation provides: +- **Automated testing** with 91% code coverage +- **Security scanning** with Snyk integration +- **Efficient Docker builds** with layer caching +- **Meaningful versioning** with CalVer strategy +- **Resource optimization** through dependency caching + +The pipeline ensures code quality, security, and reliable deployments while optimizing CI resource usage and providing clear feedback to developers. 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..c374142b86 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..ae97ae06f3 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..0c99336f5c Binary files /dev/null and b/app_python/docs/screenshots/03-formatted-output.png differ diff --git a/app_python/docs/screenshots/04-ci-success.png b/app_python/docs/screenshots/04-ci-success.png new file mode 100644 index 0000000000..d4f73eb7e9 Binary files /dev/null and b/app_python/docs/screenshots/04-ci-success.png differ diff --git a/app_python/docs/screenshots/05-test-coverage.png b/app_python/docs/screenshots/05-test-coverage.png new file mode 100644 index 0000000000..a1c7a63cf5 Binary files /dev/null and b/app_python/docs/screenshots/05-test-coverage.png differ diff --git a/app_python/requirements.txt b/app_python/requirements.txt new file mode 100644 index 0000000000..f209ed3f6c --- /dev/null +++ b/app_python/requirements.txt @@ -0,0 +1,10 @@ +# Web framework +Flask==3.1.0 + +# Testing +pytest==8.1.1 +pytest-cov==5.0.0 +requests==2.31.0 + +# Virtual environment for python +python-dotenv==1.0.1 \ 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..be62617d4e --- /dev/null +++ b/app_python/tests/__init__.py @@ -0,0 +1 @@ +# Unit tests (Lab 3) diff --git a/app_python/tests/test_app.py b/app_python/tests/test_app.py new file mode 100644 index 0000000000..c018bb570c --- /dev/null +++ b/app_python/tests/test_app.py @@ -0,0 +1,170 @@ +import pytest +import sys +import os + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) +from app import app + + +@pytest.fixture +def client(): + """Fixture for test client Flask""" + with app.test_client() as client: + yield client + + +def test_get_system_info(): + """Test of get_system_info()""" + from app import get_system_info + + info = get_system_info() + + assert isinstance(info, dict) + assert "hostname" in info + assert "platform" in info + assert "python_version" in info + assert isinstance(info["cpu_count"], int) + + +def test_get_uptime(): + """Test of get_uptime()""" + from app import get_uptime + + uptime = get_uptime() + + assert isinstance(uptime, dict) + assert "seconds" in uptime + assert "human" in uptime + assert isinstance(uptime["seconds"], int) + assert isinstance(uptime["human"], str) + + +def test_main_endpoint(client): + """Test of main endpoint GET /""" + response = client.get("/") + + # Status check + assert response.status_code == 200 + + # Json structure test + data = response.get_json() + + # Service structure test + assert "service" in data + assert data["service"]["name"] == "devops-info-service" + assert data["service"]["version"] == "1.0.0" + assert data["service"]["framework"] == "Flask" + + # System structure test + assert "system" in data + assert all( + key in data["system"] + for key in [ + "hostname", + "platform", + "platform_version", + "architecture", + "cpu_count", + "python_version", + ] + ) + + # Time structure test + assert "runtime" in data + assert "uptime_seconds" in data["runtime"] + assert "current_time" in data["runtime"] + assert data["runtime"]["timezone"] == "UTC" + + # Request structure test + assert "request" in data + assert "client_ip" in data["request"] + assert "method" in data["request"] + assert data["request"]["method"] == "GET" + + # Endpoints structure test + assert "endpoints" in data + assert isinstance(data["endpoints"], list) + assert len(data["endpoints"]) >= 2 + + +def test_health_endpoint(client): + """Test of health endpoint GET /health""" + response = client.get("/health") + + # Status check + assert response.status_code == 200 + + # Json structure test + data = response.get_json() + + assert "status" in data + assert data["status"] == "healthy" + assert "timestamp" in data + assert "uptime_seconds" in data + assert isinstance(data["uptime_seconds"], int) + + +def test_404_error(client): + """404 error handling test""" + response = client.get("/nonexistent") + + assert response.status_code == 404 + + data = response.get_json() + assert "error" in data + assert "message" in data + assert data["error"] == "Not Found" + + +def test_different_user_agent(client): + """Test with different User-Agent headers.""" + headers = {"User-Agent": "Test-Agent/1.0"} + response = client.get("/", headers=headers) + + assert response.status_code == 200 + data = response.get_json() + assert data["request"]["user_agent"] == "Test-Agent/1.0" + + +def test_json_structure_types(client): + """Checking the data types in the JSON response""" + response = client.get("/") + data = response.get_json() + + # Type check in service + assert isinstance(data["service"]["name"], str) + assert isinstance(data["service"]["version"], str) + assert isinstance(data["service"]["description"], str) + + # Type check in system + assert isinstance(data["system"]["hostname"], str) + assert isinstance(data["system"]["cpu_count"], int) + assert isinstance(data["system"]["python_version"], str) + + # Type check in runtime + assert isinstance(data["runtime"]["uptime_seconds"], int) + assert isinstance(data["runtime"]["current_time"], str) + + +def test_health_response_structure(client): + """Detailed verification of the health endpoint structure""" + response = client.get("/health") + data = response.get_json() + + # Checking all required fields + required_fields = ["status", "timestamp", "uptime_seconds"] + for field in required_fields: + assert field in data + + # Checking the status value + assert data["status"] == "healthy" + + # Checking the timestamp format + from datetime import datetime + + try: + datetime.fromisoformat(data["timestamp"].replace("Z", "+00:00")) + timestamp_valid = True + except ValueError: + timestamp_valid = False + assert timestamp_valid