Tom716 commited on
Commit
d470514
·
verified ·
1 Parent(s): dcd3d60

Upload 42 files

Browse files
Files changed (42) hide show
  1. projects/k3s-test-app/.gitignore +14 -0
  2. projects/k3s-test-app/.python-version +1 -0
  3. projects/k3s-test-app/Containerfile +17 -0
  4. projects/k3s-test-app/DOCS/ARCHITECTURE.md +52 -0
  5. projects/k3s-test-app/DOCS/PHASES.md +40 -0
  6. projects/k3s-test-app/DOCS/PRD.md +31 -0
  7. projects/k3s-test-app/DOCS/RULES.md +40 -0
  8. projects/k3s-test-app/DOCS/TESTING.md +88 -0
  9. projects/k3s-test-app/LICENSE +21 -0
  10. projects/k3s-test-app/README.md +111 -0
  11. projects/k3s-test-app/k8s/configmap.yaml +8 -0
  12. projects/k3s-test-app/k8s/deployment.yaml +66 -0
  13. projects/k3s-test-app/k8s/hpa.yaml +18 -0
  14. projects/k3s-test-app/k8s/pvc.yaml +12 -0
  15. projects/k3s-test-app/k8s/secret.yaml +9 -0
  16. projects/k3s-test-app/k8s/service.yaml +15 -0
  17. projects/k3s-test-app/pyproject.toml +29 -0
  18. projects/k3s-test-app/pytest.ini +3 -0
  19. projects/k3s-test-app/src/k3s_test_app/__init__.py +1 -0
  20. projects/k3s-test-app/src/k3s_test_app/core/config.py +18 -0
  21. projects/k3s-test-app/src/k3s_test_app/core/logging.py +40 -0
  22. projects/k3s-test-app/src/k3s_test_app/main.py +51 -0
  23. projects/k3s-test-app/src/k3s_test_app/routers/__init__.py +1 -0
  24. projects/k3s-test-app/src/k3s_test_app/routers/config.py +38 -0
  25. projects/k3s-test-app/src/k3s_test_app/routers/health.py +21 -0
  26. projects/k3s-test-app/src/k3s_test_app/routers/identity.py +23 -0
  27. projects/k3s-test-app/src/k3s_test_app/routers/lifecycle.py +22 -0
  28. projects/k3s-test-app/src/k3s_test_app/routers/network.py +46 -0
  29. projects/k3s-test-app/src/k3s_test_app/routers/storage.py +38 -0
  30. projects/k3s-test-app/src/k3s_test_app/routers/stress.py +28 -0
  31. projects/k3s-test-app/src/k3s_test_app/routers/upload.py +22 -0
  32. projects/k3s-test-app/src/k3s_test_app/routers/version.py +15 -0
  33. projects/k3s-test-app/tests/__init__.py +1 -0
  34. projects/k3s-test-app/tests/conftest.py +12 -0
  35. projects/k3s-test-app/tests/test_phase1_health.py +41 -0
  36. projects/k3s-test-app/tests/test_phase2_config.py +54 -0
  37. projects/k3s-test-app/tests/test_phase3_identity_network.py +74 -0
  38. projects/k3s-test-app/tests/test_phase4_storage.py +39 -0
  39. projects/k3s-test-app/tests/test_phase5_resilience.py +51 -0
  40. projects/k3s-test-app/tests/test_phase6_realtime_upload.py +56 -0
  41. projects/k3s-test-app/tests/test_phase7_observability.py +15 -0
  42. projects/k3s-test-app/uv.lock +0 -0
