diff --git a/app_go/.gitignore b/app_go/.gitignore new file mode 100644 index 0000000000..9102a5aa0c --- /dev/null +++ b/app_go/.gitignore @@ -0,0 +1 @@ +devops-info diff --git a/app_go/README.md b/app_go/README.md new file mode 100644 index 0000000000..ef223cdf6b --- /dev/null +++ b/app_go/README.md @@ -0,0 +1,22 @@ +# DevOps Info Service (Go) + +## Overview +Compiled implementation of the DevOps Info Service using Go. +Designed for efficient containerization and minimal runtime footprint. + +## Requirements +- Go 1.22+ + +## Build +```bash +go build -o devops-info +``` + +## Run +```bash +./devops-info +``` + +## Endpoints +- `GET /` +- `GET /health` \ 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..b2a73f8eb3 --- /dev/null +++ b/app_go/docs/GO.md @@ -0,0 +1,5 @@ +## Why Go + +Go was chosen for the compiled implementation due to its simplicity, fast compilation, and suitability for containerized environments. +It produces a single static binary, which makes it ideal for multi-stage Docker builds and minimal runtime images. +Go is widely used in DevOps tooling, including Docker, Kubernetes, and Prometheus. \ 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..9086d88b6b --- /dev/null +++ b/app_go/docs/LAB01.md @@ -0,0 +1,17 @@ +## Implementation Details + +The Go version implements the same API structure as the Python service using the standard `net/http` package. + +## Build Process +```bash +go build -o devops-info +``` + +## Binary Size Comparison + +| Implementation | Size | +| -------------- | ---------------------------- | +| Python (Flask) | ~30–50 MB (with venv & deps) | +| Go binary | ~7–10 MB | + +The Go binary does not require an interpreter or external dependencies, making it more efficient for deployment. 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..8a1fd0664a Binary files /dev/null and b/app_go/docs/screenshots/01-main-endpoint.png differ diff --git a/app_go/docs/screenshots/02-health-endpoint.png b/app_go/docs/screenshots/02-health-endpoint.png new file mode 100644 index 0000000000..3198a1d51b Binary files /dev/null and b/app_go/docs/screenshots/02-health-endpoint.png differ diff --git a/app_go/go.mod b/app_go/go.mod new file mode 100644 index 0000000000..7a7fcedd1c --- /dev/null +++ b/app_go/go.mod @@ -0,0 +1,3 @@ +module devops-info-service + +go 1.22 diff --git a/app_go/main.go b/app_go/main.go new file mode 100644 index 0000000000..ad518ebfd9 --- /dev/null +++ b/app_go/main.go @@ -0,0 +1,123 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "os" + "runtime" + "time" +) + +var startTime = time.Now().UTC() + +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"` + Architecture string `json:"architecture"` + CPUCount int `json:"cpu_count"` + GoVersion string `json:"go_version"` +} + +type RuntimeInfo struct { + UptimeSeconds int `json:"uptime_seconds"` + UptimeHuman string `json:"uptime_human"` + CurrentTime string `json:"current_time"` + Timezone string `json:"timezone"` +} + +type RequestInfo struct { + Method string `json:"method"` + Path string `json:"path"` +} + +type Endpoint struct { + Path string `json:"path"` + Method string `json:"method"` + Description string `json:"description"` +} + +type ServiceInfo struct { + Service Service `json:"service"` + System System `json:"system"` + Runtime RuntimeInfo `json:"runtime"` + Request RequestInfo `json:"request"` + Endpoints []Endpoint `json:"endpoints"` +} + +func getUptime() (int, string) { + seconds := int(time.Since(startTime).Seconds()) + hours := seconds / 3600 + minutes := (seconds % 3600) / 60 + return seconds, fmt.Sprintf("%d hours %d minutes", hours, minutes) +} + +func mainHandler(w http.ResponseWriter, r *http.Request) { + hostname, _ := os.Hostname() + uptimeSeconds, uptimeHuman := getUptime() + + info := ServiceInfo{ + Service: Service{ + Name: "devops-info-service", + Version: "1.0.0", + Description: "DevOps Course Info Service", + Framework: "Go net/http", + }, + System: System{ + Hostname: hostname, + Platform: runtime.GOOS, + Architecture: runtime.GOARCH, + CPUCount: runtime.NumCPU(), + GoVersion: runtime.Version(), + }, + Runtime: RuntimeInfo{ + UptimeSeconds: uptimeSeconds, + UptimeHuman: uptimeHuman, + CurrentTime: time.Now().UTC().Format(time.RFC3339), + Timezone: "UTC", + }, + Request: RequestInfo{ + Method: r.Method, + Path: r.URL.Path, + }, + Endpoints: []Endpoint{ + {Path: "/", Method: "GET", Description: "Main service information endpoint"}, + {Path: "/health", Method: "GET", Description: "Health check endpoint"}, + }, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(info) +} + +func healthHandler(w http.ResponseWriter, r *http.Request) { + uptimeSeconds, _ := getUptime() + + response := map[string]interface{}{ + "status": "healthy", + "timestamp": time.Now().UTC().Format(time.RFC3339), + "uptime_seconds": uptimeSeconds, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +func main() { + http.HandleFunc("/", mainHandler) + http.HandleFunc("/health", healthHandler) + + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + + http.ListenAndServe(":"+port, nil) +} 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/README.md b/app_python/README.md new file mode 100644 index 0000000000..c4cd29f3e7 --- /dev/null +++ b/app_python/README.md @@ -0,0 +1,36 @@ +# DevOps Info Service + +## Overview +A lightweight web service, that exposes the system, runtime, and request information. +Used as a foundation for DevOps labs (Docker, CI/CD, monitoring). + +## Prerequisites +- Python 3.11+ +- pip + +## 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 +``` + +## API Endpoints +- `GET /` - Service and system information +- `GET /health` - Health check + +## Configuration + +| Variable | Default | Description | +| ---------- | --------- | ------------------ | +| `HOST` | `0.0.0.0` | Bind address | +| `PORT` | `5000` | Server port number | +| `DEBUG` | `false` | Debug mode | + diff --git a/app_python/app.py b/app_python/app.py new file mode 100644 index 0000000000..0810721be0 --- /dev/null +++ b/app_python/app.py @@ -0,0 +1,105 @@ +import os +import platform +import socket +import logging +from datetime import datetime, timezone + +from flask import Flask, jsonify, request + +### Configuration + +HOST = os.getenv("HOST", "0.0.0.0") +PORT = int(os.getenv("PORT", 5000)) +DEBUG = os.getenv("DEBUG", "False").lower() == "true" + +SERVICE_NAME = "devops-info-service" +SERVICE_VERSION = "1.0.0" +SERVICE_DESCRIPTION = "DevOps course info service" +FRAMEWORK = "Flask" + +### App and logging + +app = Flask(__name__) + +logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") +logger = logging.getLogger(__name__) + +START_TIME = datetime.now(timezone.utc) + +### Helper functions + +def get_uptime(): + delta = datetime.now(timezone.utc) - START_TIME + seconds = int(delta.total_seconds()) + minutes = (seconds % 3600) // 60 + hours = seconds // 3600 + return seconds, f"{hours} hours, {minutes} minutes" + +def get_system_info(): + return { + "hostname": socket.gethostname(), + "platform": platform.system(), + "platform-version": platform.version(), + "architecture": platform.machine(), + "cpu-count": os.cpu_count(), + "python-version": platform.python_version(), + } + +### Routes + +@app.route("/", methods=["GET"]) +def index(): + uptime_seconds, uptime_human = get_uptime() + + logger.info("Main endpoint accessed") + + response = { + "service": { + "name": SERVICE_NAME, + "version": SERVICE_VERSION, + "description": SERVICE_DESCRIPTION, + "framework": FRAMEWORK + }, + "system": get_system_info(), + "runtime": { + "uptime_seconds": uptime_seconds, + "uptime_human": uptime_human, + "current_time": datetime.now(timezone.utc).isoformat(), + "timezone": "UTC" + }, + "request": { + "client_ip": request.remote_addr, + "user_agent": request.headers.get("User-Agent"), + "method": request.method, + "path": request.path + }, + "endpoints": [ + {"path": "/", "method": "GET", "description": "Service information"}, + {"path": "/health", "method": "GET", "description": "Health check"} + ], + } + + return jsonify(response) + +@app.route("/health", methods=["GET"]) +def health(): + uptime_seconds, _ = get_uptime() + + return jsonify({ + "status": "healthy", + "timestamp": datetime.now(timezone.utc).isoformat(), + "uptime_seconds": uptime_seconds, + }) + +### Error Handlers +@app.errorhandler(404) +def not_found(error): + return jsonify({"error": "Not Found", "message": "Endpoint does not exist."}), 404 + +@app.errorhandler(500) +def internal_error(error): + return jsonify({"error": "Internal Server Error", "message": "An unexpected error occurred."}), 500 + +### Entrypoint +if __name__ == "__main__": + app.run(host=HOST, port=PORT, debug=DEBUG) \ No newline at end of file diff --git a/app_python/docs/LAB01.md b/app_python/docs/LAB01.md new file mode 100644 index 0000000000..191eede5c8 --- /dev/null +++ b/app_python/docs/LAB01.md @@ -0,0 +1,110 @@ +# Lab 1 - DevOps Info Service: Web Application Development + +## Framework selection +For this lab, I chose **Flask** as the web framework. +Flask is a lightweight and minimalistic framework that provides full control over request handling and application structure. + +I had prior experience working with Flask, which allowed me to focus on the DevOps-related goals of the assignment rather than spending time learning a new framework. + +Flask is well-suited for small services and internal tools, which aligns with the purpose of this DevOps Info Service. Its simplicity makes the application easier to understand, maintain, containerize, and monitor in later labs. + +## Best Practices Applied +- PEP 8 +- Centralized config via env vars +- Structured logging +- Explicit error handlers + +## API Documentation +### Request / response examples: +#### 1. Main Endpoint `Get /` + +**Request:** +```bash +curl http://localhost:5000 | jq +``` + +**Response Example:** +```json +{ + "endpoints": [ + { + "description": "Service information", + "method": "GET", + "path": "/" + }, + { + "description": "Health check", + "method": "GET", + "path": "/health" + } + ], + "request": { + "client_ip": "127.0.0.1", + "method": "GET", + "path": "/", + "user_agent": "curl/8.7.1" + }, + "runtime": { + "current_time": "2026-01-27T12:16:29.586466+00:00", + "timezone": "UTC", + "uptime_human": "0 hours, 0 minutes", + "uptime_seconds": 7 + }, + "service": { + "description": "DevOps course info service", + "framework": "Flask", + "name": "devops-info-service", + "version": "1.0.0" + }, + "system": { + "architecture": "x86_64", + "cpu-count": 8, + "hostname": "MacBook-Pro-Aliia.local", + "platform": "Darwin", + "platform-version": "Darwin Kernel Version 24.6.0: Wed Nov 5 21:30:23 PST 2025; root:xnu-11417.140.69.705.2~1/RELEASE_X86_64", + "python-version": "3.11.0" + } +} +``` + +#### 2. Health check `Get /health` + +**Request:** +```bash +curl http://localhost:5000 | jq +``` + +**Response Example:** +```json +{ + "status": "healthy", + "timestamp": "2026-01-27T13:44:02.759040+00:00", + "uptime_seconds": 5260 +} +``` + +## Testing Evidence +### Main Endpoint: +![01-main-endpoint](screenshots/01-main-endpoint.png) +### Health Check: +![02-health-check](screenshots/02-health-check.png) +### Formatted Output: +![03-formatted-output](screenshots/03-formatted-output.png) + +## Challenges & Solutions +### Correct Uptime Calculation +**Problem:** +The application is required to report accurate uptime information in seconds and in a human-readable format. A naive implementation based on system uptime or repeated timestamp calculations can lead to inconsistent results, especially after application restarts or during long-running sessions. + +**Solution:** +To ensure consistent and deterministic uptime values, the application records a fixed START_TIME at launch using UTC time. Uptime is then calculated as the difference between the current UTC timestamp and this initial start time. +This approach guarantees stable and reproducible uptime values and avoids issues related to local time zones or system clock changes. + +## GitHub Community +### Importance of Starring Repositories +Starring repositories on GitHub serves both practical and community purposes. From a personal perspective, stars act as bookmarks that make it easier to return to useful tools and references. From a community perspective, stars provide a visible signal of interest and trust, helping high-quality projects gain visibility and attract contributors. +For open-source maintainers, stars also function as feedback and motivation to continue development. + +### Value of Following Developers and Classmates +Following developers on GitHub allows staying informed about their activity, projects, and coding practices. This helps with learning through real-world examples and understanding how others approach problem-solving. +Following classmates supports collaboration within the course by making it easier to discover their work, exchange ideas, and build connections that can be useful in future team-based projects and professional development. \ No newline at end of file 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..f1885c3a70 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..d851293efa 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..d45bd4a7db Binary files /dev/null and b/app_python/docs/screenshots/03-formatted-output.png differ diff --git a/app_python/requirements.txt b/app_python/requirements.txt new file mode 100644 index 0000000000..78180a1ad1 --- /dev/null +++ b/app_python/requirements.txt @@ -0,0 +1 @@ +Flask==3.1.0 \ 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..e69de29bb2