diff --git a/.gitignore b/.gitignore index 30d74d2584..236b92ec13 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,13 @@ -test \ No newline at end of file +__pycache__/ +*.py[cod] +venv/ +__MACOSX/ +*.log + +.vscode/ +.idea/ + +.obsidian + +.DS_Store +.env \ No newline at end of file diff --git a/app_go/README.md b/app_go/README.md new file mode 100644 index 0000000000..9e7e356fb0 --- /dev/null +++ b/app_go/README.md @@ -0,0 +1,53 @@ +# DevOps Info Service (Go) — Bonus Task + +## Overview +A Go implementation of the DevOps Info Service. It provides two endpoints: +- `GET /` returns service/system/runtime/request information in JSON +- `GET /health` returns a simple health status JSON + +## Prerequisites +- Go installed (check with `go version`) + +## Run (from source) +```bash +go run . +``` +By default the service listens on `0.0.0.0:8080`. + +### Custom configuration + +```bash +HOST=127.0.0.1 PORT=9090 go run . +``` + +## Build (binary) + +```bash +go build -o devops-info-service +``` + +## Run (binary) + +```bash +./devops-info-service +``` + +### Custom configuration (binary) + +```bash +HOST=127.0.0.1 PORT=9090 ./devops-info-service +``` + +## API Endpoints + +### GET / + +```bash +curl -s http://127.0.0.1:8080/ | python -m json.tool +``` + +### GET /health + +```bash +curl -s http://127.0.0.1:8080/health | python -m json.tool +``` diff --git a/app_go/devops-info-service b/app_go/devops-info-service new file mode 100755 index 0000000000..2b6d5eb4fd Binary files /dev/null and b/app_go/devops-info-service differ diff --git a/app_go/docs/GO.md b/app_go/docs/GO.md new file mode 100644 index 0000000000..dd58a9af72 --- /dev/null +++ b/app_go/docs/GO.md @@ -0,0 +1,7 @@ +# Why Go (Compiled Language) + +I chose Go for the compiled-language bonus task because: +- Go compiles into a single binary, which is convenient for deployment. +- It has a minimal standard library for building HTTP services (`net/http`). +- It’s fast to build and run and is commonly used in infrastructure/DevOps tooling. +- Small, self-contained binaries work well with Docker multi-stage builds in later labs. \ 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..25a045574f --- /dev/null +++ b/app_go/docs/LAB01.md @@ -0,0 +1,49 @@ +# LAB01 — Bonus Task (Go) + +## Implemented Endpoints +- `GET /` — returns service, system, runtime, request info + endpoints list (JSON) +- `GET /health` — returns health status + timestamp + uptime_seconds (JSON) + +The JSON structure matches the Python version (same top-level fields and same key layout inside each section). + +## How to Run (from source) +```bash +go run . +``` +#### Test: + +```bash +curl -s http://127.0.0.1:8080/ | python -m json.tool curl -s http://127.0.0.1:8080/health | python -m json.tool +``` + +## How to Build and Run (binary) + +#### Build: + +```bash +go build -o devops-info-service ls -lh devops-info-service +``` + +#### Run: + +```bash +./devops-info-service +``` + +#### Test binary: + +```bash +curl -s http://127.0.0.1:8080/health | python -m json.tool +``` + +## Screenshots + +Screenshots are stored in `docs/screenshots/`: + +Recommended set: +- `01-go-run.png` — running from source (`go run .`) +- `02-main-endpoint.png` — `GET /` output +- `03-health-endpoint.png` — `GET /health` output +- `04-go-build.png` — `go build` + `ls -lh` showing binary size +- `05-binary-run.png` — running compiled binary (`./devops-info-service`) +- `06-binary-health.png` — health check from binary run \ No newline at end of file diff --git a/app_go/docs/screenshots/01-go-run.png b/app_go/docs/screenshots/01-go-run.png new file mode 100644 index 0000000000..c4c509db6b Binary files /dev/null and b/app_go/docs/screenshots/01-go-run.png differ diff --git a/app_go/docs/screenshots/02-main-endpoint.png b/app_go/docs/screenshots/02-main-endpoint.png new file mode 100644 index 0000000000..b3feb72f57 Binary files /dev/null and b/app_go/docs/screenshots/02-main-endpoint.png differ diff --git a/app_go/docs/screenshots/03-health-endpoint.png b/app_go/docs/screenshots/03-health-endpoint.png new file mode 100644 index 0000000000..da87ce83f7 Binary files /dev/null and b/app_go/docs/screenshots/03-health-endpoint.png differ diff --git a/app_go/docs/screenshots/04-go-build.png b/app_go/docs/screenshots/04-go-build.png new file mode 100644 index 0000000000..a3b615a218 Binary files /dev/null and b/app_go/docs/screenshots/04-go-build.png differ diff --git a/app_go/docs/screenshots/05-binary-run.png b/app_go/docs/screenshots/05-binary-run.png new file mode 100644 index 0000000000..ce3bea4ff6 Binary files /dev/null and b/app_go/docs/screenshots/05-binary-run.png differ diff --git a/app_go/docs/screenshots/06-binary-health.png b/app_go/docs/screenshots/06-binary-health.png new file mode 100644 index 0000000000..68dc2894a7 Binary files /dev/null and b/app_go/docs/screenshots/06-binary-health.png differ diff --git a/app_go/go.mod b/app_go/go.mod new file mode 100644 index 0000000000..e5f2d86c4b --- /dev/null +++ b/app_go/go.mod @@ -0,0 +1,3 @@ +module devops-info-service-go + +go 1.25.6 diff --git a/app_go/main.go b/app_go/main.go new file mode 100644 index 0000000000..7c57e28961 --- /dev/null +++ b/app_go/main.go @@ -0,0 +1,139 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "net" + "net/http" + "os" + "runtime" + "time" +) + +var startTime = time.Now().UTC() + +type Endpoint struct { + Path string `json:"path"` + Method string `json:"method"` + Description string `json:"description"` +} + +type ResponseRoot struct { + Service map[string]any `json:"service"` + System map[string]any `json:"system"` + Runtime map[string]any `json:"runtime"` + Request map[string]any `json:"request"` + Endpoints []Endpoint `json:"endpoints"` +} + +func uptimeSeconds() int { + return int(time.Since(startTime).Seconds()) +} + +func uptimeHuman(sec int) string { + h := sec / 3600 + m := (sec % 3600) / 60 + return fmt.Sprintf("%d hour(s), %d minute(s)", h, m) +} + +func hostname() string { + h, err := os.Hostname() + if err != nil { + return "" + } + return h +} + +func getClientIP(r *http.Request) string { + xff := r.Header.Get("X-Forwarded-For") + if xff != "" { + for i := 0; i < len(xff); i++ { + if xff[i] == ',' { + return xff[:i] + } + } + return xff + } + + host, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + return r.RemoteAddr + } + return host +} + +func writeJSON(w http.ResponseWriter, status int, v any) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + enc := json.NewEncoder(w) + enc.SetIndent("", " ") + _ = enc.Encode(v) +} + +func mainHandler(w http.ResponseWriter, r *http.Request) { + up := uptimeSeconds() + + resp := ResponseRoot{ + Service: map[string]any{ + "name": "devops-info-service", + "version": "1.0.0", + "description": "DevOps course info service", + "framework": "Go net/http", + }, + System: map[string]any{ + "hostname": hostname(), + "platform": runtime.GOOS, + "platform_version": "", + "architecture": runtime.GOARCH, + "cpu_count": runtime.NumCPU(), + "python_version": runtime.Version(), + }, + Runtime: map[string]any{ + "uptime_seconds": up, + "uptime_human": uptimeHuman(up), + "current_time": time.Now().UTC().Format(time.RFC3339Nano), + "timezone": "UTC", + }, + Request: map[string]any{ + "client_ip": getClientIP(r), + "user_agent": r.UserAgent(), + "method": r.Method, + "path": r.URL.Path, + }, + Endpoints: []Endpoint{ + {Path: "/", Method: "GET", Description: "Service information"}, + {Path: "/health", Method: "GET", Description: "Health check"}, + }, + } + + log.Printf("Request: %s %s", r.Method, r.URL.Path) + writeJSON(w, http.StatusOK, resp) +} + +func healthHandler(w http.ResponseWriter, r *http.Request) { + log.Printf("Request: %s %s", r.Method, r.URL.Path) + writeJSON(w, http.StatusOK, map[string]any{ + "status": "healthy", + "timestamp": time.Now().UTC().Format(time.RFC3339Nano), + "uptime_seconds": uptimeSeconds(), + }) +} + +func main() { + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + host := os.Getenv("HOST") + if host == "" { + host = "0.0.0.0" + } + + http.HandleFunc("/", mainHandler) + http.HandleFunc("/health", healthHandler) + + addr := host + ":" + port + log.Printf("Starting Go app on %s", addr) + log.Fatal(http.ListenAndServe(addr, nil)) +} diff --git a/app_python/.gitignore b/app_python/.gitignore new file mode 100644 index 0000000000..236b92ec13 --- /dev/null +++ b/app_python/.gitignore @@ -0,0 +1,13 @@ +__pycache__/ +*.py[cod] +venv/ +__MACOSX/ +*.log + +.vscode/ +.idea/ + +.obsidian + +.DS_Store +.env \ No newline at end of file diff --git a/app_python/README.md b/app_python/README.md new file mode 100644 index 0000000000..e9d27c4ef1 --- /dev/null +++ b/app_python/README.md @@ -0,0 +1,34 @@ +# DevOps Info Service (Lab 01) + +## Overview +Simple web service that returns service, system, runtime and request information. + +## 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 +PORT=8080 python app.py +HOST=127.0.0.1 PORT=3000 python app.py +``` + +## API Endpoints + +- `GET /` - Service and system information +- `GET /health` - Health check +## Configuration + +|Variable|Default|Description| +|---|---|---| +|HOST|0.0.0.0|Bind host| +|PORT|5000|Bind port| +|DEBUG|False|Flask debug mode \ No newline at end of file diff --git a/app_python/app.py b/app_python/app.py new file mode 100644 index 0000000000..8514d3da91 --- /dev/null +++ b/app_python/app.py @@ -0,0 +1,115 @@ +import logging +import os +import platform +import socket +from datetime import datetime, timezone + +from flask import Flask, jsonify, request + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", +) +logger = logging.getLogger("devops-info-service") + +HOST = os.getenv("HOST", "0.0.0.0") +PORT = int(os.getenv("PORT", "5000")) +DEBUG = os.getenv("DEBUG", "False").lower() == "true" + +app = Flask(__name__) +START_TIME = datetime.now(timezone.utc) + + +def _uptime_seconds() -> int: + return int((datetime.now(timezone.utc) - START_TIME).total_seconds()) + + +def _uptime_human(seconds: int) -> str: + hours = seconds // 3600 + minutes = (seconds % 3600) // 60 + return f"{hours} hour(s), {minutes} minute(s)" + + +def get_system_info() -> dict: + return { + "hostname": socket.gethostname(), + "platform": platform.system(), + "platform_version": platform.version(), + "architecture": platform.machine(), + "cpu_count": os.cpu_count() or 0, + "python_version": platform.python_version(), + } + + +def get_request_info() -> dict: + forwarded_for = request.headers.get("X-Forwarded-For", "") + client_ip = forwarded_for.split(",")[0].strip() if forwarded_for else request.remote_addr + + return { + "client_ip": client_ip or "", + "user_agent": request.headers.get("User-Agent", ""), + "method": request.method, + "path": request.path, + } + + +def list_endpoints() -> list: + return [ + {"path": "/", "method": "GET", "description": "Service information"}, + {"path": "/health", "method": "GET", "description": "Health check"}, + ] + + +@app.get("/") +def index(): + logger.info("Request: %s %s", request.method, request.path) + + uptime_sec = _uptime_seconds() + payload = { + "service": { + "name": "devops-info-service", + "version": "1.0.0", + "description": "DevOps course info service", + "framework": "Flask", + }, + "system": get_system_info(), + "runtime": { + "uptime_seconds": uptime_sec, + "uptime_human": _uptime_human(uptime_sec), + "current_time": datetime.now(timezone.utc).isoformat(), + "timezone": "UTC", + }, + "request": get_request_info(), + "endpoints": list_endpoints(), + } + return jsonify(payload), 200 + + +@app.get("/health") +def health(): + logger.info("Request: %s %s", request.method, request.path) + + return ( + jsonify( + { + "status": "healthy", + "timestamp": datetime.now(timezone.utc).isoformat(), + "uptime_seconds": _uptime_seconds(), + } + ), + 200, + ) + +@app.errorhandler(404) +def not_found(_err): + return jsonify({"error": "Not Found", "message": "Endpoint does not exist"}), 404 + + +@app.errorhandler(500) +def internal_error(_err): + return jsonify({"error": "Internal Server Error", "message": "An unexpected error occurred"}), 500 + + +if __name__ == "__main__": + logger.info("Starting app on %s:%s (debug=%s)", HOST, PORT, 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..1d18796cfa --- /dev/null +++ b/app_python/docs/LAB01.md @@ -0,0 +1,65 @@ +# Lab 1 + +## 1) Framework Selection +**Chosen framework:** Flask + +**Why Flask:** +- Minimal setup and easy to understand for a beginner +- Perfect for a small service with only a couple endpoints +- Clear request handling and simple JSON responses + +| Framework | Pros | Cons | +|---|---|---| +| Flask | Simple, lightweight, easy learning curve | Less “built-in” features than Django | +| FastAPI | Great docs, async-ready, OpenAPI | Slightly more concepts (typing, ASGI) | +| Django | Full-featured framework | Overkill for this small service | + +## 2) Best Practices Applied +### Clean Code Organization +- Separate helper functions: system info, request info, uptime +- Clear naming and small functions + +### Configuration via Environment Variables +- `HOST`, `PORT`, `DEBUG` are read from environment variables + +### Error Handling +- Custom JSON responses for 404 and 500 errors + +### Logging +- Basic logging configured (INFO level) +- Logs requests to `/` and `/health` + +## 3) API Documentation +### GET / +Returns service metadata, system information, runtime info and request details. + +Example test: +```bash +curl -s http://127.0.0.1:5000/ | python -m json.tool +``` +### GET /health + +Returns health status, timestamp and uptime. + +Example test: + +`curl -s http://127.0.0.1:5000/health | python -m json.tool` + +## 4) Testing Evidence + +Screenshots are stored in: +`docs/screenshots/` +- `main-endpoint.png` — main endpoint JSON output +- `health-check.png` — health endpoint JSON output + +## 5) Challenges & Solutions + +- **Challenge:** Understanding required JSON structure + **Solution:** Implemented endpoints step-by-step and validated output using curl + json.tool. +- **Challenge:** Making the service configurable + **Solution:** Added environment variables `HOST` and `PORT` and verified by running on port 8080. + +## 6) GitHub Community + +Starring repositories helps bookmark useful projects and signals appreciation to maintainers, improving open-source discovery. +Following developers (professor/TAs/classmates) helps networking and makes it easier to learn from others’ activity and collaborate in team projects. \ 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..dcc5cb65de 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..0df0550336 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..fc962b6bc1 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