projects/k3s-test-app/.gitignore ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+
12
+ # agentic stuffs
13
+ .agent/
14
+ review.txt
projects/k3s-test-app/.python-version ADDED
@@ -0,0 +1 @@
 
 
1
+ 3.14
projects/k3s-test-app/Containerfile ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Stage 1: build dependencies
2
+ FROM python:3.14-slim AS builder
3
+ COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
4
+ WORKDIR /app
5
+ COPY pyproject.toml uv.lock README.md ./
6
+ RUN uv sync --frozen --no-install-project --no-dev
7
+ COPY src/ src/
8
+ RUN uv sync --frozen --no-dev
9
+
10
+ # Stage 2: runtime
11
+ FROM python:3.14-slim
12
+ COPY --from=builder /app /app
13
+ WORKDIR /app
14
+ ENV PATH="/app/.venv/bin:$PATH"
15
+ EXPOSE 8000
16
+ USER nobody
17
+ CMD ["uvicorn", "k3s_test_app.main:app", "--host", "0.0.0.0", "--port", "8000", "--timeout-graceful-shutdown", "30"]
projects/k3s-test-app/DOCS/ARCHITECTURE.md ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ARCHITECTURE.md
2
+
3
+ ## App Flow
4
+ Client → k3s Ingress (Traefik) → Service → Pod(s) → FastAPI app → (ConfigMap/Secret/PVC/other Service)
5
+
6
+ ## Folder Structure
7
+ ```
8
+ k3s-test-app/
9
+ ├── app/
10
+ │ ├── main.py # app entrypoint, router includes
11
+ │ ├── routers/
12
+ │ │ ├── health.py # /healthz, /ready
13
+ │ │ ├── config.py # /config, /secret
14
+ │ │ ├── stress.py # /stress/cpu, /stress/mem
15
+ │ │ ├── lifecycle.py # /crash, /sleep, shutdown handler
16
+ │ │ ├── identity.py # /whoami
17
+ │ │ ├── storage.py # /storage
18
+ │ │ ├── network.py # /delay/{s}, /call-internal, /ws, /stream
19
+ │ │ ├── upload.py # /upload
20
+ │ │ └── version.py # /version
21
+ │ ├── core/
22
+ │ │ ├── config.py # pydantic Settings (env vars)
23
+ │ │ └── logging.py
24
+ │ └── metrics.py # prometheus_client instrumentation
25
+ ├── k8s/
26
+ │ ├── deployment.yaml
27
+ │ ├── service.yaml
28
+ │ ├── ingress.yaml
29
+ │ ├── configmap.yaml
30
+ │ ├── secret.yaml
31
+ │ ├── pvc.yaml
32
+ │ └── hpa.yaml
33
+ ├── Dockerfile
34
+ ├── requirements.txt
35
+ ├── PRD.md
36
+ ├── ARCHITECTURE.md
37
+ ├── RULES.md
38
+ └── PHASES.md
39
+ ```
40
+
41
+ ## Tech Stack
42
+ - **Runtime**: Python 3.12, FastAPI, Uvicorn (with `--timeout-graceful-shutdown`)
43
+ - **Metrics**: `prometheus-client` / `prometheus-fastapi-instrumentator`
44
+ - **Config**: `pydantic-settings` (env var driven)
45
+ - **Container**: multi-stage Dockerfile, non-root user
46
+ - **Orchestration**: k3s, Traefik ingress (built-in), standard Deployment/Service/PVC/HPA manifests
47
+ - **Load testing**: `hey` / `k6` (external, not part of app)
48
+
49
+ ## Data Flow Notes
50
+ - `/storage` reads/writes to a path backed by a PVC (`local-path` provisioner default in k3s)
51
+ - `/call-internal` hits another Service by in-cluster DNS name (`svc.namespace.svc.cluster.local`)
52
+ - `/metrics` scraped by Prometheus (or manual `curl`) in Prometheus text format
projects/k3s-test-app/DOCS/PHASES.md ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # PHASES.md
2
+
3
+ ## Phase 1 — Core App + Health
4
+ - Scaffold FastAPI app, Dockerfile, requirements.txt
5
+ - `/healthz`, `/ready`, `/version`
6
+ - Deployment + Service manifests, deploy to k3s
7
+
8
+ ## Phase 2 — Config & Secrets
9
+ - `/config` reads ConfigMap-injected env vars
10
+ - `/secret` reads mounted Secret
11
+ - ConfigMap + Secret manifests
12
+
13
+ ## Phase 3 — Identity & Networking
14
+ - `/whoami` (hostname, pod IP)
15
+ - `/delay/{seconds}`
16
+ - `/call-internal` (in-cluster service-to-service call)
17
+ - Ingress manifest + Traefik routing test
18
+
19
+ ## Phase 4 — Storage
20
+ - `/storage` read/write endpoint
21
+ - PVC manifest, verify persistence across pod restarts
22
+
23
+ ## Phase 5 — Load & Resilience
24
+ - `/stress/cpu`, `/stress/mem`
25
+ - `/crash`, `/sleep`
26
+ - Resource limits + HPA manifest
27
+ - Graceful SIGTERM shutdown handler
28
+
29
+ ## Phase 6 — Realtime & Uploads
30
+ - `/ws` or `/stream` (SSE)
31
+ - `/upload`
32
+ - Ingress body-size/timeout tuning
33
+
34
+ ## Phase 7 — Observability
35
+ - `/metrics` (Prometheus format)
36
+ - Optional: Prometheus + Grafana deploy for scraping/dashboards
37
+
38
+ ## Phase 8 — Rolling Update Validation
39
+ - `/version` bump workflow
40
+ - Rolling update + rollback test (`kubectl rollout`)
projects/k3s-test-app/DOCS/PRD.md ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # PRD.md
2
+
3
+ ## Project
4
+ k3s-test-app — FastAPI service for exercising k3s cluster behaviors (probes, config, scaling, storage, networking).
5
+
6
+ ## What to Build
7
+ A single FastAPI app exposing test endpoints (health, config/secret readers, stress, crash, storage, delay, metrics, websocket, upload, graceful shutdown) to validate k3s deployment primitives end-to-end.
8
+
9
+ ## Targeted User
10
+ Solo dev / infra engineer learning or validating a k3s cluster setup. Not a production consumer-facing app.
11
+
12
+ ## Features
13
+ - [ ] Liveness/readiness probes (`/healthz`, `/ready`)
14
+ - [ ] Config/secret introspection (`/config`, `/secret`)
15
+ - [ ] Load/stress simulation (`/stress/cpu`, `/stress/mem`)
16
+ - [ ] Controlled crash/sleep (`/crash`, `/sleep`)
17
+ - [ ] Pod identity (`/whoami` — hostname, pod IP)
18
+ - [ ] Persistent volume read/write (`/storage`)
19
+ - [ ] Configurable delay (`/delay/{seconds}`)
20
+ - [ ] Build/version info (`/version`)
21
+ - [ ] Internal service call (`/call-internal`)
22
+ - [ ] WebSocket/SSE (`/ws`, `/stream`)
23
+ - [ ] File upload (`/upload`)
24
+ - [ ] Prometheus metrics (`/metrics`)
25
+ - [ ] Graceful SIGTERM shutdown
26
+
27
+ ## Out of Scope
28
+ Auth, persistence beyond test PVC, multi-tenant concerns, UI.
29
+
30
+ ## Success Criteria
31
+ Each endpoint proves one specific k3s behavior (see list above) reproducibly via `kubectl` observation.
projects/k3s-test-app/DOCS/RULES.md ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # RULES.md
2
+
3
+ ## Use
4
+ - FastAPI + Uvicorn (async endpoints where I/O bound)
5
+ - `pydantic-settings` for all config — no hardcoded values
6
+ - `prometheus-client` for metrics
7
+ - Structured logging (`logging` + JSON formatter), log to stdout only
8
+ - Type hints on all functions
9
+ - `httpx` for internal service calls (async)
10
+
11
+ ## Preferred Libraries
12
+ | Purpose | Library |
13
+ |---|---|
14
+ | Web framework | fastapi |
15
+ | ASGI server | uvicorn[standard] |
16
+ | Config | pydantic-settings |
17
+ | HTTP client | httpx |
18
+ | Metrics | prometheus-fastapi-instrumentator |
19
+ | Testing | pytest, pytest-asyncio |
20
+
21
+ ## Error Handling
22
+ - No bare `except:` — always catch specific exceptions
23
+ - Every endpoint returns proper HTTP status codes (4xx client, 5xx server)
24
+ - Use FastAPI `HTTPException` for expected failures; let unexpected ones 500 + log traceback
25
+ - All external calls (`/call-internal`, storage I/O) wrapped with timeout + explicit error response, never hang silently
26
+ - Startup/shutdown events must not crash the app — log and degrade gracefully
27
+
28
+ ## AI Boundaries / Constraints
29
+ - AI may scaffold code, write endpoints matching PRD.md features, write k8s manifests
30
+ - AI must NOT introduce new libraries not listed in "Preferred Libraries" without explicit approval
31
+ - AI must NOT change project structure defined in ARCHITECTURE.md without approval
32
+ - AI must NOT add auth, persistence, or scope creep beyond PRD.md
33
+ - AI must ask before deleting files or modifying k8s manifests already applied to a live cluster
34
+
35
+ ## Avoid
36
+ - Unapproved libraries (Flask, Django, requests in async context, etc.)
37
+ - Silent failures / swallowed exceptions
38
+ - Global mutable state outside app lifespan
39
+ - Hardcoded secrets/config (must come from env/ConfigMap/Secret)
40
+ - AI making architectural or scope decisions outside PRD.md/ARCHITECTURE.md without flagging it
projects/k3s-test-app/DOCS/TESTING.md ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # TESTING.md
2
+
3
+ ## Overview
4
+ The test suite lives in `tests/` and is organized by phase, matching the implementation in `PHASES.md`. Tests use `pytest` with `pytest-asyncio` and `httpx`'s `AsyncClient` pointed at the ASGI app — no real server is started.
5
+
6
+ ## Structure
7
+ ```
8
+ tests/
9
+ ├── __init__.py
10
+ ├── conftest.py # Shared fixtures (async TestClient)
11
+ ├── test_phase1_health.py # /healthz, /ready, /version
12
+ ├── test_phase2_config.py # /config, /secret
13
+ ├── test_phase3_identity_network.py # /whoami, /delay, /call-internal
14
+ ├── test_phase4_storage.py # /storage
15
+ ├── test_phase5_resilience.py # /stress/cpu, /stress/mem, /crash, /sleep
16
+ ├── test_phase6_realtime_upload.py # /ws, /upload
17
+ └── test_phase7_observability.py # /metrics
18
+ ```
19
+
20
+ ## Running Tests
21
+
22
+ ### Install dev dependencies
23
+ ```bash
24
+ uv sync --group dev
25
+ ```
26
+
27
+ ### Run all tests
28
+ ```bash
29
+ uv run pytest
30
+ ```
31
+
32
+ ### Run a specific phase
33
+ ```bash
34
+ uv run pytest tests/test_phase1_health.py
35
+ uv run pytest tests/test_phase5_resilience.py
36
+ ```
37
+
38
+ ### Run with verbose output
39
+ ```bash
40
+ uv run pytest -v
41
+ ```
42
+
43
+ ### Run with coverage (if you add pytest-cov later)
44
+ ```bash
45
+ uv run pytest --cov=k3s_test_app --cov-report=term-missing
46
+ ```
47
+
48
+ ## Test Philosophy
49
+ - Each test file covers one phase's endpoints.
50
+ - External I/O (cluster DNS, secret files) is mocked using `unittest.mock.patch` and `monkeypatch` — tests run fully offline without a cluster.
51
+ - Async endpoints are tested with `pytest-asyncio` in `asyncio_mode = auto` (configured in `pytest.ini`).
52
+ - Settings are patched inline using `monkeypatch.setattr` on `cfg_module.settings` — no env file hacks needed.
53
+
54
+ ## Notable Patterns
55
+
56
+ ### Async test client (conftest.py)
57
+ ```python
58
+ @pytest_asyncio.fixture
59
+ async def client() -> AsyncClient:
60
+ async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac:
61
+ yield ac
62
+ ```
63
+
64
+ ### Patching settings at runtime
65
+ ```python
66
+ from k3s_test_app.core import config as cfg_module
67
+ monkeypatch.setattr(cfg_module.settings, "ready", False)
68
+ ```
69
+
70
+ ### Mocking httpx calls for /call-internal
71
+ ```python
72
+ with patch("k3s_test_app.routers.network.httpx.AsyncClient") as mock_http:
73
+ mock_http.return_value.__aenter__.return_value.get.side_effect = httpx.ConnectError("Name or service not known")
74
+ response = await client.get("/call-internal")
75
+ assert response.status_code == 502
76
+ ```
77
+
78
+ ### Mocking built-in loops safely (test_phase5_resilience.py)
79
+ ```python
80
+ def mock_time_gen():
81
+ t = 0
82
+ while True:
83
+ yield t
84
+ t += 10
85
+
86
+ with patch("k3s_test_app.routers.stress.time.time", side_effect=mock_time_gen()):
87
+ response = await client.get("/stress/cpu?seconds=5")
88
+ ```
projects/k3s-test-app/LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Gaurav5189
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
projects/k3s-test-app/README.md ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # k3s-test-app 🚀
2
+
3
+ `k3s-test-app` is a lightweight, purpose-built FastAPI application designed to validate and test various Kubernetes capabilities within a `k3s` environment. It provides a comprehensive suite of endpoints to simulate real-world behaviors—ranging from configuration injection and persistent storage to load-induced autoscaling and graceful degradation.
4
+
5
+ ## Features ✨
6
+
7
+ This application is built in phases, exposing endpoints to test specific k8s features:
8
+
9
+ - **Core & Probes**: `/healthz` and `/ready` for liveness and readiness probes.
10
+ - **Configuration & Secrets**: `/config` and `/secret` to validate ConfigMap environment variable injection and Secret volume mounting.
11
+ - **Identity & Networking**: `/whoami` to verify Downward API (`POD_IP`) injection, `/delay` for timeout testing, and `/call-internal` for cluster DNS resolution.
12
+ - **Storage Persistence**: `GET /storage` and `POST /storage` to validate read/write operations against PVCs.
13
+ - **Load & Resilience**: `/stress/cpu` and `/stress/mem` to trigger Horizontal Pod Autoscalers (HPA), and `/crash` / `/sleep` to test pod restart policies and event loop blocks.
14
+ - **Realtime & Uploads**: `/ws` for long-lived WebSocket connections and `/upload` to test ingress body size limits.
15
+ - **Observability**: `/metrics` exposes Prometheus-formatted application metrics automatically.
16
+
17
+ ## Tech Stack 🛠️
18
+
19
+ - **Framework**: FastAPI + Uvicorn
20
+ - **Configuration**: Pydantic Settings
21
+ - **Testing**: Pytest, Pytest-Asyncio, HTTPX
22
+ - **Containerization**: Podman + Containerfile (Multi-stage build)
23
+ - **Deployment**: Kubernetes Manifests (Deployment, Service, Ingress, ConfigMap, Secret, PVC, HPA)
24
+
25
+ ---
26
+
27
+ ## Local Development & Testing 💻
28
+
29
+ This project uses `uv` for fast Python dependency management.
30
+
31
+ ### Setup
32
+ ```bash
33
+ # Sync dependencies (including dev dependencies)
34
+ uv sync --group dev
35
+ ```
36
+
37
+ ### Running the App Locally
38
+ ```bash
39
+ uv run uvicorn k3s_test_app.main:app --host 0.0.0.0 --port 8000 --reload
40
+ ```
41
+
42
+ ### Running Tests
43
+ The project includes a full test suite built with `pytest` that mocks out infrastructure dependencies (filesystem, DNS, delays) to run instantly without a real cluster.
44
+ ```bash
45
+ # Run the entire test suite
46
+ uv run pytest -v
47
+
48
+ # Run tests for a specific phase
49
+ uv run pytest -v tests/test_phase5_resilience.py
50
+ ```
51
+ > 📚 See [DOCS/TESTING.md](DOCS/TESTING.md) for more details on the testing architecture.
52
+
53
+ ---
54
+
55
+ ## Deployment to k3s 🚢
56
+
57
+ All Kubernetes manifests are located in the `k8s/` directory.
58
+
59
+ ### 1. Build and Push the Image
60
+ We use `podman` and a `Containerfile` to build the image.
61
+ ```bash
62
+ podman build -t k3s-test-app:latest -f Containerfile .
63
+ podman push docker.io/<username>/k3s-test-app:latest
64
+ ```
65
+ *(Remember to update the `image:` reference in `k8s/deployment.yaml` if you push to a custom registry).*
66
+
67
+ ### 2. Ensure Metrics Server is Installed (HPA Requirement)
68
+ The Horizontal Pod Autoscaler (HPA) requires the `metrics.k8s.io` API to scale based on CPU utilization.
69
+ If your cluster does not have `metrics-server` installed (some k3s distributions include it by default, some don't), install it first:
70
+ ```bash
71
+ kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
72
+ ```
73
+
74
+ ### 3. Apply the Manifests
75
+ ```bash
76
+ kubectl apply -f k8s/
77
+ ```
78
+ This will deploy the application, configure Traefik Ingress (including Middlewares), mount secrets, provision local-path storage, and set up HPA rules.
79
+
80
+ ### 4. Access the Application
81
+ The `Ingress` is configured to listen on `test.local`. You can test it locally by passing the `Host` header:
82
+ ```bash
83
+ curl -H "Host: test.local" http://<k3s-node-ip>/healthz
84
+ ```
85
+
86
+ ---
87
+
88
+ ## API Reference 📖
89
+
90
+ Once the application is running, you can view the auto-generated Swagger documentation at:
91
+ - `http://<host>:8000/docs`
92
+
93
+ ### Key Endpoints:
94
+ | Endpoint | Method | Purpose |
95
+ |----------|--------|---------|
96
+ | `/healthz` | GET | Liveness probe (Always returns 200) |
97
+ | `/ready` | GET | Readiness probe (Fails if `READY=false`) |
98
+ | `/version` | GET | Application version info |
99
+ | `/config` | GET | Returns environment configuration |
100
+ | `/secret` | GET | Reads from the mounted k8s Secret |
101
+ | `/whoami` | GET | Returns Pod IP and hostname |
102
+ | `/storage` | POST/GET | Writes/reads to the PVC |
103
+ | `/stress/cpu` | GET | Spikes CPU utilization (tests HPA) |
104
+ | `/metrics` | GET | Prometheus scraping endpoint |
105
+
106
+ ---
107
+
108
+ ## Documentation Links
109
+ - [Architecture & Requirements](DOCS/ARCHITECTURE.md)
110
+ - [Implementation Phases](DOCS/PHASES.md)
111
+ - [Agent Memory/Changelog](.agent/MEMORY.md)
projects/k3s-test-app/k8s/configmap.yaml ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ apiVersion: v1
2
+ kind: ConfigMap
3
+ metadata:
4
+ name: k3s-test-app-config
5
+ data:
6
+ APP_NAME: "k3s-test-app"
7
+ LOG_LEVEL: "info"
8
+ CUSTOM_MESSAGE: "Hello from k3s ConfigMap!"
projects/k3s-test-app/k8s/deployment.yaml ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ apiVersion: apps/v1
2
+ kind: Deployment
3
+ metadata:
4
+ name: k3s-test-app
5
+ labels:
6
+ app: k3s-test-app
7
+ spec:
8
+ replicas: 1
9
+ selector:
10
+ matchLabels:
11
+ app: k3s-test-app
12
+ template:
13
+ metadata:
14
+ labels:
15
+ app: k3s-test-app
16
+ spec:
17
+ containers:
18
+ - name: k3s-test-app
19
+ image: gaurav0s/k3s-test-app:latest
20
+ imagePullPolicy: Always
21
+ ports:
22
+ - containerPort: 8000
23
+ livenessProbe:
24
+ httpGet:
25
+ path: /healthz
26
+ port: 8000
27
+ initialDelaySeconds: 10
28
+ periodSeconds: 15
29
+ timeoutSeconds: 3
30
+ failureThreshold: 3
31
+ readinessProbe:
32
+ httpGet:
33
+ path: /ready
34
+ port: 8000
35
+ initialDelaySeconds: 5
36
+ periodSeconds: 10
37
+ timeoutSeconds: 3
38
+ failureThreshold: 2
39
+ env:
40
+ - name: POD_IP
41
+ valueFrom:
42
+ fieldRef:
43
+ fieldPath: status.podIP
44
+ envFrom:
45
+ - configMapRef:
46
+ name: k3s-test-app-config
47
+ volumeMounts:
48
+ - name: test-secret-volume
49
+ mountPath: /etc/secrets/test-secret
50
+ readOnly: true
51
+ - name: storage-volume
52
+ mountPath: /data
53
+ resources:
54
+ requests:
55
+ cpu: 50m
56
+ memory: 64Mi
57
+ limits:
58
+ cpu: 500m
59
+ memory: 256Mi
60
+ volumes:
61
+ - name: test-secret-volume
62
+ secret:
63
+ secretName: k3s-test-app-secret
64
+ - name: storage-volume
65
+ persistentVolumeClaim:
66
+ claimName: k3s-test-app-pvc
projects/k3s-test-app/k8s/hpa.yaml ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ apiVersion: autoscaling/v2
2
+ kind: HorizontalPodAutoscaler
3
+ metadata:
4
+ name: k3s-test-app-hpa
5
+ spec:
6
+ scaleTargetRef:
7
+ apiVersion: apps/v1
8
+ kind: Deployment
9
+ name: k3s-test-app
10
+ minReplicas: 1
11
+ maxReplicas: 2
12
+ metrics:
13
+ - type: Resource
14
+ resource:
15
+ name: cpu
16
+ target:
17
+ type: Utilization
18
+ averageUtilization: 60
projects/k3s-test-app/k8s/pvc.yaml ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ apiVersion: v1
2
+ kind: PersistentVolumeClaim
3
+ metadata:
4
+ name: k3s-test-app-pvc
5
+ spec:
6
+ accessModes:
7
+ - ReadWriteOnce
8
+ resources:
9
+ requests:
10
+ storage: 100Mi
11
+ # k3s uses local-path provisioner by default
12
+ storageClassName: local-path
projects/k3s-test-app/k8s/secret.yaml ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ apiVersion: v1
2
+ kind: Secret
3
+ metadata:
4
+ name: k3s-test-app-secret
5
+ type: Opaque
6
+ data:
7
+ # NOTE: This is a dummy non-sensitive test fixture to validate secret volume mounting.
8
+ # Do not use this pattern for real production secrets.
9
+ api-key: c3VwZXItc2VjcmV0LWtleQ== # 'super-secret-key' in base64
projects/k3s-test-app/k8s/service.yaml ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ apiVersion: v1
2
+ kind: Service
3
+ metadata:
4
+ name: k3s-test-app
5
+ labels:
6
+ app: k3s-test-app
7
+ spec:
8
+ type: NodePort
9
+ selector:
10
+ app: k3s-test-app
11
+ ports:
12
+ - protocol: TCP
13
+ port: 80
14
+ targetPort: 8000
15
+ nodePort: 30080
projects/k3s-test-app/pyproject.toml ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "k3s-test-app"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Gaurav5189", email = "gauravsethi4627@gmail.com" }
8
+ ]
9
+ requires-python = ">=3.14"
10
+ dependencies = [
11
+ "fastapi[standard]>=0.141.1",
12
+ "uvicorn[standard]>=0.34.0",
13
+ "pydantic-settings>=2.9.1",
14
+ "httpx>=0.28.1",
15
+ "prometheus-fastapi-instrumentator>=7.0.2",
16
+ ]
17
+
18
+ [dependency-groups]
19
+ dev = [
20
+ "pytest>=8.0",
21
+ "pytest-asyncio>=0.25",
22
+ ]
23
+
24
+ [project.scripts]
25
+ k3s-test-app = "k3s_test_app:main"
26
+
27
+ [build-system]
28
+ requires = ["uv_build>=0.12.5,<0.13.0"]
29
+ build-backend = "uv_build"
projects/k3s-test-app/pytest.ini ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ [pytest]
2
+ asyncio_mode = auto
3
+ testpaths = tests
projects/k3s-test-app/src/k3s_test_app/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ __version__ = "0.1.0"
projects/k3s-test-app/src/k3s_test_app/core/config.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic_settings import BaseSettings, SettingsConfigDict
2
+
3
+
4
+ class Settings(BaseSettings):
5
+ app_name: str = "k3s-test-app"
6
+ app_version: str = "0.1.0"
7
+ log_level: str = "info"
8
+ storage_path: str = "/data"
9
+ ready: bool = True
10
+ custom_message: str = "default-message"
11
+ secret_path: str = "/etc/secrets/test-secret/api-key"
12
+
13
+ model_config = SettingsConfigDict(
14
+ env_file=".env", env_file_encoding="utf-8", extra="ignore"
15
+ )
16
+
17
+
18
+ settings = Settings()
projects/k3s-test-app/src/k3s_test_app/core/logging.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import json
3
+ import sys
4
+ from typing import Any, Dict
5
+
6
+
7
+ class JSONFormatter(logging.Formatter):
8
+ def format(self, record: logging.LogRecord) -> str:
9
+ log_data: Dict[str, Any] = {
10
+ "level": record.levelname,
11
+ "message": record.getMessage(),
12
+ "name": record.name,
13
+ "time": self.formatTime(record, self.datefmt),
14
+ }
15
+ if record.exc_info:
16
+ log_data["exc_info"] = self.formatException(record.exc_info)
17
+ return json.dumps(log_data)
18
+
19
+
20
+ def setup_logging(log_level_str: str) -> None:
21
+ level = getattr(logging, log_level_str.upper(), logging.INFO)
22
+
23
+ logger = logging.getLogger("k3s_test_app")
24
+ logger.setLevel(level)
25
+
26
+ # Avoid duplicate logs if setup_logging is called multiple times
27
+ logger.handlers.clear()
28
+
29
+ handler = logging.StreamHandler(sys.stdout)
30
+ handler.setFormatter(JSONFormatter())
31
+ logger.addHandler(handler)
32
+
33
+ # Configure uvicorn loggers to use the same formatter if they exist
34
+ for uvicorn_logger_name in ("uvicorn", "uvicorn.access", "uvicorn.error"):
35
+ uvicorn_logger = logging.getLogger(uvicorn_logger_name)
36
+ uvicorn_logger.handlers.clear()
37
+ uvicorn_logger.addHandler(handler)
38
+
39
+ # Don't propagate to root logger
40
+ uvicorn_logger.propagate = False
projects/k3s-test-app/src/k3s_test_app/main.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from contextlib import asynccontextmanager
3
+
4
+ from fastapi import FastAPI
5
+ from prometheus_fastapi_instrumentator import Instrumentator
6
+
7
+ from k3s_test_app import __version__
8
+ from k3s_test_app.core.config import settings
9
+ from k3s_test_app.core.logging import setup_logging
10
+ from k3s_test_app.routers import (
11
+ health,
12
+ version,
13
+ config,
14
+ identity,
15
+ network,
16
+ storage,
17
+ stress,
18
+ lifecycle,
19
+ upload,
20
+ )
21
+
22
+ logger = logging.getLogger("k3s_test_app")
23
+
24
+
25
+ @asynccontextmanager
26
+ async def lifespan(app: FastAPI):
27
+ setup_logging(settings.log_level)
28
+ logger.info(f"Starting {settings.app_name} v{__version__}")
29
+ yield
30
+ logger.info(f"Shutting down {settings.app_name}")
31
+
32
+
33
+ app = FastAPI(
34
+ title=settings.app_name,
35
+ version=__version__,
36
+ lifespan=lifespan,
37
+ )
38
+
39
+ # Instrument the app for Prometheus metrics
40
+ Instrumentator().instrument(app).expose(app)
41
+
42
+ # Include routers
43
+ app.include_router(health.router, tags=["Health"])
44
+ app.include_router(version.router, tags=["Info"])
45
+ app.include_router(config.router, tags=["Config"])
46
+ app.include_router(identity.router, tags=["Identity"])
47
+ app.include_router(network.router, tags=["Network"])
48
+ app.include_router(storage.router, tags=["Storage"])
49
+ app.include_router(stress.router, tags=["Stress"])
50
+ app.include_router(lifecycle.router, tags=["Lifecycle"])
51
+ app.include_router(upload.router, tags=["Upload"])
projects/k3s-test-app/src/k3s_test_app/routers/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Routers package
projects/k3s-test-app/src/k3s_test_app/routers/config.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from fastapi import APIRouter, HTTPException
3
+ from k3s_test_app.core.config import settings
4
+ import logging
5
+
6
+ router = APIRouter()
7
+ logger = logging.getLogger("k3s_test_app")
8
+
9
+
10
+ @router.get("/config")
11
+ async def get_config() -> dict:
12
+ """Return non-sensitive configuration values to verify ConfigMap injection."""
13
+ return {
14
+ "app_name": settings.app_name,
15
+ "log_level": settings.log_level,
16
+ "custom_message": settings.custom_message,
17
+ }
18
+
19
+
20
+ @router.get("/secret")
21
+ async def get_secret() -> dict:
22
+ """Read and return the mounted secret file to verify volume mounting."""
23
+ try:
24
+ with open(settings.secret_path, "r") as f:
25
+ f.read(1)
26
+ return {"secret_mounted": True}
27
+ except FileNotFoundError:
28
+ logger.error(f"Secret file not found at {settings.secret_path}")
29
+ raise HTTPException(
30
+ status_code=404,
31
+ detail="Secret file not found. Ensure it is mounted correctly."
32
+ )
33
+ except Exception as e:
34
+ logger.error(f"Error reading secret file: {e}")
35
+ raise HTTPException(
36
+ status_code=500,
37
+ detail="Error reading secret file."
38
+ )
projects/k3s-test-app/src/k3s_test_app/routers/health.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Response, status
2
+ from k3s_test_app.core.config import settings
3
+
4
+ router = APIRouter()
5
+
6
+
7
+ @router.get("/healthz", status_code=status.HTTP_200_OK)
8
+ async def healthz() -> dict:
9
+ """Liveness probe endpoint."""
10
+ return {"status": "alive"}
11
+
12
+
13
+ @router.get("/ready")
14
+ async def ready(response: Response) -> dict:
15
+ """Readiness probe endpoint."""
16
+ if settings.ready:
17
+ response.status_code = status.HTTP_200_OK
18
+ return {"status": "ready"}
19
+
20
+ response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE
21
+ return {"status": "not_ready"}
projects/k3s-test-app/src/k3s_test_app/routers/identity.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import socket
3
+ from fastapi import APIRouter
4
+
5
+ router = APIRouter()
6
+
7
+
8
+ @router.get("/whoami")
9
+ async def whoami() -> dict:
10
+ """Return pod identity information (hostname and IP)."""
11
+ hostname = socket.gethostname()
12
+ # Try to get the IP from the Downward API first, fallback to socket resolution
13
+ pod_ip = os.getenv("POD_IP")
14
+ if not pod_ip:
15
+ try:
16
+ pod_ip = socket.gethostbyname(hostname)
17
+ except Exception:
18
+ pod_ip = "unknown"
19
+
20
+ return {
21
+ "hostname": hostname,
22
+ "pod_ip": pod_ip,
23
+ }
projects/k3s-test-app/src/k3s_test_app/routers/lifecycle.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import time
3
+ from fastapi import APIRouter
4
+ import logging
5
+
6
+ router = APIRouter()
7
+ logger = logging.getLogger("k3s_test_app")
8
+
9
+
10
+ @router.get("/crash")
11
+ async def crash() -> dict:
12
+ """Intentionally crash the pod to test liveness probe and restart policy."""
13
+ logger.critical("Intentional crash triggered! Exiting process.")
14
+ os._exit(1)
15
+
16
+
17
+ @router.get("/sleep")
18
+ async def sleep_block(seconds: int = 30) -> dict:
19
+ """Block the event loop using time.sleep to simulate a stuck process (tests liveness probe)."""
20
+ logger.warning(f"Blocking the main thread for {seconds} seconds.")
21
+ time.sleep(seconds)
22
+ return {"message": "Awoke from sleep."}
projects/k3s-test-app/src/k3s_test_app/routers/network.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import httpx
3
+ from fastapi import APIRouter, HTTPException, WebSocket, WebSocketDisconnect
4
+
5
+ router = APIRouter()
6
+
7
+
8
+ @router.websocket("/ws")
9
+ async def websocket_endpoint(websocket: WebSocket):
10
+ """WebSocket endpoint that echoes messages back (tests long-lived connections)."""
11
+ await websocket.accept()
12
+ try:
13
+ while True:
14
+ data = await websocket.receive_text()
15
+ await websocket.send_text(f"Echo: {data}")
16
+ except WebSocketDisconnect:
17
+ pass
18
+
19
+
20
+ @router.get("/delay/{seconds}")
21
+ async def delay(seconds: int) -> dict:
22
+ """Simulate a delayed response for testing timeouts."""
23
+ await asyncio.sleep(seconds)
24
+ return {"message": f"Delayed for {seconds} seconds."}
25
+
26
+
27
+ @router.get("/call-internal")
28
+ async def call_internal() -> dict:
29
+ """Make an internal service-to-service call to validate cluster DNS."""
30
+ # This URL targets the service's internal cluster DNS name
31
+ url = "http://k3s-test-app.default.svc.cluster.local:80/healthz"
32
+
33
+ try:
34
+ async with httpx.AsyncClient(timeout=5.0) as client:
35
+ response = await client.get(url)
36
+ response.raise_for_status()
37
+ return {
38
+ "status": "success",
39
+ "target_url": url,
40
+ "target_response": response.json(),
41
+ }
42
+ except httpx.HTTPError as e:
43
+ raise HTTPException(
44
+ status_code=502,
45
+ detail=f"Failed to communicate with internal service: {e}"
46
+ )
projects/k3s-test-app/src/k3s_test_app/routers/storage.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from fastapi import APIRouter, HTTPException
3
+ from pydantic import BaseModel
4
+ from k3s_test_app.core.config import settings
5
+
6
+ router = APIRouter()
7
+
8
+
9
+ class StorageData(BaseModel):
10
+ content: str
11
+
12
+
13
+ @router.post("/storage")
14
+ async def write_storage(data: StorageData) -> dict:
15
+ """Write data to the persistent volume."""
16
+ file_path = os.path.join(settings.storage_path, "test-file.txt")
17
+ try:
18
+ os.makedirs(settings.storage_path, exist_ok=True)
19
+ with open(file_path, "w") as f:
20
+ f.write(data.content)
21
+ return {"status": "success", "file": file_path}
22
+ except Exception as e:
23
+ raise HTTPException(status_code=500, detail=f"Failed to write to storage: {e}")
24
+
25
+
26
+ @router.get("/storage")
27
+ async def read_storage() -> dict:
28
+ """Read data from the persistent volume."""
29
+ file_path = os.path.join(settings.storage_path, "test-file.txt")
30
+ if not os.path.exists(file_path):
31
+ raise HTTPException(status_code=404, detail="File not found in storage.")
32
+
33
+ try:
34
+ with open(file_path, "r") as f:
35
+ content = f.read()
36
+ return {"content": content, "file": file_path}
37
+ except Exception as e:
38
+ raise HTTPException(status_code=500, detail=f"Failed to read from storage: {e}")
projects/k3s-test-app/src/k3s_test_app/routers/stress.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+ from fastapi import APIRouter
3
+ import logging
4
+
5
+ router = APIRouter()
6
+ logger = logging.getLogger("k3s_test_app")
7
+
8
+
9
+ @router.get("/stress/cpu")
10
+ async def stress_cpu(seconds: int = 5) -> dict:
11
+ """Stress the CPU to trigger HPA scaling."""
12
+ logger.info(f"Stressing CPU for {seconds} seconds.")
13
+ end_time = time.time() + seconds
14
+ while time.time() < end_time:
15
+ pass
16
+ return {"message": f"Completed CPU stress for {seconds} seconds."}
17
+
18
+
19
+ @router.get("/stress/mem")
20
+ async def stress_mem(mb: int = 50) -> dict:
21
+ """Stress memory allocation. Do not allocate too much or OOMKilled will trigger."""
22
+ logger.info(f"Allocating {mb}MB of memory.")
23
+ # Allocate bytearray
24
+ data = bytearray(mb * 1024 * 1024)
25
+ # Give the system a second to register the memory usage before releasing
26
+ time.sleep(1)
27
+ del data
28
+ return {"message": f"Successfully allocated and freed {mb}MB."}
projects/k3s-test-app/src/k3s_test_app/routers/upload.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, UploadFile, File, HTTPException
2
+
3
+ router = APIRouter()
4
+
5
+
6
+ @router.post("/upload")
7
+ async def upload_file(file: UploadFile = File(...)) -> dict:
8
+ """Accept a file upload and return its metadata to test body size limits."""
9
+ size_bytes = 0
10
+ max_upload_bytes = 10 * 1024 * 1024 # 10MB hard limit in the app
11
+
12
+ while chunk := await file.read(1024 * 1024):
13
+ size_bytes += len(chunk)
14
+ if size_bytes > max_upload_bytes:
15
+ raise HTTPException(status_code=413, detail="Upload exceeds the 10MB app size limit.")
16
+
17
+ return {
18
+ "filename": file.filename,
19
+ "content_type": file.content_type,
20
+ "size_bytes": size_bytes,
21
+ "message": f"Successfully received {size_bytes} bytes."
22
+ }
projects/k3s-test-app/src/k3s_test_app/routers/version.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ from fastapi import APIRouter
3
+ from k3s_test_app.core.config import settings
4
+
5
+ router = APIRouter()
6
+
7
+
8
+ @router.get("/version")
9
+ async def version() -> dict:
10
+ """Return version and environment information."""
11
+ return {
12
+ "app_name": settings.app_name,
13
+ "version": settings.app_version,
14
+ "python_version": sys.version,
15
+ }
projects/k3s-test-app/tests/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Tests package
projects/k3s-test-app/tests/conftest.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Shared pytest fixtures for the k3s-test-app test suite."""
2
+ import pytest
3
+ import pytest_asyncio
4
+ from httpx import AsyncClient, ASGITransport
5
+ from k3s_test_app.main import app
6
+
7
+
8
+ @pytest_asyncio.fixture
9
+ async def client() -> AsyncClient:
10
+ """Provide an async test client for the FastAPI app."""
11
+ async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac:
12
+ yield ac
projects/k3s-test-app/tests/test_phase1_health.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for Phase 1: health and version endpoints."""
2
+ import pytest
3
+ from httpx import AsyncClient
4
+
5
+
6
+ @pytest.mark.asyncio
7
+ async def test_healthz_returns_200(client: AsyncClient) -> None:
8
+ """Liveness probe should always return 200 with status=alive."""
9
+ response = await client.get("/healthz")
10
+ assert response.status_code == 200
11
+ assert response.json() == {"status": "alive"}
12
+
13
+
14
+ @pytest.mark.asyncio
15
+ async def test_ready_returns_200_by_default(client: AsyncClient) -> None:
16
+ """Readiness probe should return 200 when READY env var is true (default)."""
17
+ response = await client.get("/ready")
18
+ assert response.status_code == 200
19
+ assert response.json() == {"status": "ready"}
20
+
21
+
22
+ @pytest.mark.asyncio
23
+ async def test_ready_returns_503_when_not_ready(client: AsyncClient, monkeypatch) -> None:
24
+ """Readiness probe should return 503 when settings.ready is False."""
25
+ from k3s_test_app.core import config as cfg_module
26
+ monkeypatch.setattr(cfg_module.settings, "ready", False)
27
+ response = await client.get("/ready")
28
+ assert response.status_code == 503
29
+ assert response.json() == {"status": "not_ready"}
30
+
31
+
32
+ @pytest.mark.asyncio
33
+ async def test_version_returns_expected_fields(client: AsyncClient) -> None:
34
+ """/version should return app_name, version, and python_version."""
35
+ response = await client.get("/version")
36
+ assert response.status_code == 200
37
+ data = response.json()
38
+ assert "app_name" in data
39
+ assert "version" in data
40
+ assert "python_version" in data
41
+ assert data["app_name"] == "k3s-test-app"
projects/k3s-test-app/tests/test_phase2_config.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for Phase 2: config and secret endpoints."""
2
+ import pytest
3
+ import tempfile
4
+ import os
5
+ from httpx import AsyncClient
6
+
7
+
8
+ @pytest.mark.asyncio
9
+ async def test_config_returns_expected_fields(client: AsyncClient) -> None:
10
+ """/config should return app_name, log_level, and custom_message."""
11
+ response = await client.get("/config")
12
+ assert response.status_code == 200
13
+ data = response.json()
14
+ assert "app_name" in data
15
+ assert "log_level" in data
16
+ assert "custom_message" in data
17
+
18
+
19
+ @pytest.mark.asyncio
20
+ async def test_config_custom_message_is_overridable(client: AsyncClient, monkeypatch) -> None:
21
+ """CUSTOM_MESSAGE setting should be reflected in /config response."""
22
+ from k3s_test_app.core import config as cfg_module
23
+ monkeypatch.setattr(cfg_module.settings, "custom_message", "override-value")
24
+ response = await client.get("/config")
25
+ assert response.status_code == 200
26
+ assert response.json()["custom_message"] == "override-value"
27
+
28
+
29
+ @pytest.mark.asyncio
30
+ async def test_secret_returns_404_when_file_missing(client: AsyncClient, monkeypatch) -> None:
31
+ """/secret should return 404 when the secret file does not exist."""
32
+ from k3s_test_app.core import config as cfg_module
33
+ monkeypatch.setattr(cfg_module.settings, "secret_path", "/nonexistent/path/secret")
34
+ response = await client.get("/secret")
35
+ assert response.status_code == 404
36
+ assert "not found" in response.json()["detail"].lower()
37
+
38
+
39
+ @pytest.mark.asyncio
40
+ async def test_secret_returns_content_when_file_exists(client: AsyncClient, monkeypatch) -> None:
41
+ """/secret should return the file contents when the secret file exists."""
42
+ from k3s_test_app.core import config as cfg_module
43
+
44
+ with tempfile.NamedTemporaryFile(mode="w", suffix="-api-key", delete=False) as f:
45
+ f.write("my-test-secret")
46
+ tmp_path = f.name
47
+
48
+ try:
49
+ monkeypatch.setattr(cfg_module.settings, "secret_path", tmp_path)
50
+ response = await client.get("/secret")
51
+ assert response.status_code == 200
52
+ assert response.json() == {"secret_mounted": True}
53
+ finally:
54
+ os.unlink(tmp_path)
projects/k3s-test-app/tests/test_phase3_identity_network.py ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for Phase 3: identity and network endpoints."""
2
+ import pytest
3
+ import socket
4
+ from unittest.mock import AsyncMock, patch, MagicMock
5
+ from httpx import AsyncClient
6
+ import httpx
7
+
8
+
9
+ @pytest.mark.asyncio
10
+ async def test_whoami_returns_hostname_and_ip(client: AsyncClient) -> None:
11
+ """/whoami should return hostname and pod_ip fields."""
12
+ response = await client.get("/whoami")
13
+ assert response.status_code == 200
14
+ data = response.json()
15
+ assert "hostname" in data
16
+ assert "pod_ip" in data
17
+ # hostname should match the real system hostname
18
+ assert data["hostname"] == socket.gethostname()
19
+
20
+
21
+ @pytest.mark.asyncio
22
+ async def test_whoami_uses_pod_ip_env_var(client: AsyncClient, monkeypatch) -> None:
23
+ """/whoami should use the POD_IP env var when set (Downward API)."""
24
+ monkeypatch.setenv("POD_IP", "10.42.0.99")
25
+ response = await client.get("/whoami")
26
+ assert response.status_code == 200
27
+ assert response.json()["pod_ip"] == "10.42.0.99"
28
+
29
+
30
+ @pytest.mark.asyncio
31
+ async def test_delay_responds_after_sleep(client: AsyncClient) -> None:
32
+ """/delay/1 should return a success message (tests that it doesn't error)."""
33
+ # We patch asyncio.sleep to avoid actually waiting
34
+ with patch("k3s_test_app.routers.network.asyncio.sleep", new_callable=AsyncMock) as mock_sleep:
35
+ response = await client.get("/delay/3")
36
+ mock_sleep.assert_awaited_once_with(3)
37
+ assert response.status_code == 200
38
+ assert response.json() == {"message": "Delayed for 3 seconds."}
39
+
40
+
41
+ @pytest.mark.asyncio
42
+ async def test_call_internal_returns_502_when_dns_fails(client: AsyncClient) -> None:
43
+ """/call-internal should return 502 when the internal service is unreachable."""
44
+ # In a local test environment, the cluster DNS won't resolve — this is expected
45
+ with patch("k3s_test_app.routers.network.httpx.AsyncClient") as mock_http:
46
+ mock_instance = AsyncMock()
47
+ mock_http.return_value.__aenter__ = AsyncMock(return_value=mock_instance)
48
+ mock_http.return_value.__aexit__ = AsyncMock(return_value=False)
49
+ mock_instance.get.side_effect = httpx.ConnectError("Name or service not known")
50
+
51
+ response = await client.get("/call-internal")
52
+ assert response.status_code == 502
53
+ assert "Failed to communicate with internal service" in response.json()["detail"]
54
+
55
+
56
+ @pytest.mark.asyncio
57
+ async def test_call_internal_returns_success_when_service_responds(client: AsyncClient) -> None:
58
+ """/call-internal should return 200 with target_response when the service is up."""
59
+ mock_response = MagicMock()
60
+ mock_response.raise_for_status = MagicMock()
61
+ mock_response.json.return_value = {"status": "alive"}
62
+
63
+ with patch("k3s_test_app.routers.network.httpx.AsyncClient") as mock_http:
64
+ mock_instance = AsyncMock()
65
+ mock_http.return_value.__aenter__ = AsyncMock(return_value=mock_instance)
66
+ mock_http.return_value.__aexit__ = AsyncMock(return_value=False)
67
+ mock_instance.get.return_value = mock_response
68
+
69
+ response = await client.get("/call-internal")
70
+ assert response.status_code == 200
71
+ data = response.json()
72
+ assert data["status"] == "success"
73
+ assert data["target_response"] == {"status": "alive"}
74
+ assert "k3s-test-app.default.svc.cluster.local" in data["target_url"]
projects/k3s-test-app/tests/test_phase4_storage.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for Phase 4: storage endpoints."""
2
+ import pytest
3
+ import os
4
+ import tempfile
5
+ import shutil
6
+ from httpx import AsyncClient
7
+
8
+
9
+ @pytest.fixture
10
+ def temp_storage_dir(monkeypatch):
11
+ """Fixture to mock settings.storage_path with a temporary directory."""
12
+ temp_dir = tempfile.mkdtemp()
13
+ from k3s_test_app.core import config as cfg_module
14
+ monkeypatch.setattr(cfg_module.settings, "storage_path", temp_dir)
15
+ yield temp_dir
16
+ shutil.rmtree(temp_dir)
17
+
18
+
19
+ @pytest.mark.asyncio
20
+ async def test_read_storage_not_found(client: AsyncClient, temp_storage_dir: str) -> None:
21
+ """GET /storage should return 404 if nothing has been written yet."""
22
+ response = await client.get("/storage")
23
+ assert response.status_code == 404
24
+ assert "not found" in response.json()["detail"].lower()
25
+
26
+
27
+ @pytest.mark.asyncio
28
+ async def test_write_and_read_storage(client: AsyncClient, temp_storage_dir: str) -> None:
29
+ """POST /storage should write data, and GET /storage should read it back."""
30
+ # Write data
31
+ post_response = await client.post("/storage", json={"content": "hello storage"})
32
+ assert post_response.status_code == 200
33
+ assert post_response.json()["status"] == "success"
34
+
35
+ # Read data
36
+ get_response = await client.get("/storage")
37
+ assert get_response.status_code == 200
38
+ assert get_response.json()["content"] == "hello storage"
39
+ assert get_response.json()["file"].startswith(temp_storage_dir)
projects/k3s-test-app/tests/test_phase5_resilience.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for Phase 5: load and resilience endpoints."""
2
+ import pytest
3
+ from httpx import AsyncClient
4
+ from unittest.mock import patch
5
+
6
+
7
+ def mock_time_gen():
8
+ t = 0
9
+ while True:
10
+ yield t
11
+ t += 10
12
+
13
+ @pytest.mark.asyncio
14
+ async def test_stress_cpu(client: AsyncClient) -> None:
15
+ """GET /stress/cpu should return 200 (mocking the time.time() to finish instantly)."""
16
+ with patch("k3s_test_app.routers.stress.time.time", side_effect=mock_time_gen()):
17
+ response = await client.get("/stress/cpu?seconds=5")
18
+ assert response.status_code == 200
19
+ assert "Completed CPU stress" in response.json()["message"]
20
+
21
+
22
+ @pytest.mark.asyncio
23
+ async def test_stress_mem(client: AsyncClient) -> None:
24
+ """GET /stress/mem should return 200."""
25
+ with patch("k3s_test_app.routers.stress.time.sleep"):
26
+ response = await client.get("/stress/mem?mb=1")
27
+ assert response.status_code == 200
28
+ assert "allocated and freed" in response.json()["message"]
29
+
30
+
31
+ @pytest.mark.asyncio
32
+ async def test_crash(client: AsyncClient) -> None:
33
+ """GET /crash should call os._exit(1)."""
34
+ class IntentionalCrash(Exception): pass
35
+
36
+ with patch("k3s_test_app.routers.lifecycle.os._exit", side_effect=IntentionalCrash) as mock_exit:
37
+ try:
38
+ await client.get("/crash")
39
+ except IntentionalCrash:
40
+ pass # Expected
41
+ mock_exit.assert_called_once_with(1)
42
+
43
+
44
+ @pytest.mark.asyncio
45
+ async def test_sleep_block(client: AsyncClient) -> None:
46
+ """GET /sleep should call time.sleep and block the thread."""
47
+ with patch("k3s_test_app.routers.lifecycle.time.sleep") as mock_sleep:
48
+ response = await client.get("/sleep?seconds=2")
49
+ mock_sleep.assert_called_once_with(2)
50
+ assert response.status_code == 200
51
+ assert response.json()["message"] == "Awoke from sleep."
projects/k3s-test-app/tests/test_phase6_realtime_upload.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for Phase 6: realtime and upload endpoints."""
2
+ import pytest
3
+ from httpx import AsyncClient
4
+ import io
5
+
6
+
7
+ @pytest.mark.asyncio
8
+ async def test_websocket_echo() -> None:
9
+ """Test the WebSocket /ws endpoint."""
10
+ # We use a synchronous TestClient from fastapi.testclient for websockets
11
+ # because httpx AsyncClient doesn't have built-in WS support yet
12
+ from fastapi.testclient import TestClient
13
+ from k3s_test_app.main import app
14
+
15
+ client = TestClient(app)
16
+ with client.websocket_connect("/ws") as websocket:
17
+ websocket.send_text("Hello k3s")
18
+ data = websocket.receive_text()
19
+ assert data == "Echo: Hello k3s"
20
+
21
+
22
+ @pytest.mark.asyncio
23
+ async def test_upload_file(client: AsyncClient) -> None:
24
+ """POST /upload should accept a file and return its size."""
25
+ file_content = b"This is a test file for upload."
26
+ files = {"file": ("test.txt", io.BytesIO(file_content), "text/plain")}
27
+
28
+ response = await client.post("/upload", files=files)
29
+ assert response.status_code == 200
30
+
31
+ data = response.json()
32
+ assert data["filename"] == "test.txt"
33
+ assert data["size_bytes"] == len(file_content)
34
+
35
+
36
+ @pytest.mark.asyncio
37
+ async def test_upload_file_exceeds_limit(client: AsyncClient) -> None:
38
+ """POST /upload should return 413 if the file exceeds the 10MB limit."""
39
+ # We can create a fake file-like object that returns data to simulate a large upload
40
+ # without allocating 10MB+ in memory all at once.
41
+ class LargeMockFile:
42
+ def __init__(self, size):
43
+ self.remaining = size
44
+ def read(self, size=-1):
45
+ if self.remaining <= 0:
46
+ return b""
47
+ chunk_size = size if size > 0 else self.remaining
48
+ chunk_size = min(chunk_size, self.remaining)
49
+ self.remaining -= chunk_size
50
+ return b"0" * chunk_size
51
+
52
+ files = {"file": ("huge.txt", LargeMockFile(11 * 1024 * 1024), "text/plain")}
53
+
54
+ response = await client.post("/upload", files=files)
55
+ assert response.status_code == 413
56
+ assert "exceeds" in response.json()["detail"].lower()
projects/k3s-test-app/tests/test_phase7_observability.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for Phase 7: observability endpoints."""
2
+ import pytest
3
+ from httpx import AsyncClient
4
+
5
+
6
+ @pytest.mark.asyncio
7
+ async def test_metrics_endpoint_returns_prometheus_data(client: AsyncClient) -> None:
8
+ """GET /metrics should return Prometheus metrics in text format."""
9
+ response = await client.get("/metrics")
10
+ assert response.status_code == 200
11
+
12
+ # Prometheus metrics are plain text
13
+ content = response.text
14
+ assert "TYPE" in content or "HELP" in content
15
+ assert "fastapi_" in content or "python_" in content or "process_" in content
projects/k3s-test-app/uv.lock ADDED
The diff for this file is too large to render. See raw diff