Skip to content
61 changes: 51 additions & 10 deletions helm/codeapi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,16 +134,16 @@ verifier through environment variables on the api component, e.g.:

```yaml
api:
extraEnv:
- name: CODEAPI_AUTH_PROVIDER
value: librechat-jwt
- name: CODEAPI_JWT_PUBLIC_KEY # single PEM/base64-DER verifier key
valueFrom:
secretKeyRef:
name: codeapi-jwt-verifier
key: public-key
- name: CODEAPI_JWT_KID
value: my-key-id
extraEnv:
- name: CODEAPI_AUTH_PROVIDER
value: librechat-jwt
- name: CODEAPI_JWT_PUBLIC_KEY # single PEM/base64-DER verifier key
valueFrom:
secretKeyRef:
name: codeapi-jwt-verifier
key: public-key
- name: CODEAPI_JWT_KID
value: my-key-id
```

`CODEAPI_JWT_PUBLIC_KEYS_DIR` (a mounted directory of PEM files) and
Expand All @@ -154,6 +154,30 @@ For development only, `LOCAL_MODE=true` bypasses authentication β€” see
**TLS to Redis.** Set `REDIS_TLS=true` via `extraEnv` on each component when
your Redis (e.g. a managed cache) requires TLS.

**Redis Cluster mode (GCP Memorystore cluster, AWS ElastiCache cluster).** Set
`redis.enabled=false` (so the bundled Redis subchart doesn't take precedence)
and `redis.cluster.enabled=true`, and provide the comma-separated startup nodes
in `redis.cluster.nodes`. The chart will set `USE_REDIS_CLUSTER=true` and
populate `REDIS_HOST` with the node list on every component. For TLS with a CA
certificate (recommended for GCP Memorystore), also set:

```yaml
redis:
enabled: false # required β€” otherwise the bundled subchart is used instead
cluster:
enabled: true
nodes: 'node-1:6379,node-2:6379,node-3:6379'
Comment thread
pedrojreis marked this conversation as resolved.
tls:
enabled: true
caSecretName: my-memorystore-secret # Secret that holds the CA cert
caKey: ca # Key inside the Secret
caMountPath: /etc/redis-tls/ca.crt # Mount path in each pod
useAlternativeDnsLookup: true # Required for GCP Memorystore cluster
```

The CA certificate is mounted from the Secret into every component pod and
passed to ioredis via `REDIS_CA`. No sidecar is needed.

**Package delivery.** KVM deployments default to
`workerSandbox.packages.source=image`. Build and publish the baked runner target
under the existing `workerSandbox.sandboxImage` repository and tag:
Expand Down Expand Up @@ -181,11 +205,13 @@ override remains unchanged in either mode.
## Quick Start (Local Development)

### 1. Start Minikube

```bash
minikube start --cpus=4 --memory=8192
```

### 2. Build Images Inside Minikube

```bash
# Point docker to minikube's daemon
eval $(minikube docker-env)
Expand All @@ -200,6 +226,7 @@ docker build -t codeapi-package-init:latest -f docker/Dockerfile.package-init .
```

### 3. Install Dependencies & Deploy

```bash
cd helm/codeapi

Expand Down Expand Up @@ -255,6 +282,7 @@ kubectl rollout restart deployment/codeapi-sandbox-runner
```

### 5. Access the API

```bash
# Port forward (in another terminal)
kubectl port-forward svc/codeapi-api 3112:3112
Expand All @@ -268,6 +296,7 @@ curl http://localhost:3112/v1/health
## Commands Reference

### Startup

```bash
# Start minikube
minikube start
Expand All @@ -280,6 +309,7 @@ kubectl port-forward svc/codeapi-api 3112:3112
```

### Check Status

```bash
# View all pods
kubectl get pods
Expand All @@ -294,6 +324,7 @@ kubectl describe pod <pod-name>
```

### Scaling

```bash
# Scale the sandbox execution tier
kubectl scale deployment/codeapi-sandbox-runner --replicas=10
Expand All @@ -304,6 +335,7 @@ helm upgrade codeapi ./helm/codeapi -f ./helm/codeapi/values-local.yaml \
```

### Update After Code Changes

```bash
# Rebuild images (must be in minikube docker env)
eval $(minikube docker-env)
Expand All @@ -316,6 +348,7 @@ kubectl rollout restart deployment/codeapi-sandbox-runner
```

### Teardown

```bash
# Uninstall the Helm release (removes all K8s resources)
helm uninstall codeapi
Expand All @@ -332,12 +365,14 @@ minikube delete
## Testing

### Health Check

```bash
curl http://localhost:3112/v1/health
# Expected: OK
```

### Execute Python Code

```bash
curl -X POST http://localhost:3112/v1/exec \
-H "Content-Type: application/json" \
Expand All @@ -346,6 +381,7 @@ curl -X POST http://localhost:3112/v1/exec \
```

### Verify Horizontal Scaling

```bash
# Check which service-worker processed the job
kubectl logs deployment/codeapi-service-worker --tail=5
Expand Down Expand Up @@ -403,6 +439,7 @@ kubectl logs deployment/codeapi-service-worker --tail=5
## Troubleshooting

### Pod stuck in `ErrImageNeverPull`

```bash
# Images must be built inside minikube's docker
eval $(minikube docker-env)
Expand All @@ -411,13 +448,15 @@ kubectl rollout restart deployment/<deployment-name>
```

### Pod stuck in `CrashLoopBackOff`

```bash
# Check logs
kubectl logs <pod-name> --previous
kubectl describe pod <pod-name>
```

### "runtime is unknown" error

```bash
# In source=pvc mode, check whether the package-init job populated the PVC:
kubectl get jobs -l app.kubernetes.io/component=package-init
Expand All @@ -435,12 +474,14 @@ In the default `source=image` mode, rebuild and publish
package-init Job or packages PVC is rendered.

### Connection refused on port 3112

```bash
# Make sure port-forward is running
kubectl port-forward svc/codeapi-api 3112:3112
```

### MinIO `ImagePullBackOff` (production values)

```bash
# The Bitnami MinIO chart may reference unavailable image tags.
# For local dev, values-local.yaml uses minio.useSimple=true which
Expand Down
128 changes: 127 additions & 1 deletion helm/codeapi/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -152,11 +152,15 @@ app.kubernetes.io/component: tool-call-server
{{- end }}

{{/*
Redis host - either from subchart or external
Redis host - either from subchart or external.
In cluster mode this renders the comma-separated node list from redis.cluster.nodes
(falling back to redis.external.host for single-node external deployments).
*/}}
{{- define "codeapi.redis.host" -}}
{{- if .Values.redis.enabled }}
{{- printf "%s-redis-master" .Release.Name }}
{{- else if and .Values.redis.cluster.enabled .Values.redis.cluster.nodes }}
{{- .Values.redis.cluster.nodes }}
Comment thread
pedrojreis marked this conversation as resolved.
{{- else }}
{{- .Values.redis.external.host }}
{{- end }}
Expand All @@ -173,6 +177,128 @@ Redis port
{{- end }}
{{- end }}

{{/*
Redis host used ONLY for the wait-for-redis readiness probe. In cluster mode
codeapi.redis.host renders the full comma-separated startup-node list
("n1:6379,n2:6379,..."), which `nc -z` cannot resolve as a single
[destination] [port] pair. This always resolves to one reachable host β€”
the first cluster startup node when in cluster mode, otherwise the same
value as codeapi.redis.host.
*/}}
{{- define "codeapi.redis.probeHost" -}}
{{- if .Values.redis.enabled }}
{{- printf "%s-redis-master" .Release.Name }}
{{- else if and .Values.redis.cluster.enabled .Values.redis.cluster.nodes }}
{{- $firstNode := .Values.redis.cluster.nodes | splitList "," | first | trim }}
{{- if hasPrefix "[" $firstNode }}
{{- $closingBracket := index (splitList "]" $firstNode) 0 }}
{{- trimPrefix "[" $closingBracket }}
{{- else if eq (len (splitList ":" $firstNode)) 2 }}
{{- index (splitList ":" $firstNode) 0 }}
{{- else }}
{{- $firstNode }}
{{- end }}
{{- else }}
{{- .Values.redis.external.host }}
{{- end }}
{{- end }}

{{/*
Port companion to codeapi.redis.probeHost.
*/}}
{{- define "codeapi.redis.probePort" -}}
{{- if .Values.redis.enabled }}
{{- "6379" }}
{{- else if and .Values.redis.cluster.enabled .Values.redis.cluster.nodes }}
{{- $firstNode := .Values.redis.cluster.nodes | splitList "," | first | trim }}
{{- if hasPrefix "[" $firstNode }}
{{- $suffix := index (splitList "]" $firstNode) 1 }}
{{- if hasPrefix ":" $suffix }}
{{- trimPrefix ":" $suffix }}
{{- else }}
{{- "6379" }}
{{- end }}
{{- else if eq (len (splitList ":" $firstNode)) 2 }}
{{- index (splitList ":" $firstNode) 1 }}
{{- else }}
{{- "6379" }}
{{- end }}
{{- else }}
{{- .Values.redis.external.port | default "6379" }}
{{- end }}
{{- end }}

{{/*
USE_REDIS_CLUSTER value – "true" when redis.cluster.enabled or when
redis.cluster.nodes contains a comma (auto-detect multiple nodes).
*/}}
{{- define "codeapi.redis.clusterEnabled" -}}
{{- if .Values.redis.cluster.enabled }}
{{- "true" }}
{{- else if and .Values.redis.cluster.nodes (contains "," .Values.redis.cluster.nodes) }}
{{- "true" }}
{{- else }}
{{- "false" }}
{{- end }}
{{- end }}

{{/*
Emit the Redis TLS + CA environment variables and volume mount for each
component that needs it. These only apply to an external managed Redis, so
they render nothing while the bundled subchart is enabled (redis.enabled=true
always uses plain TCP to the in-cluster Bitnami Redis) or when redis.tls.enabled
is false.
Usage: {{ include "codeapi.redis.tlsEnv" . }}
*/}}
{{- define "codeapi.redis.tlsEnv" -}}
{{- if not .Values.redis.enabled }}
{{- if .Values.redis.tls.enabled }}
- name: REDIS_TLS
value: "true"
Comment thread
pedrojreis marked this conversation as resolved.
{{- if .Values.redis.tls.caSecretName }}
- name: REDIS_CA
value: {{ .Values.redis.tls.caMountPath | quote }}
{{- end }}
{{- end }}
{{- if .Values.redis.useAlternativeDnsLookup }}
- name: REDIS_USE_ALTERNATIVE_DNS_LOOKUP
value: "true"
{{- end }}
{{- end }}
{{- end }}

{{/*
Volume definition for the Redis CA certificate secret.
Renders nothing when redis.tls.caSecretName is empty, or the bundled Redis
subchart is enabled (it never speaks TLS).
Usage: {{ include "codeapi.redis.caVolume" . }}
*/}}
{{- define "codeapi.redis.caVolume" -}}
{{- if and (not .Values.redis.enabled) .Values.redis.tls.enabled .Values.redis.tls.caSecretName }}
- name: redis-ca
secret:
secretName: {{ .Values.redis.tls.caSecretName }}
items:
- key: {{ .Values.redis.tls.caKey }}
path: ca.crt
{{- end }}
{{- end }}

{{/*
VolumeMount for the Redis CA certificate inside a container.
Renders nothing when redis.tls.caSecretName is empty, or the bundled Redis
subchart is enabled (it never speaks TLS).
Usage: {{ include "codeapi.redis.caVolumeMount" . }}
*/}}
{{- define "codeapi.redis.caVolumeMount" -}}
{{- if and (not .Values.redis.enabled) .Values.redis.tls.enabled .Values.redis.tls.caSecretName }}
- name: redis-ca
mountPath: {{ .Values.redis.tls.caMountPath | quote }}
subPath: ca.crt
readOnly: true
{{- end }}
{{- end }}

{{/*
Redis NetworkPolicy egress. Kubernetes NetworkPolicy cannot match DNS names,
so external Redis can be scoped with CIDRs when available; otherwise the chart
Expand Down
13 changes: 12 additions & 1 deletion helm/codeapi/templates/api-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,17 @@ spec:
{{- end }}
# Redis connection
- name: REDIS_HOST
value: {{ include "codeapi.redis.host" . }}
value: {{ include "codeapi.redis.host" . | quote }}
- name: REDIS_PORT
value: {{ include "codeapi.redis.port" . | quote }}
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: {{ include "codeapi.fullname" . }}-secrets
key: redis-password
- name: USE_REDIS_CLUSTER
value: {{ include "codeapi.redis.clusterEnabled" . | quote }}
{{- include "codeapi.redis.tlsEnv" . | nindent 12 }}
- name: CODEAPI_BRIDGE_PAIRING_ROLLBACK_EPOCH
valueFrom:
configMapKeyRef:
Expand Down Expand Up @@ -101,6 +104,14 @@ spec:
periodSeconds: 10
resources:
{{- toYaml .Values.api.resources | nindent 12 }}
{{- with (include "codeapi.redis.caVolumeMount" . | trim) }}
volumeMounts:
{{- . | nindent 12 }}
{{- end }}
{{- with (include "codeapi.redis.caVolume" . | trim) }}
volumes:
{{- . | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
Expand Down
Loading