Skip to content

Commit 97ee6ab

Browse files
authored
Merge pull request #14 from Cdeth567/lab12
Add ConfigMaps and PVC support for lab 12
2 parents 481cb94 + 9efab87 commit 97ee6ab

16 files changed

Lines changed: 953 additions & 30 deletions

File tree

‎app-policy.hcl‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
path "secret/data/devops-info-service/config" {
2+
capabilities = ["read"]
3+
}
4+
5+
path "secret/metadata/devops-info-service/config" {
6+
capabilities = ["read", "list"]
7+
}
8+
9+
path "sys/internal/ui/mounts/secret/data/devops-info-service/config" {
10+
capabilities = ["read"]
11+
}

‎app_python/README.md‎

Lines changed: 69 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@
44

55
## Overview
66
DevOps Info Service is a small Flask web application that exposes:
7-
- `GET /` — service metadata, system/runtime info, and request details
7+
- `GET /` — service metadata, system/runtime info, request details, and visit counter increment
8+
- `GET /visits` — current persisted visit count
89
- `GET /health` — simple health check endpoint
10+
- `GET /ready` — readiness probe endpoint
11+
- `GET /metrics` — Prometheus metrics
912

10-
This project is the foundation for future DevOps labs (Docker, CI/CD, monitoring, Kubernetes).
13+
The application stores visit statistics in a file (default: `/data/visits`) so the counter survives container restarts when a volume is mounted.
1114

1215
## Prerequisites
1316
- Python 3.11+
@@ -59,14 +62,15 @@ Custom config:
5962

6063
**Linux/Mac:**
6164
```bash
62-
HOST=127.0.0.1 PORT=3000 DEBUG=True python app.py
65+
HOST=127.0.0.1 PORT=3000 DEBUG=True VISITS_FILE=./data/visits python app.py
6366
```
6467

6568
**Windows (PowerShell):**
6669
```powershell
6770
$env:HOST="127.0.0.1"
6871
$env:PORT="3000"
6972
$env:DEBUG="True"
73+
$env:VISITS_FILE=".\data\visits"
7074
python app.py
7175
```
7276

