Skip to content

Commit 077ed81

Browse files
committed
new shiny demopod
1 parent 26f4873 commit 077ed81

File tree

8 files changed

+458
-0
lines changed

8 files changed

+458
-0
lines changed
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
name: Build and Push Demo Pod
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
paths:
8+
- Dockerfile.demopod
9+
- uv.lock
10+
- pyproject.toml
11+
- demopod/**/*
12+
- .github/workflows/build-push-demopod.yaml
13+
14+
env:
15+
REGISTRY: ghcr.io
16+
IMAGE_NAME: ${{ github.repository }}-demopod
17+
18+
jobs:
19+
build:
20+
runs-on: ubuntu-latest
21+
permissions:
22+
contents: read
23+
packages: write
24+
attestations: write
25+
id-token: write
26+
steps:
27+
- name: Checkout repository
28+
uses: actions/checkout@v5
29+
30+
- name: Set up Docker Buildx
31+
uses: docker/setup-buildx-action@v3
32+
with:
33+
install: true
34+
driver-opts: |
35+
image=moby/buildkit:latest
36+
37+
- name: Login to GitHub Container Registry
38+
uses: docker/login-action@v3
39+
with:
40+
registry: ${{ env.REGISTRY }}
41+
username: ${{ github.actor }}
42+
password: ${{ secrets.GITHUB_TOKEN }}
43+
44+
- name: Extract metadata (tags, labels) for Docker
45+
id: meta
46+
uses: docker/metadata-action@v5
47+
with:
48+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
49+
50+
- name: Cache Docker layers
51+
uses: actions/cache@v4
52+
with:
53+
path: /tmp/.buildx-cache
54+
key: ${{ runner.os }}-buildx-${{ github.sha }}
55+
restore-keys: |
56+
${{ runner.os }}-buildx-
57+
58+
- name: Build and Push Docker image
59+
id: push
60+
uses: docker/build-push-action@v6
61+
with:
62+
context: .
63+
file: Dockerfile.demopod
64+
platforms: linux/amd64,linux/arm64
65+
push: true
66+
tags: ${{ steps.meta.outputs.tags }}
67+
labels: ${{ steps.meta.outputs.labels }}
68+
cache-from: type=local,src=/tmp/.buildx-cache
69+
cache-to: type=local,dest=/tmp/.buildx-cache
70+
71+
- name: Generate artifact attestation
72+
uses: actions/attest-build-provenance@v1
73+
with:
74+
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
75+
subject-digest: ${{ steps.push.outputs.digest }}
76+
push-to-registry: true

Dockerfile.demopod

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
### BUILDER
2+
FROM python:3.12-alpine as builder
3+
4+
# Install uv
5+
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
6+
7+
# Change the working directory to `/app`
8+
WORKDIR /app
9+
10+
# Enable bytecode compilation
11+
ENV UV_COMPILE_BYTECODE=1
12+
13+
# Copy from the cache instead of linking since it's a mounted volume
14+
ENV UV_LINK_MODE=copy
15+
16+
# Install dependencies
17+
RUN --mount=type=cache,target=/root/.cache/uv \
18+
--mount=type=bind,source=uv.lock,target=uv.lock \
19+
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
20+
uv sync --frozen --no-install-project --no-dev
21+
22+
### RUNTIME
23+
FROM python:3.12-alpine as runtime
24+
25+
WORKDIR /app
26+
27+
ENV PATH="/app/.venv/bin:$PATH" \
28+
PYTHONPATH="/app/demopod"
29+
30+
COPY --from=builder /app/.venv /app/.venv
31+
32+
COPY demopod ./demopod
33+
34+
RUN set -x && \
35+
adduser -D demopod
36+
37+
USER demopod:root
38+
39+
EXPOSE 8080
40+
41+
CMD ["python", "demopod/main.py"]

demopod/__init__.py

Whitespace-only changes.

