From 681e41f54f0ec5c836ea591f7db357c7409ab7e1 Mon Sep 17 00:00:00 2001 From: Alex Soldatchenko Date: Mon, 10 Aug 2026 20:27:32 -0600 Subject: [PATCH 01/11] feat: add fixed-capacity API workload isolation --- braintrust/README.md | 25 ++ braintrust/templates/_api-deployment.tpl | 224 ++++++++++++++++++ braintrust/templates/_api-service.tpl | 29 +++ braintrust/templates/_helpers.tpl | 45 ++++ braintrust/templates/api-deployment.yaml | 219 +---------------- braintrust/templates/api-pdb.yaml | 27 +++ braintrust/templates/api-service.yaml | 30 +-- .../brainstore-fastreader-deployment.yaml | 2 +- .../brainstore-reader-deployment.yaml | 2 +- .../brainstore-writer-deployment.yaml | 2 +- .../tests/api-workload-isolation_test.yaml | 193 +++++++++++++++ braintrust/tests/brainstore-reader_test.yaml | 15 ++ braintrust/tests/brainstore-writer_test.yaml | 15 ++ braintrust/values.yaml | 39 +++ 14 files changed, 631 insertions(+), 236 deletions(-) create mode 100644 braintrust/templates/_api-deployment.tpl create mode 100644 braintrust/templates/_api-service.tpl create mode 100644 braintrust/templates/api-pdb.yaml create mode 100644 braintrust/tests/api-workload-isolation_test.yaml diff --git a/braintrust/README.md b/braintrust/README.md index 4e0395e..6d5e10f 100644 --- a/braintrust/README.md +++ b/braintrust/README.md @@ -217,6 +217,31 @@ Size the request for the pod's full local-storage usage: When you enable `tmpVolume`, make sure the `ephemeralStorage.request` still covers that extra space. +## API workload isolation + +`api.workloadIsolation.enabled` creates fixed-capacity `braintrust-api-ingest` +and `braintrust-api-background` Deployments and Services alongside the existing +default `braintrust-api` pool. The pools share the same image and base +configuration, while allowing independent replicas, resources, probes, rollout +settings, environment overrides, topology spreading, and disruption budgets. + +Enabling the pools does not configure a public ingress. The ingress or gateway +must preserve `braintrust-api` as its default backend and route these paths: + +| Pool | Paths | +| --- | --- | +| `braintrust-api-ingest` | `/logs3`, `/otel/v1/traces`, `/attachment`, `/attachment/status` | +| `braintrust-api-background` | `/v1/eval`, `/v1/eval/*`, `/function/eval`, `/function/sandbox`, `/function/use`, `/function/invoke-async-batch`, `/function/insert-functions`, `/automation/logs/trigger`, `/v1/proxy/chat/completions`, `/v1/proxy/responses` | + +Brainstore's internal `BRAINSTORE_AI_PROXY_URL` automatically targets the +background Service while isolation is enabled. Disabling the feature removes +the additional workloads and returns Brainstore to the default API Service, +providing the rollback path. + +This feature does not enable autoscaling. Configure fixed replica counts under +`api.replicas`, `api.workloadIsolation.ingest.replicas`, and +`api.workloadIsolation.background.replicas`. + ## Testing This Helm chart includes comprehensive automated unit tests. diff --git a/braintrust/templates/_api-deployment.tpl b/braintrust/templates/_api-deployment.tpl new file mode 100644 index 0000000..4c6662d --- /dev/null +++ b/braintrust/templates/_api-deployment.tpl @@ -0,0 +1,224 @@ +{{/* Render one API Deployment from a merged pool configuration. */}} +{{- define "braintrust.apiDeployment" -}} +{{- $root := .root -}} +{{- $api := .api -}} +{{- $role := .role -}} +{{- $customCA := $api.customCA -}} +{{- $customCAMountPath := "" -}} +{{- $customCAFilename := "" -}} +{{- $customCASecretName := "" -}} +{{- $customCASecretKey := "" -}} +{{- if $customCA.enabled -}} +{{- $customCAMountPath = required "api.customCA.mountPath is required when api.customCA.enabled is true" $customCA.mountPath -}} +{{- $customCAFilename = required "api.customCA.filename is required when api.customCA.enabled is true" $customCA.filename -}} +{{- $customCASecretName = required "api.customCA.secretName is required when api.customCA.enabled is true" $customCA.secretName -}} +{{- $customCASecretKey = required "api.customCA.secretKey is required when api.customCA.enabled is true" $customCA.secretKey -}} +{{- end -}} +{{- $resourceLabels := mergeOverwrite (deepCopy $root.Values.global.labels) (deepCopy $api.labels) (dict "braintrust.com/api-pool" $role) -}} +{{- $podLabels := mergeOverwrite (deepCopy $root.Values.global.labels) (deepCopy $api.labels) (deepCopy $api.podLabels) (dict "app" $api.name "braintrust.com/api-pool" $role) -}} +{{- if eq $root.Values.cloud "azure" -}} +{{- $_ := set $podLabels "azure.workload.identity/use" "true" -}} +{{- end -}} +{{- if and (eq $root.Values.cloud "google") $api.enableGcsAuth -}} +{{- $_ := set $podLabels "gke-workload-identity/use" "true" -}} +{{- end -}} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ $api.name }} + namespace: {{ include "braintrust.namespace" $root }} + {{- with $resourceLabels }} + labels: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with $api.annotations.deployment }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + replicas: {{ $api.replicas }} + strategy: + {{- toYaml $api.strategy | nindent 4 }} + selector: + matchLabels: + app: {{ $api.name }} + template: + metadata: + labels: + {{- with $podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + annotations: + checksum/config: {{ include (print $root.Template.BasePath "/api-configmap.yaml") $root | sha256sum }} + {{- if and (eq $root.Values.cloud "google") $api.enableGcsAuth }} + iam.gke.io/gcp-service-account: {{ required "api.serviceAccount.googleServiceAccount is required when api.enableGcsAuth is true" $api.serviceAccount.googleServiceAccount }} + {{- end }} + {{- with $api.annotations.pod }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + serviceAccountName: {{ $api.serviceAccount.name }} + {{- with $api.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with $api.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with $api.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- if $api.topologySpread.enabled }} + topologySpreadConstraints: + - maxSkew: {{ $api.topologySpread.maxSkew }} + topologyKey: {{ $api.topologySpread.topologyKey | quote }} + whenUnsatisfiable: {{ $api.topologySpread.whenUnsatisfiable }} + labelSelector: + matchLabels: + app: {{ $api.name }} + {{- end }} + {{- with $api.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: api + image: "{{ $api.image.repository }}:{{ $api.image.tag }}" + imagePullPolicy: {{ $api.image.pullPolicy }} + {{- with $api.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + ports: + - containerPort: {{ $api.service.port }} + resources: + {{- toYaml $api.resources | nindent 12 }} + {{- with $api.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with $api.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + envFrom: + - configMapRef: + name: {{ $root.Values.api.name }} + env: + - name: PG_URL + valueFrom: + secretKeyRef: + name: braintrust-secrets + key: PG_URL + - name: REDIS_URL + valueFrom: + secretKeyRef: + name: braintrust-secrets + key: REDIS_URL + - name: FUNCTION_SECRET_KEY + valueFrom: + secretKeyRef: + name: braintrust-secrets + key: FUNCTION_SECRET_KEY + - name: BRAINSTORE_LICENSE_KEY + valueFrom: + secretKeyRef: + name: braintrust-secrets + key: BRAINSTORE_LICENSE_KEY + {{- if eq $root.Values.cloud "azure" }} + - name: AZURE_STORAGE_CONNECTION_STRING + valueFrom: + secretKeyRef: + name: braintrust-secrets + key: AZURE_STORAGE_CONNECTION_STRING + {{- end }} + {{- if and (eq $root.Values.cloud "google") (not $api.enableGcsAuth) }} + - name: AWS_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: braintrust-secrets + key: GCS_ACCESS_KEY_ID + - name: AWS_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: braintrust-secrets + key: GCS_SECRET_ACCESS_KEY + {{- end }} + - name: TS_API_HEALTHSERVER_HOST + value: {{ $api.healthServer.host | quote }} + - name: TS_API_HEALTHSERVER_PORT + value: {{ $api.healthServer.port | quote }} + {{- if $customCA.enabled }} + {{- $customCAPath := printf "%s/%s" $customCAMountPath $customCAFilename }} + - name: NODE_EXTRA_CA_CERTS + value: {{ $customCAPath | quote }} + - name: REQUESTS_CA_BUNDLE + value: {{ $customCAPath | quote }} + - name: SSL_CERT_FILE + value: {{ $customCAPath | quote }} + - name: CURL_CA_BUNDLE + value: {{ $customCAPath | quote }} + - name: AWS_CA_BUNDLE + value: {{ $customCAPath | quote }} + - name: PIP_CERT + value: {{ $customCAPath | quote }} + {{- end }} + {{- with $api.extraEnvVars }} + {{- toYaml . | nindent 12 }} + {{- end }} + {{- if or $api.tmpVolume.enabled (and (eq $root.Values.cloud "azure") $root.Values.azure.enableAzureKeyVaultDriver) $customCA.enabled }} + volumeMounts: + {{- if $api.tmpVolume.enabled }} + - name: tmp-volume + mountPath: /tmp + {{- end }} + {{- if and (eq $root.Values.cloud "azure") $root.Values.azure.enableAzureKeyVaultDriver }} + - name: secrets-store-inline + mountPath: "/mnt/secrets-store" + readOnly: true + {{- end }} + {{- if $customCA.enabled }} + - name: custom-ca-bundle + mountPath: {{ $customCAMountPath | quote }} + readOnly: true + {{- end }} + {{- end }} + {{- with $api.extraContainers }} + {{- toYaml . | nindent 8 }} + {{- end }} + volumes: + {{- if or $api.tmpVolume.enabled (and (eq $root.Values.cloud "azure") $root.Values.azure.enableAzureKeyVaultDriver) $customCA.enabled $api.extraVolumes }} + {{- if $api.tmpVolume.enabled }} + - name: tmp-volume + emptyDir: + {{- if $api.tmpVolume.sizeLimit }} + sizeLimit: {{ $api.tmpVolume.sizeLimit | quote }} + {{- else }} + {} + {{- end }} + {{- end }} + {{- if and (eq $root.Values.cloud "azure") $root.Values.azure.enableAzureKeyVaultDriver }} + - name: secrets-store-inline + csi: + driver: secrets-store.csi.k8s.io + readOnly: true + volumeAttributes: + secretProviderClass: {{ $root.Values.azure.keyVaultName }} + {{- end }} + {{- if $customCA.enabled }} + - name: custom-ca-bundle + secret: + secretName: {{ $customCASecretName | quote }} + items: + - key: {{ $customCASecretKey | quote }} + path: {{ $customCAFilename | quote }} + {{- end }} + {{- with $api.extraVolumes }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- else }} + [] + {{- end }} +{{- end -}} diff --git a/braintrust/templates/_api-service.tpl b/braintrust/templates/_api-service.tpl new file mode 100644 index 0000000..68a4ae9 --- /dev/null +++ b/braintrust/templates/_api-service.tpl @@ -0,0 +1,29 @@ +{{/* Render one API Service from a merged pool configuration. */}} +{{- define "braintrust.apiService" -}} +{{- $root := .root -}} +{{- $api := .api -}} +{{- $role := .role -}} +{{- $resourceLabels := mergeOverwrite (deepCopy $root.Values.global.labels) (deepCopy $api.labels) (dict "braintrust.com/api-pool" $role) -}} +apiVersion: v1 +kind: Service +metadata: + name: {{ $api.service.name | default $api.name }} + namespace: {{ include "braintrust.namespace" $root }} + {{- with $resourceLabels }} + labels: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with $api.annotations.service }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + selector: + app: {{ $api.name }} + ports: + - name: {{ $api.service.portName }} + protocol: TCP + port: {{ $api.service.port }} + targetPort: {{ $api.service.port }} + type: {{ $api.service.type }} +{{- end -}} diff --git a/braintrust/templates/_helpers.tpl b/braintrust/templates/_helpers.tpl index d27e28f..54a308d 100644 --- a/braintrust/templates/_helpers.tpl +++ b/braintrust/templates/_helpers.tpl @@ -32,6 +32,51 @@ Internal cluster URL for the API service. http://{{ .Values.api.service.name | default .Values.api.name }}.{{ include "braintrust.namespace" . }}:{{ .Values.api.service.port }} {{- end -}} +{{/* +Build one API pool from the shared api values plus pool-specific overrides. +Pool extraEnvVars are appended so shared provider configuration is retained. +*/}} +{{- define "braintrust.apiPoolConfig" -}} +{{- $base := deepCopy .root.Values.api -}} +{{- $_ := unset $base "workloadIsolation" -}} +{{- $overrides := deepCopy (.overrides | default dict) -}} +{{- $baseExtraEnvVars := get $base "extraEnvVars" | default (list) -}} +{{- $poolExtraEnvVars := get $overrides "extraEnvVars" | default (list) -}} +{{- $_ := unset $overrides "extraEnvVars" -}} +{{- $pool := mergeOverwrite $base $overrides -}} +{{- $_ := set $pool "extraEnvVars" (concat $baseExtraEnvVars $poolExtraEnvVars) -}} +{{- toYaml $pool -}} +{{- end -}} + +{{/* +Return the API pools rendered by the chart. The default pool always exists; +ingest and background are added only when workload isolation is enabled. +*/}} +{{- define "braintrust.apiPools" -}} +{{- $default := include "braintrust.apiPoolConfig" (dict "root" . "overrides" (dict)) | fromYaml -}} +{{- $pools := list (dict "role" "default" "config" $default) -}} +{{- if .Values.api.workloadIsolation.enabled -}} +{{- $ingest := include "braintrust.apiPoolConfig" (dict "root" . "overrides" .Values.api.workloadIsolation.ingest) | fromYaml -}} +{{- $background := include "braintrust.apiPoolConfig" (dict "root" . "overrides" .Values.api.workloadIsolation.background) | fromYaml -}} +{{- $pools = append $pools (dict "role" "ingest" "config" $ingest) -}} +{{- $pools = append $pools (dict "role" "background" "config" $background) -}} +{{- end -}} +{{- toYaml $pools -}} +{{- end -}} + +{{/* +Internal cluster URL Brainstore uses for function/scoring traffic. When API +workload isolation is enabled, all Brainstore roles use the background pool. +*/}} +{{- define "braintrust.apiAiProxyInternalUrl" -}} +{{- if .Values.api.workloadIsolation.enabled -}} +{{- $background := include "braintrust.apiPoolConfig" (dict "root" . "overrides" .Values.api.workloadIsolation.background) | fromYaml -}} +http://{{ $background.service.name | default $background.name }}:{{ $background.service.port }} +{{- else -}} +http://{{ .Values.api.service.name | default .Values.api.name }}:{{ .Values.api.service.port }} +{{- end -}} +{{- end -}} + {{/* Internal cluster URL for the AI Gateway service. */}} diff --git a/braintrust/templates/api-deployment.yaml b/braintrust/templates/api-deployment.yaml index 43d6c12..3575630 100644 --- a/braintrust/templates/api-deployment.yaml +++ b/braintrust/templates/api-deployment.yaml @@ -1,211 +1,8 @@ -{{- $customCA := .Values.api.customCA -}} -{{- $customCAMountPath := "" -}} -{{- $customCAFilename := "" -}} -{{- $customCASecretName := "" -}} -{{- $customCASecretKey := "" -}} -{{- if $customCA.enabled -}} -{{- $customCAMountPath = required "api.customCA.mountPath is required when api.customCA.enabled is true" $customCA.mountPath -}} -{{- $customCAFilename = required "api.customCA.filename is required when api.customCA.enabled is true" $customCA.filename -}} -{{- $customCASecretName = required "api.customCA.secretName is required when api.customCA.enabled is true" $customCA.secretName -}} -{{- $customCASecretKey = required "api.customCA.secretKey is required when api.customCA.enabled is true" $customCA.secretKey -}} -{{- end -}} -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ .Values.api.name }} - namespace: {{ include "braintrust.namespace" . }} - {{- with (merge (deepCopy .Values.api.labels) .Values.global.labels) }} - labels: - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.api.annotations.deployment }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -spec: - replicas: {{ .Values.api.replicas }} - strategy: - type: RollingUpdate - rollingUpdate: - maxSurge: 100% - maxUnavailable: 0 - selector: - matchLabels: - app: {{ .Values.api.name }} - template: - metadata: - labels: - app: {{ .Values.api.name }} - {{- if eq .Values.cloud "azure" }} - azure.workload.identity/use: "true" - {{- end }} - {{- if and (eq .Values.cloud "google") .Values.api.enableGcsAuth }} - gke-workload-identity/use: "true" - {{- end }} - {{- with (merge (deepCopy .Values.api.podLabels) .Values.api.labels .Values.global.labels) }} - {{- toYaml . | nindent 8 }} - {{- end }} - annotations: - checksum/config: {{ include (print $.Template.BasePath "/api-configmap.yaml") . | sha256sum }} - {{- if and (eq .Values.cloud "google") .Values.api.enableGcsAuth }} - iam.gke.io/gcp-service-account: {{ required "api.serviceAccount.googleServiceAccount is required when api.enableGcsAuth is true" .Values.api.serviceAccount.googleServiceAccount }} - {{- end }} - {{- with .Values.api.annotations.pod }} - {{- toYaml . | nindent 8 }} - {{- end }} - spec: - serviceAccountName: {{ .Values.api.serviceAccount.name }} - {{- with .Values.api.podSecurityContext }} - securityContext: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.api.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.api.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.api.affinity }} - affinity: - {{- toYaml . | nindent 8 }} - {{- end }} - containers: - - name: api - image: "{{ .Values.api.image.repository }}:{{ .Values.api.image.tag }}" - imagePullPolicy: {{ .Values.api.image.pullPolicy }} - {{- with .Values.api.securityContext }} - securityContext: - {{- toYaml . | nindent 12 }} - {{- end }} - ports: - - containerPort: {{ .Values.api.service.port }} - resources: - {{- toYaml .Values.api.resources | nindent 12 }} - {{- with .Values.api.livenessProbe }} - livenessProbe: - {{- toYaml . | nindent 12 }} - {{- end }} - {{- with .Values.api.readinessProbe }} - readinessProbe: - {{- toYaml . | nindent 12 }} - {{- end }} - envFrom: - - configMapRef: - name: {{ .Values.api.name }} - env: - - name: PG_URL - valueFrom: - secretKeyRef: - name: braintrust-secrets - key: PG_URL - - name: REDIS_URL - valueFrom: - secretKeyRef: - name: braintrust-secrets - key: REDIS_URL - - name: FUNCTION_SECRET_KEY - valueFrom: - secretKeyRef: - name: braintrust-secrets - key: FUNCTION_SECRET_KEY - - name: BRAINSTORE_LICENSE_KEY - valueFrom: - secretKeyRef: - name: braintrust-secrets - key: BRAINSTORE_LICENSE_KEY - {{- if eq .Values.cloud "azure" }} - - name: AZURE_STORAGE_CONNECTION_STRING - valueFrom: - secretKeyRef: - name: braintrust-secrets - key: AZURE_STORAGE_CONNECTION_STRING - {{- end }} - {{- if and (eq .Values.cloud "google") (not .Values.api.enableGcsAuth) }} - - name: AWS_ACCESS_KEY_ID - valueFrom: - secretKeyRef: - name: braintrust-secrets - key: GCS_ACCESS_KEY_ID - - name: AWS_SECRET_ACCESS_KEY - valueFrom: - secretKeyRef: - name: braintrust-secrets - key: GCS_SECRET_ACCESS_KEY - {{- end }} - - name: TS_API_HEALTHSERVER_HOST - value: {{ .Values.api.healthServer.host | quote }} - - name: TS_API_HEALTHSERVER_PORT - value: {{ .Values.api.healthServer.port | quote }} - {{- if $customCA.enabled }} - {{- $customCAPath := printf "%s/%s" $customCAMountPath $customCAFilename }} - - name: NODE_EXTRA_CA_CERTS - value: {{ $customCAPath | quote }} - - name: REQUESTS_CA_BUNDLE - value: {{ $customCAPath | quote }} - - name: SSL_CERT_FILE - value: {{ $customCAPath | quote }} - - name: CURL_CA_BUNDLE - value: {{ $customCAPath | quote }} - - name: AWS_CA_BUNDLE - value: {{ $customCAPath | quote }} - - name: PIP_CERT - value: {{ $customCAPath | quote }} - {{- end }} - {{- if .Values.api.extraEnvVars }} - {{- toYaml .Values.api.extraEnvVars | nindent 12 }} - {{- end }} - {{- if or .Values.api.tmpVolume.enabled (and (eq .Values.cloud "azure") .Values.azure.enableAzureKeyVaultDriver) $customCA.enabled }} - volumeMounts: - {{- if .Values.api.tmpVolume.enabled }} - - name: tmp-volume - mountPath: /tmp - {{- end }} - {{- if and (eq .Values.cloud "azure") .Values.azure.enableAzureKeyVaultDriver }} - - name: secrets-store-inline - mountPath: "/mnt/secrets-store" - readOnly: true - {{- end }} - {{- if $customCA.enabled }} - - name: custom-ca-bundle - mountPath: {{ $customCAMountPath | quote }} - readOnly: true - {{- end }} - {{- end }} - {{- with .Values.api.extraContainers }} - {{- toYaml . | nindent 8 }} - {{- end }} - volumes: - {{- if or .Values.api.tmpVolume.enabled (and (eq .Values.cloud "azure") .Values.azure.enableAzureKeyVaultDriver) $customCA.enabled .Values.api.extraVolumes }} - {{- if .Values.api.tmpVolume.enabled }} - - name: tmp-volume - emptyDir: - {{- if .Values.api.tmpVolume.sizeLimit }} - sizeLimit: {{ .Values.api.tmpVolume.sizeLimit | quote }} - {{- else }} - {} - {{- end }} - {{- end }} - {{- if and (eq .Values.cloud "azure") .Values.azure.enableAzureKeyVaultDriver }} - - name: secrets-store-inline - csi: - driver: secrets-store.csi.k8s.io - readOnly: true - volumeAttributes: - secretProviderClass: {{ .Values.azure.keyVaultName }} - {{- end }} - {{- if $customCA.enabled }} - - name: custom-ca-bundle - secret: - secretName: {{ $customCASecretName | quote }} - items: - - key: {{ $customCASecretKey | quote }} - path: {{ $customCAFilename | quote }} - {{- end }} - {{- with .Values.api.extraVolumes }} - {{- toYaml . | nindent 8 }} - {{- end }} - {{- else }} - [] - {{- end }} +{{- $root := . -}} +{{- $pools := include "braintrust.apiPools" . | fromYamlArray -}} +{{- range $index, $pool := $pools }} +{{- if gt $index 0 }} +--- +{{- end }} +{{ include "braintrust.apiDeployment" (dict "root" $root "api" $pool.config "role" $pool.role) }} +{{- end }} diff --git a/braintrust/templates/api-pdb.yaml b/braintrust/templates/api-pdb.yaml new file mode 100644 index 0000000..025a072 --- /dev/null +++ b/braintrust/templates/api-pdb.yaml @@ -0,0 +1,27 @@ +{{- $root := . -}} +{{- $pools := include "braintrust.apiPools" . | fromYamlArray -}} +{{- $rendered := 0 -}} +{{- range $pool := $pools -}} +{{- $api := $pool.config -}} +{{- if $api.podDisruptionBudget.enabled }} +{{- if gt $rendered 0 }} +--- +{{- end }} +{{- $resourceLabels := mergeOverwrite (deepCopy $root.Values.global.labels) (deepCopy $api.labels) (dict "braintrust.com/api-pool" $pool.role) -}} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ $api.name }} + namespace: {{ include "braintrust.namespace" $root }} + {{- with $resourceLabels }} + labels: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + minAvailable: {{ $api.podDisruptionBudget.minAvailable }} + selector: + matchLabels: + app: {{ $api.name }} +{{- $rendered = add1 $rendered -}} +{{- end }} +{{- end }} diff --git a/braintrust/templates/api-service.yaml b/braintrust/templates/api-service.yaml index 0ff776c..f5e6dbf 100644 --- a/braintrust/templates/api-service.yaml +++ b/braintrust/templates/api-service.yaml @@ -1,22 +1,8 @@ -apiVersion: v1 -kind: Service -metadata: - name: {{ .Values.api.service.name | default .Values.api.name }} - namespace: {{ include "braintrust.namespace" . }} - {{- with (merge (deepCopy .Values.api.labels) .Values.global.labels) }} - labels: - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.api.annotations.service }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -spec: - selector: - app: {{ .Values.api.name }} - ports: - - name: {{ .Values.api.service.portName }} - protocol: TCP - port: {{ .Values.api.service.port }} - targetPort: {{ .Values.api.service.port }} - type: {{ .Values.api.service.type }} +{{- $root := . -}} +{{- $pools := include "braintrust.apiPools" . | fromYamlArray -}} +{{- range $index, $pool := $pools }} +{{- if gt $index 0 }} +--- +{{- end }} +{{ include "braintrust.apiService" (dict "root" $root "api" $pool.config "role" $pool.role) }} +{{- end }} diff --git a/braintrust/templates/brainstore-fastreader-deployment.yaml b/braintrust/templates/brainstore-fastreader-deployment.yaml index ea43276..2270917 100644 --- a/braintrust/templates/brainstore-fastreader-deployment.yaml +++ b/braintrust/templates/brainstore-fastreader-deployment.yaml @@ -119,7 +119,7 @@ spec: name: braintrust-secrets key: REDIS_URL - name: BRAINSTORE_AI_PROXY_URL - value: "http://{{ .Values.api.service.name | default .Values.api.name }}:{{ .Values.api.service.port }}" + value: {{ include "braintrust.apiAiProxyInternalUrl" . | quote }} {{- if eq .Values.brainstore.locksBackend "redis" }} - name: BRAINSTORE_LOCKS_URI valueFrom: diff --git a/braintrust/templates/brainstore-reader-deployment.yaml b/braintrust/templates/brainstore-reader-deployment.yaml index d3aa0b5..bab062a 100644 --- a/braintrust/templates/brainstore-reader-deployment.yaml +++ b/braintrust/templates/brainstore-reader-deployment.yaml @@ -119,7 +119,7 @@ spec: name: braintrust-secrets key: REDIS_URL - name: BRAINSTORE_AI_PROXY_URL - value: "http://{{ .Values.api.service.name | default .Values.api.name }}:{{ .Values.api.service.port }}" + value: {{ include "braintrust.apiAiProxyInternalUrl" . | quote }} {{- if eq .Values.brainstore.locksBackend "redis" }} - name: BRAINSTORE_LOCKS_URI valueFrom: diff --git a/braintrust/templates/brainstore-writer-deployment.yaml b/braintrust/templates/brainstore-writer-deployment.yaml index 0a2dab2..3ffc61b 100644 --- a/braintrust/templates/brainstore-writer-deployment.yaml +++ b/braintrust/templates/brainstore-writer-deployment.yaml @@ -119,7 +119,7 @@ spec: name: braintrust-secrets key: REDIS_URL - name: BRAINSTORE_AI_PROXY_URL - value: "http://{{ .Values.api.service.name | default .Values.api.name }}:{{ .Values.api.service.port }}" + value: {{ include "braintrust.apiAiProxyInternalUrl" . | quote }} {{- if eq .Values.brainstore.locksBackend "redis" }} - name: BRAINSTORE_LOCKS_URI valueFrom: diff --git a/braintrust/tests/api-workload-isolation_test.yaml b/braintrust/tests/api-workload-isolation_test.yaml new file mode 100644 index 0000000..ef3ea1c --- /dev/null +++ b/braintrust/tests/api-workload-isolation_test.yaml @@ -0,0 +1,193 @@ +suite: test API workload isolation +templates: + - api-deployment.yaml + - api-configmap.yaml + - api-service.yaml + - api-pdb.yaml + - brainstore-fastreader-deployment.yaml + - brainstore-fastreader-configmap.yaml +tests: + - it: should preserve one default deployment when isolation is disabled + template: api-deployment.yaml + values: + - __fixtures__/base-values.yaml + asserts: + - hasDocuments: + count: 1 + - equal: + path: metadata.name + value: braintrust-api + - equal: + path: spec.replicas + value: 1 + + - it: should render the ingest deployment with its own fixed capacity and topology budget + template: api-deployment.yaml + values: + - __fixtures__/base-values.yaml + set: + api.workloadIsolation.enabled: true + api.workloadIsolation.ingest.replicas: 3 + documentIndex: 1 + asserts: + - equal: + path: metadata.name + value: braintrust-api-ingest + - equal: + path: spec.replicas + value: 3 + - equal: + path: spec.template.metadata.labels["braintrust.com/api-pool"] + value: ingest + - equal: + path: spec.template.spec.topologySpreadConstraints[0].labelSelector.matchLabels.app + value: braintrust-api-ingest + + - it: should merge shared and background-specific API configuration + template: api-deployment.yaml + values: + - __fixtures__/base-values.yaml + set: + api.workloadIsolation.enabled: true + api.workloadIsolation.background.replicas: 5 + api.extraEnvVars: + - name: SHARED_SETTING + value: shared + api.workloadIsolation.background.extraEnvVars: + - name: POOL_SETTING + value: background + api.workloadIsolation.background.resources: + requests: + cpu: "3" + memory: 6Gi + limits: + cpu: "3" + memory: 6Gi + documentIndex: 2 + asserts: + - equal: + path: metadata.name + value: braintrust-api-background + - equal: + path: spec.replicas + value: 5 + - equal: + path: spec.template.spec.containers[0].resources.requests.cpu + value: "3" + - contains: + path: spec.template.spec.containers[0].env + content: + name: SHARED_SETTING + value: shared + - contains: + path: spec.template.spec.containers[0].env + content: + name: POOL_SETTING + value: background + + - it: should render an isolated ingest Service with inherited annotations + template: api-service.yaml + values: + - __fixtures__/base-values.yaml + set: + api.workloadIsolation.enabled: true + api.annotations.service: + cloud.google.com/neg: '{"ingress":true}' + documentIndex: 1 + asserts: + - equal: + path: metadata.name + value: braintrust-api-ingest + - equal: + path: metadata.annotations["cloud.google.com/neg"] + value: '{"ingress":true}' + - equal: + path: spec.selector.app + value: braintrust-api-ingest + + - it: should render a dedicated disruption budget for the background pool + template: api-pdb.yaml + values: + - __fixtures__/base-values.yaml + set: + api.workloadIsolation.enabled: true + api.workloadIsolation.background.podDisruptionBudget.minAvailable: 2 + documentIndex: 1 + asserts: + - isKind: + of: PodDisruptionBudget + - equal: + path: metadata.name + value: braintrust-api-background + - equal: + path: spec.minAvailable + value: 2 + - equal: + path: spec.selector.matchLabels.app + value: braintrust-api-background + + - it: should route fast-reader AI proxy traffic to the background pool + template: brainstore-fastreader-deployment.yaml + values: + - __fixtures__/base-values.yaml + set: + api.workloadIsolation.enabled: true + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: BRAINSTORE_AI_PROXY_URL + value: "http://braintrust-api-background:8000" + + - it: should isolate Deployment labels between API pools + template: api-deployment.yaml + values: + - __fixtures__/base-values.yaml + set: + api.workloadIsolation.enabled: true + api.workloadIsolation.ingest.labels.ingest-only: ingest + api.workloadIsolation.background.labels.background-only: background + documentIndex: 1 + asserts: + - equal: + path: metadata.labels["ingest-only"] + value: ingest + - isNull: + path: metadata.labels["background-only"] + - equal: + path: spec.template.metadata.labels["ingest-only"] + value: ingest + - isNull: + path: spec.template.metadata.labels["background-only"] + + - it: should isolate Service labels between API pools + template: api-service.yaml + values: + - __fixtures__/base-values.yaml + set: + api.workloadIsolation.enabled: true + api.workloadIsolation.ingest.labels.ingest-only: ingest + api.workloadIsolation.background.labels.background-only: background + documentIndex: 2 + asserts: + - equal: + path: metadata.labels["background-only"] + value: background + - isNull: + path: metadata.labels["ingest-only"] + + - it: should isolate disruption-budget labels between API pools + template: api-pdb.yaml + values: + - __fixtures__/base-values.yaml + set: + api.workloadIsolation.enabled: true + api.workloadIsolation.ingest.labels.ingest-only: ingest + api.workloadIsolation.background.labels.background-only: background + documentIndex: 0 + asserts: + - equal: + path: metadata.labels["ingest-only"] + value: ingest + - isNull: + path: metadata.labels["background-only"] diff --git a/braintrust/tests/brainstore-reader_test.yaml b/braintrust/tests/brainstore-reader_test.yaml index b885550..dd5e99e 100644 --- a/braintrust/tests/brainstore-reader_test.yaml +++ b/braintrust/tests/brainstore-reader_test.yaml @@ -325,6 +325,21 @@ tests: name: BRAINSTORE_AI_PROXY_URL value: "http://braintrust-api:8000" + - it: should route AI proxy traffic to the background API pool when isolation is enabled + template: brainstore-reader-deployment.yaml + values: + - __fixtures__/base-values.yaml + release: + namespace: "braintrust" + set: + api.workloadIsolation.enabled: true + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: BRAINSTORE_AI_PROXY_URL + value: "http://braintrust-api-background:8000" + - it: should include extraContainers when provided (e.g. OTEL collector sidecar) template: brainstore-reader-deployment.yaml values: diff --git a/braintrust/tests/brainstore-writer_test.yaml b/braintrust/tests/brainstore-writer_test.yaml index c656c32..a2564b5 100644 --- a/braintrust/tests/brainstore-writer_test.yaml +++ b/braintrust/tests/brainstore-writer_test.yaml @@ -323,3 +323,18 @@ tests: content: name: BRAINSTORE_AI_PROXY_URL value: "http://braintrust-api:8000" + + - it: should route AI proxy traffic to the background API pool when isolation is enabled + template: brainstore-writer-deployment.yaml + values: + - __fixtures__/base-values.yaml + release: + namespace: "braintrust" + set: + api.workloadIsolation.enabled: true + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: BRAINSTORE_AI_PROXY_URL + value: "http://braintrust-api-background:8000" diff --git a/braintrust/values.yaml b/braintrust/values.yaml index 6dd8d34..a3b9270 100644 --- a/braintrust/values.yaml +++ b/braintrust/values.yaml @@ -111,6 +111,32 @@ api: pod: {} serviceaccount: {} replicas: 4 + # Optional fixed-capacity workload isolation. This creates dedicated API + # Deployments and Services for ingestion and background work while retaining + # api.name as the default pool for all remaining traffic. Public ingress must + # route the documented paths to these Services when this is enabled. + workloadIsolation: + enabled: false + ingest: + name: "braintrust-api-ingest" + replicas: 2 + service: + name: "braintrust-api-ingest" + podDisruptionBudget: + enabled: true + minAvailable: 1 + topologySpread: + enabled: true + background: + name: "braintrust-api-background" + replicas: 2 + service: + name: "braintrust-api-background" + podDisruptionBudget: + enabled: true + minAvailable: 1 + topologySpread: + enabled: true image: repository: public.ecr.aws/braintrust/standalone-api tag: v2.9.0 @@ -136,6 +162,19 @@ api: limits: cpu: "4" memory: "8Gi" + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 100% + maxUnavailable: 0 + podDisruptionBudget: + enabled: false + minAvailable: 1 + topologySpread: + enabled: false + maxSkew: 1 + topologyKey: topology.kubernetes.io/zone + whenUnsatisfiable: ScheduleAnyway tmpVolume: enabled: false sizeLimit: "" From 13e8fa1f57b35c8527647a85e339154546732e6c Mon Sep 17 00:00:00 2001 From: Alex Soldatchenko Date: Tue, 11 Aug 2026 13:31:41 -0600 Subject: [PATCH 02/11] fix: keep default API pool inert when isolation is disabled --- braintrust/templates/_api-deployment.tpl | 8 ++++++-- braintrust/templates/_api-service.tpl | 6 +++++- .../tests/api-workload-isolation_test.yaml | 17 +++++++++++++++++ 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/braintrust/templates/_api-deployment.tpl b/braintrust/templates/_api-deployment.tpl index 4c6662d..96443af 100644 --- a/braintrust/templates/_api-deployment.tpl +++ b/braintrust/templates/_api-deployment.tpl @@ -14,8 +14,12 @@ {{- $customCASecretName = required "api.customCA.secretName is required when api.customCA.enabled is true" $customCA.secretName -}} {{- $customCASecretKey = required "api.customCA.secretKey is required when api.customCA.enabled is true" $customCA.secretKey -}} {{- end -}} -{{- $resourceLabels := mergeOverwrite (deepCopy $root.Values.global.labels) (deepCopy $api.labels) (dict "braintrust.com/api-pool" $role) -}} -{{- $podLabels := mergeOverwrite (deepCopy $root.Values.global.labels) (deepCopy $api.labels) (deepCopy $api.podLabels) (dict "app" $api.name "braintrust.com/api-pool" $role) -}} +{{- $poolLabels := dict -}} +{{- if or $root.Values.api.workloadIsolation.enabled (ne $role "default") -}} +{{- $_ := set $poolLabels "braintrust.com/api-pool" $role -}} +{{- end -}} +{{- $resourceLabels := mergeOverwrite (deepCopy $root.Values.global.labels) (deepCopy $api.labels) $poolLabels -}} +{{- $podLabels := mergeOverwrite (deepCopy $root.Values.global.labels) (deepCopy $api.labels) (deepCopy $api.podLabels) (dict "app" $api.name) $poolLabels -}} {{- if eq $root.Values.cloud "azure" -}} {{- $_ := set $podLabels "azure.workload.identity/use" "true" -}} {{- end -}} diff --git a/braintrust/templates/_api-service.tpl b/braintrust/templates/_api-service.tpl index 68a4ae9..1344ed5 100644 --- a/braintrust/templates/_api-service.tpl +++ b/braintrust/templates/_api-service.tpl @@ -3,7 +3,11 @@ {{- $root := .root -}} {{- $api := .api -}} {{- $role := .role -}} -{{- $resourceLabels := mergeOverwrite (deepCopy $root.Values.global.labels) (deepCopy $api.labels) (dict "braintrust.com/api-pool" $role) -}} +{{- $poolLabels := dict -}} +{{- if or $root.Values.api.workloadIsolation.enabled (ne $role "default") -}} +{{- $_ := set $poolLabels "braintrust.com/api-pool" $role -}} +{{- end -}} +{{- $resourceLabels := mergeOverwrite (deepCopy $root.Values.global.labels) (deepCopy $api.labels) $poolLabels -}} apiVersion: v1 kind: Service metadata: diff --git a/braintrust/tests/api-workload-isolation_test.yaml b/braintrust/tests/api-workload-isolation_test.yaml index ef3ea1c..d9e298a 100644 --- a/braintrust/tests/api-workload-isolation_test.yaml +++ b/braintrust/tests/api-workload-isolation_test.yaml @@ -20,6 +20,23 @@ tests: - equal: path: spec.replicas value: 1 + - isNull: + path: metadata.labels["braintrust.com/api-pool"] + - isNull: + path: spec.template.metadata.labels["braintrust.com/api-pool"] + + - it: should not label the default Service when isolation is disabled + template: api-service.yaml + values: + - __fixtures__/base-values.yaml + asserts: + - hasDocuments: + count: 1 + - equal: + path: metadata.name + value: braintrust-api + - isNull: + path: metadata.labels["braintrust.com/api-pool"] - it: should render the ingest deployment with its own fixed capacity and topology budget template: api-deployment.yaml From 1304354ef5916b76db7bf8c7cb9789870deb33cb Mon Sep 17 00:00:00 2001 From: Alex Soldatchenko Date: Tue, 11 Aug 2026 14:55:01 -0600 Subject: [PATCH 03/11] docs: clarify workload isolation rollback --- braintrust/README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/braintrust/README.md b/braintrust/README.md index 6d5e10f..92c39d5 100644 --- a/braintrust/README.md +++ b/braintrust/README.md @@ -230,13 +230,15 @@ must preserve `braintrust-api` as its default backend and route these paths: | Pool | Paths | | --- | --- | +| `braintrust-api` (default) | All requests not matched by an explicit ingest or background route | | `braintrust-api-ingest` | `/logs3`, `/otel/v1/traces`, `/attachment`, `/attachment/status` | | `braintrust-api-background` | `/v1/eval`, `/v1/eval/*`, `/function/eval`, `/function/sandbox`, `/function/use`, `/function/invoke-async-batch`, `/function/insert-functions`, `/automation/logs/trigger`, `/v1/proxy/chat/completions`, `/v1/proxy/responses` | Brainstore's internal `BRAINSTORE_AI_PROXY_URL` automatically targets the -background Service while isolation is enabled. Disabling the feature removes -the additional workloads and returns Brainstore to the default API Service, -providing the rollback path. +background Service while isolation is enabled. To roll back, first route the +isolated public paths back to the default API Service and verify it is serving +them. Only then disable workload isolation in the chart; the chart cannot +update an external ingress or gateway on its own. This feature does not enable autoscaling. Configure fixed replica counts under `api.replicas`, `api.workloadIsolation.ingest.replicas`, and From 2b28d0501887effa32c8d512ddb3b3a97451c1dc Mon Sep 17 00:00:00 2001 From: Alex Soldatchenko Date: Tue, 11 Aug 2026 15:34:02 -0600 Subject: [PATCH 04/11] fix: preserve PDB document separators --- braintrust/templates/api-pdb.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/braintrust/templates/api-pdb.yaml b/braintrust/templates/api-pdb.yaml index 025a072..7e78952 100644 --- a/braintrust/templates/api-pdb.yaml +++ b/braintrust/templates/api-pdb.yaml @@ -7,7 +7,7 @@ {{- if gt $rendered 0 }} --- {{- end }} -{{- $resourceLabels := mergeOverwrite (deepCopy $root.Values.global.labels) (deepCopy $api.labels) (dict "braintrust.com/api-pool" $pool.role) -}} +{{ $resourceLabels := mergeOverwrite (deepCopy $root.Values.global.labels) (deepCopy $api.labels) (dict "braintrust.com/api-pool" $pool.role) -}} apiVersion: policy/v1 kind: PodDisruptionBudget metadata: From 5706d0346d1f6c5b110dac5176fc72559d941459 Mon Sep 17 00:00:00 2001 From: Alex Soldatchenko Date: Wed, 12 Aug 2026 13:11:22 -0600 Subject: [PATCH 05/11] fix: stage Brainstore workload isolation activation --- braintrust/README.md | 15 ++++++++----- braintrust/templates/_api-deployment.tpl | 8 +++++-- braintrust/templates/_api-service.tpl | 2 +- braintrust/templates/_helpers.tpl | 7 +++--- braintrust/templates/api-pdb.yaml | 2 +- .../tests/api-workload-isolation_test.yaml | 22 +++++++++++++++---- braintrust/values.yaml | 7 ++++-- 7 files changed, 45 insertions(+), 18 deletions(-) diff --git a/braintrust/README.md b/braintrust/README.md index 92c39d5..59e56b8 100644 --- a/braintrust/README.md +++ b/braintrust/README.md @@ -234,11 +234,16 @@ must preserve `braintrust-api` as its default backend and route these paths: | `braintrust-api-ingest` | `/logs3`, `/otel/v1/traces`, `/attachment`, `/attachment/status` | | `braintrust-api-background` | `/v1/eval`, `/v1/eval/*`, `/function/eval`, `/function/sandbox`, `/function/use`, `/function/invoke-async-batch`, `/function/insert-functions`, `/automation/logs/trigger`, `/v1/proxy/chat/completions`, `/v1/proxy/responses` | -Brainstore's internal `BRAINSTORE_AI_PROXY_URL` automatically targets the -background Service while isolation is enabled. To roll back, first route the -isolated public paths back to the default API Service and verify it is serving -them. Only then disable workload isolation in the chart; the chart cannot -update an external ingress or gateway on its own. +By default, Brainstore's internal `BRAINSTORE_AI_PROXY_URL` targets the +background Service while isolation is enabled. For an existing deployment, +first create the pools with +`api.workloadIsolation.brainstoreAiProxyToBackground: false`, and keep public +paths on the default Service. Verify the background pool is ready, then route +the classified public paths and set `brainstoreAiProxyToBackground: true` in a +later release. To roll back, first return both the public paths and +`brainstoreAiProxyToBackground` to the default API Service and verify it is +serving them. Only then disable workload isolation in the chart; the chart +cannot update an external ingress or gateway on its own. This feature does not enable autoscaling. Configure fixed replica counts under `api.replicas`, `api.workloadIsolation.ingest.replicas`, and diff --git a/braintrust/templates/_api-deployment.tpl b/braintrust/templates/_api-deployment.tpl index 96443af..8e2d39e 100644 --- a/braintrust/templates/_api-deployment.tpl +++ b/braintrust/templates/_api-deployment.tpl @@ -16,7 +16,7 @@ {{- end -}} {{- $poolLabels := dict -}} {{- if or $root.Values.api.workloadIsolation.enabled (ne $role "default") -}} -{{- $_ := set $poolLabels "braintrust.com/api-pool" $role -}} +{{- $_ := set $poolLabels "braintrust.dev/api-pool" $role -}} {{- end -}} {{- $resourceLabels := mergeOverwrite (deepCopy $root.Values.global.labels) (deepCopy $api.labels) $poolLabels -}} {{- $podLabels := mergeOverwrite (deepCopy $root.Values.global.labels) (deepCopy $api.labels) (deepCopy $api.podLabels) (dict "app" $api.name) $poolLabels -}} @@ -42,7 +42,11 @@ metadata: spec: replicas: {{ $api.replicas }} strategy: - {{- toYaml $api.strategy | nindent 4 }} + type: {{ $api.strategy.type }} + {{- with $api.strategy.rollingUpdate }} + rollingUpdate: + {{- toYaml . | nindent 6 }} + {{- end }} selector: matchLabels: app: {{ $api.name }} diff --git a/braintrust/templates/_api-service.tpl b/braintrust/templates/_api-service.tpl index 1344ed5..b8a2831 100644 --- a/braintrust/templates/_api-service.tpl +++ b/braintrust/templates/_api-service.tpl @@ -5,7 +5,7 @@ {{- $role := .role -}} {{- $poolLabels := dict -}} {{- if or $root.Values.api.workloadIsolation.enabled (ne $role "default") -}} -{{- $_ := set $poolLabels "braintrust.com/api-pool" $role -}} +{{- $_ := set $poolLabels "braintrust.dev/api-pool" $role -}} {{- end -}} {{- $resourceLabels := mergeOverwrite (deepCopy $root.Values.global.labels) (deepCopy $api.labels) $poolLabels -}} apiVersion: v1 diff --git a/braintrust/templates/_helpers.tpl b/braintrust/templates/_helpers.tpl index 54a308d..cf42b32 100644 --- a/braintrust/templates/_helpers.tpl +++ b/braintrust/templates/_helpers.tpl @@ -65,11 +65,12 @@ ingest and background are added only when workload isolation is enabled. {{- end -}} {{/* -Internal cluster URL Brainstore uses for function/scoring traffic. When API -workload isolation is enabled, all Brainstore roles use the background pool. +Internal cluster URL Brainstore uses for function/scoring traffic. The +background pool is used only after workload isolation has been activated for +Brainstore, allowing existing deployments to stage a ready background pool. */}} {{- define "braintrust.apiAiProxyInternalUrl" -}} -{{- if .Values.api.workloadIsolation.enabled -}} +{{- if and .Values.api.workloadIsolation.enabled .Values.api.workloadIsolation.brainstoreAiProxyToBackground -}} {{- $background := include "braintrust.apiPoolConfig" (dict "root" . "overrides" .Values.api.workloadIsolation.background) | fromYaml -}} http://{{ $background.service.name | default $background.name }}:{{ $background.service.port }} {{- else -}} diff --git a/braintrust/templates/api-pdb.yaml b/braintrust/templates/api-pdb.yaml index 7e78952..3fe35ff 100644 --- a/braintrust/templates/api-pdb.yaml +++ b/braintrust/templates/api-pdb.yaml @@ -7,7 +7,7 @@ {{- if gt $rendered 0 }} --- {{- end }} -{{ $resourceLabels := mergeOverwrite (deepCopy $root.Values.global.labels) (deepCopy $api.labels) (dict "braintrust.com/api-pool" $pool.role) -}} +{{ $resourceLabels := mergeOverwrite (deepCopy $root.Values.global.labels) (deepCopy $api.labels) (dict "braintrust.dev/api-pool" $pool.role) -}} apiVersion: policy/v1 kind: PodDisruptionBudget metadata: diff --git a/braintrust/tests/api-workload-isolation_test.yaml b/braintrust/tests/api-workload-isolation_test.yaml index d9e298a..6464553 100644 --- a/braintrust/tests/api-workload-isolation_test.yaml +++ b/braintrust/tests/api-workload-isolation_test.yaml @@ -21,9 +21,9 @@ tests: path: spec.replicas value: 1 - isNull: - path: metadata.labels["braintrust.com/api-pool"] + path: metadata.labels["braintrust.dev/api-pool"] - isNull: - path: spec.template.metadata.labels["braintrust.com/api-pool"] + path: spec.template.metadata.labels["braintrust.dev/api-pool"] - it: should not label the default Service when isolation is disabled template: api-service.yaml @@ -36,7 +36,7 @@ tests: path: metadata.name value: braintrust-api - isNull: - path: metadata.labels["braintrust.com/api-pool"] + path: metadata.labels["braintrust.dev/api-pool"] - it: should render the ingest deployment with its own fixed capacity and topology budget template: api-deployment.yaml @@ -54,7 +54,7 @@ tests: path: spec.replicas value: 3 - equal: - path: spec.template.metadata.labels["braintrust.com/api-pool"] + path: spec.template.metadata.labels["braintrust.dev/api-pool"] value: ingest - equal: path: spec.template.spec.topologySpreadConstraints[0].labelSelector.matchLabels.app @@ -156,6 +156,20 @@ tests: name: BRAINSTORE_AI_PROXY_URL value: "http://braintrust-api-background:8000" + - it: should retain the default AI proxy target while the background pool is staged + template: brainstore-fastreader-deployment.yaml + values: + - __fixtures__/base-values.yaml + set: + api.workloadIsolation.enabled: true + api.workloadIsolation.brainstoreAiProxyToBackground: false + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: BRAINSTORE_AI_PROXY_URL + value: "http://braintrust-api:8000" + - it: should isolate Deployment labels between API pools template: api-deployment.yaml values: diff --git a/braintrust/values.yaml b/braintrust/values.yaml index a3b9270..2354996 100644 --- a/braintrust/values.yaml +++ b/braintrust/values.yaml @@ -117,9 +117,12 @@ api: # route the documented paths to these Services when this is enabled. workloadIsolation: enabled: false + # Keep this false while staging pools for an existing deployment. Set it + # true only after the background pool is healthy and its routes are ready. + brainstoreAiProxyToBackground: true ingest: name: "braintrust-api-ingest" - replicas: 2 + replicas: 3 service: name: "braintrust-api-ingest" podDisruptionBudget: @@ -129,7 +132,7 @@ api: enabled: true background: name: "braintrust-api-background" - replicas: 2 + replicas: 3 service: name: "braintrust-api-background" podDisruptionBudget: From fad67b44705619a78c8eb1e11c0ff90a55ba43ea Mon Sep 17 00:00:00 2001 From: Alex Soldatchenko Date: Wed, 12 Aug 2026 14:19:30 -0600 Subject: [PATCH 06/11] feat: add Istio workload isolation routing --- braintrust/README.md | 15 ++++- .../files/api-workload-isolation-routes.yaml | 26 ++++++++ braintrust/templates/_helpers.tpl | 25 +++++++ braintrust/templates/virtualservice.yaml | 10 +++ braintrust/tests/virtualservice_test.yaml | 66 +++++++++++++++++++ braintrust/values.yaml | 11 +++- 6 files changed, 149 insertions(+), 4 deletions(-) create mode 100644 braintrust/files/api-workload-isolation-routes.yaml diff --git a/braintrust/README.md b/braintrust/README.md index 59e56b8..ac40cf9 100644 --- a/braintrust/README.md +++ b/braintrust/README.md @@ -225,8 +225,10 @@ default `braintrust-api` pool. The pools share the same image and base configuration, while allowing independent replicas, resources, probes, rollout settings, environment overrides, topology spreading, and disruption budgets. -Enabling the pools does not configure a public ingress. The ingress or gateway -must preserve `braintrust-api` as its default backend and route these paths: +The product-owned route contract is defined in +[`files/api-workload-isolation-routes.yaml`](files/api-workload-isolation-routes.yaml). +An ingress or gateway must preserve `braintrust-api` as its default backend and +route these paths: | Pool | Paths | | --- | --- | @@ -245,6 +247,15 @@ later release. To roll back, first return both the public paths and serving them. Only then disable workload isolation in the chart; the chart cannot update an external ingress or gateway on its own. +When using the chart-managed Istio `VirtualService`, set +`virtualService.workloadIsolation.enabled: true` during the second release. +The chart then renders the same product-owned route contract ahead of the +existing `virtualService.http` rules, which remain available for fallback or +custom routing of non-classified paths. The product-owned routes take +precedence, so do not use `virtualService.http` to override a classified path. +This option requires both `virtualService.enabled: true` and +`api.workloadIsolation.enabled: true`. + This feature does not enable autoscaling. Configure fixed replica counts under `api.replicas`, `api.workloadIsolation.ingest.replicas`, and `api.workloadIsolation.background.replicas`. diff --git a/braintrust/files/api-workload-isolation-routes.yaml b/braintrust/files/api-workload-isolation-routes.yaml new file mode 100644 index 0000000..6c3e57e --- /dev/null +++ b/braintrust/files/api-workload-isolation-routes.yaml @@ -0,0 +1,26 @@ +# Canonical public route ownership for API workload isolation. +# +# This is product-controlled rather than a chart value: changing it changes +# which API pool receives customer traffic. Ingress and gateway integrations +# must implement this contract and retain the default API Service for all +# unmatched requests. +version: v1 +pools: + ingest: + exact: + - /logs3 + - /otel/v1/traces + - /attachment + - /attachment/status + background: + prefix: + - /v1/eval + exact: + - /function/eval + - /function/sandbox + - /function/use + - /function/invoke-async-batch + - /function/insert-functions + - /automation/logs/trigger + - /v1/proxy/chat/completions + - /v1/proxy/responses diff --git a/braintrust/templates/_helpers.tpl b/braintrust/templates/_helpers.tpl index cf42b32..2f0ea6a 100644 --- a/braintrust/templates/_helpers.tpl +++ b/braintrust/templates/_helpers.tpl @@ -64,6 +64,31 @@ ingest and background are added only when workload isolation is enabled. {{- toYaml $pools -}} {{- end -}} +{{/* +Render the product-owned workload-isolation routes for an Istio VirtualService. +The static route contract is packaged with the chart at +files/api-workload-isolation-routes.yaml. User-supplied virtualService.http +routes are rendered after these routes as custom fallback behavior. +*/}} +{{- define "braintrust.apiWorkloadIsolationVirtualServiceRoutes" -}} +{{- $contract := .Files.Get "files/api-workload-isolation-routes.yaml" | fromYaml -}} +{{- $ingest := include "braintrust.apiPoolConfig" (dict "root" . "overrides" .Values.api.workloadIsolation.ingest) | fromYaml -}} +{{- $background := include "braintrust.apiPoolConfig" (dict "root" . "overrides" .Values.api.workloadIsolation.background) | fromYaml -}} +{{- $ingestDestination := dict "host" ($ingest.service.name | default $ingest.name) "port" (dict "number" $ingest.service.port) -}} +{{- $backgroundDestination := dict "host" ($background.service.name | default $background.name) "port" (dict "number" $background.service.port) -}} +{{- $routes := list -}} +{{- range $path := $contract.pools.ingest.exact -}} +{{- $routes = append $routes (dict "match" (list (dict "uri" (dict "exact" $path))) "route" (list (dict "destination" $ingestDestination))) -}} +{{- end -}} +{{- range $path := $contract.pools.background.prefix -}} +{{- $routes = append $routes (dict "match" (list (dict "uri" (dict "prefix" $path))) "route" (list (dict "destination" $backgroundDestination))) -}} +{{- end -}} +{{- range $path := $contract.pools.background.exact -}} +{{- $routes = append $routes (dict "match" (list (dict "uri" (dict "exact" $path))) "route" (list (dict "destination" $backgroundDestination))) -}} +{{- end -}} +{{- toYaml $routes -}} +{{- end -}} + {{/* Internal cluster URL Brainstore uses for function/scoring traffic. The background pool is used only after workload isolation has been activated for diff --git a/braintrust/templates/virtualservice.yaml b/braintrust/templates/virtualservice.yaml index 9b05ee4..4a4be85 100644 --- a/braintrust/templates/virtualservice.yaml +++ b/braintrust/templates/virtualservice.yaml @@ -1,3 +1,10 @@ +{{- $virtualServiceWorkloadIsolation := .Values.virtualService.workloadIsolation | default dict -}} +{{- if and $virtualServiceWorkloadIsolation.enabled (not .Values.virtualService.enabled) -}} +{{- fail "virtualService.workloadIsolation.enabled requires virtualService.enabled." -}} +{{- end -}} +{{- if and $virtualServiceWorkloadIsolation.enabled (not .Values.api.workloadIsolation.enabled) -}} +{{- fail "virtualService.workloadIsolation.enabled requires api.workloadIsolation.enabled." -}} +{{- end -}} {{- if .Values.virtualService.enabled }} apiVersion: networking.istio.io/v1 kind: VirtualService @@ -22,6 +29,9 @@ spec: {{- toYaml . | nindent 4 }} {{- end }} http: + {{- if $virtualServiceWorkloadIsolation.enabled }} +{{ include "braintrust.apiWorkloadIsolationVirtualServiceRoutes" . | nindent 4 }} + {{- end }} {{- range .Values.virtualService.http }} - {{- toYaml . | nindent 6 }} {{- end }} diff --git a/braintrust/tests/virtualservice_test.yaml b/braintrust/tests/virtualservice_test.yaml index cb38a25..309e318 100644 --- a/braintrust/tests/virtualservice_test.yaml +++ b/braintrust/tests/virtualservice_test.yaml @@ -250,3 +250,69 @@ tests: - equal: path: spec.http[0].route[0].weight value: 100 + + - it: should route the workload-isolation contract ahead of custom fallback rules + values: + - __fixtures__/base-values.yaml + set: + virtualService.enabled: true + virtualService.workloadIsolation.enabled: true + virtualService.http: + - match: + - uri: + prefix: "/" + route: + - destination: + host: "braintrust-api" + port: + number: 8000 + api.workloadIsolation.enabled: true + api.workloadIsolation.ingest.service.name: "custom-ingest" + api.workloadIsolation.background.service.name: "custom-background" + asserts: + - equal: + path: spec.http[0].match[0].uri.exact + value: /logs3 + - equal: + path: spec.http[0].route[0].destination.host + value: custom-ingest + - equal: + path: spec.http[4].match[0].uri.prefix + value: /v1/eval + - equal: + path: spec.http[4].route[0].destination.host + value: custom-background + - equal: + path: spec.http[12].match[0].uri.exact + value: /v1/proxy/responses + - equal: + path: spec.http[12].route[0].destination.host + value: custom-background + - equal: + path: spec.http[13].match[0].uri.prefix + value: / + - equal: + path: spec.http[13].route[0].destination.host + value: braintrust-api + + - it: should reject workload-isolation routes without the API pools + values: + - __fixtures__/base-values.yaml + set: + virtualService.enabled: true + virtualService.workloadIsolation.enabled: true + api.workloadIsolation.enabled: false + asserts: + - failedTemplate: + errorMessage: "virtualService.workloadIsolation.enabled requires api.workloadIsolation.enabled." + + - it: should reject workload-isolation routes when the VirtualService is disabled + values: + - __fixtures__/base-values.yaml + set: + virtualService.enabled: false + virtualService.workloadIsolation.enabled: true + api.workloadIsolation.enabled: true + asserts: + - failedTemplate: + errorMessage: "virtualService.workloadIsolation.enabled requires virtualService.enabled." diff --git a/braintrust/values.yaml b/braintrust/values.yaml index 2354996..c0dcd7c 100644 --- a/braintrust/values.yaml +++ b/braintrust/values.yaml @@ -545,9 +545,16 @@ virtualService: # List of hosts/domains hosts: - "braintrust.example.com" + # Optional integration with api.workloadIsolation. When enabled, the chart + # emits the product-owned ingest/background route contract before the routes + # below. This must be enabled only after the isolated pools are healthy. + workloadIsolation: + enabled: false # HTTP routing rules - # The destination host will use api.service.name (or api.name if service.name is empty) - # The destination port will use api.service.port (default: 8000) + # These routes are retained after workload-isolation routes as fallback or + # custom behavior for non-classified paths. The destination host will use + # api.service.name (or api.name if service.name is empty); the destination + # port defaults to 8000. http: - match: - uri: From 4f6f194f0297d8d6e79c71e7ebf40bb3252be3fb Mon Sep 17 00:00:00 2001 From: Alex Soldatchenko Date: Wed, 12 Aug 2026 14:26:50 -0600 Subject: [PATCH 07/11] chore: organize routing contracts --- braintrust/README.md | 2 +- .../files/{ => contracts}/api-workload-isolation-routes.yaml | 0 braintrust/templates/_helpers.tpl | 4 ++-- 3 files changed, 3 insertions(+), 3 deletions(-) rename braintrust/files/{ => contracts}/api-workload-isolation-routes.yaml (100%) diff --git a/braintrust/README.md b/braintrust/README.md index ac40cf9..23b58eb 100644 --- a/braintrust/README.md +++ b/braintrust/README.md @@ -226,7 +226,7 @@ configuration, while allowing independent replicas, resources, probes, rollout settings, environment overrides, topology spreading, and disruption budgets. The product-owned route contract is defined in -[`files/api-workload-isolation-routes.yaml`](files/api-workload-isolation-routes.yaml). +[`files/contracts/api-workload-isolation-routes.yaml`](files/contracts/api-workload-isolation-routes.yaml). An ingress or gateway must preserve `braintrust-api` as its default backend and route these paths: diff --git a/braintrust/files/api-workload-isolation-routes.yaml b/braintrust/files/contracts/api-workload-isolation-routes.yaml similarity index 100% rename from braintrust/files/api-workload-isolation-routes.yaml rename to braintrust/files/contracts/api-workload-isolation-routes.yaml diff --git a/braintrust/templates/_helpers.tpl b/braintrust/templates/_helpers.tpl index 2f0ea6a..2dfd914 100644 --- a/braintrust/templates/_helpers.tpl +++ b/braintrust/templates/_helpers.tpl @@ -67,11 +67,11 @@ ingest and background are added only when workload isolation is enabled. {{/* Render the product-owned workload-isolation routes for an Istio VirtualService. The static route contract is packaged with the chart at -files/api-workload-isolation-routes.yaml. User-supplied virtualService.http +files/contracts/api-workload-isolation-routes.yaml. User-supplied virtualService.http routes are rendered after these routes as custom fallback behavior. */}} {{- define "braintrust.apiWorkloadIsolationVirtualServiceRoutes" -}} -{{- $contract := .Files.Get "files/api-workload-isolation-routes.yaml" | fromYaml -}} +{{- $contract := .Files.Get "files/contracts/api-workload-isolation-routes.yaml" | fromYaml -}} {{- $ingest := include "braintrust.apiPoolConfig" (dict "root" . "overrides" .Values.api.workloadIsolation.ingest) | fromYaml -}} {{- $background := include "braintrust.apiPoolConfig" (dict "root" . "overrides" .Values.api.workloadIsolation.background) | fromYaml -}} {{- $ingestDestination := dict "host" ($ingest.service.name | default $ingest.name) "port" (dict "number" $ingest.service.port) -}} From b0993565989cc8d79c8c70356dcc1a629a4380a1 Mon Sep 17 00:00:00 2001 From: Alex Soldatchenko Date: Wed, 12 Aug 2026 15:59:41 -0600 Subject: [PATCH 08/11] fix: preserve API route match boundaries --- braintrust/README.md | 5 +- .../api-workload-isolation-routes.yaml | 61 ++++++++++++++----- braintrust/templates/_helpers.tpl | 17 ++++-- braintrust/tests/virtualservice_test.yaml | 21 +++++-- 4 files changed, 76 insertions(+), 28 deletions(-) diff --git a/braintrust/README.md b/braintrust/README.md index 23b58eb..c584430 100644 --- a/braintrust/README.md +++ b/braintrust/README.md @@ -254,7 +254,10 @@ existing `virtualService.http` rules, which remain available for fallback or custom routing of non-classified paths. The product-owned routes take precedence, so do not use `virtualService.http` to override a classified path. This option requires both `virtualService.enabled: true` and -`api.workloadIsolation.enabled: true`. +`api.workloadIsolation.enabled: true`. It preserves the AWS route methods: +ingest, eval, function, and automation routes match `POST`; proxy routes match +all methods. GKE Ingress cannot route by method, so its equivalent integration +classifies matching paths for all methods. This feature does not enable autoscaling. Configure fixed replica counts under `api.replicas`, `api.workloadIsolation.ingest.replicas`, and diff --git a/braintrust/files/contracts/api-workload-isolation-routes.yaml b/braintrust/files/contracts/api-workload-isolation-routes.yaml index 6c3e57e..d37f441 100644 --- a/braintrust/files/contracts/api-workload-isolation-routes.yaml +++ b/braintrust/files/contracts/api-workload-isolation-routes.yaml @@ -7,20 +7,49 @@ version: v1 pools: ingest: - exact: - - /logs3 - - /otel/v1/traces - - /attachment - - /attachment/status + routes: + - path: /logs3 + pathType: exact + method: POST + - path: /otel/v1/traces + pathType: exact + method: POST + - path: /attachment + pathType: exact + method: POST + - path: /attachment/status + pathType: exact + method: POST background: - prefix: - - /v1/eval - exact: - - /function/eval - - /function/sandbox - - /function/use - - /function/invoke-async-batch - - /function/insert-functions - - /automation/logs/trigger - - /v1/proxy/chat/completions - - /v1/proxy/responses + routes: + # Keep this distinct from the trailing-slash prefix. Istio URI + # prefixes are string prefixes, so /v1/eval would also match + # /v1/evaluate. + - path: /v1/eval + pathType: exact + method: POST + - path: /v1/eval/ + pathType: prefix + method: POST + - path: /function/eval + pathType: exact + method: POST + - path: /function/sandbox + pathType: exact + method: POST + - path: /function/use + pathType: exact + method: POST + - path: /function/invoke-async-batch + pathType: exact + method: POST + - path: /function/insert-functions + pathType: exact + method: POST + - path: /automation/logs/trigger + pathType: exact + method: POST + - path: /v1/proxy/chat/completions + pathType: exact + - path: /v1/proxy/responses + pathType: exact diff --git a/braintrust/templates/_helpers.tpl b/braintrust/templates/_helpers.tpl index 2dfd914..d18b731 100644 --- a/braintrust/templates/_helpers.tpl +++ b/braintrust/templates/_helpers.tpl @@ -77,14 +77,19 @@ routes are rendered after these routes as custom fallback behavior. {{- $ingestDestination := dict "host" ($ingest.service.name | default $ingest.name) "port" (dict "number" $ingest.service.port) -}} {{- $backgroundDestination := dict "host" ($background.service.name | default $background.name) "port" (dict "number" $background.service.port) -}} {{- $routes := list -}} -{{- range $path := $contract.pools.ingest.exact -}} -{{- $routes = append $routes (dict "match" (list (dict "uri" (dict "exact" $path))) "route" (list (dict "destination" $ingestDestination))) -}} +{{- range $route := $contract.pools.ingest.routes -}} +{{- $match := dict "uri" (dict $route.pathType $route.path) -}} +{{- if $route.method -}} +{{- $_ := set $match "method" (dict "exact" $route.method) -}} {{- end -}} -{{- range $path := $contract.pools.background.prefix -}} -{{- $routes = append $routes (dict "match" (list (dict "uri" (dict "prefix" $path))) "route" (list (dict "destination" $backgroundDestination))) -}} +{{- $routes = append $routes (dict "match" (list $match) "route" (list (dict "destination" $ingestDestination))) -}} {{- end -}} -{{- range $path := $contract.pools.background.exact -}} -{{- $routes = append $routes (dict "match" (list (dict "uri" (dict "exact" $path))) "route" (list (dict "destination" $backgroundDestination))) -}} +{{- range $route := $contract.pools.background.routes -}} +{{- $match := dict "uri" (dict $route.pathType $route.path) -}} +{{- if $route.method -}} +{{- $_ := set $match "method" (dict "exact" $route.method) -}} +{{- end -}} +{{- $routes = append $routes (dict "match" (list $match) "route" (list (dict "destination" $backgroundDestination))) -}} {{- end -}} {{- toYaml $routes -}} {{- end -}} diff --git a/braintrust/tests/virtualservice_test.yaml b/braintrust/tests/virtualservice_test.yaml index 309e318..99913b2 100644 --- a/braintrust/tests/virtualservice_test.yaml +++ b/braintrust/tests/virtualservice_test.yaml @@ -277,22 +277,33 @@ tests: path: spec.http[0].route[0].destination.host value: custom-ingest - equal: - path: spec.http[4].match[0].uri.prefix + path: spec.http[0].match[0].method.exact + value: POST + - equal: + path: spec.http[4].match[0].uri.exact value: /v1/eval - equal: path: spec.http[4].route[0].destination.host value: custom-background - equal: - path: spec.http[12].match[0].uri.exact + path: spec.http[5].match[0].uri.prefix + value: /v1/eval/ + - equal: + path: spec.http[5].match[0].method.exact + value: POST + - equal: + path: spec.http[13].match[0].uri.exact value: /v1/proxy/responses - equal: - path: spec.http[12].route[0].destination.host + path: spec.http[13].route[0].destination.host value: custom-background + - isNull: + path: spec.http[13].match[0].method - equal: - path: spec.http[13].match[0].uri.prefix + path: spec.http[14].match[0].uri.prefix value: / - equal: - path: spec.http[13].route[0].destination.host + path: spec.http[14].route[0].destination.host value: braintrust-api - it: should reject workload-isolation routes without the API pools From 4b3b933b281c5c3488fa83d9ee7c37a35319e14a Mon Sep 17 00:00:00 2001 From: Alex Soldatchenko Date: Wed, 12 Aug 2026 16:00:35 -0600 Subject: [PATCH 09/11] docs: clarify API route methods --- braintrust/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/braintrust/README.md b/braintrust/README.md index c584430..59b2fd9 100644 --- a/braintrust/README.md +++ b/braintrust/README.md @@ -233,8 +233,8 @@ route these paths: | Pool | Paths | | --- | --- | | `braintrust-api` (default) | All requests not matched by an explicit ingest or background route | -| `braintrust-api-ingest` | `/logs3`, `/otel/v1/traces`, `/attachment`, `/attachment/status` | -| `braintrust-api-background` | `/v1/eval`, `/v1/eval/*`, `/function/eval`, `/function/sandbox`, `/function/use`, `/function/invoke-async-batch`, `/function/insert-functions`, `/automation/logs/trigger`, `/v1/proxy/chat/completions`, `/v1/proxy/responses` | +| `braintrust-api-ingest` | `POST /logs3`, `POST /otel/v1/traces`, `POST /attachment`, `POST /attachment/status` | +| `braintrust-api-background` | `POST /v1/eval`, `POST /v1/eval/*`, `POST /function/eval`, `POST /function/sandbox`, `POST /function/use`, `POST /function/invoke-async-batch`, `POST /function/insert-functions`, `POST /automation/logs/trigger`; all methods for `/v1/proxy/chat/completions`, `/v1/proxy/responses` | By default, Brainstore's internal `BRAINSTORE_AI_PROXY_URL` targets the background Service while isolation is enabled. For an existing deployment, From 129efaf0728bfe87b2beb985212c688b1ae12ac3 Mon Sep 17 00:00:00 2001 From: Alexey Soldatchenko <85141617+soldatchenko@users.noreply.github.com> Date: Wed, 12 Aug 2026 16:36:20 -0600 Subject: [PATCH 10/11] Update braintrust/templates/virtualservice.yaml Co-authored-by: brianvans <1323225+brianvans@users.noreply.github.com> --- braintrust/templates/virtualservice.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/braintrust/templates/virtualservice.yaml b/braintrust/templates/virtualservice.yaml index 4a4be85..81079ae 100644 --- a/braintrust/templates/virtualservice.yaml +++ b/braintrust/templates/virtualservice.yaml @@ -33,6 +33,6 @@ spec: {{ include "braintrust.apiWorkloadIsolationVirtualServiceRoutes" . | nindent 4 }} {{- end }} {{- range .Values.virtualService.http }} - - {{- toYaml . | nindent 6 }} + - {{ toYaml . | nindent 6 | trim }} {{- end }} {{- end }} From eab740432160efbe45e87f7a76454b01615bb6cb Mon Sep 17 00:00:00 2001 From: Alexey Soldatchenko <85141617+soldatchenko@users.noreply.github.com> Date: Wed, 12 Aug 2026 16:36:27 -0600 Subject: [PATCH 11/11] Update braintrust/templates/virtualservice.yaml Co-authored-by: brianvans <1323225+brianvans@users.noreply.github.com> --- braintrust/templates/virtualservice.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/braintrust/templates/virtualservice.yaml b/braintrust/templates/virtualservice.yaml index 81079ae..5b4dd8d 100644 --- a/braintrust/templates/virtualservice.yaml +++ b/braintrust/templates/virtualservice.yaml @@ -30,7 +30,7 @@ spec: {{- end }} http: {{- if $virtualServiceWorkloadIsolation.enabled }} -{{ include "braintrust.apiWorkloadIsolationVirtualServiceRoutes" . | nindent 4 }} + {{- include "braintrust.apiWorkloadIsolationVirtualServiceRoutes" . | nindent 4 }} {{- end }} {{- range .Values.virtualService.http }} - {{ toYaml . | nindent 6 | trim }}