diff --git a/.dockerignore b/.dockerignore index 5137e0f..3f166a4 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,6 +3,7 @@ .github .gitignore tests +deploy DEVELOPMENT.md AGENTS.md example.env diff --git a/.github/workflows/helm.yml b/.github/workflows/helm.yml new file mode 100644 index 0000000..5396e64 --- /dev/null +++ b/.github/workflows/helm.yml @@ -0,0 +1,66 @@ +name: Helm chart + +# Lint and schema-validate the Kubernetes Helm chart. Runs only when the chart (or this +# workflow) changes, so it stays out of the way of pure-Python PRs. +on: + push: + branches: [master] + paths: + - "deploy/helm/**" + - ".github/workflows/helm.yml" + pull_request: + branches: [master] + paths: + - "deploy/helm/**" + - ".github/workflows/helm.yml" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: helm-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + CHART: deploy/helm/coderag + KUBECONFORM_VERSION: "0.6.7" + +jobs: + lint-and-validate: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v6 + + - name: Set up Helm + uses: azure/setup-helm@v4 + with: + version: v3.16.4 + + - name: Helm lint (default + full value sets) + run: | + helm lint "$CHART" -f "$CHART/ci/default-values.yaml" + helm lint "$CHART" -f "$CHART/ci/full-values.yaml" + + - name: Install kubeconform + run: | + curl -fsSL \ + "https://github.com/yannh/kubeconform/releases/download/v${KUBECONFORM_VERSION}/kubeconform-linux-amd64.tar.gz" \ + | tar -xz -C /usr/local/bin kubeconform + kubeconform -v + + - name: Render and schema-validate + run: | + # Standalone: the chart must render and install with zero required config. + echo "::group::standalone defaults (no values)" + helm template coderag "$CHART" | kubeconform -strict -summary -kubernetes-version 1.29.0 + echo "::endgroup::" + for values in default full; do + for kube in 1.27.0 1.29.0 1.31.0; do + echo "::group::$values values @ k8s $kube" + helm template coderag "$CHART" -f "$CHART/ci/${values}-values.yaml" \ + | kubeconform -strict -summary -kubernetes-version "$kube" + echo "::endgroup::" + done + done diff --git a/README.md b/README.md index d7a4406..244a8bf 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,30 @@ adds a `-ui` suffix. The container indexes `/workspace` and stores its index in (`CODERAG_WATCHED_DIR` / `CODERAG_STORE_DIR`). For OpenAI embeddings/answers, add `-e OPENAI_API_KEY=…`. +## ☸️ Kubernetes (Helm) + +For teams who want a shared, always-on deployment, a Helm chart self-hosts the HTTP API +(and optional UI) with a persistent index, scheduled re-indexing, and hardened defaults +(non-root, read-only rootfs, single-writer-safe). It runs **standalone with zero config** +on your cluster's default storage: + +```bash +helm install coderag ./deploy/helm/coderag --namespace coderag --create-namespace +``` + +Then point it at your code (a git repo, or a PVC you already have): + +```bash +helm upgrade coderag ./deploy/helm/coderag -n coderag --reuse-values \ + --set workspace.source=git \ + --set workspace.git.repository=https://github.com/Neverdecel/CodeRAG.git +``` + +It provisions the index volume, clones the repo into the pod, and builds the index +automatically. Not a Helm user? `helm template … | kubectl apply -f -` works too. See the +full guide — storage options, private repos, OpenAI/Anthropic keys, ingress, the UI, +scheduled reindex — in [`deploy/README.md`](deploy/README.md). + ## 🏗️ How it works ```mermaid diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000..f54d0c6 --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,385 @@ +# ☸️ Deploying CodeRAG on Kubernetes + +A Helm chart that self-hosts the CodeRAG **HTTP/REST API** (and, optionally, the +**Streamlit UI**) on any Kubernetes cluster, with a persistent index, a git-sourced +workspace, scheduled re-indexing, and sensible security defaults. + +> **Don't use Helm?** Every example below works with plain `kubectl` too — just pipe +> `helm template …` into `kubectl apply -f -` (see [Without Helm](#without-helm)). + +- Chart: [`helm/coderag`](helm/coderag) · Values reference: [`helm/coderag/values.yaml`](helm/coderag/values.yaml) +- Images: `ghcr.io/neverdecel/coderag:beta` (API) and `:beta-ui` (UI), built by + [`docker-beta.yml`](../.github/workflows/docker-beta.yml). + +--- + +## How it's designed (read this first) + +CodeRAG keeps its index in **SQLite** (the source of truth) plus a **FAISS** cache, and +the engine is a **single writer** — the FAISS file is written non-atomically, so two +processes writing one index would corrupt it. The chart is built around that fact: + +- **One replica, `Recreate` strategy, `ReadWriteOnce` PVC.** Never scale the writer + horizontally; it is not safe and the chart intentionally pins `replicas: 1`. +- **Indexing is driven over HTTP**, not by a second pod mounting the volume. An initial + Job (and an optional CronJob) call `POST /index` on the running server, so exactly one + process ever touches the index files. +- **The embedding model is downloaded once** (≈130 MB) and cached on the data volume + (`CODERAG_CACHE_DIR=/data/.model-cache`), so restarts don't re-download it. A generous + **startup probe** covers that first download before liveness kicks in. +- **Standalone by default.** `helm install` with no arguments boots a healthy server on + your cluster's default storage (empty index); point it at your code with one setting. +- **The codebase is mounted read-only** into the app and refreshed by a git init + container (and optional git-sync sidecar), never written by the engine. +- **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). + +--- + +## Prerequisites + +- A Kubernetes cluster (v1.25+) and `kubectl` configured for it. +- A default `StorageClass` (or set `persistence.storageClass`) that can provision + `ReadWriteOnce` volumes. +- [Helm 3](https://helm.sh/docs/intro/install/) (only for the Helm workflow). + +--- + +## Quick start + +**Standalone (zero config).** Installs and runs anywhere with a default StorageClass — +no required flags: + +```bash +helm install coderag ./deploy/helm/coderag --namespace coderag --create-namespace +``` + +The server comes up healthy on a freshly provisioned 10Gi volume with an *empty* index. +Now point it at your code — clone a git repo into the pod: + +```bash +helm upgrade coderag ./deploy/helm/coderag -n coderag --reuse-values \ + --set workspace.source=git \ + --set workspace.git.repository=https://github.com/Neverdecel/CodeRAG.git +``` + +That provisions the index volume, clones the repo into the pod, and runs a one-shot Job +that builds the index once the server is ready. (You can pass both `--set`s on the first +`install` too, to do it in one step.) Watch it come up: + +```bash +kubectl -n coderag get pods -w +kubectl -n coderag logs -f job/coderag-index-1 # initial indexing progress +``` + +Query it: + +```bash +kubectl -n coderag port-forward svc/coderag-server 8000:8000 +curl "http://127.0.0.1:8000/status" +curl "http://127.0.0.1:8000/search?q=where%20is%20retry%20handled&k=5" +``` + +--- + +## Without Helm + +The chart needs no Tiller/cluster-side component, so you can render it locally and apply +the plain manifests: + +```bash +helm template coderag ./deploy/helm/coderag \ + --namespace coderag \ + --set workspace.git.repository=https://github.com/Neverdecel/CodeRAG.git \ + > coderag.yaml + +kubectl create namespace coderag +kubectl -n coderag apply -f coderag.yaml +``` + +Re-render and re-apply to upgrade. (You lose Helm's release tracking and the automatic +revision-suffixed index Job, but the manifests are otherwise identical.) + +--- + +## Configuration reference + +Full list with comments: [`values.yaml`](helm/coderag/values.yaml). The most-used knobs: + +| Value | Default | Purpose | +| --- | --- | --- | +| `image.tag` | `beta` | Image tag. Pin to `sha-` for reproducibility. | +| `workspace.source` | `emptyDir` | `emptyDir` (standalone) · `git` · `existingClaim`. | +| `workspace.git.repository` | – | **Required** for `source=git`. Repo to index. | +| `workspace.git.ref` | `""` | Branch/tag (empty = default branch). | +| `workspace.git.sync.enabled` | `false` | Sidecar that `git pull`s on an interval. | +| `persistence.enabled` | `true` | Persist the index to a PVC (false = ephemeral). | +| `persistence.size` | `10Gi` | Index volume size. | +| `persistence.storageClass` | `""` | `""` default class · `` · `"-"` static. | +| `persistence.volumeName` / `persistence.selector` | – | Bind a pre-provisioned PV (static). | +| `config.provider` | `fastembed` | `fastembed` (local, no key) · `openai` · `fake`. | +| `config.openaiBaseUrl` | `""` | Self-hosted OpenAI-compatible endpoint. | +| `secrets.openaiApiKey` / `secrets.anthropicApiKey` | `""` | API keys (→ Secret). | +| `secrets.existingSecret` | `""` | Use a pre-created Secret instead. | +| `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 Streamlit UI (independent instance). | +| `ingress.enabled` | `false` | Expose via an Ingress. | +| `resources` (`server.*`, `ui.*`) | see values | CPU/memory requests & limits. | + +--- + +## Storage + +The index needs one `ReadWriteOnce` volume per writer. The chart works with whatever your +cluster already provides — you rarely need to configure anything. + +**Use the cluster default StorageClass (recommended).** Leave `persistence.storageClass: ""` +and the PVC binds to your default class. That covers virtually every managed and +self-managed cluster out of the box: + +| Environment | Typical default class | +| --- | --- | +| Amazon EKS | `gp3` / `gp2` (EBS CSI) | +| Google GKE | `standard-rwo` (PD CSI) | +| Azure AKS | `managed-csi` / `default` (Disk CSI) | +| k3s / Rancher | `local-path` | +| Minikube / kind | `standard` | +| DigitalOcean / Civo / … | provider block-storage class | + +**Pick a specific class** when you run your own provisioner: + +```bash +--set persistence.storageClass=longhorn # Longhorn +--set persistence.storageClass=nfs-client # NFS subdir provisioner +--set persistence.storageClass=openebs-hostpath # OpenEBS LocalPV +``` + +**Bind a pre-provisioned PersistentVolume (static).** Common on-prem when there's no +dynamic provisioner — e.g. a hand-made NFS, `hostPath`, or local PV. Disable provisioning +with `storageClass: "-"` and point at the PV by name (or label): + +```yaml +persistence: + storageClass: "-" # storageClassName: "" — no dynamic provisioning + volumeName: coderag-data-pv # bind this specific PV + # or match by labels instead of by name: + # selector: + # matchLabels: { app: coderag } +``` + +```yaml +# Example PV backed by an NFS export (apply once, cluster-wide): +apiVersion: v1 +kind: PersistentVolume +metadata: + name: coderag-data-pv +spec: + capacity: { storage: 10Gi } + accessModes: [ReadWriteOnce] + storageClassName: "" + nfs: { server: nfs.internal, path: /export/coderag } +``` + +**Bring your own PVC.** If you already manage the claim, reference it directly and the +chart won't create one: `--set persistence.existingClaim=my-index-pvc`. + +> The index is single-writer, so `ReadWriteOnce` is the right access mode. `ReadWriteMany` +> (NFS, CephFS) also works if that's all you have, but it buys you nothing here. + +--- + +## Common scenarios + +### Use OpenAI or Anthropic for answers/embeddings + +```bash +helm install coderag ./deploy/helm/coderag -n coderag --create-namespace \ + --set workspace.git.repository=https://github.com/org/repo.git \ + --set secrets.openaiApiKey=sk-... \ + --set config.provider=openai # optional: OpenAI embeddings too +``` + +Prefer a pre-created Secret (so keys never sit in your values/CI): + +```bash +kubectl -n coderag create secret generic coderag-keys \ + --from-literal=OPENAI_API_KEY=sk-... \ + --from-literal=ANTHROPIC_API_KEY=sk-ant-... +helm install coderag ./deploy/helm/coderag -n coderag \ + --set workspace.git.repository=https://github.com/org/repo.git \ + --set secrets.existingSecret=coderag-keys +``` + +### Point at a self-hosted / local model (Ollama, vLLM, …) + +```bash +--set config.openaiBaseUrl=http://ollama.ai-system.svc:11434/v1 \ +--set config.llmProvider=openai \ +--set config.chatModel=llama3.1 +``` + +### Keep the index fresh automatically + +Pair a git-sync sidecar (pulls new commits) with a reindex CronJob (re-embeds changes): + +```bash +--set workspace.git.sync.enabled=true \ +--set workspace.git.sync.periodSeconds=300 \ +--set index.cronjob.enabled=true \ +--set index.cronjob.schedule="*/30 * * * *" +``` + +### Index a private git repository + +**Option A — pre-populated volume (no in-cluster git auth).** Put your code on a PVC +(e.g. via a CI job or `kubectl cp`) and mount it: + +```bash +--set workspace.source=existingClaim \ +--set workspace.existingClaim=my-code-pvc +``` + +**Option B — your own clone init container + a Secret.** Skip the built-in clone +(`source=emptyDir`) and supply credentials from a Secret. The `workspace` volume is a +shared `emptyDir`; your init container clones into it: + +```yaml +# private-repo.yaml +workspace: + source: emptyDir # disables the built-in (public) git clone +extraInitContainers: + - name: git-clone + image: alpine/git:2.45.2 + securityContext: { allowPrivilegeEscalation: false, readOnlyRootFilesystem: true, capabilities: { drop: [ALL] } } + env: + - name: HOME + value: /tmp + - name: GIT_TOKEN + valueFrom: { secretKeyRef: { name: git-creds, key: token } } + command: ["/bin/sh","-c"] + args: + - git clone --depth=1 "https://x-access-token:${GIT_TOKEN}@github.com/org/private-repo.git" /workspace + volumeMounts: + - { name: workspace, mountPath: /workspace } + - { name: tmp, mountPath: /tmp } +``` + +```bash +kubectl -n coderag create secret generic git-creds --from-literal=token=ghp_... +helm install coderag ./deploy/helm/coderag -n coderag -f private-repo.yaml +``` + +### Expose it with an Ingress + +```yaml +ingress: + enabled: true + className: nginx + hosts: + - host: coderag.example.com + paths: + - { path: /, pathType: Prefix, service: server } + tls: + - { secretName: coderag-tls, hosts: [coderag.example.com] } +``` + +### Also run the web UI + +```bash +--set ui.enabled=true +``` + +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. + +### Pin to an immutable image (reproducible / air-gapped) + +No versioned tags are published yet; the default is the rolling `:beta`. Pin to a commit: + +```bash +--set image.tag=sha- # API → ghcr.io/.../coderag:sha- + # UI → :sha--ui (image.uiSuffix) +``` + +For private registries, set `image.pullSecrets: [{ name: my-regcred }]`. + +--- + +## Operations + +```bash +# Trigger a reindex by hand (incremental): +kubectl -n coderag exec deploy/coderag-server -c server -- \ + python -c "import urllib.request as u; print(u.urlopen(u.Request('http://127.0.0.1:8000/index', data=b'{\"full\":false}', headers={'content-type':'application/json'})).read().decode())" + +# Or from your laptop after a port-forward: +curl -X POST localhost:8000/index -H 'content-type: application/json' -d '{"full": true}' + +# Status, logs: +curl localhost:8000/status +kubectl -n coderag logs deploy/coderag-server -c server +kubectl -n coderag logs deploy/coderag-server -c git-sync # if sync enabled +``` + +### Upgrade + +```bash +helm upgrade coderag ./deploy/helm/coderag -n coderag --reuse-values +``` + +Each upgrade runs a fresh `…-index-` Job to refresh the index. A ConfigMap +checksum annotation rolls the pod automatically when configuration changes. + +### Uninstall (and reclaiming storage) + +The index PVCs are annotated `helm.sh/resource-policy: keep`, so your index **survives** +an uninstall. Remove the volumes explicitly when you're done: + +```bash +helm uninstall coderag -n coderag +kubectl -n coderag delete pvc -l app.kubernetes.io/instance=coderag +``` + +--- + +## Validate changes to the chart + +The same checks run in CI ([`helm.yml`](../.github/workflows/helm.yml)): + +```bash +helm lint deploy/helm/coderag -f deploy/helm/coderag/ci/default-values.yaml +helm template coderag deploy/helm/coderag -f deploy/helm/coderag/ci/full-values.yaml \ + | kubeconform -strict -summary -kubernetes-version 1.29.0 +``` + +--- + +## Troubleshooting + +- **Pod stuck `ContainerCreating` / `Pending`** — usually the PVC can't be provisioned. + Check `kubectl -n coderag describe pvc` and set `persistence.storageClass` to a class + that supports `ReadWriteOnce`. +- **First start is slow / startup probe restarts** — the embedding model (~130 MB) is + downloading. It's cached on the data volume afterwards. Raise + `server.startupProbe.failureThreshold` on very slow networks. +- **A read-only-filesystem write error** (rare; some model backend writing outside the + mounted caches) — the pod runs with `readOnlyRootFilesystem: true` and writable `/tmp`, + `/data`, and `/home/coderag`. If a backend insists on another path, mount it via + `extraVolumes`/`extraVolumeMounts`, or relax the hardening: + `--set securityContext.readOnlyRootFilesystem=false`. + +## Limitations + +- **Single writer by design** — do not raise `replicas`. For higher search throughput, + 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 SQLite. +- 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. diff --git a/deploy/helm/coderag/.helmignore b/deploy/helm/coderag/.helmignore new file mode 100644 index 0000000..fd6f1c1 --- /dev/null +++ b/deploy/helm/coderag/.helmignore @@ -0,0 +1,12 @@ +# Patterns to ignore when packaging Helm charts. +.DS_Store +.git/ +.gitignore +*.tmproj +*.bak +*.orig +*.swp +.idea/ +.vscode/ +# CI-only values shipped in the repo, not needed inside the packaged chart. +ci/ diff --git a/deploy/helm/coderag/Chart.yaml b/deploy/helm/coderag/Chart.yaml new file mode 100644 index 0000000..428dacc --- /dev/null +++ b/deploy/helm/coderag/Chart.yaml @@ -0,0 +1,31 @@ +apiVersion: v2 +name: coderag +description: >- + Standalone, local-first semantic code-search engine. Self-host the HTTP/REST + API (and optional Streamlit UI) on Kubernetes with a persistent index, a git + workspace, and scheduled re-indexing. +type: application + +# Chart version — bump on every chart change (independent of the app version). +version: 0.1.0 + +# Version of CodeRAG this chart deploys by default. No versioned container images +# are published yet, so the default image tag is the rolling `:beta` channel; pin +# `image.tag` to an immutable `:sha-` tag for reproducible deploys. +appVersion: "1.0.0" + +home: https://github.com/Neverdecel/CodeRAG +sources: + - https://github.com/Neverdecel/CodeRAG +keywords: + - code-search + - rag + - embeddings + - semantic-search + - faiss +maintainers: + - name: Neverdecel + url: https://github.com/Neverdecel +annotations: + category: Developer Tools + licenses: Apache-2.0 diff --git a/deploy/helm/coderag/ci/default-values.yaml b/deploy/helm/coderag/ci/default-values.yaml new file mode 100644 index 0000000..e2002cb --- /dev/null +++ b/deploy/helm/coderag/ci/default-values.yaml @@ -0,0 +1,17 @@ +# CI scenario A — standalone on self-managed STATIC storage. +# No git repository is set, so this also proves the chart renders with zero required +# config (emptyDir workspace default). Exercises the static-binding storage paths: +# storageClass "-", volumeName, selector, and PVC annotations. +persistence: + storageClass: "-" # disable dynamic provisioning, bind a pre-created PV + volumeName: coderag-data-pv + annotations: + backup.velero.io/backup-volumes: data + +ui: + enabled: true + persistence: + storageClass: nfs-client + selector: + matchLabels: + app: coderag-ui diff --git a/deploy/helm/coderag/ci/full-values.yaml b/deploy/helm/coderag/ci/full-values.yaml new file mode 100644 index 0000000..2491908 --- /dev/null +++ b/deploy/helm/coderag/ci/full-values.yaml @@ -0,0 +1,55 @@ +# Exercises every optional template path for CI rendering: UI, ingress, the reindex +# CronJob, git-sync sidecar, API-key Secret, and an existing-claim workspace variant +# is covered separately. Not meant as a recommended production configuration. +workspace: + source: git + git: + repository: https://github.com/Neverdecel/CodeRAG.git + ref: master + sync: + enabled: true + periodSeconds: 120 + +ui: + enabled: true + +secrets: + create: true + openaiApiKey: "sk-test-not-a-real-key" + anthropicApiKey: "sk-ant-test-not-a-real-key" + +config: + provider: openai + openaiBaseUrl: "http://ollama.ai-system.svc:11434/v1" + extraEnv: + CODERAG_WORKERS: "8" + +index: + initJob: + enabled: true + full: true + cronjob: + enabled: true + schedule: "*/15 * * * *" + +ingress: + enabled: true + className: nginx + annotations: + nginx.ingress.kubernetes.io/proxy-body-size: "0" + hosts: + - host: coderag.example.com + paths: + - path: / + pathType: Prefix + service: server + - host: coderag-ui.example.com + paths: + - path: / + pathType: Prefix + service: ui + tls: + - secretName: coderag-tls + hosts: + - coderag.example.com + - coderag-ui.example.com diff --git a/deploy/helm/coderag/templates/NOTES.txt b/deploy/helm/coderag/templates/NOTES.txt new file mode 100644 index 0000000..7ed3891 --- /dev/null +++ b/deploy/helm/coderag/templates/NOTES.txt @@ -0,0 +1,74 @@ +CodeRAG has been deployed as release "{{ .Release.Name }}" in namespace "{{ .Release.Namespace }}". + +{{- if .Values.server.enabled }} + +HTTP / REST API +--------------- +Service: {{ include "coderag.serverServiceName" . }}:{{ .Values.server.service.port }} + + # Port-forward and query the API: + kubectl --namespace {{ .Release.Namespace }} port-forward svc/{{ include "coderag.serverServiceName" . }} 8000:{{ .Values.server.service.port }} + curl "http://127.0.0.1:8000/status" + curl "http://127.0.0.1:8000/search?q=where%20is%20retry%20handled&k=5" + +{{- if .Values.index.initJob.enabled }} + +The index is being built automatically by Job +"{{ include "coderag.fullname" . }}-index-{{ .Release.Revision }}". Follow it with: + + kubectl --namespace {{ .Release.Namespace }} logs -f job/{{ include "coderag.fullname" . }}-index-{{ .Release.Revision }} +{{- else }} + +Build the index once the server is ready: + + curl -X POST "http://127.0.0.1:8000/index" -H 'content-type: application/json' -d '{"full": true}' +{{- end }} +{{- end }} + +{{- if .Values.ui.enabled }} + +Web UI (Streamlit) +------------------ + kubectl --namespace {{ .Release.Namespace }} port-forward svc/{{ include "coderag.fullname" . }}-ui 8501:{{ .Values.ui.service.port }} + # then open http://127.0.0.1:8501 (use the in-app "Reindex" button to build its index) +{{- end }} + +{{- if .Values.ingress.enabled }} + +Ingress +------- +{{- range .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ .host }}{{ .path }} -> {{ .service }} + {{- end }} +{{- end }} +{{- end }} + +{{- if not .Values.persistence.enabled }} + +WARNING: persistence is disabled — the index lives in an emptyDir and is REBUILT +on every restart. Set persistence.enabled=true for anything beyond a quick demo. +{{- end }} + +{{- if eq .Values.workspace.source "git" }} +{{- if not .Values.workspace.git.repository }} + +ACTION REQUIRED: workspace.source=git but workspace.git.repository is empty. +Set it to the repository you want to index, e.g.: + + --set workspace.git.repository=https://github.com/Neverdecel/CodeRAG.git +{{- end }} +{{- else if eq .Values.workspace.source "emptyDir" }} + +NOTE: running standalone with an EMPTY workspace, so searches return nothing yet. +Point CodeRAG at your code by upgrading with one of: + + # clone a git repo into the pod: + helm upgrade {{ .Release.Name }} --reuse-values \ + --set workspace.source=git \ + --set workspace.git.repository=https://github.com/your-org/your-repo.git + + # or mount a PVC you've populated with your code: + helm upgrade {{ .Release.Name }} --reuse-values \ + --set workspace.source=existingClaim --set workspace.existingClaim= +{{- end }} diff --git a/deploy/helm/coderag/templates/_helpers.tpl b/deploy/helm/coderag/templates/_helpers.tpl new file mode 100644 index 0000000..d2d59d1 --- /dev/null +++ b/deploy/helm/coderag/templates/_helpers.tpl @@ -0,0 +1,239 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "coderag.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name (truncated to 63 chars for DNS). +*/}} +{{- define "coderag.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Chart name and version, as used in the helm.sh/chart label. +*/}} +{{- define "coderag.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Common labels. +*/}} +{{- define "coderag.labels" -}} +helm.sh/chart: {{ include "coderag.chart" . }} +{{ include "coderag.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +app.kubernetes.io/part-of: coderag +{{- end -}} + +{{/* +Selector labels (stable across upgrades — never add version here). +*/}} +{{- define "coderag.selectorLabels" -}} +app.kubernetes.io/name: {{ include "coderag.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end -}} + +{{/* +ServiceAccount name to use. +*/}} +{{- define "coderag.serviceAccountName" -}} +{{- if .Values.serviceAccount.create -}} +{{- default (include "coderag.fullname" .) .Values.serviceAccount.name -}} +{{- else -}} +{{- default "default" .Values.serviceAccount.name -}} +{{- end -}} +{{- end -}} + +{{/* +Resolved image tag. No versioned images are published, so default to the +rolling `beta` channel. +*/}} +{{- define "coderag.imageTag" -}} +{{- .Values.image.tag | default "beta" -}} +{{- end -}} + +{{- define "coderag.serverImage" -}} +{{- printf "%s:%s" .Values.image.repository (include "coderag.imageTag" .) -}} +{{- end -}} + +{{- define "coderag.uiImage" -}} +{{- printf "%s:%s%s" .Values.image.repository (include "coderag.imageTag" .) .Values.image.uiSuffix -}} +{{- end -}} + +{{/* +Name of the Secret holding API keys (existing or chart-managed). +*/}} +{{- define "coderag.secretName" -}} +{{- if .Values.secrets.existingSecret -}} +{{- .Values.secrets.existingSecret -}} +{{- else -}} +{{- printf "%s-secrets" (include "coderag.fullname" .) -}} +{{- end -}} +{{- end -}} + +{{/* +Service name / in-cluster base URL for the HTTP API (used by the index jobs). +*/}} +{{- define "coderag.serverServiceName" -}} +{{- printf "%s-server" (include "coderag.fullname" .) -}} +{{- end -}} + +{{- define "coderag.serverUrl" -}} +{{- printf "http://%s:%v" (include "coderag.serverServiceName" .) .Values.server.service.port -}} +{{- end -}} + +{{/* +envFrom block shared by the server and UI containers: non-secret config plus the +optional API-key Secret. +*/}} +{{- define "coderag.envFrom" -}} +- configMapRef: + name: {{ include "coderag.fullname" . }}-config +- secretRef: + name: {{ include "coderag.secretName" . }} + optional: true +{{- end -}} + +{{/* +PersistentVolumeClaim spec body. Call with (dict "p" ). +Storage class resolution (Bitnami convention): + "" -> omit storageClassName -> use the cluster's default StorageClass + "-" -> storageClassName: "" -> static binding, no dynamic provisioning + "" -> storageClassName: +Plus optional volumeName (bind a specific PV) and selector (match a PV by labels) — +the common ways to use pre-provisioned storage (NFS, local/hostPath, Longhorn, …). +*/}} +{{- define "coderag.pvcSpec" -}} +{{- $p := .p -}} +accessModes: + {{- toYaml $p.accessModes | nindent 2 }} +resources: + requests: + storage: {{ $p.size | quote }} +{{- if $p.storageClass }} +{{- if eq "-" $p.storageClass }} +storageClassName: "" +{{- else }} +storageClassName: {{ $p.storageClass | quote }} +{{- end }} +{{- end }} +{{- with $p.volumeName }} +volumeName: {{ . | quote }} +{{- end }} +{{- with $p.selector }} +selector: + {{- toYaml . | nindent 2 }} +{{- end }} +{{- end -}} + +{{/* +git clone/sync container spec. Call with a dict: (dict "ctx" . "name" "git-clone" "mode" "clone"). +mode = "clone" (init container) | "sync" (sidecar loop). +*/}} +{{- define "coderag.gitContainer" -}} +{{- $ctx := .ctx -}} +{{- $ws := $ctx.Values.workspace -}} +- name: {{ .name }} + image: {{ $ws.git.image | quote }} + imagePullPolicy: {{ $ctx.Values.image.pullPolicy }} + securityContext: + {{- toYaml $ctx.Values.securityContext | nindent 4 }} + env: + - name: HOME + value: /tmp + - name: GIT_REPO + value: {{ $ws.git.repository | required "workspace.git.repository is required when workspace.source=git" | quote }} + - name: GIT_REF + value: {{ $ws.git.ref | quote }} + - name: GIT_DEPTH + value: {{ $ws.git.depth | quote }} + - name: DEST + value: {{ $ws.mountPath | quote }} + - name: SYNC_PERIOD + value: {{ $ws.git.sync.periodSeconds | quote }} + command: ["/bin/sh", "-c"] + args: + {{- if eq .mode "clone" }} + - | + set -e + if [ -d "$DEST/.git" ]; then + echo "Workspace already present; pulling latest." + git config --global --add safe.directory "$DEST" || true + git -C "$DEST" pull --ff-only || true + else + echo "Cloning $GIT_REPO into $DEST ..." + if [ -n "$GIT_REF" ]; then + git clone --depth="$GIT_DEPTH" --branch "$GIT_REF" "$GIT_REPO" "$DEST" + else + git clone --depth="$GIT_DEPTH" "$GIT_REPO" "$DEST" + fi + fi + git config --global --add safe.directory "$DEST" || true + {{- else }} + - | + git config --global --add safe.directory "$DEST" || true + while true; do + sleep "$SYNC_PERIOD" + echo "[git-sync] pulling $DEST ..." + git -C "$DEST" pull --ff-only || echo "[git-sync] pull failed (continuing)" + done + {{- end }} + volumeMounts: + - name: workspace + mountPath: {{ $ws.mountPath }} + - name: tmp + mountPath: /tmp +{{- end -}} + +{{/* +Pod volumes for a writer (server/ui). Call with (dict "ctx" . "component" "server"). +*/}} +{{- define "coderag.volumes" -}} +{{- $ctx := .ctx -}} +{{- $component := .component -}} +- name: data + {{- if $ctx.Values.persistence.enabled }} + persistentVolumeClaim: + {{- if and (eq $component "server") $ctx.Values.persistence.existingClaim }} + claimName: {{ $ctx.Values.persistence.existingClaim }} + {{- else if and (eq $component "ui") $ctx.Values.ui.persistence.existingClaim }} + claimName: {{ $ctx.Values.ui.persistence.existingClaim }} + {{- else }} + claimName: {{ printf "%s-%s-data" (include "coderag.fullname" $ctx) $component }} + {{- end }} + {{- else }} + emptyDir: {} + {{- end }} +- name: workspace + {{- if eq $ctx.Values.workspace.source "existingClaim" }} + persistentVolumeClaim: + claimName: {{ $ctx.Values.workspace.existingClaim | required "workspace.existingClaim is required when workspace.source=existingClaim" }} + {{- else }} + emptyDir: {} + {{- end }} +- name: tmp + emptyDir: {} +# Writable HOME so stray ~/.cache, ~/.config, and ~/.streamlit writes succeed under a +# read-only root filesystem (Kubernetes does not set HOME from the image's passwd entry). +- name: home + emptyDir: {} +{{- with $ctx.Values.extraVolumes }} +{{- toYaml . | nindent 0 }} +{{- end }} +{{- end -}} diff --git a/deploy/helm/coderag/templates/configmap.yaml b/deploy/helm/coderag/templates/configmap.yaml new file mode 100644 index 0000000..34c2e25 --- /dev/null +++ b/deploy/helm/coderag/templates/configmap.yaml @@ -0,0 +1,24 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "coderag.fullname" . }}-config + labels: + {{- include "coderag.labels" . | nindent 4 }} +data: + CODERAG_PROVIDER: {{ .Values.config.provider | quote }} + CODERAG_MODEL: {{ .Values.config.model | quote }} + CODERAG_INDEX_TYPE: {{ .Values.config.indexType | quote }} + CODERAG_IVF_THRESHOLD: {{ .Values.config.ivfThreshold | quote }} + CODERAG_TOP_K: {{ .Values.config.topK | quote }} + CODERAG_LLM_PROVIDER: {{ .Values.config.llmProvider | quote }} + CODERAG_CHAT_MODEL: {{ .Values.config.chatModel | quote }} + CODERAG_ANTHROPIC_MODEL: {{ .Values.config.anthropicModel | quote }} + CODERAG_WATCHED_DIR: {{ .Values.workspace.mountPath | quote }} + CODERAG_STORE_DIR: {{ .Values.persistence.mountPath | quote }} + CODERAG_CACHE_DIR: {{ .Values.modelCache.dir | quote }} + {{- if .Values.config.openaiBaseUrl }} + OPENAI_BASE_URL: {{ .Values.config.openaiBaseUrl | quote }} + {{- end }} + {{- range $k, $v := .Values.config.extraEnv }} + {{ $k }}: {{ $v | quote }} + {{- end }} diff --git a/deploy/helm/coderag/templates/index-job.yaml b/deploy/helm/coderag/templates/index-job.yaml new file mode 100644 index 0000000..9d9655a --- /dev/null +++ b/deploy/helm/coderag/templates/index-job.yaml @@ -0,0 +1,71 @@ +{{- if and .Values.server.enabled .Values.index.initJob.enabled -}} +apiVersion: batch/v1 +kind: Job +metadata: + # Revision-suffixed so each `helm upgrade` runs a fresh index without colliding + # with the previous (immutable) Job. + name: {{ include "coderag.fullname" . }}-index-{{ .Release.Revision }} + labels: + {{- include "coderag.labels" . | nindent 4 }} + app.kubernetes.io/component: index +spec: + backoffLimit: {{ .Values.index.initJob.backoffLimit }} + ttlSecondsAfterFinished: {{ .Values.index.initJob.ttlSecondsAfterFinished }} + template: + metadata: + labels: + {{- include "coderag.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: index + spec: + restartPolicy: Never + serviceAccountName: {{ include "coderag.serviceAccountName" . }} + automountServiceAccountToken: {{ .Values.automountServiceAccountToken }} + {{- with .Values.image.pullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: index + image: {{ .Values.index.image | quote }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + env: + - name: HOME + value: /tmp + - name: CODERAG_URL + value: {{ include "coderag.serverUrl" . | quote }} + - name: FULL + value: {{ .Values.index.initJob.full | quote }} + - name: MAX_TRIES + value: "120" + command: ["/bin/sh", "-c"] + args: + - | + set -eu + echo "Waiting for CodeRAG API at $CODERAG_URL ..." + i=0 + until curl -fsS "$CODERAG_URL/status" >/dev/null 2>&1; do + i=$((i+1)) + if [ "$i" -ge "$MAX_TRIES" ]; then + echo "ERROR: server not ready after $((MAX_TRIES * 5))s"; exit 1 + fi + sleep 5 + done + echo "Server ready. Triggering index (full=$FULL) ..." + curl -fsS -X POST "$CODERAG_URL/index" \ + -H 'content-type: application/json' \ + -d "{\"full\": $FULL}" + echo + echo "Index request complete." + resources: + {{- toYaml .Values.index.resources | nindent 12 }} + volumeMounts: + - name: tmp + mountPath: /tmp + volumes: + - name: tmp + emptyDir: {} +{{- end }} diff --git a/deploy/helm/coderag/templates/ingress.yaml b/deploy/helm/coderag/templates/ingress.yaml new file mode 100644 index 0000000..d08aabd --- /dev/null +++ b/deploy/helm/coderag/templates/ingress.yaml @@ -0,0 +1,42 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "coderag.fullname" . -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "coderag.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with .Values.ingress.className }} + ingressClassName: {{ . }} + {{- end }} + {{- with .Values.ingress.tls }} + tls: + {{- toYaml . | nindent 4 }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + pathType: {{ .pathType }} + backend: + service: + {{- if eq .service "ui" }} + name: {{ $fullName }}-ui + port: + number: {{ $.Values.ui.service.port }} + {{- else }} + name: {{ $fullName }}-server + port: + number: {{ $.Values.server.service.port }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/deploy/helm/coderag/templates/reindex-cronjob.yaml b/deploy/helm/coderag/templates/reindex-cronjob.yaml new file mode 100644 index 0000000..ff62c0a --- /dev/null +++ b/deploy/helm/coderag/templates/reindex-cronjob.yaml @@ -0,0 +1,63 @@ +{{- if and .Values.server.enabled .Values.index.cronjob.enabled -}} +apiVersion: batch/v1 +kind: CronJob +metadata: + name: {{ include "coderag.fullname" . }}-reindex + labels: + {{- include "coderag.labels" . | nindent 4 }} + app.kubernetes.io/component: reindex +spec: + schedule: {{ .Values.index.cronjob.schedule | quote }} + concurrencyPolicy: {{ .Values.index.cronjob.concurrencyPolicy }} + successfulJobsHistoryLimit: 3 + failedJobsHistoryLimit: 3 + jobTemplate: + spec: + backoffLimit: {{ .Values.index.cronjob.backoffLimit }} + template: + metadata: + labels: + {{- include "coderag.selectorLabels" . | nindent 12 }} + app.kubernetes.io/component: reindex + spec: + restartPolicy: Never + serviceAccountName: {{ include "coderag.serviceAccountName" . }} + automountServiceAccountToken: {{ .Values.automountServiceAccountToken }} + {{- with .Values.image.pullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 12 }} + {{- end }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 12 }} + containers: + - name: reindex + image: {{ .Values.index.image | quote }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + securityContext: + {{- toYaml .Values.securityContext | nindent 16 }} + env: + - name: HOME + value: /tmp + - name: CODERAG_URL + value: {{ include "coderag.serverUrl" . | quote }} + - name: FULL + value: {{ .Values.index.cronjob.full | quote }} + command: ["/bin/sh", "-c"] + args: + - | + set -eu + echo "Reindex (full=$FULL) via $CODERAG_URL ..." + curl -fsS -X POST "$CODERAG_URL/index" \ + -H 'content-type: application/json' \ + -d "{\"full\": $FULL}" + echo + echo "Reindex request complete." + resources: + {{- toYaml .Values.index.resources | nindent 16 }} + volumeMounts: + - name: tmp + mountPath: /tmp + volumes: + - name: tmp + emptyDir: {} +{{- end }} diff --git a/deploy/helm/coderag/templates/secret.yaml b/deploy/helm/coderag/templates/secret.yaml new file mode 100644 index 0000000..e93d520 --- /dev/null +++ b/deploy/helm/coderag/templates/secret.yaml @@ -0,0 +1,18 @@ +{{- if and .Values.secrets.create (not .Values.secrets.existingSecret) -}} +{{- if or .Values.secrets.openaiApiKey .Values.secrets.anthropicApiKey -}} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "coderag.secretName" . }} + labels: + {{- include "coderag.labels" . | nindent 4 }} +type: Opaque +stringData: + {{- if .Values.secrets.openaiApiKey }} + OPENAI_API_KEY: {{ .Values.secrets.openaiApiKey | quote }} + {{- end }} + {{- if .Values.secrets.anthropicApiKey }} + ANTHROPIC_API_KEY: {{ .Values.secrets.anthropicApiKey | quote }} + {{- end }} +{{- end }} +{{- end }} diff --git a/deploy/helm/coderag/templates/server-deployment.yaml b/deploy/helm/coderag/templates/server-deployment.yaml new file mode 100644 index 0000000..0a61bc3 --- /dev/null +++ b/deploy/helm/coderag/templates/server-deployment.yaml @@ -0,0 +1,124 @@ +{{- if .Values.server.enabled -}} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "coderag.fullname" . }}-server + labels: + {{- include "coderag.labels" . | nindent 4 }} + app.kubernetes.io/component: server +spec: + # CodeRAG is a single SQLite/FAISS writer — keep this at 1. The FAISS index is + # written non-atomically, so two replicas on one volume would corrupt it. + replicas: 1 + strategy: + # Recreate (never RollingUpdate) so two pods never hold the ReadWriteOnce volume. + type: Recreate + selector: + matchLabels: + {{- include "coderag.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: server + template: + metadata: + annotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + {{- with .Values.server.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "coderag.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: server + spec: + serviceAccountName: {{ include "coderag.serviceAccountName" . }} + automountServiceAccountToken: {{ .Values.automountServiceAccountToken }} + {{- with .Values.image.pullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + {{- if or (eq .Values.workspace.source "git") .Values.extraInitContainers }} + initContainers: + {{- if eq .Values.workspace.source "git" }} + {{- include "coderag.gitContainer" (dict "ctx" . "name" "git-clone" "mode" "clone") | nindent 8 }} + {{- end }} + {{- with .Values.extraInitContainers }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- end }} + containers: + - name: server + image: {{ include "coderag.serverImage" . }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + args: + - serve + - --host + - "0.0.0.0" + - --port + - {{ .Values.server.containerPort | quote }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + ports: + - name: http + containerPort: {{ .Values.server.containerPort }} + protocol: TCP + env: + - name: HOME + value: /home/coderag + {{- with .Values.server.extraEnv }} + {{- toYaml . | nindent 12 }} + {{- end }} + envFrom: + {{- include "coderag.envFrom" . | nindent 12 }} + startupProbe: + httpGet: + path: /status + port: http + periodSeconds: {{ .Values.server.startupProbe.periodSeconds }} + failureThreshold: {{ .Values.server.startupProbe.failureThreshold }} + readinessProbe: + httpGet: + path: /status + port: http + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + livenessProbe: + httpGet: + path: /status + port: http + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + resources: + {{- toYaml .Values.server.resources | nindent 12 }} + volumeMounts: + - name: data + mountPath: {{ .Values.persistence.mountPath }} + - name: workspace + mountPath: {{ .Values.workspace.mountPath }} + readOnly: {{ .Values.workspace.readOnly }} + - name: tmp + mountPath: /tmp + - name: home + mountPath: /home/coderag + {{- with .Values.extraVolumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + {{- if and (eq .Values.workspace.source "git") .Values.workspace.git.sync.enabled }} + {{- include "coderag.gitContainer" (dict "ctx" . "name" "git-sync" "mode" "sync") | nindent 8 }} + {{- end }} + volumes: + {{- include "coderag.volumes" (dict "ctx" . "component" "server") | nindent 8 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} diff --git a/deploy/helm/coderag/templates/server-pvc.yaml b/deploy/helm/coderag/templates/server-pvc.yaml new file mode 100644 index 0000000..6e261d0 --- /dev/null +++ b/deploy/helm/coderag/templates/server-pvc.yaml @@ -0,0 +1,17 @@ +{{- if and .Values.server.enabled .Values.persistence.enabled (not .Values.persistence.existingClaim) -}} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "coderag.fullname" . }}-server-data + labels: + {{- include "coderag.labels" . | nindent 4 }} + app.kubernetes.io/component: server + annotations: + # Keep the index when the release is uninstalled; delete the PVC manually to reclaim. + helm.sh/resource-policy: keep + {{- with .Values.persistence.annotations }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- include "coderag.pvcSpec" (dict "p" .Values.persistence) | nindent 2 }} +{{- end }} diff --git a/deploy/helm/coderag/templates/server-service.yaml b/deploy/helm/coderag/templates/server-service.yaml new file mode 100644 index 0000000..ad20497 --- /dev/null +++ b/deploy/helm/coderag/templates/server-service.yaml @@ -0,0 +1,23 @@ +{{- if .Values.server.enabled -}} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "coderag.serverServiceName" . }} + labels: + {{- include "coderag.labels" . | nindent 4 }} + app.kubernetes.io/component: server + {{- with .Values.server.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.server.service.type }} + ports: + - name: http + port: {{ .Values.server.service.port }} + targetPort: http + protocol: TCP + selector: + {{- include "coderag.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: server +{{- end }} diff --git a/deploy/helm/coderag/templates/serviceaccount.yaml b/deploy/helm/coderag/templates/serviceaccount.yaml new file mode 100644 index 0000000..239b5c6 --- /dev/null +++ b/deploy/helm/coderag/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "coderag.serviceAccountName" . }} + labels: + {{- include "coderag.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.automountServiceAccountToken }} +{{- end }} diff --git a/deploy/helm/coderag/templates/tests/test-connection.yaml b/deploy/helm/coderag/templates/tests/test-connection.yaml new file mode 100644 index 0000000..205c5dc --- /dev/null +++ b/deploy/helm/coderag/templates/tests/test-connection.yaml @@ -0,0 +1,37 @@ +{{- if .Values.server.enabled -}} +apiVersion: v1 +kind: Pod +metadata: + name: {{ include "coderag.fullname" . }}-test-connection + labels: + {{- include "coderag.labels" . | nindent 4 }} + app.kubernetes.io/component: test + annotations: + helm.sh/hook: test + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded +spec: + restartPolicy: Never + automountServiceAccountToken: false + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 4 }} + containers: + - name: test + image: {{ .Values.index.image | quote }} + securityContext: + {{- toYaml .Values.securityContext | nindent 8 }} + env: + - name: HOME + value: /tmp + command: ["/bin/sh", "-c"] + args: + - | + set -eu + echo "Probing {{ include "coderag.serverUrl" . }}/status ..." + curl -fsS "{{ include "coderag.serverUrl" . }}/status" + volumeMounts: + - name: tmp + mountPath: /tmp + volumes: + - name: tmp + emptyDir: {} +{{- end }} diff --git a/deploy/helm/coderag/templates/ui-deployment.yaml b/deploy/helm/coderag/templates/ui-deployment.yaml new file mode 100644 index 0000000..3bb442d --- /dev/null +++ b/deploy/helm/coderag/templates/ui-deployment.yaml @@ -0,0 +1,118 @@ +{{- if .Values.ui.enabled -}} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "coderag.fullname" . }}-ui + labels: + {{- include "coderag.labels" . | nindent 4 }} + app.kubernetes.io/component: ui +spec: + # The UI bundles the engine and writes its own index — single writer, one replica. + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + {{- include "coderag.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: ui + template: + metadata: + annotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + {{- with .Values.ui.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "coderag.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: ui + spec: + serviceAccountName: {{ include "coderag.serviceAccountName" . }} + automountServiceAccountToken: {{ .Values.automountServiceAccountToken }} + {{- with .Values.image.pullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + {{- if or (eq .Values.workspace.source "git") .Values.extraInitContainers }} + initContainers: + {{- if eq .Values.workspace.source "git" }} + {{- include "coderag.gitContainer" (dict "ctx" . "name" "git-clone" "mode" "clone") | nindent 8 }} + {{- end }} + {{- with .Values.extraInitContainers }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- end }} + containers: + - name: ui + image: {{ include "coderag.uiImage" . }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + ports: + - name: http + containerPort: {{ .Values.ui.containerPort }} + protocol: TCP + env: + - name: STREAMLIT_SERVER_PORT + value: {{ .Values.ui.containerPort | quote }} + - name: HOME + value: /home/coderag + {{- with .Values.ui.extraEnv }} + {{- toYaml . | nindent 12 }} + {{- end }} + envFrom: + {{- include "coderag.envFrom" . | nindent 12 }} + startupProbe: + httpGet: + path: /_stcore/health + port: http + periodSeconds: {{ .Values.ui.startupProbe.periodSeconds }} + failureThreshold: {{ .Values.ui.startupProbe.failureThreshold }} + readinessProbe: + httpGet: + path: /_stcore/health + port: http + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + livenessProbe: + httpGet: + path: /_stcore/health + port: http + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + resources: + {{- toYaml .Values.ui.resources | nindent 12 }} + volumeMounts: + - name: data + mountPath: {{ .Values.persistence.mountPath }} + - name: workspace + mountPath: {{ .Values.workspace.mountPath }} + readOnly: {{ .Values.workspace.readOnly }} + - name: tmp + mountPath: /tmp + - name: home + mountPath: /home/coderag + {{- with .Values.extraVolumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + {{- if and (eq .Values.workspace.source "git") .Values.workspace.git.sync.enabled }} + {{- include "coderag.gitContainer" (dict "ctx" . "name" "git-sync" "mode" "sync") | nindent 8 }} + {{- end }} + volumes: + {{- include "coderag.volumes" (dict "ctx" . "component" "ui") | nindent 8 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} diff --git a/deploy/helm/coderag/templates/ui-pvc.yaml b/deploy/helm/coderag/templates/ui-pvc.yaml new file mode 100644 index 0000000..afb048a --- /dev/null +++ b/deploy/helm/coderag/templates/ui-pvc.yaml @@ -0,0 +1,16 @@ +{{- if and .Values.ui.enabled .Values.persistence.enabled (not .Values.ui.persistence.existingClaim) -}} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "coderag.fullname" . }}-ui-data + labels: + {{- include "coderag.labels" . | nindent 4 }} + app.kubernetes.io/component: ui + annotations: + helm.sh/resource-policy: keep + {{- with .Values.ui.persistence.annotations }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- include "coderag.pvcSpec" (dict "p" .Values.ui.persistence) | nindent 2 }} +{{- end }} diff --git a/deploy/helm/coderag/templates/ui-service.yaml b/deploy/helm/coderag/templates/ui-service.yaml new file mode 100644 index 0000000..d87cbf7 --- /dev/null +++ b/deploy/helm/coderag/templates/ui-service.yaml @@ -0,0 +1,23 @@ +{{- if .Values.ui.enabled -}} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "coderag.fullname" . }}-ui + labels: + {{- include "coderag.labels" . | nindent 4 }} + app.kubernetes.io/component: ui + {{- with .Values.ui.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.ui.service.type }} + ports: + - name: http + port: {{ .Values.ui.service.port }} + targetPort: http + protocol: TCP + selector: + {{- include "coderag.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: ui +{{- end }} diff --git a/deploy/helm/coderag/values.yaml b/deploy/helm/coderag/values.yaml new file mode 100644 index 0000000..7363470 --- /dev/null +++ b/deploy/helm/coderag/values.yaml @@ -0,0 +1,253 @@ +# Default values for the CodeRAG Helm chart. +# This is a YAML-formatted file. Override any of these with `--set` or `-f my-values.yaml`. +# +# Quick start (server-only, indexing a public git repo): +# +# helm install coderag ./deploy/helm/coderag \ +# --set workspace.git.repository=https://github.com/Neverdecel/CodeRAG.git +# +# See deploy/README.md for the full guide (UI, ingress, private repos, OpenAI/Anthropic). + +# -- Override the chart name used in resource names (rarely needed). +nameOverride: "" +# -- Fully override the generated resource name prefix. +fullnameOverride: "" + +image: + # -- Container image repository. The UI reuses this repo with the `uiSuffix` appended. + repository: ghcr.io/neverdecel/coderag + # -- Image tag. Empty defaults to the rolling `beta` channel. Pin to an immutable + # `sha-` tag for reproducible deploys. + tag: "" + # -- Suffix appended to `tag` for the Streamlit UI image (published as `:beta-ui`). + uiSuffix: "-ui" + pullPolicy: IfNotPresent + # -- Names of pre-created docker-registry Secrets for pulling private images. + pullSecrets: [] + +# --- The codebase CodeRAG indexes (mounted at workspace.mountPath in every pod) --- +workspace: + # -- How the codebase gets into the pod: + # emptyDir — empty volume (default): the chart installs and runs standalone with + # no required config; point it at your code by switching to one of the + # sources below, or populate it via extraInitContainers / `kubectl cp`. + # git — an init container clones workspace.git.repository into an emptyDir. + # existingClaim — mount a PVC you have already populated with your code. + source: emptyDir + # -- Where the codebase is mounted (maps to CODERAG_WATCHED_DIR). + mountPath: /workspace + # -- Mount the workspace read-only in the app container (writes happen via the + # git init/sync containers, never the app). + readOnly: true + git: + # -- Repository to clone. REQUIRED when source=git. + repository: "" + # -- Branch or tag to check out. Empty clones the default branch. + ref: "" + # -- Shallow-clone depth (1 = latest commit only). Set 0 for a full clone. + depth: 1 + # -- Image providing the `git` binary for the clone/sync containers. + image: alpine/git:2.45.2 + # -- Optional: keep the workspace fresh with a sidecar that `git pull`s on an interval. + sync: + enabled: false + # -- Seconds between pulls. + periodSeconds: 300 + # -- PVC name to mount when source=existingClaim. + existingClaim: "" + +# --- Persistent index (SQLite source-of-truth + FAISS cache + downloaded model) --- +# CodeRAG is a single-writer engine, so each writer (the server, or the UI when +# enabled) gets its own ReadWriteOnce volume. Do not point two writers at one claim. +persistence: + # -- Persist the index to a PVC. When false, an ephemeral emptyDir is used and the + # index is rebuilt on every restart (fine for demos, not for real use). + enabled: true + # -- Where the index lives (maps to CODERAG_STORE_DIR). + mountPath: /data + size: 10Gi + # -- StorageClass for dynamic provisioning. Works out of the box on most clusters: + # "" use the cluster's DEFAULT StorageClass (EKS gp3/gp2, GKE standard-rwo, + # AKS managed-csi, k3s local-path, Minikube/kind standard, …). + # "" a specific class, e.g. "longhorn", "nfs-client", "openebs-hostpath", "gp3". + # "-" disable dynamic provisioning and bind statically (see volumeName/selector). + storageClass: "" + # ReadWriteOnce suits the single-writer index. ReadWriteMany also works if your storage + # (NFS, CephFS, …) provides it, but is not required. + accessModes: + - ReadWriteOnce + # -- Bind a specific pre-provisioned PersistentVolume (static provisioning — common for + # NFS / hostPath / local PVs in self-managed clusters). Usually paired with storageClass: "-". + volumeName: "" + # -- Match a pre-provisioned PV by labels instead of by name. + selector: {} + # -- Extra annotations on the PVC (storage-driver hints, backup policies, …). + annotations: {} + # -- Mount an existing PVC for the SERVER index instead of creating one. + existingClaim: "" + +modelCache: + # -- Where the local embedding model is cached (maps to CODERAG_CACHE_DIR). Defaults + # to a subdirectory of the data volume so the ~130 MB model is downloaded once and + # survives restarts. Set to a path on a separate volume if you prefer. + dir: /data/.model-cache + +# --- Engine configuration (rendered into a ConfigMap; CODERAG_* env) --- +config: + # fastembed (local, no key) | openai | fake + provider: fastembed + model: BAAI/bge-small-en-v1.5 + indexType: auto + ivfThreshold: 50000 + topK: 8 + # LLM answer backend (only used by the optional `--answer` / UI answer feature). + llmProvider: openai + chatModel: gpt-4o-mini + anthropicModel: claude-opus-4-8 + # -- Point at a self-hosted OpenAI-compatible server (Ollama, vLLM, …). Optional. + openaiBaseUrl: "" + # -- Arbitrary extra CODERAG_* (or other) env vars added to the ConfigMap. + extraEnv: {} + +# --- API keys (only needed for OpenAI/Anthropic embeddings or LLM answers) --- +secrets: + # -- Create a Secret from the inline keys below. + create: true + openaiApiKey: "" + anthropicApiKey: "" + # -- Use a pre-existing Secret (with OPENAI_API_KEY / ANTHROPIC_API_KEY keys) instead + # of creating one. Takes precedence over the inline keys. + existingSecret: "" + +# --- HTTP/REST API (the primary, recommended surface) --- +server: + enabled: true + containerPort: 8000 + service: + type: ClusterIP + port: 8000 + annotations: {} + resources: + requests: + cpu: 250m + memory: 512Mi + limits: + cpu: "2" + memory: 2Gi + # -- First boot downloads the embedding model before the API binds, so give the + # startup probe generous headroom (periodSeconds * failureThreshold = max boot time). + startupProbe: + periodSeconds: 10 + failureThreshold: 30 + podAnnotations: {} + # -- Extra env vars (list of {name,value|valueFrom}) for the server container. + extraEnv: [] + +# --- Streamlit 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. +ui: + enabled: false + containerPort: 8501 + service: + type: ClusterIP + port: 8501 + annotations: {} + persistence: + size: 10Gi + # Same semantics as persistence.storageClass above ("" = default, "-" = static). + storageClass: "" + accessModes: + - ReadWriteOnce + volumeName: "" + selector: {} + annotations: {} + existingClaim: "" + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: "1" + memory: 1Gi + startupProbe: + periodSeconds: 10 + failureThreshold: 30 + podAnnotations: {} + extraEnv: [] + +# --- Indexing (drives the SERVER over HTTP — no second writer on the volume) --- +index: + # -- One-shot Job, re-created each `helm upgrade`, that waits for the server and then + # triggers a build via POST /index. Keeps the index populated without manual steps. + initJob: + enabled: true + # Force a clean rebuild (full=true) instead of an incremental update. + full: true + backoffLimit: 3 + # Auto-clean finished Jobs after this many seconds. + ttlSecondsAfterFinished: 600 + # -- Recurring reindex to pick up workspace changes (pairs well with git.sync). + cronjob: + enabled: false + schedule: "*/30 * * * *" + full: false + backoffLimit: 2 + concurrencyPolicy: Forbid + # -- Image used by the index/reindex jobs (needs curl + sh). + image: curlimages/curl:8.11.1 + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 200m + memory: 128Mi + +ingress: + enabled: false + className: "" + annotations: {} + hosts: + - host: coderag.local + paths: + - path: / + pathType: Prefix + # Which service to route to: server | ui + service: server + tls: [] + +serviceAccount: + create: true + name: "" + annotations: {} + +# -- CodeRAG never talks to the Kubernetes API, so the token is not mounted. +automountServiceAccountToken: false + +# --- Pod- and container-level hardening (applied to every workload) --- +podSecurityContext: + runAsNonRoot: true + runAsUser: 10001 + runAsGroup: 10001 + # Make mounted volumes group-writable by the non-root coderag user (uid/gid 10001). + fsGroup: 10001 + seccompProfile: + type: RuntimeDefault + +securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + +# --- Pod-level escape hatches (private-repo git auth, custom CA, …) --- +extraVolumes: [] +extraVolumeMounts: [] +extraInitContainers: [] + +nodeSelector: {} +tolerations: [] +affinity: {}