demopod/main.py

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
#!/usr/bin/env python3
2+
3+
import os
4+
import sys
5+
import time
6+
import threading
7+
import multiprocessing
8+
import json
9+
import signal
10+
from datetime import datetime
11+
from http.server import HTTPServer, BaseHTTPRequestHandler
12+
import random
13+
import math
14+
15+
class DemoApp:
16+
def __init__(self):
17+
# Configuration from environment variables
18+
self.startup_delay = int(os.getenv('STARTUP_DELAY', '10'))
19+
self.cpu_pattern = os.getenv('CPU_PATTERN', 'constant') # constant, sine, random, burst
20+
self.cpu_intensity = float(os.getenv('CPU_INTENSITY', '0.5')) # 0.0 to 1.0
21+
self.status_interval = int(os.getenv('STATUS_INTERVAL', '10')) # seconds
22+
self.http_port = int(os.getenv('HTTP_PORT', '8080'))
23+
24+
# State tracking
25+
self.start_time = datetime.now()
26+
self.is_ready = False
27+
self.is_running = True
28+
self.current_cpu_target = 0.0
29+
self.cpu_workers = []
30+
self.status_thread = None
31+
self.http_server = None
32+
33+
# Setup signal handling for graceful shutdown
34+
signal.signal(signal.SIGTERM, self._signal_handler)
35+
signal.signal(signal.SIGINT, self._signal_handler)
36+
37+
def _signal_handler(self, signum, frame):
38+
self.log(f"Received signal {signum}, shutting down gracefully...")
39+
self.shutdown()
40+
41+
def log(self, message):
42+
"""Log with timestamp"""
43+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
44+
print(f"[{timestamp}] {message}", flush=True)
45+
46+
def simulate_startup(self):
47+
"""Simulate slow startup with progress updates"""
48+
self.log(f"🚀 Application starting with {self.startup_delay}s startup delay")
49+
self.log(f"📊 CPU pattern: {self.cpu_pattern}, intensity: {self.cpu_intensity}")
50+
51+
# Simulate various startup phases
52+
phases = [
53+
("Initializing configuration", 0.2),
54+
("Loading dependencies", 0.3),
55+
("Connecting to services", 0.2),
56+
("Warming up caches", 0.2),
57+
("Final preparations", 0.1)
58+
]
59+
60+
for phase, duration_ratio in phases:
61+
phase_duration = self.startup_delay * duration_ratio
62+
self.log(f"⏳ {phase}...")
63+
64+
# Show progress during longer phases
65+
if phase_duration > 5:
66+
steps = int(phase_duration)
67+
for i in range(steps):
68+
time.sleep(1)
69+
progress = ((i + 1) / steps) * 100
70+
self.log(f" Progress: {progress:.0f}%")
71+
else:
72+
time.sleep(phase_duration)
73+
74+
self.is_ready = True
75+
self.log("✅ Application ready!")
76+
77+
def cpu_worker(self, worker_id):
78+
"""CPU intensive worker thread"""
79+
self.log(f"🔧 CPU worker {worker_id} started")
80+
81+
while self.is_running:
82+
if self.current_cpu_target > 0:
83+
# Calculate work duration based on target CPU usage
84+
work_duration = 0.1 * self.current_cpu_target
85+
sleep_duration = 0.1 * (1 - self.current_cpu_target)
86+
87+
# Do CPU intensive work
88+
start = time.time()
89+
while time.time() - start < work_duration:
90+
# Mathematical operations to consume CPU
91+
math.sqrt(random.random() * 1000000)
92+
93+
time.sleep(sleep_duration)
94+
else:
95+
time.sleep(0.1)
96+
97+
def update_cpu_pattern(self):
98+
"""Update CPU usage based on the configured pattern"""
99+
elapsed = (datetime.now() - self.start_time).total_seconds()
100+
101+
if self.cpu_pattern == 'constant':
102+
self.current_cpu_target = self.cpu_intensity
103+
104+
elif self.cpu_pattern == 'sine':
105+
# Sine wave pattern with 60-second period
106+
self.current_cpu_target = (math.sin(elapsed / 60 * 2 * math.pi) + 1) / 2 * self.cpu_intensity
107+
108+
elif self.cpu_pattern == 'random':
109+
# Random walk
110+
change = random.uniform(-0.1, 0.1)
111+
self.current_cpu_target = max(0, min(1, self.current_cpu_target + change))
112+
self.current_cpu_target *= self.cpu_intensity
113+
114+
elif self.cpu_pattern == 'burst':
115+
# Burst pattern: high CPU for 30s, low CPU for 30s
116+
cycle_position = elapsed % 60
117+
if cycle_position < 30:
118+
self.current_cpu_target = self.cpu_intensity
119+
else:
120+
self.current_cpu_target = self.cpu_intensity * 0.1
121+
122+
elif self.cpu_pattern == 'ramp':
123+
# Gradually increase CPU usage over 5 minutes, then reset
124+
cycle_position = elapsed % 300 # 5 minutes
125+
ramp_progress = cycle_position / 300
126+
self.current_cpu_target = ramp_progress * self.cpu_intensity
127+
128+
def status_reporter(self):
129+
"""Report status periodically"""
130+
while self.is_running:
131+
if self.is_ready:
132+
uptime = (datetime.now() - self.start_time).total_seconds()
133+
self.update_cpu_pattern()
134+
135+
status = {
136+
"status": "running",
137+
"uptime_seconds": int(uptime),
138+
"cpu_pattern": self.cpu_pattern,
139+
"cpu_target": f"{self.current_cpu_target:.2f}",
140+
"cpu_intensity": self.cpu_intensity,
141+
"active_workers": len([w for w in self.cpu_workers if w.is_alive()]),
142+
"memory_info": f"RSS: {self._get_memory_usage():.1f}MB"
143+
}
144+
145+
self.log(f"📈 Status: {json.dumps(status, indent=2)}")
146+
147+
time.sleep(self.status_interval)
148+
149+
def _get_memory_usage(self):
150+
"""Get memory usage in MB"""
151+
try:
152+
import psutil
153+
process = psutil.Process(os.getpid())
154+
return process.memory_info().rss / 1024 / 1024
155+
except ImportError:
156+
# Fallback if psutil not available
157+
return 0.0
158+
159+
class HealthHandler(BaseHTTPRequestHandler):
160+
def __init__(self, request, client_address, server, app_instance):
161+
self.app_instance = app_instance
162+
super().__init__(request, client_address, server)
163+
164+
def do_GET(self):
165+
if self.path == '/health':
166+
if self.app_instance.is_ready:
167+
self.send_response(200)
168+
self.send_header('Content-type', 'application/json')
169+
self.end_headers()
170+
171+
response = {
172+
"status": "healthy",
173+
"ready": self.app_instance.is_ready,
174+
"uptime": int((datetime.now() - self.app_instance.start_time).total_seconds()),
175+
"cpu_target": self.app_instance.current_cpu_target
176+
}
177+
self.wfile.write(json.dumps(response).encode())
178+
else:
179+
self.send_response(503)
180+
self.send_header('Content-type', 'application/json')
181+
self.end_headers()
182+
response = {"status": "starting", "ready": False}
183+
self.wfile.write(json.dumps(response).encode())
184+
else:
185+
self.send_response(404)
186+
self.end_headers()
187+
188+
def log_message(self, format, *args):
189+
# Suppress HTTP server logs
190+
pass
191+
192+
def start_http_server(self):
193+
"""Start HTTP server for health checks"""
194+
try:
195+
def handler_factory(*args, **kwargs):
196+
return self.HealthHandler(*args, app_instance=self, **kwargs)
197+
198+
self.http_server = HTTPServer(('0.0.0.0', self.http_port), handler_factory)
199+
200+
def serve():
201+
self.log(f"🌐 HTTP server starting on port {self.http_port}")
202+
self.http_server.serve_forever()
203+
204+
http_thread = threading.Thread(target=serve, daemon=True)
205+
http_thread.start()
206+
except Exception as e:
207+
self.log(f"❌ Failed to start HTTP server: {e}")
208+
209+
def run(self):
210+
"""Main application loop"""
211+
try:
212+
# Start HTTP server
213+
self.start_http_server()
214+
215+
# Simulate startup
216+
self.simulate_startup()
217+
218+
# Start CPU workers
219+
cpu_count = multiprocessing.cpu_count()
220+
worker_count = min(cpu_count, 4) # Limit to 4 workers max
221+
222+
for i in range(worker_count):
223+
worker = threading.Thread(target=self.cpu_worker, args=(i,), daemon=True)
224+
worker.start()
225+
self.cpu_workers.append(worker)
226+
227+
self.log(f"🔥 Started {worker_count} CPU workers")
228+
229+
# Start status reporter
230+
self.status_thread = threading.Thread(target=self.status_reporter, daemon=True)
231+
self.status_thread.start()
232+
233+
# Main loop
234+
self.log("🎯 Entering main application loop")
235+
while self.is_running:
236+
time.sleep(1)
237+
238+
except Exception as e:
239+
self.log(f"❌ Application error: {e}")
240+
raise
241+
242+
def shutdown(self):
243+
"""Graceful shutdown"""
244+
self.log("🛑 Shutting down application...")
245+
self.is_running = False
246+
247+
if self.http_server:
248+
self.http_server.shutdown()
249+
250+
self.log("👋 Application stopped")
251+
sys.exit(0)
252+
253+
if __name__ == "__main__":
254+
app = DemoApp()
255+
app.run()

deployment/apps/demopod/rbac.yaml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
apiVersion: rbac.authorization.k8s.io/v1
2+
kind: Role
3+
metadata:
4+
name: podstatus
5+
rules:
6+
- apiGroups: [""]
7+
resources: ["pods"]
8+
verbs: ["*"]
9+
---
10+
apiVersion: rbac.authorization.k8s.io/v1
11+
kind: RoleBinding
12+
metadata:
13+
name: podstatus
14+
subjects:
15+
- kind: ServiceAccount
16+
name: default
17+
namespace: podstatus
18+
roleRef:
19+
kind: Role
20+
name: podstatus
21+
apiGroup: rbac.authorization.k8s.io

0 commit comments

Comments
 (0)