From f50eca0441210c9852284e1f7d186ea2cdf531a9 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 19 Jun 2026 07:42:47 +0000 Subject: [PATCH] fix(helm): let the UI serve the server's index (ui.useServerIndex) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The web UI deployed as an independent instance with its own PVC (…-ui-data), while the init/reindex Jobs only ever index the server's PVC (…-server-data) over HTTP. Nothing populated the UI's volume, so a deployed UI — especially in demo mode, where the in-app Reindex button is disabled — showed an empty index (0 files / 0 chunks / 0 vectors) even though the server was fully indexed. Add ui.useServerIndex: when true the UI mounts the server's index volume read-only at CODERAG_STORE_DIR, so it serves exactly what the index Jobs built and can never corrupt the single-writer LanceDB store. No …-ui-data PVC is created. The embedding-model cache is redirected to the writable home volume so query embedding still works under the read-only mount. ui.coLocateWithServer pins the UI onto the server's node via podAffinity, required when the shared volume is ReadWriteOnce; omit it for a ReadWriteMany storageClass. Rendering fails fast if useServerIndex is set without server.enabled or persistence.enabled. Docs and CI full-values updated to exercise the shared topology. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01JPY56pX54CybUWA9ibdHny --- deploy/README.md | 51 +++++++++++++++---- deploy/helm/coderag/ci/full-values.yaml | 4 ++ deploy/helm/coderag/templates/_helpers.tpl | 15 ++++++ .../helm/coderag/templates/ui-deployment.yaml | 37 ++++++++++++++ deploy/helm/coderag/templates/ui-pvc.yaml | 2 +- deploy/helm/coderag/values.yaml | 32 ++++++++++-- 6 files changed, 126 insertions(+), 15 deletions(-) diff --git a/deploy/README.md b/deploy/README.md index 0bf78d5..c202b09 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -46,9 +46,17 @@ processes writing one index would corrupt it. The chart is built around that fac - **Hardened by default**: non-root (uid 10001), read-only root filesystem, dropped capabilities, `RuntimeDefault` seccomp, and the service-account token is not mounted. -The **server** is the primary, recommended surface. The **UI** is optional and, because it -bundles the engine and writes its own index, runs as an *independent* instance with its -own volume when enabled (build its index with the in-app **Reindex** button). +The **server** is the primary, recommended surface. The **UI** is optional and runs in one +of two topologies: + +- **Shared (recommended for a read-only/demo UI):** `ui.useServerIndex=true` mounts the + server's index volume **read-only**, so the UI serves exactly what the index/reindex + Jobs built — no separate volume, always in sync, and it can never corrupt the writer's + store. Reindexing stays a server-side Job. +- **Independent (default):** the UI gets its **own** data volume and bundles the engine. + Nothing populates that volume automatically (the index Jobs drive the *server's* volume), + so you build it with the in-app **Reindex** button — which is **disabled in demo mode**, + so a demo UI left on the default shows an empty index (0 files / 0 chunks). --- @@ -142,7 +150,9 @@ Full list with comments: [`values.yaml`](helm/coderag/values.yaml). The most-use | `server.service.type` | `ClusterIP` | `ClusterIP` · `NodePort` · `LoadBalancer`. | | `index.initJob.enabled` | `true` | Build the index automatically on install/upgrade. | | `index.cronjob.enabled` | `false` | Recurring reindex (`index.cronjob.schedule`). | -| `ui.enabled` | `false` | Also deploy the web UI (independent instance). | +| `ui.enabled` | `false` | Also deploy the web UI. | +| `ui.useServerIndex` | `false` | UI serves the **server's** index (read-only) instead of its own empty volume. | +| `ui.coLocateWithServer` | `false` | Pin the UI onto the server's node — required with `useServerIndex` on RWO storage. | | `ingress.enabled` | `false` | Expose via an Ingress (**add TLS + auth** — the API has none). | | `resources` (`server.*`, `ui.*`) | see values | CPU/memory requests & limits. | @@ -406,13 +416,27 @@ ingress: ### Also run the web UI +**Recommended — UI serves the server's index (read-only):** + ```bash ---set ui.enabled=true +--set ui.enabled=true \ +--set ui.useServerIndex=true +# On ReadWriteOnce storage (the default), also pin the UI onto the server's node: +--set ui.coLocateWithServer=true +# Omit coLocateWithServer if persistence uses a ReadWriteMany storageClass. ``` -The UI gets its own data volume and clones the same repo. Open it via port-forward -(`svc/coderag-ui:8501`) or add an Ingress path with `service: ui`, then click **Reindex** -in the sidebar to build its index. +The UI mounts the server's index volume read-only, so it shows whatever the +init/reindex Jobs built — nothing to reindex from the UI, and it stays in sync with the +server. This is the right choice for a public/demo UI, where the in-app Reindex button is +disabled. Open it via port-forward (`svc/coderag-ui:8501`) or an Ingress path with +`service: ui`. + +> **Independent UI (default, `ui.useServerIndex=false`):** the UI gets its **own** data +> volume and clones the same repo, but **nothing populates that volume** — you must click +> **Reindex** in the sidebar to build it (impossible in demo mode). If your UI shows +> 0 files / 0 chunks, this is almost always why: the index Jobs filled the *server's* +> volume, not the UI's. Switch to `ui.useServerIndex=true`. ### Pin to an immutable image (reproducible / air-gapped) @@ -489,6 +513,12 @@ helm template coderag deploy/helm/coderag -f deploy/helm/coderag/ci/full-values. `/data`, and `/home/coderag`. If a backend insists on another path, mount it via `extraVolumes`/`extraVolumeMounts`, or relax the hardening: `--set securityContext.readOnlyRootFilesystem=false`. +- **UI shows 0 files / 0 chunks / 0 vectors** — the UI is on its own (empty) data volume + while the index Jobs populated the *server's* volume. They are different PVCs + (`…-ui-data` vs `…-server-data`). Set `ui.useServerIndex=true` so the UI serves the + server's index read-only (add `ui.coLocateWithServer=true` on `ReadWriteOnce` storage). + The independent UI only fills its own volume via the in-app **Reindex** button, which is + disabled in demo mode. ## Limitations @@ -496,5 +526,6 @@ helm template coderag deploy/helm/coderag -f deploy/helm/coderag/ci/full-values. put a cache/load balancer in front of the read endpoints; the index itself stays single-writer. - **`ReadWriteOnce`** ties the index to one node at a time; that's expected for the embedded store. -- The **UI**, when enabled, maintains a *separate* index from the server. For a single - shared index, run the server and point browsers/tools at its REST API. +- The **UI**, when enabled, defaults to a *separate* index from the server. For a single + shared index, set `ui.useServerIndex=true` (the UI reads the server's volume read-only), + or run the server alone and point browsers/tools at its REST API. diff --git a/deploy/helm/coderag/ci/full-values.yaml b/deploy/helm/coderag/ci/full-values.yaml index 2491908..164659d 100644 --- a/deploy/helm/coderag/ci/full-values.yaml +++ b/deploy/helm/coderag/ci/full-values.yaml @@ -12,6 +12,10 @@ workspace: ui: enabled: true + # Exercise the shared-index topology: UI mounts the server's volume read-only (no + # …-ui-data PVC), with node co-location for the ReadWriteOnce volume. + useServerIndex: true + coLocateWithServer: true secrets: create: true diff --git a/deploy/helm/coderag/templates/_helpers.tpl b/deploy/helm/coderag/templates/_helpers.tpl index 0ab59fc..9b20510 100644 --- a/deploy/helm/coderag/templates/_helpers.tpl +++ b/deploy/helm/coderag/templates/_helpers.tpl @@ -98,6 +98,19 @@ Service name / in-cluster base URL for the HTTP API (used by the index jobs). {{- printf "http://%s:%v" (include "coderag.serverServiceName" .) .Values.server.service.port -}} {{- end -}} +{{/* +Name of the PVC holding the SERVER's index — the chart-managed claim, or the +existingClaim the operator supplied. Used by the server pod and by the UI when +ui.useServerIndex shares that same volume. +*/}} +{{- define "coderag.serverDataClaimName" -}} +{{- if .Values.persistence.existingClaim -}} +{{- .Values.persistence.existingClaim -}} +{{- else -}} +{{- printf "%s-server-data" (include "coderag.fullname" .) -}} +{{- end -}} +{{- end -}} + {{/* envFrom block shared by the server and UI containers: non-secret config plus the optional API-key Secret. @@ -242,6 +255,8 @@ Pod volumes for a writer (server/ui). Call with (dict "ctx" . "component" "serve persistentVolumeClaim: {{- if and (eq $component "server") $ctx.Values.persistence.existingClaim }} claimName: {{ $ctx.Values.persistence.existingClaim }} + {{- else if and (eq $component "ui") $ctx.Values.ui.useServerIndex }} + claimName: {{ include "coderag.serverDataClaimName" $ctx }} {{- else if and (eq $component "ui") $ctx.Values.ui.persistence.existingClaim }} claimName: {{ $ctx.Values.ui.persistence.existingClaim }} {{- else }} diff --git a/deploy/helm/coderag/templates/ui-deployment.yaml b/deploy/helm/coderag/templates/ui-deployment.yaml index 6f59184..9972b92 100644 --- a/deploy/helm/coderag/templates/ui-deployment.yaml +++ b/deploy/helm/coderag/templates/ui-deployment.yaml @@ -1,4 +1,12 @@ {{- if .Values.ui.enabled -}} +{{- if .Values.ui.useServerIndex }} +{{- if not .Values.server.enabled }} +{{- fail "ui.useServerIndex=true requires server.enabled=true (the UI shares the server's index volume)." }} +{{- end }} +{{- if not .Values.persistence.enabled }} +{{- fail "ui.useServerIndex=true requires persistence.enabled=true (there is a server PVC to share; an emptyDir cannot be shared across pods)." }} +{{- end }} +{{- end }} apiVersion: apps/v1 kind: Deployment metadata: @@ -60,6 +68,13 @@ spec: value: {{ .Values.ui.containerPort | quote }} - name: HOME value: /home/coderag + {{- if .Values.ui.useServerIndex }} + # The shared server index is mounted read-only, so the embedding model can't + # be cached onto it. Point the cache at the writable `home` volume instead + # (overrides CODERAG_CACHE_DIR from the ConfigMap) so query embedding works. + - name: CODERAG_CACHE_DIR + value: /home/coderag/.model-cache + {{- end }} {{- with .Values.ui.extraEnv }} {{- toYaml . | nindent 12 }} {{- end }} @@ -90,6 +105,11 @@ spec: volumeMounts: - name: data mountPath: {{ .Values.persistence.mountPath }} + {{- if .Values.ui.useServerIndex }} + # Shared server index: read-only so the UI can never corrupt the single + # writer's LanceDB store (reindex stays a server-side Job). + readOnly: true + {{- end }} - name: workspace mountPath: {{ .Values.workspace.mountPath }} readOnly: {{ .Values.workspace.readOnly }} @@ -109,10 +129,27 @@ spec: nodeSelector: {{- toYaml . | nindent 8 }} {{- end }} + {{- if and .Values.ui.useServerIndex .Values.ui.coLocateWithServer }} + # ReadWriteOnce volume shared with the server attaches to a single node, so pin the + # UI pod onto the node already running the server pod. (Set coLocateWithServer=false + # if persistence uses a ReadWriteMany class — then any node can mount the volume.) + affinity: + {{- with .Values.affinity }} + {{- toYaml . | nindent 8 }} + {{- end }} + podAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchLabels: + {{- include "coderag.selectorLabels" . | nindent 18 }} + app.kubernetes.io/component: server + topologyKey: kubernetes.io/hostname + {{- else }} {{- with .Values.affinity }} affinity: {{- toYaml . | nindent 8 }} {{- end }} + {{- end }} {{- with .Values.tolerations }} tolerations: {{- toYaml . | nindent 8 }} diff --git a/deploy/helm/coderag/templates/ui-pvc.yaml b/deploy/helm/coderag/templates/ui-pvc.yaml index afb048a..4e5ce57 100644 --- a/deploy/helm/coderag/templates/ui-pvc.yaml +++ b/deploy/helm/coderag/templates/ui-pvc.yaml @@ -1,4 +1,4 @@ -{{- if and .Values.ui.enabled .Values.persistence.enabled (not .Values.ui.persistence.existingClaim) -}} +{{- if and .Values.ui.enabled .Values.persistence.enabled (not .Values.ui.useServerIndex) (not .Values.ui.persistence.existingClaim) -}} apiVersion: v1 kind: PersistentVolumeClaim metadata: diff --git a/deploy/helm/coderag/values.yaml b/deploy/helm/coderag/values.yaml index 1783422..c289575 100644 --- a/deploy/helm/coderag/values.yaml +++ b/deploy/helm/coderag/values.yaml @@ -171,13 +171,37 @@ server: # -- Extra env vars (list of {name,value|valueFrom}) for the server container. extraEnv: [] -# --- Web UI (optional; runs as an INDEPENDENT instance with its own index) --- -# The UI bundles the engine and writes its own index, so when enabled it gets a -# separate data volume and workspace. Build its index with the in-app "Reindex" -# button. For a shared, server-maintained index, prefer the server + an ingress. +# --- Web UI (optional) --- +# The UI image bundles the engine and reads a LanceDB store at CODERAG_STORE_DIR. +# Two topologies: +# +# useServerIndex: false (default) — INDEPENDENT instance with its OWN data volume +# (…-ui-data). Nothing populates it automatically: the index/reindex Jobs drive the +# SERVER's volume, not this one. Build it with the in-app "Reindex" button — which is +# DISABLED in demo mode, so a demo UI left on the default will show 0 files/0 chunks. +# +# useServerIndex: true (RECOMMENDED for a read-only / demo UI) — the UI mounts the +# SERVER's index volume READ-ONLY and serves whatever the index Job built. No second +# writer, no separate PVC, always in sync with the server. See `useServerIndex` below. ui: enabled: false containerPort: 8501 + # -- Share the SERVER's index instead of keeping a separate (empty) UI volume. When + # true the UI mounts the server's data PVC READ-ONLY at persistence.mountPath, so it + # shows the index built by the init/reindex Jobs and can never corrupt the writer's + # store. No …-ui-data PVC is created. Requirements & caveats: + # * The server (persistence) must be enabled — that's the volume being shared. + # * Access mode: the server PVC is ReadWriteOnce by default, so the UI and server + # pods must land on the SAME node. Either set `coLocateWithServer: true` below, or + # give persistence a ReadWriteMany storageClass (NFS/CephFS/EFS/Longhorn-RWX/…). + # * The UI's model cache is redirected to a writable in-pod volume automatically + # (the shared index mount is read-only), so query embedding still works. + # * Reindex stays a SERVER action (Job/CronJob); the in-app button is irrelevant here. + useServerIndex: false + # -- Pin the UI pod onto the same node as the server pod via podAffinity. REQUIRED with + # useServerIndex on ReadWriteOnce storage (a single RWO volume attaches to one node). + # Harmless (but unnecessary) on ReadWriteMany. Ignored when useServerIndex is false. + coLocateWithServer: false # -- Deployment update strategy. Defaults to Recreate: the UI is a single writer on # a ReadWriteOnce volume, so the old pod must release the claim before the new one # binds it. The cost is a brief gap with no Ready pod on every image change — visible