diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml new file mode 100644 index 0000000000..8529db24df --- /dev/null +++ b/.github/workflows/python-ci.yml @@ -0,0 +1,67 @@ +name: Python CI + +on: + push: + paths: + - "app_python/**" + - ".github/workflows/python-ci.yml" + pull_request: + paths: + - "app_python/**" + +jobs: + test-and-build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Cache pip dependencies + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('app_python/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install dependencies + run: | + pip install --upgrade pip + pip install -r app_python/requirements.txt + + - name: Run tests + run: | + cd app_python + pytest + + - name: Docker login + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set Docker image version + run: echo "VERSION=$(date +%Y.%m.%d)" >> $GITHUB_ENV + + - name: Build and push Docker image + uses: docker/build-push-action@v6 + with: + context: app_python + push: true + tags: | + ${{ secrets.DOCKERHUB_USERNAME }}/devops-info-python:${{ env.VERSION }} + ${{ secrets.DOCKERHUB_USERNAME }}/devops-info-python:latest + + - name: Install Snyk + run: npm install -g snyk + + - name: Run Snyk test + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + run: snyk test --all-projects --severity-threshold=high 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/.dockerignore b/app_python/.dockerignore new file mode 100644 index 0000000000..062d1e4670 --- /dev/null +++ b/app_python/.dockerignore @@ -0,0 +1,15 @@ +__pycache__/ +*.pyc +*.pyo +*.pyd + +.git/ +.gitignore + +venv/ +.venv/ + +docs/ +tests/ + +README.md \ 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..602ef154d9 --- /dev/null +++ b/app_python/Dockerfile @@ -0,0 +1,23 @@ +FROM python:3.13-slim + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +# Non-root user setup +RUN useradd --create-home appuser + +WORKDIR /app + +COPY requirements.txt . + +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY app.py . + +#Swithh to non-root user +USER appuser + +EXPOSE 8000 + +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..8d17200a4b --- /dev/null +++ b/app_python/README.md @@ -0,0 +1,54 @@ +# DevOps Info Service + +[![Python CI](https://github.com/Gpshfrd/DevOps-Core-Course/actions/workflows/python-ci.yml/badge.svg)](https://github.com/Gpshfrd/DevOps-Core-Course/actions/workflows/python-ci.yml) + +## 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 | + +## Docker + +Build image locally: +```bash +docker build -t devops-info-python . +``` + +Run container: +```bash +docker run -p 8000:8000 devops-info-python +``` + +Pull from Docker Hub: +```bash +docker pull gpshfrd/devops-info-python:1.0.0 +``` \ No newline at end of file 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..559a1290be --- /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/lab01/01-main-endpoint.png) +### Health Check: +![02-health-check](screenshots/lab01/02-health-check.png) +### Formatted Output: +![03-formatted-output](screenshots/lab01/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/LAB02.md b/app_python/docs/LAB02.md new file mode 100644 index 0000000000..c5b7766396 --- /dev/null +++ b/app_python/docs/LAB02.md @@ -0,0 +1,56 @@ +# Lab 2 - DevOps Info Service: Docker Containerization + +## Docker Best Practices Applied +**Non-root user** +The container runs under a non-root user created inside the image, which reduces security risks by limiting container privileges in case of compromise. + +**Layer caching** +`requirements.txt` is copied before application code to leverage Docker layer caching, which significantly speeds up rebuilds when application code changes, but dependencies do not. + +## Image information and Decisions +Base image: python:3.13-slim +Reason: smaller image size comparing with full python image while keeping glibc compatibility. + +Final image size: 182.36MB +Acceptable for a python service with external dependencies. + +## Build and Run Processes +### Docker build +![docker-build](screenshots/lab02/docker-build.png) + +### Docker run +![docker-run](screenshots/lab02/docker-run.png) + +### Docker push +![docker-push](screenshots/lab02/docker-push.png) + +### Check +![main-endpoint](screenshots/lab02/main-endpoint.png) +![health-check](screenshots/lab02/health-check.png) + +### Docker Hub URL +https://hub.docker.com/r/gpshfrd/devops-info-python + +## Technical Analysis +### Why does your Dockerfile work the way it does? +The Dockerfile is written step by step so Docker can build the image correctly and efficiently. +A base image is chosen first, then the working directory is set. Dependencies are installed before copying the application code so Docker can reuse cached layers. +The application is started with a clear command, so the container always runs the same way. + +### What would happen if you changed the layer order? +Docker uses caching. +If the source code is copied before installing dependencies, Docker will reinstall dependencies every time the code changes. This makes builds slower. Correct layer order helps Docker reuse layers and build images faster. + +### What security considerations did you implement? +- The application runs as a non-root user +- A minimal base image is used +- Only necessary files are copied into the container + +### How does .dockerignore improve your build? +`.dockerignore` prevents unnecessary files from being included in the Docker build, which makes the build faster, reduces image size, and avoids copying files that are not needed for running the app. + +## Challenges and Solutions +Initially, I tried to launch the container using the wrong name of the local image. I was also a little confused with the port mapping, so curl returned connection errors. + +When starting the container, I rechecked the name of the image with the tag (docker run -p 8000:5000 username/image:tag). +Also I checked port mapping between container and host to make sure endpoints were reachable. \ No newline at end of file diff --git a/app_python/docs/LAB03.md b/app_python/docs/LAB03.md new file mode 100644 index 0000000000..74376504f9 --- /dev/null +++ b/app_python/docs/LAB03.md @@ -0,0 +1,63 @@ +# Lab 3 — Continuous Integration (CI/CD) + +## 1. Overview + +**Testing Framework:** 'pytest' — simple syntax, powerful fixtures, convenient integration with CI. + +**Endpoints Covered:** +- `GET /` — checking the JSON structure and the presence of fields +- `GET /health` — checking the health status of the app + +**CI Workflow Triggers:** +- Push and pull request for `app_python/**` +- Workflow starts only when the Python application code is changed + +**Versioning Strategy:** +- **CalVer (Calendar Versioning)** — the `YYYY.MM.DD` format version +- Automatically generated from the current date +- Docker tags: `latest` and `YYYY.MM.DD` + +--- + +## 2. Workflow Evidence + +**GitHub Actions CI Run:** [Link to successful workflow](https://github.com/Gpshfrd/DevOps-Core-Course/actions/runs/21863939737) +![run-complete](screenshots/lab03/run_complete.png) + +**Local Test Run:** +![tests-passed](screenshots/lab03/tests_passed.png) + +Docker Image on Docker Hub: +![tags](screenshots/lab03/tags.png) + +[![Python CI](https://github.com/Gpshfrd/DevOps-Core-Course/actions/workflows/python-ci.yml/badge.svg)](https://github.com/Gpshfrd/DevOps-Core-Course/actions/workflows/python-ci.yml) displayed at the top of the README, shows the current CI status (passing/failing) + +## 3. Best Practices Implemented + +- **Dependency Caching** — speeds up the installation of Python dependencies: +`actions/setup-python@v5 with cache: pip` +**Speed improvement**: from 15s to 5s on repeated launches (approximately 3x acceleration) +- **Fail Fast** — workflow stops at the first error, saving CI time +- **Conditional Steps** — Docker image build and push are performed only on the main branch +- **Snyk Security Scan** — vulnerabilities found in the requests package (low/medium), fixed by version update: +```yaml + - name: Run Snyk security scan + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + run: | + pip install snyk + snyk test --severity-threshold=medium +``` +- **Status Badge** — shows the current workflow status directly in the README + +## 4. Key Decisions + +- **Versioning Strategy**: CalVer — convenient for continuous deployment and easy to compare build dates +- **Docker Tags**: two tags are created: latest and YYYY.MM.DD for historicity and identification of builds +- **Workflow Triggers**: push and pull request in Python code — saves resources, CI does not run on changes in other directories +- **Test Coverage**: the main functional endpoints are covered, utilities and auxiliary scripts are excluded; the current coverage level is ~85% + +## 5. Challenges +- Snyk API Token Search: Personal Access Token (PAT) from Snyk account is now used → added as SNYK_TOKEN secret +- Configuring caching: it was necessary to specify the correct path for pip cache +- Docker images Versioning: CalVer is easier to automate than SemVer for daily builds \ No newline at end of file diff --git a/app_python/docs/screenshots/lab01/01-main-endpoint.png b/app_python/docs/screenshots/lab01/01-main-endpoint.png new file mode 100644 index 0000000000..f1885c3a70 Binary files /dev/null and b/app_python/docs/screenshots/lab01/01-main-endpoint.png differ diff --git a/app_python/docs/screenshots/lab01/02-health-check.png b/app_python/docs/screenshots/lab01/02-health-check.png new file mode 100644 index 0000000000..d851293efa Binary files /dev/null and b/app_python/docs/screenshots/lab01/02-health-check.png differ diff --git a/app_python/docs/screenshots/lab01/03-formatted-output.png b/app_python/docs/screenshots/lab01/03-formatted-output.png new file mode 100644 index 0000000000..d45bd4a7db Binary files /dev/null and b/app_python/docs/screenshots/lab01/03-formatted-output.png differ diff --git a/app_python/docs/screenshots/lab02/docker-build.png b/app_python/docs/screenshots/lab02/docker-build.png new file mode 100644 index 0000000000..00bef19aea Binary files /dev/null and b/app_python/docs/screenshots/lab02/docker-build.png differ diff --git a/app_python/docs/screenshots/lab02/docker-push.png b/app_python/docs/screenshots/lab02/docker-push.png new file mode 100644 index 0000000000..007314b2bb Binary files /dev/null and b/app_python/docs/screenshots/lab02/docker-push.png differ diff --git a/app_python/docs/screenshots/lab02/docker-run.png b/app_python/docs/screenshots/lab02/docker-run.png new file mode 100644 index 0000000000..607c91af30 Binary files /dev/null and b/app_python/docs/screenshots/lab02/docker-run.png differ diff --git a/app_python/docs/screenshots/lab02/health-check.png b/app_python/docs/screenshots/lab02/health-check.png new file mode 100644 index 0000000000..5edb11b825 Binary files /dev/null and b/app_python/docs/screenshots/lab02/health-check.png differ diff --git a/app_python/docs/screenshots/lab02/main-endpoint.png b/app_python/docs/screenshots/lab02/main-endpoint.png new file mode 100644 index 0000000000..64201a8e8c Binary files /dev/null and b/app_python/docs/screenshots/lab02/main-endpoint.png differ diff --git a/app_python/docs/screenshots/lab03/run_complete.png b/app_python/docs/screenshots/lab03/run_complete.png new file mode 100644 index 0000000000..6fbea445a2 Binary files /dev/null and b/app_python/docs/screenshots/lab03/run_complete.png differ diff --git a/app_python/docs/screenshots/lab03/tags.png b/app_python/docs/screenshots/lab03/tags.png new file mode 100644 index 0000000000..ac51c6a82e Binary files /dev/null and b/app_python/docs/screenshots/lab03/tags.png differ diff --git a/app_python/docs/screenshots/lab03/tests_passed.png b/app_python/docs/screenshots/lab03/tests_passed.png new file mode 100644 index 0000000000..cae6f15648 Binary files /dev/null and b/app_python/docs/screenshots/lab03/tests_passed.png differ diff --git a/app_python/requirements.txt b/app_python/requirements.txt new file mode 100644 index 0000000000..249eb908a2 --- /dev/null +++ b/app_python/requirements.txt @@ -0,0 +1,2 @@ +Flask==3.1.0 +pytest \ 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 diff --git a/app_python/tests/test_health.py b/app_python/tests/test_health.py new file mode 100644 index 0000000000..52381e658f --- /dev/null +++ b/app_python/tests/test_health.py @@ -0,0 +1,10 @@ +from app import app + +def test_health_endpoint(): + client = app.test_client() + response = client.get('/health') + + assert response.status_code == 200 + + data = response.get_json() + assert data["status"] == "healthy" \ No newline at end of file diff --git a/app_python/tests/test_root.py b/app_python/tests/test_root.py new file mode 100644 index 0000000000..cdaada74fa --- /dev/null +++ b/app_python/tests/test_root.py @@ -0,0 +1,15 @@ +import json +from app import app + +def test_root_endpoint(): + client = app.test_client() + response = client.get('/') + + assert response.status_code == 200 + + data = response.get_json() + assert isinstance(data, dict) + + assert "service" in data + assert "system" in data + assert "runtime" in data \ No newline at end of file