Skip to content

Commit 41bba75

Browse files
authored
Merge pull request #51 from kagenti/docs/p0prime-ocp-fs-free-deployment
Deploy the FS-free harness on OpenShift with a durable sandbox (P0′)
2 parents d199484 + 6c928a9 commit 41bba75

9 files changed

Lines changed: 947 additions & 76 deletions

File tree

deploy/knative/README-ocp.md

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,26 @@
33
`deploy/knative/setup-ocp.sh` stands up the serverless-harness stack on
44
**OpenShift 4.20+** — the OpenShift-native sibling of [`setup-kind.sh`](setup-kind.sh).
55
It installs OpenShift Serverless (Knative + Kourier), Redis, the sandbox pod, the
6-
`leaf-work` PVC, the LLM-credentials secret, and the harness Knative Service,
7-
reachable over its **auto-created OpenShift Route**.
6+
LLM-credentials secret, and the harness Knative Service, reachable over its
7+
**auto-created OpenShift Route**.
88

99
Base bring-up only — see [Scope](#scope) for what is deferred.
1010

1111
## Prerequisites
1212

1313
- **`oc`**, logged in to an OpenShift **4.20+** cluster as **cluster-admin**
1414
(operator installs + SCC assignment require it).
15-
- A default **StorageClass** for the `leaf-work` PVC (the script fails fast if
15+
- A default **StorageClass** for the sandbox's durable `/workspace` PVC (the script fails fast if
1616
none exists). See the [storage caveat](#storage--scc).
1717
- A model credential:
1818
- `ANTHROPIC_API_KEY` (direct), **or**
1919
- `ANTHROPIC_AUTH_TOKEN` + `ANTHROPIC_BASE_URL` (Bearer-token gateway, e.g. LiteLLM).
2020
- The harness image. By default the script pulls the published
2121
`ghcr.io/kagenti/serverless-harness:latest`; override with `--image`.
22+
- **agent-sandbox controller** (kubernetes-sigs v0.5.0) is installed by the script
23+
(`sandboxes.agents.x-k8s.io`); it creates the `sandbox-0` pod from the Sandbox CR
24+
and provisions its durable `/workspace` PVC. The harness resolves the pod via the
25+
CR's `.status.selector` and `kubectl exec`s tool calls into it.
2226

2327
## Quick start
2428

@@ -63,7 +67,7 @@ creates a real Route per Knative Service (`oc get ksvc serverless-harness -o jso
6367
| Knative config | Autoscaler tuning + the `podspec-persistent-volume-claim`/`-write`/`-securitycontext` feature flags are set in the **`KnativeServing` CR spec** (the operator reverts direct `config-*` ConfigMap patches). |
6468
| Redis | Lightweight in-repo Deployment (`redis:7-alpine`), runs under `restricted-v2`. |
6569
| Sandbox | Pre-baked image ([`sandbox.Dockerfile`](sandbox.Dockerfile), `USER 65532`), built in-cluster against the internal registry (or supplied via `--sandbox-image`). |
66-
| `leaf-work` PVC | `ReadWriteOnce`, cluster-default StorageClass. |
70+
| Sandbox `/workspace` PVC | `ReadWriteOnce` (Sandbox CR `volumeClaimTemplates`), cluster-default StorageClass. |
6771
| Harness | Knative Service applied via the [`overlays/ocp`](overlays/ocp) kustomize overlay; SA granted the `nonroot-v2` SCC. |
6872
| Ingress | Auto-created OpenShift Route. |
6973

@@ -144,11 +148,12 @@ and verifying the async-leaf path itself on OpenShift is a further step.
144148

145149
## Storage & SCC
146150

147-
- **Storage / RWX.** `leaf-work` is `ReadWriteOnce`. On block storage (e.g. AWS EBS
151+
- **Storage / RWX.** The sandbox's `/workspace` PVC is `ReadWriteOnce`. On block storage (e.g. AWS EBS
148152
`gp3-csi`) it binds to a single node — fine for a single harness consumer.
149153
Concurrent multi-node scale-out, or co-mounting with the leaf-orchestrator, needs
150154
a **RWX** StorageClass (a filesystem provisioner). The base bring-up does not deploy
151155
the orchestrator. Set a specific class by making it the cluster default before install.
156+
RWX, if ever needed for a shared sandbox pool, lives on the sandbox tier (P2) — never the harness.
152157
- **SCC.** The published harness image declares no `USER` (defaults to root), so it
153158
runs as an explicit non-root UID (65532) and the script grants the harness
154159
ServiceAccount the `nonroot-v2` SCC (`oc adm policy add-scc-to-user nonroot-v2 -z
@@ -174,7 +179,7 @@ and verifying the async-leaf path itself on OpenShift is a further step.
174179
|---------|-------------|
175180
| `ksvc` never Ready, pod `CreateContainerConfigError: container has runAsNonRoot and image will run as root` | The `nonroot-v2` SCC grant didn't apply. Re-run the script, or `oc adm policy add-scc-to-user nonroot-v2 -z serverless-harness -n <ns>`. |
176181
| `ksvc` never Ready, pod `CrashLoopBackOff` with `ERR_MODULE_NOT_FOUND` | The harness image is broken/stale. Use a newer `--image` (the fix shipped in the image build; see the repo history). |
177-
| `leaf-work` PVC stuck `Pending` | No (default) StorageClass. Set one, or ensure a provisioner is installed. |
182+
| Sandbox `/workspace` PVC stuck `Pending` | No (default) StorageClass. Set one, or ensure a provisioner is installed. |
178183
| `oc apply -k overlays/ocp` fails with a load-restrictor / "not in or below" error | The overlay references shared base YAMLs one level up. Render with `oc kustomize --load-restrictor LoadRestrictionsNone deploy/knative/overlays/ocp \| oc apply -f -``setup-ocp.sh` does this for you. |
179184
| `/turn` returns `"Connection error"` | The harness can't reach its configured Anthropic endpoint from the cluster (egress/gateway reachability). `/health` and session creation still work. |
180185

@@ -183,7 +188,11 @@ and verifying the async-leaf path itself on OpenShift is a further step.
183188
```bash
184189
oc delete ksvc serverless-harness -n default
185190
oc delete -k <(oc kustomize --load-restrictor LoadRestrictionsNone deploy/knative/overlays/ocp) 2>/dev/null || true
186-
oc delete pod sandbox-0 deployment/redis svc/redis pvc/leaf-work secret/llm-credentials -n default
191+
oc delete sandbox sandbox-0 deployment/redis svc/redis secret/llm-credentials -n default
192+
# The durable /workspace PVC is provisioned StatefulSet-style from the Sandbox CR's
193+
# volumeClaimTemplates and is NOT garbage-collected when the CR is deleted — remove it
194+
# explicitly to reclaim the backing EBS volume:
195+
oc delete pvc workspace-sandbox-0 -n default
187196
# Operators (optional): oc delete knativeserving knative-serving -n knative-serving; oc delete subscription serverless-operator -n openshift-serverless
188197
```
189198

deploy/knative/leaf-smoke.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ dispatch_item() {
4040
local body
4141
body=$(jq -nc --arg s "$sid" --arg m "$model" --arg id "$id" --arg f "$file" --arg p "$pat" --arg ws "$SBOX_REPO" \
4242
'{sessionId:$s, model:$m, workspaceRef:$ws, item:{item_id:$id, file:$f, pattern:$p}}')
43-
curl -s --max-time 240 -H "$HOST_HEADER" -H "Content-Type: application/json" -d "$body" "$BASE/runs"
43+
curl -s $CURL_OPTS --max-time 240 ${CURL_HDR[@]+"${CURL_HDR[@]}"} -H "Content-Type: application/json" -d "$body" "$BASE/runs"
4444
}
4545

4646
# dispatch <item_id> [model] -> echoes terminal JSON from /runs
@@ -129,7 +129,7 @@ if [ "$bogus" = "failed" ] && [ "$good" = "done" ]; then ok "bogus model -> fail
129129
# is the server's isLeafEnvelope/validateItem guard: a body with no `item` must return HTTP 400.
130130
claim 5 "Malformed envelope (missing item) is rejected with HTTP 400"
131131
neg_body=$(jq -nc --arg s "$RUN/ineg" '{sessionId:$s}')
132-
neg_code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 30 -H "$HOST_HEADER" -H "Content-Type: application/json" -d "$neg_body" "$BASE/runs")
132+
neg_code=$(curl -s $CURL_OPTS -o /dev/null -w '%{http_code}' --max-time 30 ${CURL_HDR[@]+"${CURL_HDR[@]}"} -H "Content-Type: application/json" -d "$neg_body" "$BASE/runs")
133133
if [ "$neg_code" = "400" ]; then ok "malformed envelope rejected (HTTP 400)"; else ko "expected HTTP 400, got $neg_code"; fi
134134

135135
# --- Claim 6: idempotent re-invoke returns a valid verdict ---

deploy/knative/overlays/ocp/kustomization.yaml

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,42 +6,36 @@
66
# - Harness image: pull the published multi-arch image from GHCR instead of the
77
# Kind-local dev.local tag. Override with `kustomize edit set image` /
88
# setup-ocp.sh --image.
9-
# - Sandbox image: the pre-baked image built in-cluster (sandbox.Dockerfile),
10-
# referenced by its internal-registry pullspec. Default assumes namespace
11-
# `default`; setup-ocp.sh rewrites this for --namespace.
12-
# - Sandbox pod: no root apk-add at start-up (tools are pre-baked), plus
13-
# restricted-v2-compatible securityContext and a writable /workspace emptyDir
14-
# (patch-sandbox.yaml).
9+
# - Sandbox image + pod: the pre-baked image (sandbox.Dockerfile) and the
10+
# non-root securityContext are set via a JSON6902 patch onto the Sandbox CR
11+
# (patch-sandbox.yaml): runAsUser/fsGroup 65532, seccomp RuntimeDefault,
12+
# drop ALL caps, and serviceAccountName serverless-harness-sandbox. The
13+
# command is `sleep infinity` (no root apk-add).
1514
#
16-
# The harness Service keeps the base runAsUser/fsGroup 65532: the published GHCR
17-
# image declares no USER (defaults to root), so a non-root UID must be set
18-
# explicitly — the SCC does not reliably inject one. setup-ocp.sh grants the
19-
# harness ServiceAccount the `nonroot-v2` SCC so that explicit non-root UID is
20-
# admitted (issue #41 item #4, approach b). The pre-baked sandbox image, by
21-
# contrast, sets USER 65532 itself, so its pod needs no SCC grant.
15+
# Both tiers run non-root: setup-ocp.sh grants nonroot-v2 to the harness SA
16+
# (serverless-harness) AND the sandbox SA (serverless-harness-sandbox), since the
17+
# GHCR harness image declares no USER and the SCC does not reliably inject a UID.
2218
#
23-
# The leaf-work PVC keeps ReadWriteOnce (the only mode AWS EBS / gp3-csi offers);
24-
# see SMOKE.md for the RWX caveat.
19+
# The sandbox's /workspace is backed by the Sandbox CR's durable RWO PVC
20+
# (volumeClaimTemplates) — the only mode AWS EBS / gp3-csi offers. RWX is a P2
21+
# concern on the sandbox tier, never the harness. There is no leaf-work PVC after P1.
2522
apiVersion: kustomize.config.k8s.io/v1beta1
2623
kind: Kustomization
2724

2825
resources:
2926
- ../../redis.yaml
3027
- ../../sandbox.yaml
31-
- ../../leaf-pvc.yaml
3228
- ../../service.yaml
3329

3430
images:
3531
- name: dev.local/serverless-harness
3632
newName: ghcr.io/kagenti/serverless-harness
3733
newTag: latest
38-
- name: alpine
39-
newName: image-registry.openshift-image-registry.svc:5000/default/serverless-harness-sandbox
40-
newTag: latest
4134

4235
patches:
4336
- path: patch-sandbox.yaml
4437
target:
45-
version: v1
46-
kind: Pod
38+
group: agents.x-k8s.io
39+
version: v1beta1
40+
kind: Sandbox
4741
name: sandbox-0
Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,32 @@
1-
# OpenShift variant of the sandbox pod. The base runs `apk add ...` as root at
2-
# start-up (blocked by restricted-v2); the tools are pre-baked into the image
3-
# (sandbox.Dockerfile, swapped in via the overlay `images:` transformer), so the
4-
# command is just `sleep infinity`. Adds restricted-v2-compatible securityContext
5-
# and mounts an emptyDir at /workspace (fsGroup-owned → writable by the assigned
6-
# UID) so the sandbox can write regardless of root-filesystem permissions.
7-
apiVersion: v1
8-
kind: Pod
9-
metadata:
10-
name: sandbox-0
11-
spec:
12-
securityContext:
1+
# OpenShift variant of the sandbox pod, applied as a JSON6902 patch onto the
2+
# Sandbox CR (agents.x-k8s.io/v1beta1). The base (Kind) runs `apk add ...` as root
3+
# at start-up — blocked by OCP SCCs. Here: tools are pre-baked into the image
4+
# (sandbox.Dockerfile), so the command is just `sleep infinity`; the image is the
5+
# in-cluster-built internal-registry pullspec; the pod runs non-root (UID 65532,
6+
# admitted by nonroot-v2 on the serverless-harness-sandbox SA) with fsGroup 65532
7+
# so the Sandbox CR's durable /workspace PVC (from volumeClaimTemplates) is writable.
8+
# NOTE: we deliberately do NOT touch volumeClaimTemplates/volumeMounts — /workspace
9+
# stays on the durable RWO PVC (the point of P0′), not an emptyDir.
10+
- op: replace
11+
path: /spec/podTemplate/spec/containers/0/image
12+
value: image-registry.openshift-image-registry.svc:5000/default/serverless-harness-sandbox:latest
13+
- op: replace
14+
path: /spec/podTemplate/spec/containers/0/command
15+
value: ["sleep", "infinity"]
16+
- op: add
17+
path: /spec/podTemplate/spec/serviceAccountName
18+
value: serverless-harness-sandbox
19+
- op: add
20+
path: /spec/podTemplate/spec/securityContext
21+
value:
22+
runAsUser: 65532
1323
runAsNonRoot: true
24+
fsGroup: 65532
1425
seccompProfile:
1526
type: RuntimeDefault
16-
containers:
17-
- name: sandbox
18-
command: ["sleep", "infinity"]
19-
securityContext:
20-
allowPrivilegeEscalation: false
21-
capabilities:
22-
drop: ["ALL"]
23-
volumeMounts:
24-
- name: workspace
25-
mountPath: /workspace
26-
volumes:
27-
- name: workspace
28-
emptyDir: {}
27+
- op: add
28+
path: /spec/podTemplate/spec/containers/0/securityContext
29+
value:
30+
allowPrivilegeEscalation: false
31+
capabilities:
32+
drop: ["ALL"]

deploy/knative/sandbox.Dockerfile

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,14 @@
1010
# Built in-cluster against the OpenShift internal registry by setup-ocp.sh
1111
# (oc new-build --binary --strategy=docker). The harness routes agent tool
1212
# execution into this pod via `kubectl exec`, so it needs bash + GNU coreutils,
13-
# findutils and grep on PATH.
13+
# findutils, grep and ripgrep (the agent's find/grep tools shell out to `rg`) on PATH.
1414
FROM alpine:3.20
1515

16-
RUN apk add --no-cache bash coreutils findutils grep
16+
RUN apk add --no-cache bash coreutils findutils grep ripgrep
1717

18-
# OpenShift assigns an arbitrary UID at runtime that belongs to the root group
19-
# (GID 0). Make /workspace owned by and writable for the root group so the
20-
# sandbox can write there regardless of which UID the SCC injects. (setup-ocp.sh
21-
# also mounts an emptyDir over /workspace, which fsGroup makes writable — this is
22-
# belt-and-suspenders for direct `docker run` / non-mounted use.)
18+
# OpenShift assigns the pod a non-root UID; the OCP overlay pins runAsUser/fsGroup
19+
# 65532 and backs /workspace with the Sandbox CR's durable PVC (fsGroup-owned, so
20+
# writable). The chgrp/chmod below is belt-and-suspenders for GID-0 arbitrary-UID use.
2321
RUN mkdir -p /workspace \
2422
&& chgrp -R 0 /workspace \
2523
&& chmod -R g=u /workspace

0 commit comments

Comments
 (0)