Skip to content

Commit df19a09

Browse files
kernel-gdclaude
andcommitted
chore: recast demo/local as truthful listener-sidecar smoke demo
Remove mock-upstream from default setup flow, fix broken wget-based metrics check to use port-forward+curl, bound synthetic request timeouts, correct stale ConfigMap comments in values.yaml, and reframe README as listener-audit validation with explicit scope boundaries. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 1deb3aa commit df19a09

4 files changed

Lines changed: 112 additions & 48 deletions

File tree

demo/local/README.md

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,20 @@
11
# Local Cluster Demo
22

3-
This demo creates a local Kubernetes cluster with kind, installs Koshi in listener mode, deploys a mock upstream and sample workload, sends synthetic traffic, and validates structured events and metrics.
3+
This demo creates a local kind cluster, installs Koshi in listener mode, injects a sidecar into a sample workload, sends synthetic traffic, and validates that structured events and Prometheus metrics are emitted by the listener sidecar.
44

55
> **This demo is for local development and validation only.** It requires a repo checkout, a local Docker build, and a kind cluster. To install Koshi on a real cluster using published artifacts, see [Koshi Onboarding](../../docs/onboarding.md).
66
7+
## What This Demo Validates
8+
9+
- Sidecar injection via the mutating admission webhook
10+
- Structured governance events (`stream: "event"`) emitted by the listener
11+
- Prometheus metrics (`koshi_listener_*`) exposed by the sidecar
12+
13+
## What This Demo Does Not Validate
14+
15+
- Real upstream AI provider responses — the sidecar routes to `api.openai.com` by default. Upstream responses may fail due to auth or connectivity. Demo success is based on listener events and metrics, not upstream response status.
16+
- Enforcement mode, custom policy, or standalone deployment — this demo is listener-only.
17+
718
## Prerequisites
819