@@ -75,16 +79,21 @@ python app.py
7579
set HOST=127.0.0.1
7680
set PORT=3000
7781
set DEBUG=True
82+
set VISITS_FILE=.\data\visits
7883
python app.py
7984
```
8085

8186
## API Endpoints
82-
- `GET /` — service and system information
87+
- `GET /` — service and system information, increments the counter
88+
- `GET /visits` — current visit counter value from persisted storage
8389
- `GET /health` — health check
90+
- `GET /ready` — readiness check
91+
- `GET /metrics` — Prometheus metrics
8492

8593
Examples:
8694
```bash
8795
curl http://127.0.0.1:5000/
96+
curl http://127.0.0.1:5000/visits
8897
curl http://127.0.0.1:5000/health
8998
```
9099

@@ -97,46 +106,88 @@ curl http://127.0.0.1:5000/ | python -m json.tool
97106

98107
| Variable | Default | Description |
99108
|----------|---------|-------------|
100-
| HOST | 0.0.0.0 | Bind address |
101-
| PORT | 5000 | HTTP port |
102-
| DEBUG | False | Flask debug mode |
109+
| HOST | 0.0.0.0 | Bind address |
110+
| PORT | 5000 | HTTP port |
111+
| DEBUG | False | Flask debug mode |
112+
| VISITS_FILE | /data/visits | Path to the persisted visits counter |
113+
| SERVICE_NAME | devops-info-service | Service name shown in the API |
114+
| SERVICE_VERSION | 1.0.0 | Service version shown in the API |
115+
| SERVICE_DESCRIPTION | DevOps course info service | Service description shown in the API |
103116

104117
## Docker
105118

106119
This application can also be built and run as a Docker container.
107120

108121
### Build (local)
109-
Pattern:
110122
```bash
111-
docker build -t <image_name>:<tag> .
123+
docker build -t devops-info-service:lab12 .
112124
```
113125

114-
### Run
115-
Pattern:
126+
### Run with a bind mount
116127
```bash
117-
docker run --rm -p <host_port>:5000 --name <container_name> <image_name>:<tag>
128+
mkdir -p data
129+
docker run --rm -p 5000:5000 \
130+
-e VISITS_FILE=/data/visits \
131+
-v "$(pwd)/data:/data" \
132+
--name devops-info-service \
133+
devops-info-service:lab12
118134
```
119135

120136
Then test:
121137
```bash
122-
curl http://127.0.0.1:<host_port>/
123-
curl http://127.0.0.1:<host_port>/health
138+
curl http://127.0.0.1:5000/
139+
curl http://127.0.0.1:5000/
140+
curl http://127.0.0.1:5000/visits
141+
cat ./data/visits
142+
```
143+
144+
### Docker Compose
145+
`docker-compose.yml` is included for local persistence testing.
146+
147+
Start the application:
148+
```bash
149+
mkdir -p data
150+
docker compose up -d --build
151+
```
152+
153+
Verify persistence:
154+
```bash
155+
curl http://127.0.0.1:5000/
156+
curl http://127.0.0.1:5000/
157+
curl http://127.0.0.1:5000/visits
158+
cat ./data/visits
159+
docker compose restart
160+
curl http://127.0.0.1:5000/visits
161+
cat ./data/visits
162+
```
163+
164+
Stop it:
165+
```bash
166+
docker compose down
124167
```
125168

169+
Expected persistence flow:
170+
1. First request to `/` creates `./data/visits` with value `1`
171+
2. Additional requests keep incrementing the file
172+
3. After container restart, `/visits` returns the previously saved value instead of resetting to `0`
173+
126174
### Pull from Docker Hub
127175
Pattern:
128176
```bash
129177
docker pull <dockerhub_username>/<repo_name>:<tag>
130178
docker run --rm -p <host_port>:5000 <dockerhub_username>/<repo_name>:<tag>
131179
```
132180

133-
> Note (Windows PowerShell): `curl` is an alias for `Invoke-WebRequest`.
181+
> Note (Windows PowerShell): `curl` is an alias for `Invoke-WebRequest`.
134182
> For classic curl behavior, use `curl.exe`.
135183
136184
## Testing
137185
Install dev dependencies:
138-
- python -m pip install -r requirements-dev.txt
186+
```bash
187+
python -m pip install -r requirements-dev.txt
188+
```
139189

140190
Run tests:
141-
- pytest
142-
191+
```bash
192+
pytest
193+
```

‎app_python/app.py‎

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
import platform
55
import socket
66
import sys
7+
import threading
78
import time
89
from datetime import datetime, timezone
10+
from pathlib import Path
911

1012
from flask import Flask, Response, g, has_request_context, jsonify, request
1113
from prometheus_client import CONTENT_TYPE_LATEST, Counter, Gauge, Histogram, generate_latest
@@ -18,6 +20,7 @@
1820
PORT = int(os.getenv("PORT", 5000))
1921
DEBUG = os.getenv("DEBUG", "False").lower() == "true"
2022
START_TIME = datetime.now(timezone.utc)
23+
DEFAULT_VISITS_FILE = "/data/visits"
2124

2225
REQUEST_LATENCY_BUCKETS = (
2326
0.005,
@@ -101,6 +104,58 @@ def format(self, record: logging.LogRecord) -> str:
101104
return json.dumps(payload, ensure_ascii=False)
102105

103106

107+
class VisitCounter:
108+
"""File-backed counter used to persist the number of root endpoint visits."""
109+
110+
def __init__(self, path_getter):
111+
self._path_getter = path_getter
112+
self._lock = threading.Lock()
113+
self._value = 0
114+
self.reload()
115+
116+
def _path(self) -> Path:
117+
return Path(self._path_getter())
118+
119+
def _read_from_disk(self) -> int:
120+
path = self._path()
121+
try:
122+
return int(path.read_text(encoding="utf-8").strip() or "0")
123+
except FileNotFoundError:
124+
return 0
125+
except ValueError:
126+
logger.warning(
127+
"Visits file contained invalid data, resetting counter to 0",
128+
extra={
129+
"event": "visits.invalid_data",
130+
"service": SERVICE_NAME,
131+
},
132+
)
133+
return 0
134+
135+
def _write_to_disk(self, value: int) -> None:
136+
path = self._path()
137+
path.parent.mkdir(parents=True, exist_ok=True)
138+
tmp_path = path.with_suffix(f"{path.suffix}.tmp")
139+
tmp_path.write_text(str(value), encoding="utf-8")
140+
os.replace(tmp_path, path)
141+
142+
def reload(self) -> int:
143+
with self._lock:
144+
self._value = self._read_from_disk()
145+
return self._value
146+
147+
def get_value(self) -> int:
148+
with self._lock:
149+
return self._value
150+
151+
def increment(self) -> int:
152+
with self._lock:
153+
self._value += 1
154+
self._write_to_disk(self._value)
155+
return self._value
156+
157+
158+
104159
def configure_logging() -> logging.Logger:
105160
"""Configure application logging to stdout in JSON format."""
106161
handler = logging.StreamHandler(sys.stdout)
@@ -121,6 +176,15 @@ def configure_logging() -> logging.Logger:
121176

122177
logger = configure_logging()
123178
app = Flask(__name__)
179+
app.config.setdefault("VISITS_FILE", os.getenv("VISITS_FILE", DEFAULT_VISITS_FILE))
180+
181+
182+
def get_visits_file_path() -> str:
183+
"""Return the configured path to the visits counter file."""
184+
return app.config.get("VISITS_FILE", DEFAULT_VISITS_FILE)
185+
186+
187+
visit_counter = VisitCounter(get_visits_file_path)
124188

125189

126190
def get_platform_version() -> str:
@@ -135,6 +199,7 @@ def get_platform_version() -> str:
135199
return platform.platform()
136200

137201

202+
138203
def get_uptime() -> dict:
139204
"""Calculate the application's uptime."""
140205
delta = datetime.now(timezone.utc) - START_TIME
@@ -147,6 +212,7 @@ def get_uptime() -> dict:
147212
}
148213

149214

215+
150216
def get_system_info() -> dict:
151217
"""Collect system information."""
152218
with system_info_collection_seconds.time():
@@ -160,6 +226,7 @@ def get_system_info() -> dict:
160226
}
161227

162228

229+
163230
def get_client_ip() -> str | None:
164231
"""Return the client IP, preferring X-Forwarded-For when present."""
165232
if not has_request_context():
@@ -171,17 +238,19 @@ def get_client_ip() -> str | None:
171238
return request.remote_addr
172239

173240

241+
174242
def normalize_endpoint() -> str:
175243
"""Return a low-cardinality endpoint label for Prometheus metrics."""
176244
if not has_request_context():
177245
return "unknown"
178246
if request.url_rule and request.url_rule.rule:
179247
return request.url_rule.rule
180-
if request.path in {"/", "/health", "/metrics"}:
248+
if request.path in {"/", "/health", "/metrics", "/ready", "/visits"}:
181249
return request.path
182250
return "unmatched"
183251

184252

253+
185254
def build_request_log_context(status_code: int | None = None) -> dict:
186255
"""Build structured context for request-related logs."""
187256
context: dict[str, object] = {"event": "http.request", "service": SERVICE_NAME}
@@ -209,6 +278,7 @@ def build_request_log_context(status_code: int | None = None) -> dict:
209278
return context
210279

211280

281+
212282
def finalize_request_metrics(status_code: int) -> None:
213283
"""Record Prometheus metrics for the current request exactly once."""
214284
if not has_request_context() or not getattr(g, "metrics_tracked", False):
@@ -280,6 +350,7 @@ def cleanup_request_metrics(exc) -> None:
280350
@app.route("/")
281351
def index():
282352
"""Main endpoint - service and system information."""
353+
current_visits = visit_counter.increment()
283354
uptime = get_uptime()
284355

285356
response = {
@@ -304,8 +375,13 @@ def index():
304375
"method": request.method,
305376
"path": request.path,
306377
},
378+
"visits": {
379+
"count": current_visits,
380+
"storage": get_visits_file_path(),
381+
},
307382
"endpoints": [
308383
{"path": "/", "method": "GET", "description": "Service information"},
384+
{"path": "/visits", "method": "GET", "description": "Visit counter"},
309385
{"path": "/health", "method": "GET", "description": "Health check"},
310386
{"path": "/ready", "method": "GET", "description": "Readiness check"},
311387
{"path": "/metrics", "method": "GET", "description": "Prometheus metrics"},
@@ -315,6 +391,17 @@ def index():
315391
return jsonify(response)
316392

317393

394+
@app.route("/visits")
395+
def visits():
396+
"""Return the current persisted visit count."""
397+
return jsonify(
398+
{
399+
"visits": visit_counter.get_value(),
400+
"storage": get_visits_file_path(),
401+
}
402+
)
403+
404+
318405
@app.route("/health")
319406
def health():
320407
"""Health check endpoint for monitoring."""
@@ -397,6 +484,7 @@ def handle_unexpected_error(error):
397484

398485

399486
if __name__ == "__main__":
487+
visit_counter.reload()
400488
logger.info(
401489
"Application startup",
402490
extra={

‎app_python/data/.gitkeep‎

Whitespace-only changes.

‎app_python/data/visits‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
7

‎app_python/docker-compose.yml‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
services:
2+
devops-info-service:
3+
build:
4+
context: .
5+
dockerfile: Dockerfile
6+
container_name: devops-info-service
7+
environment:
8+
HOST: 0.0.0.0
9+
PORT: 5000
10+
DEBUG: "False"
11+
VISITS_FILE: /data/visits
12+
ports:
13+
- "5000:5000"
14+
volumes:
15+
- ./data:/data
16+
restart: unless-stopped

0 commit comments

Comments
 (0)