920
- [Docker](https://docs.docker.com/get-docker/)
@@ -40,42 +51,41 @@ helm install koshi ../../deploy/helm/koshi/ \
4051
-f values.yaml
4152
```
4253

43-
### 3. Deploy the mock upstream
44-
45-
The mock upstream returns OpenAI-shaped responses with token usage data:
54+
### 3. Label the namespace and deploy a workload
4655

4756
```bash
48-
kubectl apply -f mock-upstream.yaml
57+
kubectl label namespace default runtime.getkoshi.ai/inject=true
58+
kubectl apply -f workload.yaml
59+
kubectl wait --for=condition=available deploy/demo-workload -n default --timeout=120s
4960
```
5061

51-
### 4. Label the namespace and deploy a workload
62+
### 4. Verify sidecar injection
5263

5364
```bash
54-
kubectl label namespace default runtime.getkoshi.ai/inject=true
55-
kubectl apply -f workload.yaml
65+
kubectl get pod -l app=demo-workload -o jsonpath='{.items[0].spec.containers[*].name}'
66+
# Should include "koshi-listener"
5667
```
5768

58-
### 5. Wait for pods and send traffic
69+
### 5. Send synthetic traffic
5970

6071
```bash
61-
kubectl wait --for=condition=ready pod -l app=demo-workload -n default --timeout=120s
62-
63-
# Send a few requests through the sidecar
6472
kubectl exec deploy/demo-workload -c app -- \
6573
curl -s -X POST http://localhost:15080/v1/chat/completions \
6674
-H "Content-Type: application/json" \
6775
-H "Host: api.openai.com" \
6876
-d '{"model":"gpt-4","max_tokens":100,"messages":[{"role":"user","content":"hello"}]}'
6977
```
7078

79+
The sidecar routes to `api.openai.com` by default. Upstream responses may fail due to auth or connectivity — the listener emits structured events and metrics before upstream proxying, so failures do not affect demo validation. The setup script bounds request time (`--connect-timeout 2 --max-time 5`) for the same reason.
80+
7181
### 6. Validate structured events
7282

7383
```bash
7484
kubectl logs deploy/demo-workload -c koshi-listener --tail=50 | \
7585
jq 'select(.stream == "event")'
7686
```
7787

78-
You should see events with `decision_shadow: "allow"` and shadow fields like `namespace`, `workload_kind`, `provider`.
88+
You should see events with `decision_shadow` and fields like `namespace`, `workload_kind`, `provider`.
7989

8090
### 7. Check metrics
8191

@@ -95,26 +105,21 @@ Expected metrics:
95105
curl -s http://localhost:15080/status | jq .
96106
```
97107

98-
## What to Look For
108+
## Notes
99109

100-
- **Events with `"stream": "event"`** — these are the structured governance events
101-
- **`decision_shadow: "allow"`** — traffic is flowing and passing all checks
102-
- **`koshi_listener_decisions_total`** — Prometheus counter with namespace and shadow decision labels
103-
- **No 403/429/503 responses** — listener mode never blocks traffic
110+
`mock-upstream.yaml` is retained in this directory but is not part of the default demo flow. Injected sidecars use `DefaultListenerConfig()`, which routes to real AI provider APIs — the mock upstream is not reachable without overriding sidecar upstream routing, which is not supported for injected sidecars in the current release.
104111

105112
## Using Released Artifacts
106113

107-
The local demo above uses a locally built image and a mock upstream. To use published artifacts on a real cluster with real AI API providers:
114+
To use published artifacts on a real cluster with real AI API providers:
108115

109116
```bash
110117
helm install koshi oci://ghcr.io/koshihq/charts/koshi \
111118
--version 0.2.12 \
112119
--namespace koshi-system --create-namespace
113120
```
114121

115-
Injected sidecars use the built-in default listener config, which routes traffic to `https://api.openai.com` and `https://api.anthropic.com`. The mock-upstream flow in this demo is for local development only — it is not part of the released-artifact validation path.
116-
117-
For partner onboarding on a real cluster, see [Koshi Onboarding](../../docs/onboarding.md).
122+
For onboarding on a real cluster, see [Koshi Onboarding](../../docs/onboarding.md).
118123

119124
## Cleanup
120125

demo/local/setup.sh

Lines changed: 75 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,20 @@ set -euo pipefail
44
CLUSTER_NAME="koshi-demo"
55
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
66
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
7+
PORT_FWD_PID=""
8+
9+
cleanup_port_forward() {
10+
[ -n "${PORT_FWD_PID}" ] && kill "${PORT_FWD_PID}" 2>/dev/null || true
11+
[ -n "${PORT_FWD_PID}" ] && wait "${PORT_FWD_PID}" 2>/dev/null || true
12+
PORT_FWD_PID=""
13+
}
14+
trap cleanup_port_forward EXIT
715

816
echo "=== Koshi Runtime Local Demo ==="
917
echo ""
18+
echo "This demo validates listener-sidecar injection, structured events,"
19+
echo "and Prometheus metrics on a local kind cluster."
20+
echo ""
1021

1122
# 1. Create kind cluster.
1223
if kind get clusters 2>/dev/null | grep -q "^${CLUSTER_NAME}$"; then
@@ -31,13 +42,7 @@ helm upgrade --install koshi "$REPO_ROOT/deploy/helm/koshi/" \
3142
-f "$SCRIPT_DIR/values.yaml" \
3243
--wait --timeout 120s
3344

34-
# 4. Deploy mock upstream.
35-
echo ""
36-
echo "Deploying mock upstream..."
37-
kubectl apply -f "$SCRIPT_DIR/mock-upstream.yaml"
38-
kubectl wait --for=condition=available deploy/mock-upstream -n default --timeout=120s
39-
40-
# 5. Label namespace and deploy workload.
45+
# 4. Label namespace and deploy workload.
4146
echo ""
4247
echo "Labeling default namespace for injection..."
4348
kubectl label namespace default runtime.getkoshi.ai/inject=true --overwrite
@@ -48,37 +53,88 @@ kubectl apply -f "$SCRIPT_DIR/workload.yaml"
4853
echo "Waiting for demo workload to be ready..."
4954
kubectl wait --for=condition=available deploy/demo-workload -n default --timeout=120s
5055

56+
# 5. Verify sidecar injection.
57+
echo ""
58+
CONTAINERS=$(kubectl get pod -l app=demo-workload -o jsonpath='{.items[0].spec.containers[*].name}')
59+
if echo "${CONTAINERS}" | grep -q "koshi-listener"; then
60+
echo "PASS: Sidecar injected (containers: ${CONTAINERS})"
61+
else
62+
echo "FAIL: Sidecar not found in pod containers: ${CONTAINERS}"
63+
exit 1
64+
fi
65+
5166
# 6. Send synthetic traffic.
67+
# The listener emits shadow events and metrics before proxying upstream,
68+
# so these requests only need to trigger listener-side evaluation.
69+
# Timeouts are bounded to avoid waiting on upstream auth/connectivity.
70+
echo ""
71+
echo "Sending synthetic requests through the sidecar..."
72+
echo "(Listener events and metrics are emitted before upstream proxying."
73+
echo " Timeouts are bounded — upstream auth or connectivity failures do not"
74+
echo " affect demo success.)"
5275
echo ""
53-
echo "Sending synthetic requests..."
5476
for i in 1 2 3; do
5577
echo " Request $i..."
5678
kubectl exec deploy/demo-workload -c app -- \
57-
curl -s -X POST http://localhost:15080/v1/chat/completions \
79+
curl -s --connect-timeout 2 --max-time 5 \
80+
-X POST http://localhost:15080/v1/chat/completions \
5881
-H "Content-Type: application/json" \
5982
-H "Host: api.openai.com" \
6083
-d "{\"model\":\"gpt-4\",\"max_tokens\":100,\"messages\":[{\"role\":\"user\",\"content\":\"hello $i\"}]}" \
61-
|| echo " (request $i failed — sidecar may not be injected yet)"
84+
> /dev/null 2>&1 || true
6285
sleep 1
6386
done
6487

65-
# 7. Validate events.
88+
# 7. Validate structured events.
6689
echo ""
6790
echo "=== Structured Events (stream: event) ==="
68-
kubectl logs deploy/demo-workload -c koshi-listener --tail=20 2>/dev/null | \
69-
jq -c 'select(.stream == "event")' 2>/dev/null || \
70-
echo "(no koshi-listener container found — webhook may not have injected the sidecar)"
91+
EVENTS=$(kubectl logs deploy/demo-workload -c koshi-listener --tail=50 2>/dev/null | \
92+
grep '"stream":"event"' || true)
93+
if [ -n "${EVENTS}" ]; then
94+
EVENT_COUNT=$(echo "${EVENTS}" | wc -l | tr -d ' ')
95+
echo "PASS: Found ${EVENT_COUNT} structured events"
96+
echo "${EVENTS}" | head -3 | jq -c . 2>/dev/null || echo "${EVENTS}" | head -3
97+
else
98+
echo "FAIL: No structured events found in sidecar logs"
99+
exit 1
100+
fi
71101

72-
# 8. Validate metrics.
102+
# 8. Validate metrics via port-forward.
73103
echo ""
74104
echo "=== Listener Metrics ==="
75-
kubectl exec deploy/demo-workload -c koshi-listener -- \
76-
wget -qO- http://localhost:15080/metrics 2>/dev/null | \
77-
grep "^koshi_listener" || \
78-
echo "(no listener metrics found)"
105+
kubectl port-forward deploy/demo-workload 15080:15080 >/dev/null 2>&1 &
106+
PORT_FWD_PID=$!
107+
108+
METRICS=""
109+
for attempt in $(seq 1 10); do
110+
METRICS=$(curl -fsS http://127.0.0.1:15080/metrics 2>/dev/null) && break
111+
METRICS=""
112+
sleep 1
113+
done
114+
115+
cleanup_port_forward
116+
117+
if echo "${METRICS}" | grep -q "koshi_listener_decisions_total"; then
118+
echo "PASS: Listener metrics present"
119+
echo "${METRICS}" | grep "^koshi_listener" | head -5
120+
else
121+
echo "FAIL: koshi_listener_decisions_total not found in metrics output"
122+
exit 1
123+
fi
79124

80125
echo ""
81126
echo "=== Demo Complete ==="
127+
echo ""
128+
echo "What this demo validated:"
129+
echo " - Sidecar injection via mutating webhook"
130+
echo " - Structured governance events (stream: event) emitted by the listener"
131+
echo " - Prometheus metrics (koshi_listener_*) exposed by the sidecar"
132+
echo ""
133+
echo "Note: Injected sidecars use the built-in default listener config, which"
134+
echo "routes to real AI provider APIs. Upstream responses may fail due to auth"
135+
echo "or connectivity — this does not affect the listener audit signal. Events"
136+
echo "and metrics are emitted regardless of upstream response status."
137+
echo ""
82138
echo "To explore further:"
83139
echo " kubectl logs deploy/demo-workload -c koshi-listener | jq 'select(.stream == \"event\")'"
84140
echo " kubectl port-forward deploy/demo-workload 15080:15080"

demo/local/teardown.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ echo "=== Tearing down Koshi demo ==="
77

88
# Remove workloads.
99
kubectl delete -f "$(dirname "$0")/workload.yaml" --ignore-not-found 2>/dev/null || true
10+
# Defensive: clean up mock-upstream if it was deployed manually.
1011
kubectl delete -f "$(dirname "$0")/mock-upstream.yaml" --ignore-not-found 2>/dev/null || true
1112

1213
# Remove namespace label.

demo/local/values.yaml

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
1-
# Helm values for the local demo.
2-
# Uses listener mode with the mock upstream.
1+
# Helm values for the local demo (listener mode).
2+
#
3+
# Injected sidecars use DefaultListenerConfig() — the built-in in-memory config
4+
# that routes to https://api.openai.com and https://api.anthropic.com. Sidecars
5+
# do NOT read the chart ConfigMap. The values below (config.*) affect only the
6+
# control-plane ConfigMap, which is used by standalone deployments.
7+
#
8+
# This demo validates listener-sidecar injection, structured events, and metrics.
9+
# It does not deploy a mock upstream or override sidecar upstreams.
310

411
mode: listener
512

@@ -21,8 +28,3 @@ sidecar:
2128
requests:
2229
cpu: 50m
2330
memory: 32Mi
24-
25-
# NOTE: The chart's ConfigMap template (templates/configmap.yaml) uses hardcoded
26-
# listener config and does not template from .Values.config.
27-
# Upstream overrides for the local demo are applied via the mock-upstream service
28-
# that the ConfigMap's hardcoded upstream URLs are replaced with at deploy time.

0 commit comments

Comments
 (0)